Index: /branches/eam_branches/psModules.20240412/AUTHORS
===================================================================
--- /branches/eam_branches/psModules.20240412/AUTHORS	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/AUTHORS	(revision 42651)
@@ -0,0 +1,1 @@
+Maui High Performance Computing Center, University of Hawai'i
Index: /branches/eam_branches/psModules.20240412/COPYING
===================================================================
--- /branches/eam_branches/psModules.20240412/COPYING	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/COPYING	(revision 42651)
@@ -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_branches/psModules.20240412/Doxyfile.in
===================================================================
--- /branches/eam_branches/psModules.20240412/Doxyfile.in	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/Doxyfile.in	(revision 42651)
@@ -0,0 +1,1311 @@
+# Doxyfile 1.5.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
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the config file that 
+# follow. The default is UTF-8 which is also the encoding used for all text before 
+# the first occurrence of this tag. Doxygen uses libiconv (or the iconv built into 
+# libc) for the transcoding. See http://www.gnu.org/software/libiconv for the list of 
+# possible encodings.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# 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         = 1.1.0
+
+# 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       = ./docs
+
+# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create 
+# 4096 sub-directories (in 2 levels) under the output directory of each output 
+# format and will distribute the generated files over these directories. 
+# Enabling this option can be useful when feeding doxygen a huge amount of 
+# source files, where putting all generated files in the same directory would 
+# otherwise cause performance problems for the file system.
+
+CREATE_SUBDIRS         = NO
+
+# 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: 
+# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, 
+# Croatian, Czech, Danish, Dutch, Finnish, French, German, Greek, Hungarian, 
+# Italian, Japanese, Japanese-en (Japanese with English messages), Korean, 
+# Korean-en, Lithuanian, Norwegian, Polish, Portuguese, Romanian, Russian, 
+# Serbian, Slovak, Slovene, Spanish, Swedish, and Ukrainian.
+
+OUTPUT_LANGUAGE        = English
+
+# 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
+
+# This tag implements a quasi-intelligent brief description abbreviator 
+# that is used to form the text in various listings. Each string 
+# in this list, if found as the leading text of the brief description, will be 
+# stripped from the text and the result after processing the whole list, is 
+# used as the annotated text. Otherwise, the brief description is used as-is. 
+# If left blank, the following values are used ("$name" is automatically 
+# replaced with the name of the entity): "The $name class" "The $name widget" 
+# "The $name file" "is" "provides" "specifies" "contains" 
+# "represents" "a" "an" "the"
+
+ABBREVIATE_BRIEF       = 
+
+# 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. The tag can be used to show relative paths in the file list. 
+# If left blank the directory from which doxygen is run is used as the 
+# path to strip.
+
+STRIP_FROM_PATH        = 
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of 
+# the path mentioned in the documentation of a class, which tells 
+# the reader which header file to include in order to use a class. 
+# If left blank only the name of the header file containing the class 
+# definition is used. Otherwise one should specify the include paths that 
+# are normally passed to the compiler using the -I flag.
+
+STRIP_FROM_INC_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 regular Qt-style comments 
+# (thus requiring an explicit @brief command for a brief description.)
+
+JAVADOC_AUTOBRIEF      = YES
+
+# If the QT_AUTOBRIEF tag is set to YES then Doxygen will 
+# interpret the first line (until the first dot) of a Qt-style 
+# comment as the brief description. If set to NO, the comments 
+# will behave just like regular Qt-style comments (thus requiring 
+# an explicit \brief command for a brief description.)
+
+QT_AUTOBRIEF           = NO
+
+# 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 
+# re-implements.
+
+INHERIT_DOCS           = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce 
+# a new page for each member. If set to NO, the documentation of a member will 
+# be part of the file/class/namespace that contains it.
+
+SEPARATE_MEMBER_PAGES  = 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
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want to 
+# include (a tag file for) the STL sources as input, then you should 
+# set this tag to YES in order to let doxygen match functions declarations and 
+# definitions whose arguments contain STL classes (e.g. func(std::string); v.s. 
+# func(std::string) {}). This also make the inheritance and collaboration 
+# diagrams that involve STL classes more complete and accurate.
+
+BUILTIN_STL_SUPPORT    = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+
+CPP_CLI_SUPPORT        = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. 
+# Doxygen will parse them like normal C++ but will assume all classes use public 
+# instead of private inheritance when no explicit protection keyword is present.
+
+SIP_SUPPORT            = NO
+
+# 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
+
+# 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
+
+# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct (or union) is 
+# documented as struct with the name of the typedef. So 
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct 
+# with name TypeT. When disabled the typedef will appear as a member of a file, 
+# namespace, or class. And the struct will be named TypeS. This can typically 
+# be useful for C code where the coding convention is that all structs are 
+# typedef'ed and only the typedef is referenced never the struct's name.
+
+TYPEDEF_HIDES_STRUCT   = 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
+
+# This flag is only useful for Objective-C code. When set to YES local 
+# methods, which are defined in the implementation section but not in 
+# the interface are included in the documentation. 
+# If set to NO (the default) only methods in the interface are included.
+
+EXTRACT_LOCAL_METHODS  = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be extracted 
+# and appear in the documentation as a namespace called 'anonymous_namespace{file}', 
+# where file will be replaced with the base name of the file that contains the anonymous 
+# namespace. By default anonymous namespace are hidden.
+
+EXTRACT_ANON_NSPACES   = 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 
+# and Mac 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
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the 
+# brief documentation of file, namespace and class members alphabetically 
+# by member name. If set to NO (the default) the members will appear in 
+# declaration order.
+
+SORT_BRIEF_DOCS        = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be 
+# sorted by fully-qualified names, including namespaces. If set to 
+# NO (the default), the class list will be sorted only by class name, 
+# not including the namespace part. 
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the 
+# alphabetical list.
+
+SORT_BY_SCOPE_NAME     = NO
+
+# 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
+
+# If the sources in your project are distributed over multiple directories 
+# then setting the SHOW_DIRECTORIES tag to YES will show the directory hierarchy 
+# in the documentation. The default is NO.
+
+SHOW_DIRECTORIES       = NO
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that 
+# doxygen should invoke to get the current version for each file (typically from the 
+# version control system). Doxygen will invoke the program by executing (via 
+# popen()) the command <command> <input-file>, where <command> is the value of 
+# the FILE_VERSION_FILTER tag, and <input-file> is the name of an input file 
+# provided by doxygen. Whatever the program writes to standard output 
+# is used as the file version. See the manual for examples.
+
+FILE_VERSION_FILTER    = 
+
+#---------------------------------------------------------------------------
+# 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
+
+# This WARN_NO_PARAMDOC option can be abled to get warnings for 
+# functions that are documented, but have no documentation for their parameters 
+# or return value. If set to NO (the default) doxygen will only warn about 
+# wrong or incomplete parameter documentation, but not about the absence of 
+# documentation.
+
+WARN_NO_PARAMDOC       = NO
+
+# 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. Optionally the format may contain 
+# $version, which will be replaced by the version of the file (if it could 
+# be obtained via FILE_VERSION_FILTER)
+
+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
+
+# This tag can be used to specify the character encoding of the source files that 
+# doxygen parses. Internally doxygen uses the UTF-8 encoding, which is also the default 
+# input encoding. Doxygen uses libiconv (or the iconv built into libc) for the transcoding. 
+# See http://www.gnu.org/software/libiconv for the list of possible encodings.
+
+INPUT_ENCODING         = UTF-8
+
+# 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 *.m *.mm *.py *.f90
+
+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. Note that the wildcards are matched 
+# against the file with absolute path, so to exclude all test directories 
+# for example use the pattern */test/*
+
+EXCLUDE_PATTERNS       = *_wrap.c
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names 
+# (namespaces, classes, functions, etc.) that should be excluded from the output. 
+# The symbol name can be a fully qualified name, a word, or if the wildcard * is used, 
+# a substring. Examples: ANamespace, AClass, AClass::ANamespace, ANamespace::*Test
+
+EXCLUDE_SYMBOLS        = 
+
+# 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.  If FILTER_PATTERNS is specified, this tag will be 
+# ignored.
+
+INPUT_FILTER           = 
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern 
+# basis.  Doxygen will compare the file name with each pattern and apply the 
+# filter if there is a match.  The filters are a list of the form: 
+# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further 
+# info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER 
+# is applied to all files.
+
+FILTER_PATTERNS        = 
+
+# 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. 
+# Note: To get rid of all source code in the generated output, make sure also 
+# VERBATIM_HEADERS is set to NO. If you have enabled CALL_GRAPH or CALLER_GRAPH 
+# then you must also enable this option. If you don't then doxygen will produce 
+# a warning and turn it on anyway
+
+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 REFERENCES_LINK_SOURCE tag is set to YES (the default)
+# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from
+# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will
+# link to the source code.  Otherwise they will link to the documentstion.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code 
+# will point to the HTML generated by the htags(1) tool instead of doxygen 
+# built-in source browser. The htags tool is part of GNU's global source 
+# tagging system (see http://www.gnu.org/software/global/global.html). You 
+# will need version 4.8.6 or higher.
+
+USE_HTAGS              = NO
+
+# 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. Note that doxygen will try to copy 
+# the style sheet file to the HTML output directory, so don't put your own 
+# stylesheet in the HTML output directory as well, or it will be erased!
+
+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 HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML 
+# documentation will contain sections that can be hidden and shown after the 
+# page has loaded. For this to work a browser that supports 
+# JavaScript and DHTML is required (for instance Mozilla 1.0+, Firefox 
+# Netscape 6.0+, Internet explorer 5.0+, Konqueror, or Safari).
+
+HTML_DYNAMIC_SECTIONS  = 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 directory.
+
+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 optimized 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 assignments. 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.
+
+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                = 
+
+# If the XML_PROGRAMLISTING tag is set to YES Doxygen will 
+# dump the program listings (including syntax highlighting 
+# and cross-referencing information) to the XML output. Note that 
+# enabling this will significantly increase the size of the XML output.
+
+XML_PROGRAMLISTING     = YES
+
+#---------------------------------------------------------------------------
+# 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_DEFINED 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. To prevent a macro definition from being 
+# undefined via #undef or recursively expanded use the := operator 
+# instead of the = operator.
+
+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::additions 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              = /usr/bin/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 superseded 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
+
+# You can define message sequence charts within doxygen comments using the \msc 
+# command. Doxygen will then run the mscgen tool (see http://www.mcternan.me.uk/mscgen/) to 
+# produce the chart and insert it in the documentation. The MSCGEN_PATH tag allows you to 
+# specify the directory where the mscgen tool resides. If left empty the tool is assumed to 
+# be found in the default search path.
+
+MSCGEN_PATH            = 
+
+# 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 GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen 
+# will generate a graph for groups, showing the direct groups dependencies
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES doxygen will generate inheritance and 
+# collaboration diagrams in a style similar 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, SOURCE_BROWSER 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 CALLER_GRAPH, SOURCE_BROWSER and HAVE_DOT tags are set to YES then doxygen will 
+# generate a caller 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 caller graphs for selected 
+# functions only using the \callergraph command.
+
+CALLER_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
+
+# If the DIRECTORY_GRAPH, SHOW_DIRECTORIES and HAVE_DOT tags are set to YES 
+# then doxygen will show the dependencies a directory has on other directories 
+# in a graphical way. The dependency relations are determined by the #include
+# relations between the files in the directories.
+
+DIRECTORY_GRAPH        = 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 in 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_MAX_NODES tag can be used to set the maximum number of 
+# nodes that will be shown in the graph. If the number of nodes in a graph 
+# becomes larger than this value, doxygen will truncate the graph, which is 
+# visualized by representing a node as a red box. Note that doxygen if the number 
+# of direct children of the root node in a graph is already larger than 
+# MAX_DOT_GRAPH_NOTES then the graph will not be shown at all. Also note 
+# that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+
+DOT_GRAPH_MAX_NODES    = 50
+
+# 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 the size of a graph can be further restricted by 
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent 
+# background. This is disabled by default, which results in a white background. 
+# Warning: Depending on the platform used, enabling this option may lead to 
+# badly anti-aliased labels on the edges of a graph (i.e. they become hard to 
+# read).
+
+DOT_TRANSPARENT        = YES
+
+# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output 
+# files in one run (i.e. multiple -o and -T options on the command line). This 
+# makes dot run faster, but since only newer versions of dot (>1.8.10) 
+# support this, this feature is disabled by default.
+
+DOT_MULTI_TARGETS      = NO
+
+# 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::additions 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_branches/psModules.20240412/INSTALL
===================================================================
--- /branches/eam_branches/psModules.20240412/INSTALL	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/INSTALL	(revision 42651)
@@ -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_branches/psModules.20240412/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/NEWS
===================================================================
--- /branches/eam_branches/psModules.20240412/NEWS	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/NEWS	(revision 42651)
@@ -0,0 +1,1 @@
+ 
Index: /branches/eam_branches/psModules.20240412/README
===================================================================
--- /branches/eam_branches/psModules.20240412/README	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/README	(revision 42651)
@@ -0,0 +1,1 @@
+
Index: /branches/eam_branches/psModules.20240412/TODO
===================================================================
--- /branches/eam_branches/psModules.20240412/TODO	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/TODO	(revision 42651)
@@ -0,0 +1,1 @@
+ 
Index: /branches/eam_branches/psModules.20240412/autogen.sh
===================================================================
--- /branches/eam_branches/psModules.20240412/autogen.sh	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/autogen.sh	(revision 42651)
@@ -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_branches/psModules.20240412/configure.ac
===================================================================
--- /branches/eam_branches/psModules.20240412/configure.ac	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/configure.ac	(revision 42651)
@@ -0,0 +1,352 @@
+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 subdir-objects])
+AM_CONFIG_HEADER([src/config.h])
+AM_MAINTAINER_MODE
+
+PSMODULES_LT_VERSION="1:1:0"
+AC_SUBST(PSMODULES_LT_VERSION,$PSMODULES_LT_VERSION)
+
+IPP_STDLDFLAGS
+
+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"
+
+IPP_VERSION(PSMODULES)
+AM_CONDITIONAL([HAVE_SVNVERSION], [test "x$SVNVERSION" != x])
+AM_CONDITIONAL([HAVE_SVN], [test "x$SVN" != x])
+
+dnl ------- enable -Werror after all of the probes have run ---------
+IPP_STDOPTS
+IPP_STDCFLAGS
+
+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_branches/psModules.20240412/psmodules-config.in
===================================================================
--- /branches/eam_branches/psModules.20240412/psmodules-config.in	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/psmodules-config.in	(revision 42651)
@@ -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_branches/psModules.20240412/psmodules.pc.in
===================================================================
--- /branches/eam_branches/psModules.20240412/psmodules.pc.in	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/psmodules.pc.in	(revision 42651)
@@ -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_branches/psModules.20240412/src/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/src/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/src/astrom/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/Makefile.am	(revision 42651)
@@ -0,0 +1,27 @@
+noinst_LTLIBRARIES = libpsmodulesastrom.la
+
+libpsmodulesastrom_la_CPPFLAGS = $(SRCINC) $(PSMODULES_CFLAGS) -I../pslib/
+libpsmodulesastrom_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+libpsmodulesastrom_la_SOURCES  = \
+	pmAstrometryObjects.c \
+	pmAstrometryRegions.c \
+	pmAstrometryDistortion.c \
+	pmAstrometryUtils.c \
+	pmAstrometryModel.c \
+	pmAstrometryRefstars.c \
+	pmAstrometryWCS.c \
+	pmAstrometryVisual.c \
+	pmKHcorrect.c
+
+pkginclude_HEADERS = \
+	pmAstrometryObjects.h \
+	pmAstrometryRegions.h \
+	pmAstrometryDistortion.h \
+	pmAstrometryUtils.h \
+	pmAstrometryModel.h \
+	pmAstrometryRefstars.h \
+	pmAstrometryWCS.h \
+	pmAstrometryVisual.h \
+	pmKHcorrect.h
+
+CLEANFILES = *~
Index: /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryDistortion.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryDistortion.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryDistortion.c	(revision 42651)
@@ -0,0 +1,368 @@
+/** @file  pmAstrometryDistortion.c
+*
+*  @brief This file defines the basic types for measuring the focal-plane distortion.
+*
+*  @ingroup AstroImage
+*
+*  @author EAM, IfA
+*
+*  @version $Revision: 1.23 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2009-01-27 06:39:38 $
+*
+*  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 "pmAstrometryUtils.h"
+#include "pmKapaPlots.h"
+
+static void pmAstromGradientFree (pmAstromGradient *grad)
+{
+
+    if (grad == NULL)
+        return;
+
+    return;
+}
+
+pmAstromGradient *pmAstromGradientAlloc (void)
+{
+
+    pmAstromGradient *gradient = psAlloc (sizeof(pmAstromGradient));
+    psMemSetDeallocator(gradient, (psFreeFunc) pmAstromGradientFree);
+
+    return (gradient);
+}
+
+psArray *pmAstromMeasureGradients(psArray *gradients, psArray *rawstars, psArray *refstars, psArray *matches, psRegion *region, int Nx, int Ny)
+{
+
+    if (gradients == NULL) {
+        gradients = psArrayAllocEmpty (100);
+    }
+
+    // NOTE: region specifies the FP region in pixels covered by the chip (NOT in FP units)
+    // determine range
+    int DX = (region->x1 - region->x0) / Nx;
+    int DY = (region->y1 - region->y0) / Ny;
+
+    psPolynomial2D *local = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, 1, 1);
+    local->coeffMask[1][1] = PS_POLY_MASK_SET;
+
+    // measure gradient for fractional chip regions
+    for (int nx = 0; nx < Nx; nx++) {
+        for (int ny = 0; ny < Ny; ny++) {
+            int Xmin = nx*DX;
+            int Xmax = Xmin + DX;
+            int Ymin = ny*DY;
+            int Ymax = Ymin + DY;
+
+            psStats *stats = NULL;
+            psVector *mask = NULL;
+            pmAstromGradient *grad = NULL;
+
+            psVector *L  = psVectorAllocEmpty (100, PS_TYPE_F32);
+            psVector *M  = psVectorAllocEmpty (100, PS_TYPE_F32);
+            psVector *dP = psVectorAllocEmpty (100, PS_TYPE_F32);
+            psVector *dQ = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+            // XXX this is a bit inefficient: first sorting by X or Y could speed this up.
+            // XXX or assigning to a segment in a single pass first
+            // select the stars within this chip region
+            int Npts = 0;
+            for (int i = 0; i < matches->n; i++) {
+
+                pmAstromMatch *match = matches->data[i];
+
+                pmAstromObj *raw = rawstars->data[match->raw];
+
+                if (raw->chip->x < Xmin) continue;
+                if (raw->chip->x > Xmax) continue;
+                if (raw->chip->y < Ymin) continue;
+                if (raw->chip->y > Ymax) continue;
+
+                pmAstromObj *ref = refstars->data[match->ref];
+
+                L->data.F32[Npts] = raw->FP->x;
+                M->data.F32[Npts] = raw->FP->y;
+
+                // P,Q = L,M + terms of order epsilon.
+                // measuring the gradient constrains thos terms
+                dP->data.F32[Npts] = ref->TP->x - raw->FP->x;
+                dQ->data.F32[Npts] = ref->TP->y - raw->FP->y;
+
+                psVectorExtend (L, 100, 1);
+                psVectorExtend (M, 100, 1);
+                psVectorExtend (dP, 100, 1);
+                psVectorExtend (dQ, 100, 1);
+                Npts++;
+            }
+
+            psTrace ("psModules.astrom", 4, "Npts: %d (%d,%d) : (%d - %d),(%d - %d)\n", Npts, nx, ny, Xmin, Xmax, Ymin, Ymax);
+
+            if (Npts < 5)
+                goto skip;
+
+            // stats structure and mask for use in measuring the clipping statistic
+            // this analysis has too few data points to use the robust median method
+            stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+            mask = psVectorAlloc (Npts, PS_TYPE_VECTOR_MASK);
+            psVectorInit (mask, 0);
+
+            grad = pmAstromGradientAlloc ();
+
+            // XXX psTraceSetLevel("psLib.math.psVectorClipFitPolynomial2D", 7);
+
+            // fit the collection of positions and offsets with a local 1st order gradient
+            // apply 3hi/3lo sigma clipping to the fitted data values
+            // the mask is used to mark the points which pass / fail the fit
+            if (!psVectorClipFitPolynomial2D (local, stats, mask, 0xff, dP, NULL, L, M)) {
+                goto skip;
+            }
+
+            grad->dTPdL.x = local->coeff[1][0];
+            grad->dTPdM.x = local->coeff[0][1];
+
+            // XXX psTraceSetLevel("psLib.math.psVectorClipFitPolynomial2D", 0);
+
+            // fit the collection of positions and offsets with a local 1st order gradient
+            // apply 3hi/3lo sigma clipping to the fitted data values
+            // the mask is used to mark the points which pass / fail the fit
+            if (!psVectorClipFitPolynomial2D (local, stats, mask, 0xff, dQ, NULL, L, M)) {
+                goto skip;
+            }
+
+            grad->dTPdL.y = local->coeff[1][0];
+            grad->dTPdM.y = local->coeff[0][1];
+
+            // also measure the L and M median positions as a representative coordinate
+            if (!psVectorStats (stats, L, NULL, NULL, 0)) {
+		psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+		goto skip;
+	    }
+            grad->FP.x = stats->sampleMedian;
+
+            if (!psVectorStats (stats, M, NULL, NULL, 0)) {
+		psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+		goto skip;
+	    }
+            grad->FP.y = stats->sampleMedian;
+
+            psArrayAdd (gradients, 100, grad);
+
+skip:
+            psFree (grad);
+            psFree (stats);
+            psFree (mask);
+            psFree (L);
+            psFree (M);
+            psFree (dP);
+            psFree (dQ);
+        }
+    }
+    psFree (local);
+    return gradients;
+}
+
+bool pmAstromFitDistortion(pmFPA *fpa, psArray *gradients, double pixelScale)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_ARRAY_NON_NULL(gradients, false);
+
+    int ExtraOrders = pmAstrometryGetExtraOrders();
+
+    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_VECTOR_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)) {
+	// this failure has raised an error.  the return will also raise
+	// errors, but these are finally cleared (and bad data quality is set) in
+	// psastroAnalysis.c:143
+        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)) {
+	// this failure has raised an error.  the return will also raise
+	// errors, but these are finally cleared (and bad data quality is set) in
+	// psastroAnalysis.c:143
+        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);
+
+    // as of r40806, psPlaneTransformInvert supplies the extra order (if non-linear)
+    psFree (fpa->fromTPA);
+    fpa->fromTPA = psPlaneTransformInvert(NULL, fpa->toTPA, *region, 50, ExtraOrders);
+    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_branches/psModules.20240412/src/astrom/pmAstrometryDistortion.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryDistortion.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryDistortion.h	(revision 42651)
@@ -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 (void);
+
+/* 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_branches/psModules.20240412/src/astrom/pmAstrometryModel.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryModel.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryModel.c	(revision 42651)
@@ -0,0 +1,781 @@
+/** @file  pmAstrometryModel.c
+ *
+ *  @brief Functions to read and write astrometric model
+ *
+ *  The generic model does not specify the location of the boresite on the sky, and it includes
+ *  a model for the rotator and motion of the boresite.
+ *
+ *  @ingroup AstroImage
+ *
+ *  @author EAM, IfA
+ *  @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-09-17 23:07:02 $
+ *
+ *  Copyright 2007 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+/******************************************************************************/
+/*  INCLUDE FILES                                                             */
+/******************************************************************************/
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <unistd.h>   // for unlink
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPAExtent.h"
+#include "pmFPAfileFitsIO.h"
+#include "pmConcepts.h"
+#include "pmAstrometryWCS.h"
+#include "pmAstrometryUtils.h"
+#include "pmAstrometryRegions.h"
+#include "pmAstrometryModel.h"
+
+# define REQUIRE(TEST,MESSAGE){ if (!(TEST)) { psAbort (MESSAGE); }}
+
+/********************* CheckDataStatus functions *****************************/
+
+bool pmAstromModelCheckDataStatusForView (const pmFPAview *view, pmFPAfile *file) {
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        bool exists = pmAstromModelCheckDataStatusForFPA (fpa);
+        return exists;
+    }
+    if (view->chip >= fpa->chips->n) {
+        psError(PS_ERR_IO, true, "Requested chip == %d >= fpa->chips->n == %ld", view->chip, fpa->chips->n);
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        bool exists = pmAstromModelCheckDataStatusForChip (chip);
+        return exists;
+    }
+    if (view->cell >= chip->cells->n) {
+        psError(PS_ERR_IO, true, "Requested cell == %d >= chip->cells->n == %ld", view->cell, chip->cells->n);
+        return false;
+    }
+    psError(PS_ERR_IO, false, "Astrometry only valid at the chip level");
+    return false;
+}
+
+bool pmAstromModelCheckDataStatusForFPA (const pmFPA *fpa) {
+
+    if (!fpa->toTPA) return false;
+    if (!fpa->fromTPA) return false;
+    if (!fpa->toSky) return false;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!chip) continue;
+        if (pmAstromModelCheckDataStatusForChip (chip)) return true;
+    }
+    return false;
+}
+
+bool pmAstromModelCheckDataStatusForChip (const pmChip *chip) {
+
+    if (!chip->toFPA) return false;
+    if (!chip->fromFPA) return false;  // XXX not strictly needed?
+    return true;
+}
+
+/********************* Write Data functions *****************************/
+
+bool pmAstromModelWriteForView (const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    // write the full model in one pass: require the level to be FPA
+    if (view->chip != -1) {
+        psError(PS_ERR_IO, false, "Astrometry must be written at the FPA level");
+        return false;
+    }
+
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // Suitable FPA for writing
+
+    if (!pmAstromModelWriteFPA(file, fpa)) {
+        psError(PS_ERR_IO, false, "Failed to write Astrometry for fpa");
+        psFree(fpa);
+        return false;
+    }
+
+    psFree(fpa);
+
+    return true;
+}
+
+// write out all chip-level Astrometry data for this FPA
+bool pmAstromModelWriteFPA (pmFPAfile *file, const pmFPA *fpa)
+{
+
+
+    if (!pmAstromModelWritePHU (file, fpa)) {
+        psError(PS_ERR_IO, false, "Failed to write PHU for Astrometry model");
+        return false;
+    }
+
+    if (!pmAstromModelWriteChips (file)) {
+        psError(PS_ERR_IO, false, "Failed to write Astrometry for chips");
+        return false;
+    }
+
+    if (!pmAstromModelWriteFP (file)) {
+        psError(PS_ERR_IO, false, "Failed to write Sky for Astrometry model");
+        return false;
+    }
+
+    if (!pmAstromModelWriteTP (file)) {
+        psError(PS_ERR_IO, false, "Failed to write Sky for Astrometry model");
+        return false;
+    }
+
+    if (!pmAstromModelWriteSky (file)) {
+        psError(PS_ERR_IO, false, "Failed to write Sky for Astrometry model");
+        return false;
+    }
+
+    return true;
+}
+
+bool pmAstromModelWritePHU (pmFPAfile *file, const pmFPA *fpa) {
+    // Need to have an FPA suitable for writing, so that the headers are all kosher
+
+    // output header data
+    psMetadata *outhead = psMetadataAlloc();
+
+    // use the FPA phu to generate the PHU header
+    pmHDU *phu = fpa->hdu;
+
+    // if there is no FPA PHU, this is a single header+image (extension-less) file. This could be
+    // the case for an input SPLIT set of files being written out as a MEF.  if there is a PHU,
+    // write it out as a 'blank'
+    if (phu) {
+        psMetadataCopy (outhead, phu->header);
+    } else {
+        pmConfigConformHeader (outhead, file->format);
+    }
+
+    psMetadataAddBool (outhead, PS_LIST_TAIL, "EXTEND", PS_META_REPLACE, "this file has extensions", true);
+    psFitsWriteBlank (file->fits, outhead, "");
+    file->wrote_phu = true;
+
+    psTrace ("pmFPAfile", 5, "wrote phu %s (type: %d)\n", file->filename, file->type);
+    psFree (outhead);
+
+    return true;
+}
+
+// fourth layer holds the chips
+bool pmAstromModelWriteChips (pmFPAfile *file) {
+
+    psMetadata *header = psMetadataAlloc();
+    psMetadataAddStr(header, PS_LIST_TAIL, "COORD",    PS_META_REPLACE, "name of this layer",   "CHIPS");
+    psMetadataAddStr(header, PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "FOCAL_PLANE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "BOUNDARY", PS_META_REPLACE, "validity region",      "RECTANGLE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "TRANSFRM", PS_META_REPLACE, "mapping to parent",    "POLYNOMIAL");
+
+    psArray *model = psArrayAllocEmpty (1);
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    pmChip *chip = NULL;
+    while ((chip = pmFPAviewNextChip (view, file->fpa, 1)) != NULL) {
+
+        if (!chip->toFPA) continue;
+        assert (chip->toFPA->x);
+        assert (chip->toFPA->y);
+
+        psRegion *region = pmChipPixels (chip);
+
+        // set the chip name
+        char *chiprule = psStringCopy ("{CHIP.NAME}");
+        char *chipname = pmFPAfileNameFromRule (chiprule, file, view);
+
+        for (int i = 0; i <= chip->toFPA->x->nX; i++) {
+            for (int j = 0; j <= chip->toFPA->x->nY; j++) {
+                psMetadata *row = psMetadataAlloc ();
+
+                psMetadataAddStr(row,    PS_LIST_TAIL, "SEGMENT",  PS_META_REPLACE, "name of this segment", chipname);
+                psMetadataAddStr(row,    PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "FOCAL_PLANE");
+                psMetadataAddF32(row,    PS_LIST_TAIL, "MINX",     PS_META_REPLACE, "range", region->x0);
+                psMetadataAddF32(row,    PS_LIST_TAIL, "MAXX",     PS_META_REPLACE, "range", region->x1);
+                psMetadataAddF32(row,    PS_LIST_TAIL, "MINY",     PS_META_REPLACE, "range", region->y0);
+                psMetadataAddF32(row,    PS_LIST_TAIL, "MAXY",     PS_META_REPLACE, "range", region->y1);
+
+                psMetadataAddS32(row,    PS_LIST_TAIL, "XORDER",   PS_META_REPLACE, "", i);
+                psMetadataAddS32(row,    PS_LIST_TAIL, "YORDER",   PS_META_REPLACE, "", j);
+                psMetadataAddS32(row,    PS_LIST_TAIL, "NXORDER",  PS_META_REPLACE, "", chip->toFPA->x->nX);
+                psMetadataAddS32(row,    PS_LIST_TAIL, "NYORDER",  PS_META_REPLACE, "", chip->toFPA->x->nY);
+                psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_X",   PS_META_REPLACE, "", chip->toFPA->x->coeff[i][j]);
+                psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_Y",   PS_META_REPLACE, "", chip->toFPA->y->coeff[i][j]);
+                psMetadataAddF32(row,    PS_LIST_TAIL, "ERROR_X",  PS_META_REPLACE, "", chip->toFPA->x->coeffErr[i][j]);
+                psMetadataAddF32(row,    PS_LIST_TAIL, "ERROR_Y",  PS_META_REPLACE, "", chip->toFPA->y->coeffErr[i][j]);
+                psMetadataAddU8 (row,    PS_LIST_TAIL, "MASK_X",   PS_META_REPLACE, "", chip->toFPA->x->coeffMask[i][j]);
+                psMetadataAddU8 (row,    PS_LIST_TAIL, "MASK_Y",   PS_META_REPLACE, "", chip->toFPA->y->coeffMask[i][j]);
+                psArrayAdd (model, 100, row);
+                psFree (row);
+            }
+        }
+        psFree (chiprule);
+        psFree (chipname);
+        psFree (region);
+    }
+
+    if (!psFitsWriteTable (file->fits, header, model, "CHIPS")) {
+        psError(PS_ERR_IO, false, "writing sky data\n");
+        psFree(model);
+        return false;
+    }
+
+    psFree (view);
+    psFree (model);
+    psFree (header);
+    return true;
+}
+
+// third layer is the focal plane
+bool pmAstromModelWriteFP (pmFPAfile *file) {
+
+    psMetadata *header = psMetadataAlloc();
+    psMetadataAddStr(header, PS_LIST_TAIL, "COORD",    PS_META_REPLACE, "name of this layer",   "FOCAL_PLANE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "TANGENT_PLANE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "BOUNDARY", PS_META_REPLACE, "validity region",      "RECTANGLE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "TRANSFRM", PS_META_REPLACE, "mapping to parent",    "POLYNOMIAL");
+
+    psArray *model = psArrayAllocEmpty (1);
+
+    // region over which the fromTPA projection is valid
+    psRegion *region = pmAstromFPInTP (file->fpa);
+
+    psPlaneTransform *toTPA   = file->fpa->toTPA;
+
+    for (int i = 0; i <= toTPA->x->nX; i++) {
+        for (int j = 0; j <= toTPA->x->nY; j++) {
+            psMetadata *row = psMetadataAlloc ();
+            psMetadataAddStr(row,    PS_LIST_TAIL, "SEGMENT",  PS_META_REPLACE, "name of this segment", "FOCAL_PLANE");
+            psMetadataAddStr(row,    PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "TANGENT_PLANE");
+            psMetadataAddF32(row,    PS_LIST_TAIL, "MINX",     PS_META_REPLACE, "range", region->x0);
+            psMetadataAddF32(row,    PS_LIST_TAIL, "MAXX",     PS_META_REPLACE, "range", region->x1);
+            psMetadataAddF32(row,    PS_LIST_TAIL, "MINY",     PS_META_REPLACE, "range", region->y0);
+            psMetadataAddF32(row,    PS_LIST_TAIL, "MAXY",     PS_META_REPLACE, "range", region->y1);
+
+            psMetadataAddS32(row,    PS_LIST_TAIL, "XORDER",   PS_META_REPLACE, "", i);
+            psMetadataAddS32(row,    PS_LIST_TAIL, "YORDER",   PS_META_REPLACE, "", j);
+            psMetadataAddS32(row,    PS_LIST_TAIL, "NXORDER",  PS_META_REPLACE, "", toTPA->x->nX);
+            psMetadataAddS32(row,    PS_LIST_TAIL, "NYORDER",  PS_META_REPLACE, "", toTPA->x->nY);
+            psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_X",   PS_META_REPLACE, "", toTPA->x->coeff[i][j]);
+            psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_Y",   PS_META_REPLACE, "", toTPA->y->coeff[i][j]);
+            psMetadataAddF32(row,    PS_LIST_TAIL, "ERROR_X",  PS_META_REPLACE, "", toTPA->x->coeffErr[i][j]);
+            psMetadataAddF32(row,    PS_LIST_TAIL, "ERROR_Y",  PS_META_REPLACE, "", toTPA->y->coeffErr[i][j]);
+            psMetadataAddU8 (row,    PS_LIST_TAIL, "MASK_X",   PS_META_REPLACE, "", toTPA->x->coeffMask[i][j]);
+            psMetadataAddU8 (row,    PS_LIST_TAIL, "MASK_Y",   PS_META_REPLACE, "", toTPA->y->coeffMask[i][j]);
+
+            psArrayAdd (model, 100, row);
+            psFree (row);
+        }
+    }
+
+    if (!psFitsWriteTable (file->fits, header, model, "FP")) {
+        psError(PS_ERR_IO, false, "writing sky data\n");
+        psFree(model);
+        psFree (header);
+        psFree (region);
+        return false;
+    }
+
+    psFree (model);
+    psFree (header);
+    psFree (region);
+    return true;
+}
+
+// second layer is the tangent plane
+bool pmAstromModelWriteTP (pmFPAfile *file) {
+
+    bool status;
+
+    // get the boresite model parameters.  these track the position of the boresite
+    // as a function of the rotator angle
+    float Xo = psMetadataLookupF32 (&status, file->fpa->concepts, "FPA.BORE.X0");
+    float Yo = psMetadataLookupF32 (&status, file->fpa->concepts, "FPA.BORE.Y0");
+    float RX = psMetadataLookupF32 (&status, file->fpa->concepts, "FPA.BORE.RX");
+    float RY = psMetadataLookupF32 (&status, file->fpa->concepts, "FPA.BORE.RY");
+    float To = psMetadataLookupF32 (&status, file->fpa->concepts, "FPA.BORE.T0");
+    float Po = psMetadataLookupF32 (&status, file->fpa->concepts, "FPA.BORE.P0");
+
+    // the PosZero is the offset between the reported and actual POSANGLE values
+    float PosZero = psMetadataLookupF32 (&status, file->fpa->concepts, "FPA.POS_ZERO");  /// XXX be consistent with degrees v radians
+    // Indicate which direction the position angle goes
+    int rotParity = psMetadataLookupS32 (&status, file->fpa->concepts, "FPA.ROT_PARITY");
+    char *refChip = psMetadataLookupStr (&status, file->fpa->concepts, "FPA.REF.CHIP");
+
+    psMetadata *header = psMetadataAlloc();
+    psMetadataAddStr(header, PS_LIST_TAIL, "COORD",    PS_META_REPLACE, "name of this layer",   "TANGENT_PLANE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "SKY");
+    psMetadataAddStr(header, PS_LIST_TAIL, "BOUNDARY", PS_META_REPLACE, "validity region",      "RECTANGLE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "TRANSFRM", PS_META_REPLACE, "mapping to parent",    "PROJECTION");
+
+    psArray *model = psArrayAllocEmpty (1);
+    psMetadata *row = psMetadataAlloc ();
+    psMetadataAddStr(row,    PS_LIST_TAIL, "SEGMENT",  PS_META_REPLACE, "name of this segment", "TANGENT_PLANE");
+    psMetadataAddStr(row,    PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "SKY");
+
+    psRegion *region = pmAstromFPAExtent (file->fpa);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "MINX",     PS_META_REPLACE, "range", region->x0);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "MAXX",     PS_META_REPLACE, "range", region->x1);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "MINY",     PS_META_REPLACE, "range", region->y0);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "MAXY",     PS_META_REPLACE, "range", region->y1);
+
+    psMetadataAddF32(row,    PS_LIST_TAIL, "XSCALE",   PS_META_REPLACE, "", file->fpa->toSky->Xs * PS_DEG_RAD);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "YSCALE",   PS_META_REPLACE, "", file->fpa->toSky->Ys * PS_DEG_RAD);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "BORE_X0",  PS_META_REPLACE, "boresite parameter", Xo);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "BORE_Y0",  PS_META_REPLACE, "boresite parameter", Yo);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "BORE_RX",  PS_META_REPLACE, "boresite parameter", RX);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "BORE_RY",  PS_META_REPLACE, "boresite parameter", RY);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "BORE_T0",  PS_META_REPLACE, "boresite parameter", To);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "BORE_P0",  PS_META_REPLACE, "boresite parameter", Po);
+
+    psMetadataAddF32(row,    PS_LIST_TAIL, "POS_ZERO", PS_META_REPLACE, "POSANGLE offset (degrees)", PosZero);
+    psMetadataAddS32(row,    PS_LIST_TAIL, "ROT_PARITY", PS_META_REPLACE, "rotator parity", rotParity);
+    psMetadataAddStr(row,    PS_LIST_TAIL, "REF_CHIP", PS_META_REPLACE, "reference chip for model", refChip);
+
+    psArrayAdd (model, 100, row);
+    psFree (row);
+
+    if (!psFitsWriteTable (file->fits, header, model, "TP")) {
+        psError(PS_ERR_IO, false, "writing sky data\n");
+        psFree (region);
+        psFree (model);
+        psFree (header);
+        return false;
+    }
+
+    psFree (region);
+    psFree (model);
+    psFree (header);
+    return (true);
+}
+
+// first layer is the sky
+bool pmAstromModelWriteSky (pmFPAfile *file) {
+
+    psMetadata *header = psMetadataAlloc();
+    psMetadataAddStr(header, PS_LIST_TAIL, "COORD",    PS_META_REPLACE, "name of this layer",   "SKY");
+    psMetadataAddStr(header, PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "NONE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "BOUNDARY", PS_META_REPLACE, "validity region",      "NONE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "TRANSFRM", PS_META_REPLACE, "mapping to parent",    "NONE");
+
+    psArray *model = psArrayAllocEmpty (1);
+    psMetadata *row = psMetadataAlloc ();
+    psMetadataAddStr(row,    PS_LIST_TAIL, "SEGMENT",  PS_META_REPLACE, "name of this segment", "SKY");
+    psMetadataAddStr(row,    PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "NONE");
+
+    psArrayAdd (model, 100, row);
+    psFree (row);
+
+    if (!psFitsWriteTable (file->fits, header, model, "SKY")) {
+        psError(PS_ERR_IO, false, "writing sky data\n");
+        psFree(model);
+        return false;
+    }
+
+    psFree (model);
+    psFree (header);
+    return (true);
+}
+
+/********************* Read Data functions *****************************/
+
+bool pmAstromModelReadForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+    {
+
+        // write the full model in one pass: require the level to be FPA
+        if (view->chip != -1) {
+            psError(PS_ERR_IO, false, "Astrometry must be read at the FPA level");
+            return false;
+        }
+
+        if (!pmAstromModelReadFPA (file)) {
+            psError(PS_ERR_IO, false, "Failed to read Astrometry for fpa");
+            return false;
+        }
+        return true;
+    }
+
+// read out all chip-level Astrometry data for this FPA
+bool pmAstromModelReadFPA (pmFPAfile *file) {
+
+    if (!pmAstromModelReadPHU (file)) {
+        psError(PS_ERR_IO, false, "Failed to read PHU for Astrometry model");
+        return false;
+    }
+
+    if (!pmAstromModelReadChips (file)) {
+        psError(PS_ERR_IO, false, "Failed to read Astrometry for chips");
+        return false;
+    }
+
+    if (!pmAstromModelReadFP (file)) {
+        psError(PS_ERR_IO, false, "Failed to read Sky for Astrometry model");
+        return false;
+    }
+
+    // NOTE : TP must come after FP as it applies the POS, ROT boresite corrections to the
+    // transformation determined in FP
+    if (!pmAstromModelReadTP (file)) {
+        psError(PS_ERR_IO, false, "Failed to read Sky for Astrometry model");
+        return false;
+    }
+
+    if (!pmAstromModelReadSky (file)) {
+        psError(PS_ERR_IO, false, "Failed to read Sky for Astrometry model");
+        return false;
+    }
+
+    return true;
+}
+
+bool pmAstromModelReadPHU (pmFPAfile *file) {
+
+    // not necessary to read the PHU
+    return true;
+}
+
+// first layer converts Chip to Focal Plane
+bool pmAstromModelReadChips (pmFPAfile *file) {
+
+    bool status;
+
+    int ExtraOrders = pmAstrometryGetExtraOrders();
+
+    // set FITS cursor
+    if (!psFitsMoveExtName (file->fits, "CHIPS")) {
+        psError(PS_ERR_IO, false, "missing CHIPS extension in astrometry model\n");
+        return false;
+    }
+
+    // free exising tranformations in prep for new alloc below
+    for (int i = 0; i < file->fpa->chips->n; i++) {
+        pmChip *chip = file->fpa->chips->data[i];
+        psFree (chip->toFPA);
+        chip->toFPA = NULL;
+    }
+
+    // load the header
+    psMetadata *header = psFitsReadHeader(NULL, file->fits); // The FITS header
+    if (!header) psAbort("cannot read model header");
+
+    // load the full model in one shot
+    psArray *model = psFitsReadTable (file->fits);
+    if (!model) psAbort("cannot read model");
+    psLogMsg ("psModules.astrom", 4, "read %ld rows from FP\n", model->n);
+
+    // parse the model entries
+    for (int i = 0; i < model->n; i++) {
+        psMetadata *row = model->data[i];
+
+        // name of the chip for this row.
+        char *chipname = psMetadataLookupStr (&status, row, "SEGMENT");
+
+        // get chip from name
+        pmChip *chip = pmConceptsChipFromName (file->fpa, chipname);
+        REQUIRE (chip, "invalid chip name");
+
+        // define the toFPA transform if not already defined
+        int nX = psMetadataLookupS32(&status, row, "NXORDER"); REQUIRE (status, "missing NXORDER");
+        int nY = psMetadataLookupS32(&status, row, "NYORDER"); REQUIRE (status, "missing NYORDER");
+        if (chip->toFPA == NULL) {
+	    chip->toFPA = psPlaneTransformAlloc(nX, nY, PS_POLYNOMIAL_ORD); // chip->fpa uses ordinary poly
+        } else {
+            REQUIRE (chip->toFPA->x->nX == nX, "mismatch in chip order");
+            REQUIRE (chip->toFPA->x->nY == nY, "mismatch in chip order");
+            REQUIRE (chip->toFPA->y->nX == nX, "mismatch in chip order");
+            REQUIRE (chip->toFPA->y->nY == nY, "mismatch in chip order");
+        }
+
+        int ix = psMetadataLookupS32(&status, row, "XORDER");  REQUIRE (status, "missing XORDER");
+        int iy = psMetadataLookupS32(&status, row, "YORDER");  REQUIRE (status, "missing YORDER");
+
+        chip->toFPA->x->coeff[ix][iy]    = psMetadataLookupF32(&status, row, "POLY_X");
+        chip->toFPA->y->coeff[ix][iy]    = psMetadataLookupF32(&status, row, "POLY_Y");
+        chip->toFPA->x->coeffErr[ix][iy] = psMetadataLookupF32(&status, row, "ERROR_X");
+        chip->toFPA->y->coeffErr[ix][iy] = psMetadataLookupF32(&status, row, "ERROR_Y");
+        chip->toFPA->x->coeffMask[ix][iy] = psMetadataLookupU8(&status, row, "MASK_X");
+        chip->toFPA->y->coeffMask[ix][iy] = psMetadataLookupU8(&status, row, "MASK_Y");
+    }
+
+    // convert the toFPA transfomations to fromFPA transformations
+    for (int i = 0; i < file->fpa->chips->n; i++) {
+        pmChip *chip = file->fpa->chips->data[i];
+        if (!chip->toFPA) continue;
+        psRegion *region = pmChipPixels (chip);
+
+	// as of r40806, psPlaneTransformInvert supplies the extra order (if non-linear)
+        psFree (chip->fromFPA);
+        chip->fromFPA = psPlaneTransformInvert(NULL, chip->toFPA, *region, 50, ExtraOrders);
+        psFree (region);
+    }
+
+    psFree (model);
+    psFree (header);
+    return true;
+}
+
+// second layer converts Focal Plane to Tangent Plane (unrotated)
+bool pmAstromModelReadFP (pmFPAfile *file) {
+
+    bool status;
+
+    int ExtraOrders = pmAstrometryGetExtraOrders();
+
+    if (!psFitsMoveExtName (file->fits, "FP")) {
+        psError(PS_ERR_IO, false, "missing FP extension in astrometry model\n");
+        return false;
+    }
+
+    psMetadata *header = psFitsReadHeader(NULL, file->fits); // The FITS header
+    if (!header) psAbort("cannot read model header");
+
+    // free the old
+    psFree (file->fpa->toTPA);
+    file->fpa->toTPA = NULL;
+
+    // read the complete model data at one shot
+    psArray *model = psFitsReadTable (file->fits);
+    psLogMsg ("psModules.astrom", 4, "read %ld rows from FP\n", model->n);
+
+    // parse the model
+    for (int i = 0; i < model->n; i++) {
+        psMetadata *row = model->data[i];
+
+        // there is only one transformation in this model; the order is defined in the header
+        int nX = psMetadataLookupS32(&status, row, "NXORDER"); REQUIRE (status, "missing NXORDER");
+        int nY = psMetadataLookupS32(&status, row, "NYORDER"); REQUIRE (status, "missing NYORDER");
+        if (file->fpa->toTPA == NULL) {
+            // allocate the new transformation
+            file->fpa->toTPA = psPlaneTransformAlloc(nX, nY, PS_POLYNOMIAL_ORD); // fpa->tpa uses ORD
+        } else {
+            REQUIRE (file->fpa->toTPA->x->nX == nX, "mismatch in chip order");
+            REQUIRE (file->fpa->toTPA->x->nY == nY, "mismatch in chip order");
+            REQUIRE (file->fpa->toTPA->y->nX == nX, "mismatch in chip order");
+            REQUIRE (file->fpa->toTPA->y->nY == nY, "mismatch in chip order");
+        }
+
+        int ix = psMetadataLookupS32(&status, row, "XORDER"); REQUIRE (status, "missing XORDER");
+        int iy = psMetadataLookupS32(&status, row, "YORDER"); REQUIRE (status, "missing YORDER");
+        file->fpa->toTPA->x->coeff[ix][iy]     = psMetadataLookupF32(&status, row, "POLY_X");  REQUIRE (status, "missing POLY_X");
+        file->fpa->toTPA->y->coeff[ix][iy]     = psMetadataLookupF32(&status, row, "POLY_Y");  REQUIRE (status, "missing POLY_Y");
+        file->fpa->toTPA->x->coeffErr[ix][iy]  = psMetadataLookupF32(&status, row, "ERROR_X"); REQUIRE (status, "missing ERROR_X");
+        file->fpa->toTPA->y->coeffErr[ix][iy]  = psMetadataLookupF32(&status, row, "ERROR_Y"); REQUIRE (status, "missing ERROR_Y");
+        file->fpa->toTPA->x->coeffMask[ix][iy] = psMetadataLookupU8 (&status, row, "MASK_X");  REQUIRE (status, "missing MASK_X");
+        file->fpa->toTPA->y->coeffMask[ix][iy] = psMetadataLookupU8 (&status, row, "MASK_Y");  REQUIRE (status, "missing MASK_Y");
+    }
+
+    psRegion *region = pmAstromFPAExtent (file->fpa);
+
+    // as of r40806, psPlaneTransformInvert supplies the extra order (if non-linear)
+    psFree (file->fpa->fromTPA);
+    file->fpa->fromTPA = psPlaneTransformInvert(NULL, file->fpa->toTPA, *region, 50, ExtraOrders);
+
+    psFree (model);
+    psFree (header);
+    psFree (region);
+    return true;
+}
+
+# define TRANSFER(TO,FROM,NAME) { \
+    psMetadataItem *item = psMetadataLookup(FROM,NAME); \
+    if (!item) psAbort ("cannot find %s", NAME); \
+    psMetadataItem *newItem = psMetadataItemCopy(item); \
+    if (!psMetadataAddItem(TO, newItem, PS_LIST_TAIL, PS_META_REPLACE)) { \
+        psAbort ("cannot copy %s", NAME); \
+    } \
+    psFree (newItem); }
+
+// third layer applies boresite corrections and converts tangent plane to sky
+bool pmAstromModelReadTP (pmFPAfile *file) {
+
+    if (!psFitsMoveExtName (file->fits, "TP")) {
+        psError(PS_ERR_IO, false, "missing TP extension in astrometry model\n");
+        return false;
+    }
+
+    psMetadata *header = psFitsReadHeader(NULL, file->fits); // The FITS header
+    if (!header) psAbort("cannot read model header");
+
+    psArray *model = psFitsReadTable (file->fits);
+    psLogMsg ("psModules.astrom", 4, "read %ld rows from TP\n", model->n);
+    if (model->n != 1) psAbort("invalid number of rows in TP model (%ld)", model->n);
+
+    psMetadata *row = model->data[0];
+
+    // move needed items to the concepts
+    TRANSFER (file->fpa->concepts, row, "XSCALE");
+    TRANSFER (file->fpa->concepts, row, "XSCALE");
+    TRANSFER (file->fpa->concepts, row, "YSCALE");
+    TRANSFER (file->fpa->concepts, row, "BORE_X0");
+    TRANSFER (file->fpa->concepts, row, "BORE_Y0");
+    TRANSFER (file->fpa->concepts, row, "BORE_RX");
+    TRANSFER (file->fpa->concepts, row, "BORE_RY");
+    TRANSFER (file->fpa->concepts, row, "BORE_T0");
+    TRANSFER (file->fpa->concepts, row, "BORE_P0");
+    TRANSFER (file->fpa->concepts, row, "POS_ZERO");
+    //   TRANSFER (file->fpa->concepts, row, "ROT_PARITY");
+    TRANSFER (file->fpa->concepts, row, "REF_CHIP");
+
+    // DEFAULT value of +1 for ROT_PARITY (do not abort on missing value)
+    psMetadataItem *item = psMetadataLookup(row, "ROT_PARITY");
+    if (!item) {
+      psMetadataAddS32(row, PS_LIST_TAIL, "ROT_PARITY", PS_META_REPLACE, "rotator parity", +1);
+      psLogMsg ("psModules.astrom", 4, "setting default ROT_PARITY of +1\n");
+    } else {
+      psMetadataItem *newItem = psMetadataItemCopy(item); 
+      if (!psMetadataAddItem(file->fpa->concepts, newItem, PS_LIST_TAIL, PS_META_REPLACE)) {
+        psAbort ("cannot copy %s", "ROT_PARITY"); 
+      }
+      psFree (newItem); 
+    }
+
+    psFree (model);
+    psFree (header);
+    return (true);
+}
+
+// first layer is the sky
+bool pmAstromModelReadSky (pmFPAfile *file) {
+
+    if (!psFitsMoveExtName (file->fits, "SKY")) {
+        psError(PS_ERR_IO, false, "missing SKY extension in astrometry model\n");
+        return false;
+    }
+
+    psMetadata *header = psFitsReadHeader(NULL, file->fits); // The FITS header
+    if (!header) psAbort("cannot read model header");
+
+    psArray *model = psFitsReadTable (file->fits);
+    psLogMsg ("psModules.astrom", 4, "read %ld rows from SKY\n", model->n);
+    if (model->n != 1) psAbort("invalid number of rows in SKY model (%ld)", model->n);
+
+    // XXX not much information of interest in this table...
+
+    // generate a template projection for comparisons
+    psFree (file->fpa->toSky);
+    file->fpa->toSky = psProjectionAlloc (0.0, 0.0, PS_RAD_DEG/3600.0, PS_RAD_DEG/3600.0, PS_PROJ_DIS);
+
+    psFree (model);
+    psFree (header);
+    return (true);
+}
+
+// third layer applies boresite corrections and converts tangent plane to sky
+bool pmAstromModelSetTP (pmFPAfile *file, psMetadata *concepts) {
+
+    bool status;
+
+    int ExtraOrders = pmAstrometryGetExtraOrders();
+
+    // these externally supplied values are used to set the final transformation terms
+    double RA  = psMetadataLookupF64 (&status, concepts, "FPA.RA"); REQUIRE (status, "missing FPA.RA");
+    double DEC = psMetadataLookupF64 (&status, concepts, "FPA.DEC"); REQUIRE (status, "missing FPA.DEC");
+    double POS = PM_RAD_DEG * psMetadataLookupF64 (&status, concepts, "FPA.POSANGLE"); REQUIRE (status, "missing FPA.POSANGLE");
+
+    // get projection scale; center is supplied
+    float Xs = psMetadataLookupF32(&status, file->fpa->concepts, "XSCALE") * PM_RAD_DEG; REQUIRE (status, "missing XSCALE");
+    float Ys = psMetadataLookupF32(&status, file->fpa->concepts, "YSCALE") * PM_RAD_DEG; REQUIRE (status, "missing YSCALE");
+
+    // allocate a new toSky projection using the reported position
+    psFree (file->fpa->toSky);
+    file->fpa->toSky = psProjectionAlloc (RA, DEC, Xs, Ys, PS_PROJ_DIS);
+
+    // get boresite correction terms.  RX,RY,To,Po define an ellipse along which the boresite travels
+    double Xo = psMetadataLookupF32(&status, file->fpa->concepts, "BORE_X0"); REQUIRE (status, "missing ");
+    double Yo = psMetadataLookupF32(&status, file->fpa->concepts, "BORE_Y0"); REQUIRE (status, "missing ");
+    double RX = psMetadataLookupF32(&status, file->fpa->concepts, "BORE_RX"); REQUIRE (status, "missing ");
+    double RY = psMetadataLookupF32(&status, file->fpa->concepts, "BORE_RY"); REQUIRE (status, "missing ");
+    double To = psMetadataLookupF32(&status, file->fpa->concepts, "BORE_T0"); REQUIRE (status, "missing ");
+    double Po = psMetadataLookupF32(&status, file->fpa->concepts, "BORE_P0"); REQUIRE (status, "missing ");
+
+    // the true rotation of the instrument is POSANGLE - POS_ZERO
+    double PosZero = PM_RAD_DEG * psMetadataLookupF32(&status, file->fpa->concepts, "POS_ZERO"); REQUIRE (status, "missing POS_ZERO");
+    char *refChip  = psMetadataLookupStr(&status, file->fpa->concepts, "REF_CHIP"); REQUIRE (status, "missing REF_CHIP");
+
+    int rotatorParity = psMetadataLookupS32(&status, file->fpa->concepts, "ROT_PARITY"); 
+    // REQUIRE (status, "missing ROT_PARITY");
+    if (!status) {
+      rotatorParity = +1;
+      psLogMsg ("psModules.astrom", 4, "setting default ROT_PARITY of +1\n");
+    }
+    
+    // EAM 20200208 note: the toTPA versions of the transformation are more fundamental (the are used for the WCS conversion)
+    // we should rotate the toTPA transformation and not the fromFPA.  I believe the sign is reversed, so test this
+
+    // XXX we've swapped the sign and parity of POS (add to model)
+    // apply true posangle = -(POS - POS_ZERO)
+    psLogMsg ("psModules.astrom", 4, "Position Angle: %f, Model Position Angle Zero Point: %f\n", POS, PosZero);
+
+    psPlaneTransform *toTPA = psPlaneTransformRotate (NULL, file->fpa->toTPA, rotatorParity * (POS - PosZero));
+    psFree (file->fpa->toTPA);
+    file->fpa->toTPA = toTPA;
+
+    // the inverse transformation requires a higher order model to fit the direct transformation sufficiently well
+    psRegion *region = pmAstromFPAExtent (file->fpa);
+
+    psFree (file->fpa->fromTPA);
+    file->fpa->fromTPA = psPlaneTransformInvert(NULL, file->fpa->toTPA, *region, 50, ExtraOrders);
+
+    psFree (region);
+
+    // current position of the nominal boresite in refChip coordinates
+    double X = Xo + RX*cos(POS - To)*cos(Po) + RY*sin(POS - To)*sin(Po);
+    double Y = Yo + RY*sin(POS - To)*cos(Po) - RX*cos(POS - To)*sin(Po);
+    psLogMsg ("psModules.astrom", 4, "Boresite coords on reference chip: %f, %f pix = %f, %f sky\n", X, Y, PM_DEG_RAD*RA, PM_DEG_RAD*DEC);
+
+    // get reference chip from name
+    pmChip *chip = pmConceptsChipFromName (file->fpa, refChip);
+    if (!chip) psAbort ("invalid chip name for reference");
+
+    psPlane *boreCH = psPlaneAlloc();
+    psPlane *boreFP = psPlaneAlloc();
+    psPlane *boreTP = psPlaneAlloc();
+    psSphere *boreSky = psSphereAlloc();
+
+    // find the FP coord of the reported boresite location
+    boreCH->x = X;
+    boreCH->y = Y;
+    psPlaneTransformApply (boreFP, chip->toFPA, boreCH);
+
+    // find the true RA,DEC coord of the mirror of the reported boresite FP location
+    boreFP->x = -boreFP->x;
+    boreFP->y = -boreFP->y;
+    psPlaneTransformApply (boreTP, file->fpa->toTPA, boreFP);
+    psDeproject (boreSky, boreTP, file->fpa->toSky);
+
+    // modify the projection to account for offset between true and reported boresite
+    file->fpa->toSky->R = boreSky->r;
+    file->fpa->toSky->D = boreSky->d;
+
+    psTrace ("psModules.astrom", 5, "actual boresite coordinates: %lf, %lf\n", file->fpa->toSky->R*PS_DEG_RAD, file->fpa->toSky->D*PS_DEG_RAD);
+    psTrace ("psModules.astrom", 5, "plate scale used: %lf, %lf\n", file->fpa->toSky->Xs*PS_DEG_RAD*3600.0, file->fpa->toSky->Ys*PS_DEG_RAD*3600.0);
+
+    psFree (boreCH);
+    psFree (boreFP);
+    psFree (boreTP);
+    psFree (boreSky);
+
+    return (true);
+}
+
Index: /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryModel.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryModel.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryModel.h	(revision 42651)
@@ -0,0 +1,40 @@
+/* @file  pmAstrometryModel.h
+ * @brief Astrometry model I/O functions
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-07-17 22:38:15 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_ASTROMETRY_MODEL_H
+#define PM_ASTROMETRY_MODEL_H
+
+/// @addtogroup Astrometry
+/// @{
+
+bool pmAstromModelCheckDataStatusForView (const pmFPAview *view, pmFPAfile *file);
+bool pmAstromModelCheckDataStatusForFPA (const pmFPA *fpa);
+bool pmAstromModelCheckDataStatusForChip (const pmChip *chip);
+
+bool pmAstromModelWriteForView (const pmFPAview *view, pmFPAfile *file, pmConfig *config);
+bool pmAstromModelWriteFPA (pmFPAfile *file, const pmFPA *fpa);
+bool pmAstromModelWritePHU (pmFPAfile *file, const pmFPA *fpa);
+bool pmAstromModelWriteSky (pmFPAfile *file);
+bool pmAstromModelWriteTP (pmFPAfile *file);
+bool pmAstromModelWriteFP (pmFPAfile *file);
+bool pmAstromModelWriteChips (pmFPAfile *file);
+
+bool pmAstromModelReadForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmAstromModelReadFPA (pmFPAfile *file);
+bool pmAstromModelReadPHU (pmFPAfile *file);
+bool pmAstromModelReadChips (pmFPAfile *file);
+bool pmAstromModelReadFP (pmFPAfile *file);
+bool pmAstromModelReadTP (pmFPAfile *file);
+bool pmAstromModelReadSky (pmFPAfile *file);
+
+bool pmAstromModelSetTP (pmFPAfile *file, psMetadata *concepts);
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryObjects.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryObjects.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryObjects.c	(revision 42651)
@@ -0,0 +1,1476 @@
+/** @file  pmAstrometryObjects.c
+*
+*  @brief This file defines the basic types for matching objects
+*  based on their astrometry.
+*
+*  @ingroup AstroImage
+*
+*  @author EAM, IfA
+*
+*  @version $Revision: 1.45 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2009-02-09 21:25:20 $
+*
+*  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
+*/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+
+/******************************************************************************/
+/*  INCLUDE FILES                                                             */
+/******************************************************************************/
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <unistd.h>   // for unlink
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAExtent.h"
+#include "pmFPAfile.h"
+#include "pmAstrometryObjects.h"
+#include "pmKapaPlots.h"
+#include "pmAstrometryVisual.h"
+
+// XXX this is defined in pmPSFtry.h, which makes no sense
+float psVectorSystematicError (psVector *residuals, psVector *errors, float clipFraction);
+float pmAstrom2DSystematics (psVector *xPos, psVector *yPos, psVector *value);
+float pmAstromSubsetSystematics (psVector *value);
+
+#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);
+    psVector *found1 = psVectorAlloc(x1->n, PS_TYPE_S8);
+    psVector *found2 = psVectorAlloc(x2->n, PS_TYPE_S8);
+
+    const double RADIUS_SQR = PS_SQR(RADIUS);
+    double dX, dY, dR;
+
+    psVectorInit (found1, 0);
+    psVectorInit (found2, 0);
+
+    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;
+        }
+
+        if (found1->data.S8[i]) {
+            i++;
+            continue;
+        }
+        if (found2->data.S8[j]) {
+            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;
+            }
+            if (found2->data.S8[j]) {
+                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);
+
+            found1->data.S8[i] = 1;
+            found2->data.S8[j] = 1;
+
+            j++;
+        }
+        j = jStart;
+        i++;
+    }
+    psFree (found1);
+    psFree (found2);
+
+    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(sorted1); \
+    psFree(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,
+    const psMetadata *config)
+{
+    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);
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    // sigma of gaussian window to down-weight photometric outliers (ignored if NAN or 0.0)
+    bool status;
+    double photomWindowSigma  = psMetadataLookupF32 (&status, config, "PSASTRO.PHOTOM.WINDOW.SIGMA");
+    bool   photomWindowApply  = !(isnan(photomWindowSigma) || (fabs(photomWindowSigma) < 0.01));
+    double photomWindowFactor = photomWindowApply ? -0.5/PS_SQR(photomWindowSigma) : 0.0;
+
+    // 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 is used as an error (sqrt(variance)) in the fit. the 1.01 prevents the weight from going to 0.0 for perfect matches
+        wt->data.F32[i] = 1.01 - exp(photomWindowFactor*PS_SQR(refStar->magCal - rawStar->magCal)); 
+    }
+
+    // constant errors
+    psVector *mask = psVectorAlloc (match->n, PS_TYPE_VECTOR_MASK);
+    psVectorInit (mask, 0);
+
+    // the stats options supplied are used to perform the clip fitting
+    pmAstromFitResults *results = pmAstromFitResultsAlloc();
+    results->xStats = psStatsAlloc (PS_STAT_NONE);
+    results->yStats = psStatsAlloc (PS_STAT_NONE);
+    *results->xStats = *stats;
+    *results->yStats = *stats;
+
+    int nIter = stats->clipIter;
+
+    results->xStats->clipIter = 1;
+    results->yStats->clipIter = 1;
+
+    // fit chip-to-FPA transformation
+    // we run 'clipIter' cycles clipping in each of x and y, with only one iteration each
+    // need to use the stats lookups functions to get the width and center
+    for (int i = 0; i < nIter; i++) {
+        if (!psVectorClipFitPolynomial2D (map->x, results->xStats, mask, 0xff, x, wt, X, Y)) {
+            // psError(PS_ERR_UNKNOWN, false, "failure in clip-fitting for x\n");
+            psLogMsg("psModule.astrom", 4, "failure in clip-fitting for x\n");
+            psFree (x);
+            psFree (y);
+            psFree (X);
+            psFree (Y);
+            psFree (wt);
+            psFree (mask);
+
+            return results;
+        }
+        // psTrace ("psModules.astrom", 3, "x resid: %f +/- %f (%ld of %ld)\n", results->xStats->clippedMean, results->xStats->clippedStdev, results->xStats->clippedNvalues, x->n);
+        psTrace ("psModules.astrom", 3, "x resid: %f +/- %f (%ld of %ld)\n", results->xStats->robustMedian, results->xStats->robustStdev, results->xStats->clippedNvalues, x->n);
+
+        if (!psVectorClipFitPolynomial2D (map->y, results->yStats, mask, 0xff, y, wt, X, Y)) {
+            // psError(PS_ERR_UNKNOWN, false, "failure in clip-fitting for y\n");
+            psLogMsg("psModule.astrom", 4, "failure in clip-fitting for y\n");
+            psFree (x);
+            psFree (y);
+            psFree (X);
+            psFree (Y);
+            psFree (wt);
+            psFree (mask);
+
+            return results;
+        }
+        // psTrace ("psModules.astrom", 3, "y resid: %f +/- %f (%ld of %ld)\n", results->yStats->clippedMean, results->yStats->clippedStdev, results->yStats->clippedNvalues, y->n);
+        psTrace ("psModules.astrom", 3, "y resid: %f +/- %f (%ld of %ld)\n", results->yStats->robustMedian, results->yStats->robustStdev, results->yStats->clippedNvalues, y->n);
+    }
+    results->xStats->clipIter = stats->clipIter;
+    results->yStats->clipIter = stats->clipIter;
+
+    // *** calculate the 90%-ile and the systematic scatter for each direction.
+
+    // generate the X residual vector
+    psVector *xFit = psPolynomial2DEvalVector (map->x, X, Y);
+    if (!xFit) abort();
+    psVector *xRes = (psVector *) psBinaryOp (NULL, x, "-", xFit);
+    if (!xRes) abort();
+    psFree (xFit);
+
+    psVector *yFit = psPolynomial2DEvalVector (map->y, X, Y);
+    if (!yFit) abort();
+    psVector *yRes = (psVector *) psBinaryOp (NULL, y, "-", yFit);
+    if (!yRes) abort();
+    psFree (yFit);
+
+    // extract a high-quality subset (unmasked, S/N > XXX) and position errors
+    // XXX for now, generate a position error based on the magnitude error
+    psVector *xErr     = psVectorAllocEmpty (match->n, PS_TYPE_F32);
+    psVector *yErr     = psVectorAllocEmpty (match->n, PS_TYPE_F32);
+    psVector *xResGood = psVectorAllocEmpty (match->n, PS_TYPE_F32);
+    psVector *yResGood = psVectorAllocEmpty (match->n, PS_TYPE_F32);
+
+    // we measure the stdev of the median residual in NxN bins.
+    // use only valid (not NAN) measurements
+    psVector *xPosValid = psVectorAllocEmpty (match->n, PS_TYPE_F32);
+    psVector *yPosValid = psVectorAllocEmpty (match->n, PS_TYPE_F32);
+    psVector *xResValid = psVectorAllocEmpty (match->n, PS_TYPE_F32);
+    psVector *yResValid = psVectorAllocEmpty (match->n, PS_TYPE_F32);
+
+    for (int i = 0; i < match->n; i++) {
+        if (mask->data.PS_TYPE_VECTOR_MASK_DATA[i]) continue;
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *rawStar = raw->data[pair->raw];
+        if (!isfinite(rawStar->dMag)) continue;
+
+	bool isValid = true;
+	isValid = isValid & isfinite (x->data.F32[i]);
+	isValid = isValid & isfinite (y->data.F32[i]);
+	isValid = isValid & isfinite (xRes->data.F32[i]);
+	isValid = isValid & isfinite (yRes->data.F32[i]);
+	if (isValid) {
+	  psVectorAppend (xPosValid, x->data.F32[i]);
+	  psVectorAppend (yPosValid, y->data.F32[i]);
+	  psVectorAppend (xResValid, xRes->data.F32[i]);
+	  psVectorAppend (yResValid, yRes->data.F32[i]);
+	}
+
+	// for the systematic error, use only high S/N stars
+        if (rawStar->dMag > 0.02) continue;
+
+        // two likely failure values: NAN or 0.0 --> use dMag in this case
+        float xErrValue, yErrValue;
+        if (isfinite(rawStar->chip->xErr) && (rawStar->chip->xErr > 0.0)) {
+            xErrValue = rawStar->chip->xErr;
+        } else {
+            xErrValue = PS_MAX(0.005, rawStar->dMag);
+        }
+        if (isfinite(rawStar->chip->yErr) && (rawStar->chip->yErr > 0.0)) {
+            yErrValue = rawStar->chip->yErr;
+        } else {
+            yErrValue = PS_MAX(0.005, rawStar->dMag);
+        }
+
+        psVectorAppend (xErr,     xErrValue);
+        psVectorAppend (yErr,     yErrValue);
+        psVectorAppend (xResGood, xRes->data.F32[i]);
+        psVectorAppend (yResGood, yRes->data.F32[i]);
+    }
+
+    // if xResGood or yResGood have no valid data, set these to NAN 
+    if ((xResGood->n == 0) || (yResGood->n == 0)) {
+      results->dXsys   = results->dYsys   = NAN;
+      results->dXrange = results->dYrange = NAN;
+      results->dXstdev = results->dYstdev = NAN;
+    } else {
+      results->dXsys = psVectorSystematicError (xResGood, xErr, 0.05);
+      results->dYsys = psVectorSystematicError (yResGood, yErr, 0.05);
+
+      results->dXrange = pmAstromVectorRange (xResGood, 0.1, 0.9, results->xStats->clippedStdev);
+      results->dYrange = pmAstromVectorRange (yResGood, 0.1, 0.9, results->yStats->clippedStdev);
+
+      results->dXstdev = pmAstrom2DSystematics (xPosValid, yPosValid, xResValid);
+      results->dYstdev = pmAstrom2DSystematics (xPosValid, yPosValid, yResValid);
+    }
+
+    psTrace ("psModules.astrom", 3, "dXsys: %f, dXrange: %f, dXstdev: %f\n", results->dXsys, results->dXrange, results->dXstdev);
+    psTrace ("psModules.astrom", 3, "dYsys: %f, dYrange: %f, dYstdev: %f\n", results->dYsys, results->dYrange, results->dYstdev);
+
+    psFree (xErr);
+    psFree (yErr);
+    psFree (xRes);
+    psFree (yRes);
+    psFree (xResGood);
+    psFree (yResGood);
+    psFree (xPosValid);
+    psFree (yPosValid);
+    psFree (xResValid);
+    psFree (yResValid);
+
+    psFree (x);
+    psFree (y);
+    psFree (X);
+    psFree (Y);
+    psFree (wt);
+    psFree (mask);
+
+    return (results);
+}
+
+# define VAL_COUNT 9.0
+# define MIN_COUNT 5.0
+
+float pmAstrom2DSystematics (psVector *xPos, psVector *yPos, psVector *value) {
+
+    // pre-filter the values to ensure no NANs, other invalid
+
+    // if we do not have enough measurements (< 25), use pmAstromSubsetSystematics instead (OK to 9 values)
+    if (xPos->n < MIN_COUNT*2*2) {
+      float result = pmAstromSubsetSystematics (value);
+      return result;
+    }
+
+    // generate a grid covering the full range of x,y and measure the median value in each
+    // grid cell.  calculate the stdev of those median values.
+
+    // VAL_COUNT (9) is the (min) goal density, but a single cell may have only MIN_COUNT (7)
+    // we have N points.  require a min of 9 pts per cell (configurable?).  grid is square.
+    // Ncell*Ncell*9 = Npts, Ncell = MIN(sqrt(Npts/9), 5)
+
+    int Ncell = PS_MIN(sqrt((float)(xPos->n / VAL_COUNT)), 2);  // have to at least have a 2x2 grid
+
+    // find the range of x,y values
+    float xMin = 1e9, xMax = -1e9, yMin = 1e9, yMax = 1e9;
+    for (int i = 0; i < xPos->n; i++) {
+	xMin = PS_MIN(xPos->data.F32[i],xMin);
+	xMax = PS_MAX(xPos->data.F32[i],xMax);
+	yMin = PS_MIN(yPos->data.F32[i],yMin);
+	yMax = PS_MAX(yPos->data.F32[i],yMax);
+    }
+
+    float xStep = Ncell / (xMax - xMin);
+    float yStep = Ncell / (yMax - yMin);
+  
+    if (isnan(xStep)) {
+      return NAN;
+    }
+    if (isnan(yStep)) {
+      return NAN;
+    }
+
+    psArray *xBin = psArrayAlloc (Ncell);
+    for (int ix = 0; ix < Ncell; ix++) {
+	psArray *yBin = psArrayAlloc (Ncell); 
+	xBin->data[ix] = yBin;
+	for (int iy = 0; iy < Ncell; iy++) {
+	    yBin->data[iy] = psVectorAllocEmpty(128, PS_TYPE_F32);
+	}
+    }    
+
+    // xValue = ix/xStep + xMin -> ix = (xValue - xMin) * xStep;
+
+    for (int i = 0; i < xPos->n; i++) {
+	int ix = PS_MIN(PS_MAX(0, (xPos->data.F32[i] - xMin) * xStep), Ncell - 1);
+	int iy = PS_MIN(PS_MAX(0, (yPos->data.F32[i] - yMin) * yStep), Ncell - 1);
+    
+	psArray *yBin = xBin->data[ix];
+	psVector *Bin = yBin->data[iy];
+
+	psVectorAppend(Bin, value->data.F32[i]);
+    }
+
+    psVector *sample = psVectorAllocEmpty (Ncell*Ncell, PS_TYPE_F32);
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN);
+
+    // calculate the median for each vector and save on a vector
+    for (int ix = 0; ix < Ncell; ix++) {
+	psArray *yBin = xBin->data[ix];
+	for (int iy = 0; iy < Ncell; iy++) {
+	    psVector *Bin = yBin->data[iy];
+	    if (Bin->n < MIN_COUNT) { continue; }
+
+	    // psVectorStats resets stats so we can call this repeatedly
+	    psVectorStats (stats, Bin, NULL, NULL, 0); 
+	    if (isfinite(stats->sampleMedian)) {
+		psVectorAppend (sample, stats->sampleMedian);
+	    }
+	}
+    }    
+  
+    stats->options = PS_STAT_SAMPLE_STDEV;
+    psVectorStats (stats, sample, NULL, NULL, 0); 
+    float result = stats->sampleStdev;
+
+    if (!isfinite(stats->sampleStdev)) {
+      fprintf (stderr, "*** bad solution ***\n");
+    }
+
+    // NOTE: the elements of xBin are freed automatically, which extends down to the vectors
+    psFree (xBin); 
+    psFree (stats);
+    psFree (sample);
+  
+    return result;
+}
+
+# define SUBSET_NSAMPLE 3
+// last-ditch attempt to assess the systematic error in the data.  Just split into 3 bins,
+// calculate median in each, and calculate stdev of the subset.  The minimum number of
+// values needed to make this measurement in any sensible way : 3*3 = 6.
+float pmAstromSubsetSystematics (psVector *value) {
+
+    // give up if too few stars:
+    if (value->n < 3*SUBSET_NSAMPLE) return NAN;
+
+    psVector *sample = psVectorAlloc (SUBSET_NSAMPLE, PS_TYPE_F32);
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN);
+
+    // note that this drops the last 1 or 2 values if value->n % 3 = 1 or 2
+    int nSubset = value->n / SUBSET_NSAMPLE;
+    psVector *subset = psVectorAlloc (nSubset, PS_TYPE_F32);
+    for (int iter = 0; iter < SUBSET_NSAMPLE; iter++) {
+	for (int i = 0; i < nSubset; i++) {
+	    subset->data.F32[i] = value->data.F32[i + nSubset*iter];
+	}  
+
+	// psVectorStats resets stats so we can call this repeatedly
+	psVectorStats (stats, subset, NULL, NULL, 0); 
+	if (isfinite(stats->sampleMedian)) {
+	    sample->data.F32[iter] = stats->sampleMedian;
+	}
+    }
+
+    stats->options = PS_STAT_SAMPLE_STDEV;
+    psVectorStats (stats, sample, NULL, NULL, 0); 
+    float result = stats->sampleStdev;
+
+    if (!isfinite(stats->sampleStdev)) {
+      fprintf (stderr, "*** bad solution (2) ***\n");
+    }
+
+    psFree (stats);
+    psFree (sample);
+    psFree (subset); 
+
+    return result;
+}
+
+// set the bin closest to the corresponding value.  if USE_END is +/- 1,
+// out-of-range saturates on lower/upper bin REGARDLESS of actual value
+#define PS_BIN_FOR_VALUE(RESULT, VECTOR, VALUE, USE_END) { \
+        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: \
+            psTrace("psModules.astrom", 6, "selected bin outside range"); \
+            if (USE_END == -1) { RESULT = 0; } \
+            if (USE_END == +1) { RESULT = VECTOR->n - 1; } \
+            break; \
+          case PS_BINARY_DISECT_INVALID_INPUT: \
+          case PS_BINARY_DISECT_INVALID_TYPE: \
+            psAbort ("programming error"); \
+            break; \
+        } }
+
+# define PS_BIN_INTERPOLATE(RESULT, VECTOR, BOUNDS, BIN, VALUE) { \
+        float dX, dY, Xo, Yo, Xt; \
+        if (BIN == BOUNDS->n - 1) { \
+            dX = 0.5*(BOUNDS->data.F32[BIN+1] - BOUNDS->data.F32[BIN-1]); \
+            dY = VECTOR->data.F32[BIN] - VECTOR->data.F32[BIN-1]; \
+            Xo = 0.5*(BOUNDS->data.F32[BIN+1] + BOUNDS->data.F32[BIN]); \
+            Yo = VECTOR->data.F32[BIN]; \
+        } else { \
+            dX = 0.5*(BOUNDS->data.F32[BIN+2] - BOUNDS->data.F32[BIN]); \
+            dY = VECTOR->data.F32[BIN+1] - VECTOR->data.F32[BIN]; \
+            Xo = 0.5*(BOUNDS->data.F32[BIN+1] + BOUNDS->data.F32[BIN]); \
+            Yo = VECTOR->data.F32[BIN]; \
+        } \
+        if (dY != 0) { \
+            Xt = (VALUE - Yo)*dX/dY + Xo; \
+        } else { \
+            Xt = Xo; \
+        } \
+        Xt = PS_MIN (BOUNDS->data.F32[BIN+1], PS_MAX(BOUNDS->data.F32[BIN], Xt)); \
+        psTrace("psModules.astrom", 6, "(Xo, Yo, dX, dY, Xt, Yt) is (%.2f %.2f %.2f %.2f %.2f %.2f)\n", \
+                Xo, Yo, dX, dY, Xt, VALUE); \
+        RESULT = Xt; }
+
+float pmAstromVectorRange (psVector *myVector, float minFrac, float maxFrac, float stdevGuess) {
+
+    psStats *stats = psStatsAlloc(PS_STAT_MIN | PS_STAT_MAX); // Statistics for min and max
+    psHistogram *histogram = NULL;      // Histogram of the data
+    psHistogram *cumulative = NULL;     // Cumulative histogram of the data
+    float min = NAN, max = NAN;         // Mimimum and maximum values
+
+    // Get the minimum and maximum values
+    // XXX either clear the associated error, or disallow in psVectorStats
+    if (!psVectorStats(stats, myVector, NULL, NULL, 0)) {
+        psFree(stats);
+        return NAN;
+    }
+    min = stats->min;
+    max = stats->max;
+    if (isnan(min) || isnan(max)) {
+        psFree(stats);
+        return NAN;
+    }
+
+    psTrace("psModules.astrom", 5, "Data min/max is (%.2f, %.2f)\n", min, max);
+
+    // If all data points have the same value, then we set the appropriate members of stats and return.
+    if (fabs(max - min) <= FLT_EPSILON) {
+        psFree (stats);
+        return 0.0;
+    }
+
+    // Define the histogram bin size.
+    float binSize = 0.001;
+    long numBins = PS_MAX(PS_MIN(100000, (max - min) / binSize), 2); // Number of bins
+    psTrace("psModules.astrom", 5, "Numbins is %ld\n", numBins);
+    psTrace("psModules.astrom", 5, "Creating a robust histogram from data range (%.2f - %.2f)\n", min, max);
+
+    // allocate the histogram containers
+    histogram = psHistogramAlloc(min, max, numBins);
+    cumulative = psHistogramAlloc(min, max, numBins);
+
+    if (!psVectorHistogram(histogram, myVector, NULL, NULL, 0)) {
+        // if psVectorHistogram returns false, we have a programming error
+        psAbort ("Unable to generate histogram");
+    }
+    if (psTraceGetLevel("psModules.astrom") >= 8) {
+        PS_VECTOR_PRINT_F32(histogram->bounds);
+        PS_VECTOR_PRINT_F32(histogram->nums);
+    }
+
+    // Convert the specific histogram to a cumulative histogram
+    // The cumulative histogram data points correspond to the UPPER bound value (N < Bin[i+1])
+    cumulative->nums->data.F32[0] = histogram->nums->data.F32[0];
+    for (long i = 1; i < histogram->nums->n; i++) {
+        cumulative->nums->data.F32[i] = cumulative->nums->data.F32[i-1] + histogram->nums->data.F32[i];
+        cumulative->bounds->data.F32[i-1] = histogram->bounds->data.F32[i];
+    }
+    if (psTraceGetLevel("psModules.astrom") >= 8) {
+        PS_VECTOR_PRINT_F32(cumulative->bounds);
+        PS_VECTOR_PRINT_F32(cumulative->nums);
+    }
+
+    // Find the bin which contains the first data point above the limit
+    long totalDataPoints = cumulative->nums->data.F32[numBins - 1];
+    psTrace("psModules.astrom", 6, "Total data points is %ld\n", totalDataPoints);
+
+    // find bin which is the lower bound of the limit value (value[bin] < f < value[bin+1]
+    long binMin;
+    PS_BIN_FOR_VALUE(binMin, cumulative->nums, minFrac * totalDataPoints, 0);
+    psTrace("psModules.astrom", 6, "The bin is %ld (%.4f to %.4f)\n", binMin, cumulative->bounds->data.F32[binMin], cumulative->bounds->data.F32[binMin+1]);
+
+    // Linear interpolation to the limit value in bin units
+    float valueMin;
+    PS_BIN_INTERPOLATE (valueMin, cumulative->nums, cumulative->bounds, binMin, totalDataPoints * minFrac);
+    psTrace("psModules.astrom", 6, "limit value is %f\n", valueMin);
+
+    // find bin which is the lower bound of the limit value (value[bin] < f < value[bin+1]
+    long binMax;
+    PS_BIN_FOR_VALUE(binMax, cumulative->nums, maxFrac * totalDataPoints, 0);
+    psTrace("psModules.astrom", 6, "The bin is %ld (%.4f to %.4f)\n", binMax, cumulative->bounds->data.F32[binMax], cumulative->bounds->data.F32[binMax+1]);
+
+    // Linear interpolation to the limit value in bin units
+    float valueMax;
+    PS_BIN_INTERPOLATE (valueMax, cumulative->nums, cumulative->bounds, binMax, totalDataPoints * maxFrac);
+    psTrace("psModules.astrom", 6, "limit value is %f\n", valueMax);
+
+    // Clean up
+    psFree(histogram);
+    psFree(cumulative);
+    psFree(stats);
+
+    return (valueMax - valueMin);
+}
+
+/******************************************************************************
+pmAstromRotateObj(old, center, angle, angle): rotate & scale the focal-plane coordinates
+about the center coordinate angle specified in radians
+ ******************************************************************************/
+psArray *pmAstromRotateObj(
+    const psArray *old,
+    psPlane center,
+    double angle,
+    double scale)
+{
+    PS_ASSERT_PTR_NON_NULL(old, NULL);
+
+    double X, Y;
+    pmAstromObj *newObj;
+    const pmAstromObj *oldObj;
+
+    psArray *new = psArrayAlloc (old->n);
+    double cs = scale*cos(angle);
+    double sn = scale*sin(angle);
+    double xCenter = center.x;
+    double yCenter = center.y;
+
+    for (int i = 0; i < old->n; i++) {
+
+        oldObj = (pmAstromObj *)old->data[i];
+        newObj = pmAstromObjCopy (oldObj);
+
+        X = oldObj->FP->x - xCenter;
+        Y = oldObj->FP->y - yCenter;
+
+        newObj->FP->x = X*cs + Y*sn + xCenter;
+        newObj->FP->y = Y*cs - X*sn + yCenter;
+
+        new->data[i] = newObj;
+    }
+    return (new);
+}
+
+/******************************************************************************
+pmAstromStatsFree(stats)
+ ******************************************************************************/
+static void pmAstromStatsFree(pmAstromStats *stats)
+{
+    if (stats == NULL)
+        return;
+    return;
+}
+
+/******************************************************************************
+pmAstromStatsAlloc()
+ ******************************************************************************/
+pmAstromStats *pmAstromStatsAlloc(void)
+{
+    pmAstromStats *stats = psAlloc (sizeof(pmAstromStats));
+    psMemSetDeallocator (stats, (psFreeFunc)pmAstromStatsFree);
+
+    stats->center.x    = 0;
+    stats->center.y    = 0;
+    stats->center.xErr = 0;
+    stats->center.yErr = 0;
+
+    stats->offset.x    = 0;
+    stats->offset.y    = 0;
+    stats->offset.xErr = 0;
+    stats->offset.yErr = 0;
+
+    stats->angle       = 0.0;
+    stats->scale       = 1.0;
+    stats->minMetric   = 0.0;
+    stats->minVar      = 0.0;
+    stats->nMatch      = 0;
+    stats->nTest       = 0;
+    stats->nSigma      = 0;
+
+    return (stats);
+}
+
+/******************************************************************************
+pmAstromFitResultsFree(stats)
+ ******************************************************************************/
+static void pmAstromFitResultsFree(pmAstromFitResults *results)
+{
+    if (results == NULL)
+        return;
+    psFree (results->xStats);
+    psFree (results->yStats);
+    return;
+}
+
+/******************************************************************************
+pmAstromFitResultsAlloc()
+ ******************************************************************************/
+pmAstromFitResults *pmAstromFitResultsAlloc(void)
+{
+    pmAstromFitResults *results = psAlloc (sizeof(pmAstromFitResults));
+    psMemSetDeallocator (results, (psFreeFunc)pmAstromFitResultsFree);
+
+    results->xStats    = NULL;
+    results->yStats    = NULL;
+    results->nMatch    = 0;
+    results->nSigma    = 0;
+
+    return (results);
+}
+
+/******************************************************************************
+astromObjFree(obj)
+ ******************************************************************************/
+static void astromObjFree(pmAstromObj *obj)
+{
+    if (obj == NULL) {
+        return;
+    }
+
+    psFree(obj->pix);
+    psFree(obj->cell);
+    psFree(obj->chip);
+    psFree(obj->FP);
+    psFree(obj->TP);
+    psFree(obj->sky);
+
+    return;
+}
+
+
+/******************************************************************************
+pmAstromObjAlloc()
+ ******************************************************************************/
+pmAstromObj *pmAstromObjAlloc(void)
+{
+    pmAstromObj *obj = psAlloc (sizeof(pmAstromObj));
+    psMemSetDeallocator (obj, (psFreeFunc)astromObjFree);
+
+    obj->pix  = psPlaneAlloc();
+    obj->cell = psPlaneAlloc();
+    obj->chip = psPlaneAlloc();
+    obj->FP   = psPlaneAlloc();
+    obj->TP   = psPlaneAlloc();
+    obj->sky  = psSphereAlloc();
+    obj->Mag  = 0;
+    obj->Color= 0;
+    obj->dMag = 0;
+    obj->magCal = 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->Color =  old->Color;
+    obj->dMag  =  old->dMag;
+    obj->magCal =  old->magCal;
+
+    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");
+
+    // sigma of gaussian window to down-weight photometric outliers (ignored if NAN or 0.0)
+    double photomWindowSigma  = psMetadataLookupF32 (&status, config, "PSASTRO.PHOTOM.WINDOW.SIGMA");
+    bool   photomWindowApply  = !(isnan(photomWindowSigma) || (fabs(photomWindowSigma) < 0.01));
+    double photomWindowFactor = photomWindowApply ? -0.5/PS_SQR(photomWindowSigma) : 0.0;
+
+    // set the static scaling factors
+    nPixHalf = (int)(gridOffset / gridScale + 0.5);  // half-grid
+    nPix = 2*nPixHalf + 1;                           // full grid width
+
+    // these are globals used by p_pmAstromGridBin
+    maxOffpix = gridScale * (nPixHalf + 0.5);            // max offset from true center
+    Offset    = maxOffpix / gridScale;
+    Scale     = gridScale;
+
+    // images used as accumulators for the loop below
+    psImage *gridNP = psImageAlloc (nPix, nPix, PS_TYPE_U32);
+    psImage *gridDX = psImageAlloc (nPix, nPix, PS_TYPE_F32);
+    psImage *gridDY = psImageAlloc (nPix, nPix, PS_TYPE_F32);
+    psImage *gridD2 = psImageAlloc (nPix, nPix, PS_TYPE_F32);
+    psImageInit (gridNP, 0);
+    psImageInit (gridDX, 0);
+    psImageInit (gridDY, 0);
+    psImageInit (gridD2, 0);
+
+    // short-cut names for grid images
+    psU32 **NP = gridNP->data.U32;
+    psF32 **DX = gridDX->data.F32;
+    psF32 **DY = gridDY->data.F32;
+    psF32 **D2 = gridD2->data.F32;
+
+
+    // accumulate grids for focal plane (L,M) matches
+    for (int i = 0; i < raw->n; i++) {
+        ob1 = (pmAstromObj *)raw->data[i];
+        for (int j = 0; j < ref->n; j++) {
+            ob2 = (pmAstromObj *)ref->data[j];
+            dX = ob1->FP->x - ob2->FP->x;
+            dY = ob1->FP->y - ob2->FP->y;
+
+            // find bin coordinates for this delta-delta
+            if (!AstromGridBin (&iX, &iY, dX, dY)) {
+                continue; // matched pair is too far offset
+            }
+
+	    // XXX should I make the scale factor in front a recipe value?
+	    int Npts = 10 * exp(photomWindowFactor*PS_SQR(ob1->magCal - ob2->magCal)); 
+
+            // accumulate bin stats
+            NP[iY][iX] += Npts;
+            DX[iY][iX] += dX*Npts;
+            DY[iY][iX] += dY*Npts;
+            D2[iY][iX] += PS_SQR(dX*Npts) + PS_SQR(dY*Npts);
+        }
+    }
+
+    // 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 (psTraceGetLevel("psModules.astrom") >= 5) {
+            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");
+            if (!fgets (line, 15, stdin)) {
+                fprintf(stderr, "Error waiting for RETURN.");
+            }
+        }
+
+        // only check bins with at least 1/2 of max bin
+        // XXX requiring at least 3 matches in bin
+        int minNpts = PS_MAX (0.5*imStats->max, 5);
+        psTrace("psModule.astrom", 4, "minNpts: %d, min: %d, max: %d, median: %f, stdev: %f", minNpts, (int)(imStats->min), (int)(imStats->max), imStats->sampleMedian, imStats->sampleStdev);
+
+        // find the 'best' bin
+        for (int j = 0; j < gridNP->numRows; j++) {
+            for (int i = 0; i < gridNP->numCols; i++) {
+                if (NP[j][i] < minNpts) continue;
+
+                // this metric emphasizes a narrow peak with lots of sources over one with few.
+                var = fabs((D2[j][i]/NP[j][i]) - PS_SQR(DX[j][i]/NP[j][i]) - PS_SQR(DY[j][i]/NP[j][i]));
+                metric = var / PS_SQR(NP[j][i]) / PS_SQR(NP[j][i]);
+
+                // fprintf (stderr, "try : %f %f (%d pts, %f var, %f met)\n", DX[j][i]/NP[j][i], DY[j][i]/NP[j][i], NP[j][i], var, metric);
+
+                if (metric < minMetric) {
+                    minMetric = metric;
+                    minVar    = var;
+                    minX      = i;
+                    minY      = j;
+                }
+            }
+        }
+
+        // convert the bin to delta-delta
+        if ((minX < 0) || (minY < 0))
+        {
+            // no valid matches found
+            stats->offset.x   = 0;
+            stats->offset.y   = 0;
+            stats->minMetric  = minMetric;
+            stats->minVar     = minVar;
+            stats->nMatch     = 0;
+        } else
+        {
+            stats->offset.x  = DX[minY][minX] / NP[minY][minX];
+            stats->offset.y  = DY[minY][minX] / NP[minY][minX];
+            stats->minMetric = minMetric;
+            stats->minVar    = minVar;
+            stats->nMatch    = NP[minY][minX];
+        }
+
+        // XXX this function is crashing
+        pmAstromVisualPlotGridMatch(raw, ref, gridNP, stats->offset.x, stats->offset.y, maxOffpix, Scale, Offset);
+        pmAstromVisualPlotGridMatchOverlay(raw, ref, stats->offset);
+
+        psFree (imStats);
+        // XXX EAM : This routine, and pmAstromGridMatch, need to handle failure cases better
+    }
+
+    // sort the NP values and choose
+    psVector *listNP = psVectorAlloc (nPix*nPix, PS_TYPE_U32);
+    int n = 0;
+    for (int i = 0; i < nPix; i++) {
+        for (int j = 0; j < nPix; j++) {
+            listNP->data.U32[n] = gridNP->data.U32[j][i];
+            n++;
+        }
+    }
+    psVector *sort = psVectorSort (NULL, listNP);
+    stats->nTest = sort->data.U32[sort->n - 5];
+    stats->nSigma = (stats->nMatch - stats->nTest) / sqrt(stats->nTest);
+    // XXX this needs a better analysis of the image histogram..
+    // fprintf (stderr, "sigma: nMatch: %d, nTest: %d, nTen: %d\n", stats->nMatch, stats->nTest, sort->data.U32[sort->n - 10]);
+
+    psFree (sort);
+    psFree (listNP);
+    psFree (gridNP);
+    psFree (gridDX);
+    psFree (gridDY);
+    psFree (gridD2);
+    return (stats);
+}
+
+
+
+/******************************************************************************
+pmAstromGridMatch(*raw, *ref, *config): match two star lists.
+ ******************************************************************************/
+
+pmAstromStats *pmAstromGridMatch(const psArray *raw,
+                                 const psArray *ref,
+                                 const psMetadata *config)
+{
+    PS_ASSERT_PTR_NON_NULL(raw, NULL);
+    PS_ASSERT_PTR_NON_NULL(ref, NULL);
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    bool status;
+    double xMin, xMax, yMin, yMax;
+    const pmAstromObj *obj;
+    psArray *rot;
+
+    pmAstromStats *minStat = pmAstromStatsAlloc ();
+    pmAstromStats *newStat = NULL;
+
+    psPlane center;
+
+    // find center of the raw field (focal-plane coords)
+    xMin = yMin = +1e10;
+    xMax = yMax = -1e10;
+    for (int i = 0; i < raw->n; i++) {
+        obj = (pmAstromObj *)raw->data[i];
+        xMin = PS_MIN (obj->FP->x, xMin);
+        xMax = PS_MAX (obj->FP->x, xMax);
+        yMin = PS_MIN (obj->FP->y, yMin);
+        yMax = PS_MAX (obj->FP->y, yMax);
+    }
+    center.x = 0.5*(xMin + xMax);
+    center.y = 0.5*(yMin + yMax);
+
+    double minScale = psMetadataLookupF32 (&status, config, "PSASTRO.GRID.MIN.SCALE");
+    double maxScale = psMetadataLookupF32 (&status, config, "PSASTRO.GRID.MAX.SCALE");
+    double delScale = psMetadataLookupF32 (&status, config, "PSASTRO.GRID.DEL.SCALE");
+
+    double minAngle = PS_RAD_DEG*psMetadataLookupF32 (&status, config, "PSASTRO.GRID.MIN.ANGLE");
+    double maxAngle = PS_RAD_DEG*psMetadataLookupF32 (&status, config, "PSASTRO.GRID.MAX.ANGLE");
+    double delAngle = PS_RAD_DEG*psMetadataLookupF32 (&status, config, "PSASTRO.GRID.DEL.ANGLE");
+    double minSigma = psMetadataLookupF32 (&status, config, "PSASTRO.GRID.MIN.SIGMA");
+
+    minStat->minMetric = 1e10;
+    for (double scale = minScale; scale <= maxScale; scale += delScale) {
+        for (double angle = minAngle; angle <= maxAngle; angle += delAngle) {
+            rot = pmAstromRotateObj (raw, center, angle, scale);
+
+# if 0
+            FILE *f1 = fopen ("raw.dat", "w");
+            for (int i = 0; i < rot->n; i++) {
+                pmAstromObj *obj = rot->data[i];
+                fprintf (f1, "%8.2f %8.2f   %6.2f\n", obj->FP->x, obj->FP->y, obj->Mag);
+            }
+            fclose (f1);
+            FILE *f2 = fopen ("ref.dat", "w");
+            for (int i = 0; i < ref->n; i++) {
+                pmAstromObj *obj = ref->data[i];
+                fprintf (f2, "%8.2f %8.2f   %6.2f\n", obj->FP->x, obj->FP->y, obj->Mag);
+            }
+            fclose (f2);
+            fprintf (stderr, "type return");
+            char c;
+            fscanf (stdin, "%c", &c);
+# endif
+            newStat = pmAstromGridAngle (rot, ref, config);
+            newStat->angle  = angle;
+            newStat->scale  = scale;
+            newStat->center = center;
+
+            if (isfinite(newStat->minMetric) && (newStat->minMetric > 0.0) && (newStat->nSigma >= minSigma) && (newStat->minMetric < minStat->minMetric)) {
+                *minStat = *newStat;
+                psLogMsg ("psModule.astrom", 4, "grid test - offset: %7.2f,%7.2f @ %6.1f deg x %7.3f (%4d pts, %5.1f sig, %5.1f var, %6.3f log metric) *",
+                          minStat->offset.x, minStat->offset.y, PS_DEG_RAD*minStat->angle, minStat->scale, minStat->nMatch, minStat->nSigma, minStat->minVar, log10(minStat->minMetric));
+            } else {
+                psLogMsg ("psModule.astrom", 4, "grid test - offset: %7.2f,%7.2f @ %6.1f deg x %7.3f (%4d pts, %5.1f sig, %5.1f var, %6.3f log metric)",
+                          newStat->offset.x, newStat->offset.y, PS_DEG_RAD*newStat->angle, newStat->scale, newStat->nMatch, newStat->nSigma, newStat->minVar, log10(newStat->minMetric));
+
+            }
+            psFree (newStat);
+            psFree (rot);
+        }
+    }
+    psLogMsg ("psModule.astrom.grid.match", 4, "grid best - offset: %7.2f,%7.2f @ %6.1f deg x %7.3f (%4d pts, %5.1f sig, %5.1f var, %6.3f log metric)",
+              minStat->offset.x, minStat->offset.y, PS_DEG_RAD*minStat->angle, minStat->scale, minStat->nMatch, minStat->nSigma, minStat->minVar, log10(minStat->minMetric));
+
+    // I need to decide if a solution is likely to be a good solution or just a mis-match
+    // one posibility: how significant is the peak relative to the 4th or 5th most significant pixel?
+
+    if (minStat->nSigma < minSigma) {
+	psLogMsg ("psModule.astrom.grid.match", 3, "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 xBin = 0, yBin = 0;
+
+    rot = pmAstromRotateObj (raw, stats->center, stats->angle, stats->scale);
+
+    // sampling scale of the grid
+    double tweakScale  = psMetadataLookupF32 (&status, recipe, "PSASTRO.TWEAK.SCALE");
+    double tweakRange  = psMetadataLookupF32 (&status, recipe, "PSASTRO.TWEAK.RANGE");
+    double tweakSmooth = psMetadataLookupF32 (&status, recipe, "PSASTRO.TWEAK.SMOOTH");
+    double tweakNsigma = psMetadataLookupF32 (&status, recipe, "PSASTRO.TWEAK.NSIGMA");
+
+    // sigma of gaussian window to down-weight photometric outliers (ignored if NAN or 0.0)
+    double photomWindowSigma  = psMetadataLookupF32 (&status, recipe, "PSASTRO.PHOTOM.WINDOW.SIGMA");
+    bool   photomWindowApply  = !(isnan(photomWindowSigma) || (fabs(photomWindowSigma) < 0.01));
+    double photomWindowFactor = photomWindowApply ? -0.5/PS_SQR(photomWindowSigma) : 0.0;
+
+    int 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;
+
+	    // XXX should I make the scale factor in front a recipe value?
+	    int Npts = 10 * exp(photomWindowFactor*PS_SQR(ob1->magCal - ob2->magCal));  // sigma = 0.22 mag
+
+            xHist->data.F32[xBin] += Npts;
+            yHist->data.F32[yBin] += Npts;
+        }
+    }
+
+    pmAstromVisualPlotTweak (xHist, yHist, xBin, yBin);
+
+    // smooth histgram vector with gaussian of 1sigma = radius
+    psVector *xHistNew = psVectorSmooth(NULL, xHist, tweakSmooth, tweakNsigma);
+    psVector *yHistNew = psVectorSmooth(NULL, yHist, tweakSmooth, tweakNsigma);
+
+    // if we failed to smooth, just use the original vector (probably too narrow a range)
+    if (!xHistNew) xHistNew = psMemIncrRefCounter (xHist);
+    if (!yHistNew) yHistNew = psMemIncrRefCounter (yHist);
+
+    // select peak in x and in y
+    xBin = yBin = 0;
+    double xMax = 0;
+    double yMax = 0;
+    for (int i = 0; i < nBin; i++) {
+        if (xHistNew->data.F32[i] > xMax) {
+            xBin = i;
+            xMax = xHistNew->data.F32[i];
+        }
+        if (yHistNew->data.F32[i] > yMax) {
+            yBin = i;
+            yMax = yHistNew->data.F32[i];
+        }
+    }
+    double xPeak = xBin*tweakScale - tweakRange;
+    double yPeak = yBin*tweakScale - tweakRange;
+    psLogMsg (__func__, 3, "tweak peak by %f,%f\n", xPeak, yPeak);
+
+    // adjust offset by peak center
+    pmAstromStats *tweak = pmAstromStatsAlloc();
+    *tweak = *stats;
+    tweak->offset.x += xPeak;
+    tweak->offset.y += yPeak;
+
+    pmAstromVisualPlotTweak (xHistNew, yHistNew, xBin, yBin);
+
+    psFree (rot);
+    psFree (xHist);
+    psFree (yHist);
+    psFree (xHistNew);
+    psFree (yHistNew);
+
+    return tweak;
+}
+
+/******************************************************************************
+pmAstromGridApply(*map, stat): apply the measured FPA offset and rotation
+(stat) to the fpa astrom structures.
+ ******************************************************************************/
+psPlaneTransform *pmAstromGridApply(
+    psPlaneTransform *map,
+    pmAstromStats *stat)
+{
+    PS_ASSERT_PTR_NON_NULL(map, NULL);
+    PS_ASSERT_POLY_NON_NULL(map->x, NULL);
+    PS_ASSERT_POLY_NON_NULL(map->y, NULL);
+
+    double cs = stat->scale * cos (stat->angle);
+    double sn = stat->scale * sin (stat->angle);
+
+    double dx = (map->x->coeff[0][0] - stat->center.x);
+    double dy = (map->y->coeff[0][0] - stat->center.y);
+
+    // new offset
+    map->x->coeff[0][0] =  cs*dx + sn*dy - stat->offset.x + stat->center.x;
+    map->y->coeff[0][0] = -sn*dx + cs*dy - stat->offset.y + stat->center.y;
+
+    // original rotation matrix
+    double pc1_1 = map->x->coeff[1][0];
+    double pc1_2 = map->x->coeff[0][1];
+    double pc2_1 = map->y->coeff[1][0];
+    double pc2_2 = map->y->coeff[0][1];
+
+    // new rotation matrix
+    map->x->coeff[1][0] = +cs*pc1_1 + sn*pc2_1;
+    map->x->coeff[0][1] = +cs*pc1_2 + sn*pc2_2;
+    map->y->coeff[1][0] = -sn*pc1_1 + cs*pc2_1;
+    map->y->coeff[0][1] = -sn*pc1_2 + cs*pc2_2;
+
+    return (map);
+}
+
+/* Illustration of the grid bins
+   dX        bin
+   -35:-25 -> 0     bin = dX / Scale + Offset
+   -25:-15 -> 1     Scale = 10
+   -15:-05 -> 2     Offset = 3.5
+   -05:+05 -> 3     nPix = 3 (maxOffset = 35 = (nPix + 0.5)*dXix
+   +05:+15 -> 4     dPix = 10
+   +15:+25 -> 5
+   +25:+35 -> 6
+
+   maxOffsetRequest = 30
+   nPix = (int) (maxOffset / dPix + 0.5);
+   maxOffset = (nPix + 0.5)*Scale;
+*/
+
+/*****************************************************************************/
+static void pmAstromMatchInfoFree (pmAstromMatchInfo *info)
+{
+    if (info == NULL) return;
+    return;
+}
+
+
+/*****************************************************************************/
+pmAstromMatchInfo *pmAstromMatchInfoAlloc()
+{
+    pmAstromMatchInfo *info = psAlloc (sizeof(pmAstromMatchInfo));
+    psMemSetDeallocator(info, (psFreeFunc) pmAstromMatchInfoFree);
+
+    info->match = NULL;
+    info->radius = NAN;
+
+    return (info);
+}
+
+// generate a unique set of matches (choose closest match)
+psArray *pmAstromRadiusMatchUniq (psArray *rawstars, psArray *refstars, psArray *matches) {
+
+    // I have the matches between the refstars and the rawstars.
+    // For each refstar, find the single match which has the smallest radius and reject
+    // all others.
+
+    // create an array of refstars->n arrays, each containing all of the matches for the
+    // given refstar.
+
+    psArray *refstarMatches = psArrayAlloc (refstars->n);
+
+    for (int i = 0; i < matches->n; i++) {
+
+        pmAstromMatch *match = matches->data[i];
+
+        // accumulate this refstar match on the array for this refstar (create if needed)
+        psArray *refSet = refstarMatches->data[match->ref];
+        if (!refSet) {
+            refstarMatches->data[match->ref] = psArrayAllocEmpty (8);
+            refSet = refstarMatches->data[match->ref];
+        }
+
+        pmAstromMatchInfo *matchInfo = pmAstromMatchInfoAlloc();
+
+        pmAstromObj *refStar = refstars->data[match->ref];
+        pmAstromObj *rawStar = rawstars->data[match->raw];
+
+        matchInfo->match = match; // reference to the match of interest
+        matchInfo->radius = hypot (refStar->FP->x - rawStar->FP->x, refStar->FP->y - rawStar->FP->y);
+
+        psArrayAdd (refSet, 8, matchInfo); // matchInfo->match is just a reference
+        psFree (matchInfo);
+    }
+
+    // we now have a set of matches for each refstar and their distances; create a new set
+    // keeping only the closest entry for each match
+
+    psArray *unique = psArrayAllocEmpty (PS_MAX(16, matches->n / 2));
+    for (int i = 0; i < refstars->n; i++) {
+
+        psArray *refSet = refstarMatches->data[i];
+        if (!refSet) continue;
+        if (refSet->n == 0) continue; // not certain how this can happen...
+
+        if (refSet->n == 1) {
+            pmAstromMatchInfo *matchInfo = refSet->data[0];
+            psArrayAdd (unique, 32, matchInfo->match);
+            continue;
+        }
+
+        pmAstromMatchInfo *matchInfo = refSet->data[0];
+        float minRadius = matchInfo->radius;
+        pmAstromMatch *minMatch = matchInfo->match;
+        for (int j = 1; j < refSet->n; j++) {
+            pmAstromMatchInfo *matchInfo = refSet->data[j];
+            if (minRadius < matchInfo->radius) continue;
+            minMatch = matchInfo->match;
+            minRadius = matchInfo->radius;
+        }
+
+        psArrayAdd (unique, 32, minMatch); // minMatch is just a reference to a match on matches,
+    }
+
+    psLogMsg ("psModules.astrom", 3, "generate unique matches to reference stars: %ld matches -> %ld matches\n", matches->n, unique->n);
+    psFree (refstarMatches);
+
+    return unique;
+}
Index: /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryObjects.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryObjects.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryObjects.h	(revision 42651)
@@ -0,0 +1,369 @@
+/* @file  pmAstrometryObjects.h
+ * @brief basic matching of objects based on their astrometry.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.18 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-11-20 01:26:07 $
+ * 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.
+    float Mag;				///< object magnitude in extracted filter
+    float Color;			///< object color 
+    float dMag;				///< error on object magnitude
+    float SBinst;			///< surface brightness, used for Koppenhoefer correction
+    float magCal;		        ///< object calibrated magnitude in extracted filter
+}
+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;                             ///< reference to the rawstar entry
+    int ref;                             ///< reference to the refstar entry
+}
+pmAstromMatch;
+
+
+/*
+ * The pmAstromMatchInfo structure is used to generate a unique set of matches
+ */
+typedef struct
+{
+    pmAstromMatch *match;		///< reference to the match
+    float radius;			///< distance between the object
+}
+pmAstromMatchInfo;
+
+
+/*
+ *
+ * XXX: Not in SDRS.
+ *
+ */
+typedef struct
+{
+    psPlane center;                     ///<
+    psPlane offset;                     ///<
+    double  scale;                      ///<
+    double  angle;                      ///<
+    double  minMetric;                  ///<
+    double  minVar;                     ///<
+    int     nMatch;                     ///<
+    int     nTest;                      ///<
+    double  nSigma;                     ///<
+}
+pmAstromStats;
+
+typedef struct
+{
+    psStats *xStats;
+    psStats *yStats;
+    int     nMatch;                     ///<
+    double  nSigma;                     ///<
+    double  dXsys;			///< systematic error in X
+    double  dYsys;			///< systematic error in Y
+    double  dXrange;			///< 10% - 90% range X residuals (unmasked, high S/N)
+    double  dYrange;			///< 10% - 90% range Y residuals (unmasked, high S/N)
+    double  dXstdev;			///< stdev of median residual in NxN bins
+    double  dYstdev;			///< stdev of median residual in NxN bins
+}
+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
+);
+
+psArray *pmAstromRadiusMatchUniq (psArray *refstars, psArray *rawstars, psArray *matches);
+
+pmAstromStats *pmAstromStatsAlloc(void);
+
+/*
+ *
+ * This function accepts an array of pmAstromObj objects and rotates them by
+ * the given angle about the given center coordinate pCenter,qCenter in the Focal
+ * Plane Array coordinates.
+ *
+ * XXX: This differs from the SDRS
+ *
+ */
+/* SDRS
+psArray *pmAstromRotateObj(
+    psArray *old,
+    double angle,
+    double pCenter,
+    double qCenter
+);
+*/
+psArray *pmAstromRotateObj(
+    const psArray *old,
+    psPlane center,
+    double angle,
+    double scale
+);
+
+
+/*
+ *
+ * If the two sets of coordinates are not known to agree well, but the
+ * relative scale and approximate relative rotation is known, then a much faster
+ * match can be found using pair-pair displacements. In such a case, the two
+ * lists can be considered as having the same coordinate system, with an unknown
+ * relative displacement. In this algorithm, all possible pair-wise differences
+ * between the source positions in the two lists are constructed and accumulated
+ * in a grid of possible offset values. The resulting grid is searched for a
+ * cluster representing the offset between the two input lists. This algorithm
+ * can only tolerate a small error in the relative scale or the relative rotation
+ * of the two coordinate lists. However, this process is naturally O(N2), and is
+ * thus advantageous over triangle matching in some circumstances. This process
+ * can be extended to allow a larger uncertainty in the relative rotation by
+ * allowing the procedure to scan over a range of rotations. We define the
+ * following function to apply this matching algorithm:
+ *
+ * XXX: In the SDRS, this function is a pointer.
+ *
+ */
+pmAstromStats *pmAstromGridMatch(
+    const psArray *st1,
+    const psArray *st2,
+    const psMetadata *config
+);
+
+/******************************************************************************
+pmAstromGridTweak(*raw, *ref, *recipe, stats): improve match for two star lists.
+ ******************************************************************************/
+pmAstromStats *pmAstromGridTweak(
+    psArray *raw,
+    psArray *ref,
+    psMetadata *recipe,
+    pmAstromStats *stats);
+
+/*
+ *
+ * The result of a pmAstromGridMatch may be used to modify the astrometry
+ * transformation information for a pmFPA image hierarchy structure. The result
+ * of pmAstromGridMatch defines the adjustments which should be made to the
+ * reference coordinate of the projection (pmFPA.projection.R,D) and the
+ * effective rotation of the Focal Plane.  The rotation implies modification of
+ * the linear terms of the pmFPA.toTangentPlane transformation. These two
+ * adjustments are made using the function:
+ *
+ * XXX: This function name is different in the SDRS.
+ *
+ */
+psPlaneTransform *pmAstromGridApply(
+    psPlaneTransform *map,
+    pmAstromStats *stat
+);
+
+
+/*
+ *
+ * This function is identical to pmAstromGridMatch, but is valid for only a
+ * single relative rotation. The input config information need not contain any of
+ * the GRID.*.ANGLE entries (they will be ignored).
+ *
+ * XXX: This function name is different in the SDRS.
+ *
+ */
+/* in pmAstromGrid.c */
+pmAstromStats *pmAstromGridAngle(
+    const psArray *st1,
+    const psArray *st2,
+    const psMetadata *config);
+
+
+
+/*
+ *
+ * This function accepts the raw and reference source lists and the list of
+ * matched entries. It uses the matched list to determine a polynomial
+ * transformation between the two coordinate systems. The fitting uses clipping
+ * to exclude outliers, likely representing poor matches. The config element must
+ * contain the information ASTROM.NSIGMA (specifying the number of sigma used in
+ * the clipping) and ASTROM.NCLIP (specifying the number of clipping iterations
+ * must be performed). The config element must also specify the order of the
+ * polynomial fit (keyword: ASTROM.ORDER). The result of this fit is a set of
+ * modifications of the components of the pmFPA.toTangentPlane transformation,
+ * and the modifications of the reference coordinate of the projection
+ * (pmFPA.projection.R,D) and the projection scale (pmFPA.projection.Xs,Ys). The
+ * modifications to pmFPA.toTangentPlane incorporate the rotation component of
+ * the linear terms and the higher-order terms of the polynomial fits.
+ *
+ * XXX: No prototype code.
+ *
+ */
+bool pmAstromFitFPA(pmFPA *fpa,
+                    psArray *st1,
+                    psArray *st2,
+                    psArray *match,
+                    psMetadata *config);
+
+
+
+/*
+ *
+ * This function accepts the raw and reference source lists for a single chip
+ * and the list of matched entries. It uses the matched list to determine a
+ * polynomial transformation between the two coordinate systems. The fitting
+ * uses clipping to exclude outliers, likely representing poor matches. The
+ * config element must contain the information ASTROM.NSIGMA
+ *(specifying the number of sigma used in the clipping) and ASTROM.NCLIP
+ *(specifying the number of clipping iterations must be performed). The config
+ *element must also specify the order of the polynomial fit (keyword:
+ *ASTROM.ORDER).  The result of this fit is a set of modifications of the
+ *components of the pmChip.toFPA transformation.
+ *
+ * XXX: No prototype code.
+ *
+ */
+bool pmAstromFitChip(
+    pmFPA *fpa,
+    psArray *st1,
+    psArray *st2,
+    psArray *match,
+    psMetadata *config);
+
+
+/*******************************************************************************
+ The following functions and structs were in the prototype code, but not the
+ SDRS.
+ ******************************************************************************/
+/*
+ *
+ *
+ *
+ *
+ */
+
+pmAstromFitResults *pmAstromFitResultsAlloc(void);
+
+/*
+ *
+ * Allocates a pmAstromObj struct.
+ *
+ */
+pmAstromObj *pmAstromObjAlloc (void);
+/*
+ * Is a given pointer a pmAstromObj?
+ */
+bool pmAstromObjTest(const psPtr ptr);
+
+
+/*
+ *
+ * Copies a pmAstromObj struct.
+ *
+ */
+pmAstromObj *pmAstromObjCopy(
+    const pmAstromObj *old
+);
+
+
+
+/*
+ *
+ *
+ *
+ */
+pmAstromMatch *pmAstromMatchAlloc(
+    int i1,
+    int i2
+);
+
+
+
+
+/*
+ *
+ *
+ *
+ */
+pmAstromFitResults *pmAstromMatchFit(
+    psPlaneTransform *map,
+    psArray *raw,
+    psArray *ref,
+    psArray *match,
+    psStats *stats,
+    const psMetadata *config
+);
+
+/*
+ *
+ *
+ *
+ */
+int pmAstromObjSortByMag(
+    const void **a,
+    const void **b
+);
+
+float pmAstromVectorRange (psVector *myVector, float minFrac, float maxFrac, float stdevGuess);
+
+/// @}
+#endif // PM_ASTROMETRY_OBJECTS_H
Index: /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryRefstars.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryRefstars.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryRefstars.c	(revision 42651)
@@ -0,0 +1,299 @@
+/* @file  pmAstrometryRefstars.c
+ * @brief Functions to write (and read?) astrometric reference stars
+ *
+ * @ingroup AstroImage
+ *
+ * @author EAM, IfA
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-07-17 22:38:15 $
+ * Copyright 2008 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+/******************************************************************************/
+/*  INCLUDE FILES                                                             */
+/******************************************************************************/
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <unistd.h>   // for unlink
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPAfileFitsIO.h"
+#include "pmAstrometryObjects.h"
+#include "pmAstrometryRefstars.h"
+
+/********************* CheckDataStatus functions *****************************/
+
+bool pmAstromRefstarsCheckDataStatusForView (const pmFPAview *view, pmFPAfile *file) {
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        bool exists = pmAstromRefstarsCheckDataStatusForFPA (fpa);
+        return exists;
+    }
+    if (view->chip >= fpa->chips->n) {
+        psError(PS_ERR_IO, true, "Requested chip == %d >= fpa->chips->n == %ld", view->chip, fpa->chips->n);
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        bool exists = pmAstromRefstarsCheckDataStatusForChip (chip);
+        return exists;
+    }
+    if (view->cell >= chip->cells->n) {
+        psError(PS_ERR_IO, true, "Requested cell == %d >= chip->cells->n == %ld", view->cell, chip->cells->n);
+        return false;
+    }
+    psError(PS_ERR_IO, false, "Astrometry only valid at the chip level");
+    return false;
+}
+
+// return true if data exists for any chip
+bool pmAstromRefstarsCheckDataStatusForFPA (const pmFPA *fpa) {
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!chip) continue;
+        if (pmAstromRefstarsCheckDataStatusForChip (chip)) return true;
+    }
+    return false;
+}
+
+// return true if data exists for any cell
+bool pmAstromRefstarsCheckDataStatusForChip (const pmChip *chip) {
+
+    for (int i = 0; i < chip->cells->n; i++) {
+      pmCell *cell = chip->cells->data[i];
+        if (!cell) continue;
+        if (pmAstromRefstarsCheckDataStatusForCell (cell)) return true;
+    }
+    return false;
+}
+
+// return true if data exists for any readout
+bool pmAstromRefstarsCheckDataStatusForCell (const pmCell *cell) {
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+      pmReadout *readout = cell->readouts->data[i];
+        if (!readout) continue;
+        if (pmAstromRefstarsCheckDataStatusForReadout (readout)) return true;
+    }
+    return false;
+}
+
+// check if refstars array exists
+bool pmAstromRefstarsCheckDataStatusForReadout (const pmReadout *readout) {
+
+  if (!readout->analysis) return false;
+
+  // select the raw objects for this readout
+  psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+  if (refstars == NULL) return false;
+
+  return true;
+}
+
+/********************* Write Data functions *****************************/
+
+bool pmAstromRefstarsWriteForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config) {
+
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        if (!pmAstromRefstarsWriteFPA (fpa, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write refstars for fpa");
+            return false;
+        }
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        psError(PS_ERR_UNKNOWN, false, "Writing chip == %d (>= chips->n == %ld)", view->chip, fpa->chips->n);
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        if (!pmAstromRefstarsWriteChip (chip, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write refstars for chip");
+            return false;
+        }
+        return true;
+    }
+
+    psError(PS_ERR_IO, false, "Astrometry must be written at the FPA level");
+    return false;
+}
+
+bool pmAstromRefstarsWritePHU (const pmFPAview *view, pmFPAfile *file, pmConfig *config) {
+
+    // output header data
+    psMetadata *outhead = psMetadataAlloc();
+
+    // use the FPA phu to generate the PHU header
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // Suitable FPA for writing
+    pmHDU *phu = psMemIncrRefCounter(fpa->hdu);
+    psFree(fpa);
+
+    // if there is no FPA PHU, this is a single header+image (extension-less) file. This could be
+    // the case for an input SPLIT set of files being written out as a MEF.  if there is a PHU,
+    // write it out as a 'blank'
+    if (phu) {
+        psMetadataCopy (outhead, phu->header);
+    } else {
+        pmConfigConformHeader (outhead, file->format);
+    }
+    psFree(phu);
+
+    psMetadataAddBool (outhead, PS_LIST_TAIL, "EXTEND", PS_META_REPLACE, "this file has extensions", true);
+    psFitsWriteBlank (file->fits, outhead, "");
+    file->wrote_phu = true;
+
+    psTrace ("pmFPAfile", 5, "wrote phu %s (type: %d)\n", file->filename, file->type);
+    psFree (outhead);
+
+    return true;
+}
+
+// write out all chip-level Astrometry data for this FPA
+bool pmAstromRefstarsWriteFPA (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config) {
+
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fpa->chips, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        thisView->chip = i;
+        if (!pmAstromRefstarsWriteChip (chip, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write %dth chip", i);
+            psFree (thisView);
+            return false;
+        }
+    }
+    psFree (thisView);
+    return true;
+}
+
+bool pmAstromRefstarsWriteChip (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config) {
+
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(chip->cells, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        thisView->cell = i;
+        if (!pmAstromRefstarsWriteCell (cell, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write %dth cell", i);
+            psFree (thisView);
+            return false;
+        }
+    }
+    psFree (thisView);
+    return true;
+}
+
+// read in all readout-level Objects files for this cell
+bool pmAstromRefstarsWriteCell (pmCell *cell, const pmFPAview *view, pmFPAfile *file, const pmConfig *config) {
+
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(cell->readouts, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        thisView->readout = i;
+        if (!pmAstromRefstarsWriteReadout (readout, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write %dth readout", i);
+            psFree (thisView);
+            return false;
+        }
+    }
+    psFree (thisView);
+    return true;
+}
+
+// write out all refstars files for this readout
+bool pmAstromRefstarsWriteReadout (pmReadout *readout, const pmFPAview *view, pmFPAfile *file, const pmConfig *config) {
+
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(readout->analysis, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    // select the raw objects for this readout
+    psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+    if (refstars == NULL) { return TRUE; }
+
+    psMetadata *header = psMetadataAlloc();
+
+    psArray *table = psArrayAllocEmpty (1);
+
+    // set the extname : we are really only allowed one entry per chip; check this here?
+    char *chiprule = psStringCopy ("{CHIP.NAME}");
+    char *chipname = pmFPAfileNameFromRule (chiprule, file, view);
+
+    for (int i = 0; i < refstars->n; i++) {
+      psMetadata *row = psMetadataAlloc ();
+
+      pmAstromObj *ref = refstars->data[i];
+
+      psMetadataAddF64(row,    PS_LIST_TAIL, "RA",      PS_META_REPLACE, "degrees", PS_DEG_RAD*ref->sky->r);
+      psMetadataAddF64(row,    PS_LIST_TAIL, "DEC",     PS_META_REPLACE, "degrees", PS_DEG_RAD*ref->sky->d);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "TP_X",    PS_META_REPLACE, "microns", ref->TP->x);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "TP_Y",    PS_META_REPLACE, "microns", ref->TP->y);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "FP_X",    PS_META_REPLACE, "microns", ref->FP->x);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "FP_Y",    PS_META_REPLACE, "microns", ref->FP->y);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "CHIP_X",  PS_META_REPLACE, "microns", ref->chip->x);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "CHIP_Y",  PS_META_REPLACE, "microns", ref->chip->y);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "MAG",     PS_META_REPLACE, "microns", ref->Mag);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "MAG_ERR", PS_META_REPLACE, "microns", ref->dMag);
+
+      psArrayAdd (table, 100, row);
+      psFree (row);
+    }
+    if (!psFitsWriteTable (file->fits, header, table, chipname)) {
+        psError(PS_ERR_IO, false, "writing refstars\n");
+        psFree (table);
+        psFree (header);
+        psFree (chiprule);
+        psFree (chipname);
+        return false;
+    }
+
+    psFree (chiprule);
+    psFree (chipname);
+    psFree (table);
+    psFree (header);
+    return true;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryRefstars.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryRefstars.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryRefstars.h	(revision 42651)
@@ -0,0 +1,34 @@
+/* @file  pmAstrometryRefstars.h
+ * @brief Functions to write (and read?) astrometric reference stars
+ *
+ * @ingroup AstroImage
+ *
+ * @author EAM, IfA
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-07-17 22:38:15 $
+ * Copyright 2008 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_ASTROMETRY_REFSTARS_H
+#define PM_ASTROMETRY_REFSTARS_H
+
+/// @addtogroup Astrometry
+/// @{
+
+bool pmAstromRefstarsCheckDataStatusForView (const pmFPAview *view, pmFPAfile *file);
+bool pmAstromRefstarsCheckDataStatusForFPA (const pmFPA *fpa);
+bool pmAstromRefstarsCheckDataStatusForChip (const pmChip *chip);
+bool pmAstromRefstarsCheckDataStatusForCell (const pmCell *cell);
+bool pmAstromRefstarsCheckDataStatusForReadout (const pmReadout *readout);
+
+bool pmAstromRefstarsWriteForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmAstromRefstarsWritePHU (const pmFPAview *view, pmFPAfile *file, pmConfig *config);
+
+bool pmAstromRefstarsWriteFPA (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmAstromRefstarsWriteChip (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmAstromRefstarsWriteCell (pmCell *cell, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmAstromRefstarsWriteReadout (pmReadout *readout, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryRegions.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryRegions.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryRegions.c	(revision 42651)
@@ -0,0 +1,170 @@
+/** @file  pmAstrometryRegions.c
+ *  @brief functions to define astrometry regions on FPA images
+ *  @ingroup Astrometry
+ *
+ *  @author EAM, IfA
+ *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-12-22 17:51:48 $
+ *
+ *  Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAExtent.h"
+#include "pmAstrometryRegions.h"
+
+// cell pixels corresponding to readout boundary
+psRegion *pmAstromReadoutInCell (pmReadout *readout) {
+
+    psRegion *region;
+    region = pmReadoutExtent (readout);
+    return (region);
+}
+
+// chip pixels corresponding to cell boundary
+psRegion *pmAstromCellInChip (pmCell *cell) {
+
+    psRegion *region;
+    region = pmCellExtent (cell);
+    return (region);
+}
+
+// FP pixels corresponding to chip boundary
+// since the chip may be rotated in the fpa, this region does not correspond 
+// exactly to the pixel grid of the chip
+psRegion *pmAstromChipInFP (pmChip *chip) {
+
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+
+    // if we have not astrometry for this chip, just skip it silently (no error)
+    if (!chip->toFPA) return NULL;
+
+    // determine the bounding box of this chip in chip pixels
+    psRegion *chipExtent = pmChipPixels (chip);
+    if (!chipExtent) return NULL;
+
+    // apply chip-to-fpa astrometry to determine fpa coordinates 
+    psPlane *chPix = psPlaneAlloc ();
+    psPlane *fpPix = psPlaneAlloc ();
+    psRegion *chipRegion = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of chip
+
+    // determine the outer bounding box -- does not correspond to the same square region!
+    chPix->x = chipExtent->x0;
+    chPix->y = chipExtent->y0;
+    psPlaneTransformApply(fpPix, chip->toFPA, chPix); 
+    chipRegion->x0 = PS_MIN (chipRegion->x0, fpPix->x);
+    chipRegion->x1 = PS_MAX (chipRegion->x1, fpPix->x);
+    chipRegion->y0 = PS_MIN (chipRegion->y0, fpPix->y);
+    chipRegion->y1 = PS_MAX (chipRegion->y1, fpPix->y);
+
+    chPix->x = chipExtent->x1;
+    chPix->y = chipExtent->y0;
+    psPlaneTransformApply(fpPix, chip->toFPA, chPix); 
+    chipRegion->x0 = PS_MIN (chipRegion->x0, fpPix->x);
+    chipRegion->x1 = PS_MAX (chipRegion->x1, fpPix->x);
+    chipRegion->y0 = PS_MIN (chipRegion->y0, fpPix->y);
+    chipRegion->y1 = PS_MAX (chipRegion->y1, fpPix->y);
+
+    chPix->x = chipExtent->x1;
+    chPix->y = chipExtent->y1;
+    psPlaneTransformApply(fpPix, chip->toFPA, chPix); 
+    chipRegion->x0 = PS_MIN (chipRegion->x0, fpPix->x);
+    chipRegion->x1 = PS_MAX (chipRegion->x1, fpPix->x);
+    chipRegion->y0 = PS_MIN (chipRegion->y0, fpPix->y);
+    chipRegion->y1 = PS_MAX (chipRegion->y1, fpPix->y);
+
+    chPix->x = chipExtent->x0;
+    chPix->y = chipExtent->y1;
+    psPlaneTransformApply(fpPix, chip->toFPA, chPix); 
+    chipRegion->x0 = PS_MIN (chipRegion->x0, fpPix->x);
+    chipRegion->x1 = PS_MAX (chipRegion->x1, fpPix->x);
+    chipRegion->y0 = PS_MIN (chipRegion->y0, fpPix->y);
+    chipRegion->y1 = PS_MAX (chipRegion->y1, fpPix->y);
+
+    psFree (chPix);
+    psFree (fpPix);
+    psFree (chipExtent);
+    return (chipRegion);
+}
+
+// return FPA pixels included in all chips
+// this FPA grid has 0,0 at the mosaic center and is used for astrometric reference.
+psRegion *pmAstromFPAExtent(const pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    psArray *chips = fpa->chips;       // Array of component chips
+    psRegion *fpaExtent = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of fpa
+    for (long i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // Chip of interest
+        psRegion *chipExtent = pmAstromChipInFP(chip); // Extent of chip
+	if (!chipExtent) { continue; }
+        fpaExtent->x0 = PS_MIN(fpaExtent->x0, chipExtent->x0);
+        fpaExtent->x1 = PS_MAX(fpaExtent->x1, chipExtent->x1);
+        fpaExtent->y0 = PS_MIN(fpaExtent->y0, chipExtent->y0);
+        fpaExtent->y1 = PS_MAX(fpaExtent->y1, chipExtent->y1);
+        psFree(chipExtent);
+    }
+
+    return fpaExtent;
+}
+
+// TPA pixels corresponding to FPA boundary
+psRegion *pmAstromFPInTP (pmFPA *fpa) {
+
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa->toTPA, NULL);
+
+    psRegion *fpaExtent = pmAstromFPAExtent (fpa);
+    if (!fpaExtent) return NULL;
+
+    // apply fpa-to-tpa astrometry to determine tpa coordinates 
+    psPlane *fpPix = psPlaneAlloc ();
+    psPlane *tpPix = psPlaneAlloc ();
+    psRegion *fpaRegion = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of fpa
+
+    // determine the outer bounding box -- does not correspond to the same square region!
+    fpPix->x = fpaExtent->x0;
+    fpPix->y = fpaExtent->y0;
+    psPlaneTransformApply(tpPix, fpa->toTPA, fpPix); 
+    fpaRegion->x0 = PS_MIN (fpaRegion->x0, tpPix->x);
+    fpaRegion->x1 = PS_MAX (fpaRegion->x1, tpPix->x);
+    fpaRegion->y0 = PS_MIN (fpaRegion->y0, tpPix->y);
+    fpaRegion->y1 = PS_MAX (fpaRegion->y1, tpPix->y);
+
+    fpPix->x = fpaExtent->x1;
+    fpPix->y = fpaExtent->y0;
+    psPlaneTransformApply(tpPix, fpa->toTPA, fpPix); 
+    fpaRegion->x0 = PS_MIN (fpaRegion->x0, tpPix->x);
+    fpaRegion->x1 = PS_MAX (fpaRegion->x1, tpPix->x);
+    fpaRegion->y0 = PS_MIN (fpaRegion->y0, tpPix->y);
+    fpaRegion->y1 = PS_MAX (fpaRegion->y1, tpPix->y);
+
+    fpPix->x = fpaExtent->x1;
+    fpPix->y = fpaExtent->y1;
+    psPlaneTransformApply(tpPix, fpa->toTPA, fpPix); 
+    fpaRegion->x0 = PS_MIN (fpaRegion->x0, tpPix->x);
+    fpaRegion->x1 = PS_MAX (fpaRegion->x1, tpPix->x);
+    fpaRegion->y0 = PS_MIN (fpaRegion->y0, tpPix->y);
+    fpaRegion->y1 = PS_MAX (fpaRegion->y1, tpPix->y);
+
+    fpPix->x = fpaExtent->x0;
+    fpPix->y = fpaExtent->y1;
+    psPlaneTransformApply(tpPix, fpa->toTPA, fpPix); 
+    fpaRegion->x0 = PS_MIN (fpaRegion->x0, tpPix->x);
+    fpaRegion->x1 = PS_MAX (fpaRegion->x1, tpPix->x);
+    fpaRegion->y0 = PS_MIN (fpaRegion->y0, tpPix->y);
+    fpaRegion->y1 = PS_MAX (fpaRegion->y1, tpPix->y);
+
+    psFree (fpPix);
+    psFree (tpPix);
+    psFree (fpaExtent);
+    return (fpaRegion);
+}
Index: /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryRegions.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryRegions.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryRegions.h	(revision 42651)
@@ -0,0 +1,38 @@
+/* @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
+
+// used by pmAstrometryDistortion.c, pmAstrometryWCS.c, pmAstrometryModel.c
+// # define EXTRA_ORDERS 6
+
+/// @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_branches/psModules.20240412/src/astrom/pmAstrometryUtils.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryUtils.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryUtils.c	(revision 42651)
@@ -0,0 +1,432 @@
+/** @file  pmAstrometryUtils.c
+ *
+ *  @brief utility functions for transform and distort functions
+ *
+ *  @ingroup Astrometry
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-10-08 20:18:00 $
+ *
+ *  Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmAstrometryUtils.h"
+
+static int transform_extra_orders = 3;
+
+int pmAstrometryGetExtraOrders (void) {
+  return transform_extra_orders;
+}
+
+int pmAstrometrySetExtraOrders (int orders) {
+  transform_extra_orders = orders;
+  return transform_extra_orders;
+}
+
+// this is used by the test output block
+static int Nout = 0;
+
+// 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;
+
+    if (0) {
+      // this is a test output block, not used in normal operations
+      char filename[64];
+      snprintf (filename, 64, "trans.%03d.md", Nout);
+      FILE *f = fopen (filename, "w");
+
+      psMetadata *md = psMetadataAlloc();
+
+      psPolynomial2DtoMetadata (md, trans->x, "X");
+      psPolynomial2DtoMetadata (md, trans->y, "Y");
+      psMetadataConfigPrint (f, md);
+      psFree (md);
+      fclose (f);
+      Nout ++;
+    }
+
+    // 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]
+            // original Alpha->data.F32[1][0] = psPolynomial2DEval (XdY, Xo, Yo);
+            // original Alpha->data.F32[0][1] = psPolynomial2DEval (YdX, Xo, Yo);
+            Alpha->data.F32[0][0] = psPolynomial2DEval (XdX, Xo, Yo);
+            Alpha->data.F32[0][1] = psPolynomial2DEval (XdY, Xo, Yo);
+            Alpha->data.F32[1][0] = psPolynomial2DEval (YdX, Xo, Yo);
+            Alpha->data.F32[1][1] = psPolynomial2DEval (YdY, Xo, Yo);
+
+	    // Beta = (-L,-M) for the current guess
+            Beta->data.F32[0] = -1.0 * psPolynomial2DEval (trans->x, Xo, Yo);
+            Beta->data.F32[1] = -1.0 * psPolynomial2DEval (trans->y, Xo, Yo);
+            // fprintf (stderr, " Beta: %f %f\n", Beta->data.F32[0], Beta->data.F32[1]);
+
+	    // since we want (L,M) = (0,0), Beta is also the offset
+            if (!psMatrixGJSolve (Alpha, Beta)) {
+		psError(PS_ERR_UNKNOWN, false, "Unable to solve for center.");
+		psFree (Alpha);
+		psFree (Beta);
+		psFree (XdX);
+		psFree (XdY);
+		psFree (YdX);
+		psFree (YdY);
+		return NULL;
+	    }
+
+            Xo += Beta->data.F32[0];
+            Yo += Beta->data.F32[1];
+            dPos = hypot(Beta->data.F32[0], Beta->data.F32[1]);
+	    psTrace ("psastro", 5, "i: %d, Xo,Yo : %f %f  dX, dY: %f %f dPos: %f\n", i, Xo, Yo, Beta->data.F32[0], Beta->data.F32[1], dPos);
+        }
+        psFree (Alpha);
+        psFree (Beta);
+        psFree (XdX);
+        psFree (XdY);
+        psFree (YdX);
+        psFree (YdY);
+
+	if (dPos > tol) {
+	    psError(PS_ERR_UNKNOWN, false, "Newton-Raphson method did not converge after 20 iterations (%f)\n", dPos);
+	    return NULL;
+	}
+    }
+    psPlane *center = psPlaneAlloc ();
+    center->x = Xo;
+    center->y = Yo;
+
+    return center;
+}
+
+// convert a transformation L(x,y) to L'(x-xo,y-yo)
+// is this used for an upward (e.g., chip->fpa) or a downward (fpa->chip) transform?
+psPlaneTransform *psPlaneTransformSetCenter (psPlaneTransform *output, psPlaneTransform *input, double Xo, double Yo)
+{
+
+    // validate fit type:
+    // polynomial in input transforms must match type -- and for now be ORD
+    psAssert (input->x->type == PS_POLYNOMIAL_ORD, "fix for CHEB");
+    psAssert (input->y->type == PS_POLYNOMIAL_ORD, "fix for CHEB");
+
+    if (output == NULL) {
+        output = psPlaneTransformAlloc(input->x->nX, input->x->nY, PS_POLYNOMIAL_ORD);
+    }
+
+    /* given two equivalent polynomial representations L(x,y) = \sum_i \sum_j A_{i,j} x^i y^j
+     * we can transform L(x,y) into L'(x-xo,y-yo) by taking the derivatives of both sides and 
+     * noting that the constant term in each is the coefficient in the case of L(x,y) and is the 
+     * value of L'(-xo,-yo) in the second case.
+     */
+
+    psPolynomial2D *tmp;
+
+    psPolynomial2D *xPx = psPolynomial2DCopy (NULL, input->x);
+    psPolynomial2D *yPx = psPolynomial2DCopy (NULL, input->y);
+
+    for (int i = 0; i <= input->x->nX; i++) {
+        psPolynomial2D *xPy = psPolynomial2DCopy (NULL, xPx);
+        psPolynomial2D *yPy = psPolynomial2DCopy (NULL, yPx);
+        for (int j = 0; j <= input->x->nY; j++) {
+            output->x->coeffMask[i][j] = input->x->coeffMask[i][j];
+            output->y->coeffMask[i][j] = input->y->coeffMask[i][j];
+            output->x->coeff[i][j] = (output->x->coeffMask[i][j] & PS_POLY_MASK_SET) ? 0 : psPolynomial2DEval (xPy, Xo, Yo) / tgamma(i+1) / tgamma(j+1);
+            output->y->coeff[i][j] = (output->y->coeffMask[i][j] & PS_POLY_MASK_SET) ? 0 : psPolynomial2DEval (yPy, Xo, Yo) / tgamma(i+1) / tgamma(j+1);
+
+            // take the next derivative wrt y, catch output (is NULL on last pass)
+            tmp = psPolynomial2D_dY(NULL, xPy);
+            psFree (xPy);
+            xPy = tmp;
+            tmp = psPolynomial2D_dY(NULL, yPy);
+            psFree (yPy);
+            yPy = tmp;
+        }
+        // take the next derivative wrt x, catch output (is NULL on last pass)
+        tmp = psPolynomial2D_dX(NULL, xPx);
+        psFree (xPx);
+        xPx = tmp;
+        tmp = psPolynomial2D_dX(NULL, yPx);
+        psFree (yPx);
+        yPx = tmp;
+    }
+    return output;
+}
+
+// rotate a transformation L(x,y) by theta
+psPlaneTransform *psPlaneTransformRotate  (psPlaneTransform *output, psPlaneTransform *input, double theta)
+{
+    /* given the polynomial transformations:
+     *  L(x,y) = \sum_i \sum_j A_{i,j} x^i y^j and 
+     *  M(x,y) = \sum_i \sum_j B_{i,j} x^i y^j 
+     * we can rotate L,M to L',M' by applying the rotation matrix (c,s),(-s,c).
+     * the resulting terms of L and M are:
+     * A'_{i,j} = c A_{i,j} + s B_{i,j}
+     * B'_{i,j} = c B_{i,j} - s A_{i,j}
+     */
+
+    if (output == NULL) {
+	// generate a new transform using the same order and type as the input
+        output = psPlaneTransformAlloc(input->x->nX, input->x->nY, input->x->type);
+    }
+
+    float cs = cos(theta);
+    float sn = sin(theta);
+
+    for (int i = 0; i <= input->x->nX; i++) {
+        for (int j = 0; j <= input->x->nY; j++) {
+	    // XXX what about inconsistent x and y masking?
+            output->x->coeffMask[i][j] = input->x->coeffMask[i][j];
+	    output->y->coeffMask[i][j] = input->y->coeffMask[i][j];
+	    if (output->x->coeffMask[i][j]) {
+		output->x->coeff[i][j] = 0.0;
+		output->y->coeff[i][j] = 0.0;
+	    } else {
+		output->x->coeff[i][j] = cs*input->x->coeff[i][j] + sn*input->y->coeff[i][j];
+		output->y->coeff[i][j] = cs*input->y->coeff[i][j] - sn*input->x->coeff[i][j];
+	    }
+        }
+    }
+    return output;
+}
+
+// construct a psPlaneTransform which is the identify transformation
+psPlaneTransform *psPlaneTransformIdentity (int order)
+{
+
+    psPlaneTransform *transform;
+
+    if (order < 1)
+        psAbort("invalid order");
+    if (order > 3)
+        psAbort("invalid order");
+
+    // all coeffs and masks initially set to 0
+    transform = psPlaneTransformAlloc (order, order, PS_POLYNOMIAL_ORD);
+
+    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_branches/psModules.20240412/src/astrom/pmAstrometryUtils.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryUtils.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryUtils.h	(revision 42651)
@@ -0,0 +1,31 @@
+/* @file  pmAstrometryUtils.h
+ * @brief utility functions for transform and distort functions
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-12-19 18:57:05 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_ASTROMETRY_UTILS_H
+#define PM_ASTROMETRY_UTILS_H
+
+/// @addtogroup Astrometry
+/// @{
+
+psPlane *psPlaneTransformGetCenter (psPlaneTransform *trans, double tol);
+psPlaneTransform *psPlaneTransformSetCenter (psPlaneTransform *output, psPlaneTransform *input, double Xo, double Yo);
+psPlaneTransform *psPlaneTransformRotate (psPlaneTransform *output, psPlaneTransform *input, double theta);
+
+psPlaneTransform *psPlaneTransformIdentity (int order);
+psPlaneDistort *psPlaneDistortIdentity (int order);
+
+bool psPlaneTransformIsDiagonal (psPlaneTransform *transform);
+bool psPlaneDistortIsDiagonal (psPlaneDistort *distort);
+
+int pmAstrometryGetExtraOrders (void);
+int pmAstrometrySetExtraOrders (int orders);
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryVisual.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryVisual.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryVisual.c	(revision 42651)
@@ -0,0 +1,1635 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAfile.h"
+#include "pmAstrometryObjects.h"
+#include "pmAstrometryVisual.h"
+#include "pmFPAExtent.h"
+
+#if (HAVE_KAPA)
+#include <kapa.h>
+#include "pmKapaPlots.h"
+#include "pmVisual.h"
+#include "pmVisualUtils.h"
+
+// variables to determine when things are plotted
+static bool plotGridMatch        = true;
+static bool plotTweak            = true;
+static bool plotRawStars         = true;
+static bool plotRefStars         = false;
+static bool plotLumFunc          = true;
+static bool plotRemoveClumps     = true;
+static bool plotOneChipFit       = true;
+static bool plotFixChips         = true;
+static bool plotAstromGuessCheck = true;
+static bool plotMosaicMatches    = true;
+static bool plotCommonScale      = true;
+static bool plotMosaicOneChip    = true;
+
+// variables to store plotting window indices
+static int kapa1 = -1;
+static int kapa2 = -1;
+static int kapa3 = -1;
+
+// helper prototypes
+bool residPlot (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe,
+                char *title);
+
+
+/* Initialization Routines  */
+
+bool pmAstromVisualClose(void)
+{
+    if (kapa1 != -1) KapaClose(kapa1);
+    if (kapa2 != -1) KapaClose(kapa2);
+    return true;
+}
+
+
+/* Plotting Routines */
+bool pmAstromVisualPlotRawStars (psArray *rawstars, pmFPA *fpa, pmChip *chip, psMetadata *recipe)
+{
+    if (!plotRawStars) return true;
+    if (!pmVisualTestLevel("psastro.plot1", 1)) return true;
+    if (!pmVisualInitWindow (&kapa1, "psastro:plots")) return false;
+
+    Graphdata graphdata;
+    KapaSection section;
+
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa1);
+    section.bg = KapaColorByName ("none"); // XXX probably should be 'none'
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = KAPA_POINT_BOX_SOLID;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+
+    section.dx = 0.5;
+    section.dy = 0.5;
+
+    // initialize and populate plotting vectors
+    bool status = false;
+    float iMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MIN");
+    float iMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MAX");
+
+    psVector *xVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+    psVector *zVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+
+    section.x = 0.0;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a0");
+    KapaSetSection (kapa1, &section);
+    psFree (section.name);
+
+    // Chip coordinates
+    int n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->x;
+        yVec->data.F32[n] = raw->chip->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel(kapa1, "Chip", KAPA_LABEL_XP);
+    KapaSendLabel(kapa1, "X", KAPA_LABEL_XM);
+    KapaSendLabel(kapa1, "Y", KAPA_LABEL_YM);
+    pmVisualTriplePlot (kapa1, &graphdata, xVec, yVec, zVec, false);
+
+    // Focal Plane Coordinates
+    section.x = 0.5;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a1");
+    KapaSetSection (kapa1, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+
+        xVec->data.F32[n] = raw->FP->x;
+        yVec->data.F32[n] = raw->FP->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa1, "Focal Plane", KAPA_LABEL_XP);
+    KapaSendLabel (kapa1, "L", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "M", KAPA_LABEL_YM);
+    pmVisualTriplePlot (kapa1, &graphdata, xVec, yVec, zVec, false);
+
+    // Tangent Plane Coordinates
+    section.x = 0.0;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a2");
+    KapaSetSection (kapa1, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+
+        xVec->data.F32[n] = raw->TP->x;
+        yVec->data.F32[n] = raw->TP->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa1, "Tangential Plane", KAPA_LABEL_XP);
+    KapaSendLabel (kapa1, "P", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "Q", KAPA_LABEL_YM);
+    pmVisualTriplePlot (kapa1, &graphdata, xVec, yVec, zVec, false);
+
+    // sky coordinates
+    section.x = 0.5;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a3");
+    KapaSetSection (kapa1, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+
+        xVec->data.F32[n] = DEG_RAD*raw->sky->r;
+        yVec->data.F32[n] = DEG_RAD*raw->sky->d;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa1, "Sky", KAPA_LABEL_XP);
+    KapaSendLabel(kapa1, "RA", KAPA_LABEL_XM);
+    KapaSendLabel(kapa1, "Dec", KAPA_LABEL_YM);
+    pmVisualTriplePlot (kapa1, &graphdata, xVec, yVec, zVec, false);
+
+    // flip x (East increase to left)
+    SWAP (graphdata.xmin, graphdata.xmax);
+    KapaSetLimits (kapa1, &graphdata);
+
+    // plot label
+    section.x = 0.0;
+    section.y = 0.0;
+    section.dx = 1.0;
+    section.dy = 1.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a5");
+    KapaSetSection (kapa1, &section);
+    KapaSendLabel (kapa1, "Raw Star Selection - Initial Astrometry", KAPA_LABEL_XP);
+    psFree (section.name);
+
+    // pause and wait for user input:
+    pmVisualAskUser(&plotRawStars);
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+    return true;
+}
+
+
+bool pmAstromVisualPlotRefStars (psArray *refstars, psMetadata *recipe)
+{
+    if (!plotRefStars) return true;
+    if (!pmVisualTestLevel("psastro.plot2", 1)) return true;
+    if (!pmVisualInitWindow (&kapa1, "psastro:plots")) return false;
+
+    Graphdata graphdata;
+    KapaInitGraph (&graphdata);
+    KapaClearSections (kapa1);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = KAPA_POINT_BOX_SOLID;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+
+    // initialize and populate plot vectors
+    bool status = false;
+    float rMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MIN");
+    float rMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MAX");
+
+    psVector *xVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    psVector *zVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+
+    int n = 0;
+    for (int i = 0; i < refstars->n; i++) {
+        pmAstromObj *ref = refstars->data[i];
+        if (!isfinite(ref->Mag)) continue;
+        if (ref->Mag > rMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+
+        xVec->data.F32[n] = DEG_RAD*ref->sky->r;
+        yVec->data.F32[n] = DEG_RAD*ref->sky->d;
+        zVec->data.F32[n] = ref->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa1, "RA", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "Dec", KAPA_LABEL_YM);
+    KapaSendLabel (kapa1, "Reference Stars", KAPA_LABEL_XP);
+    pmVisualTriplePlot(kapa1, &graphdata, xVec, yVec, zVec, false);
+
+    // flip x (East increase to left)
+    SWAP (graphdata.xmin, graphdata.xmax);
+    KapaSetLimits (kapa1, &graphdata);
+
+    // pause and wait for user input:
+    pmVisualAskUser(&plotRefStars);
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+    return true;
+}
+
+
+bool pmAstromVisualPlotLuminosityFunction (psVector *lnMag,   // Log(n) for each magnitude bin
+                                          psVector *Mag,     // magnitude bins
+                                          pmLumFunc *lumFunc,// Fit to the reference star luminosity function
+                                          pmLumFunc *rawFunc // Fit to the raw star luminoisty function
+                                          )
+{
+    if (!plotLumFunc ) return true;
+    if (!pmVisualTestLevel("psastro.plot3", 1)) return true;
+    if (!pmVisualInitWindow (&kapa1, "psastro:plots")) return false;
+
+    int colorNone = KapaColorByName ("none"); // XXX probably should be 'none'
+
+    Graphdata graphdata;
+    KapaSection section1 = {"s1", 0.0, 0.0, 1.0, 0.5, colorNone};
+    KapaSection section2 = {"s2", 0.0, 0.5, 1.0, 0.5, colorNone};
+    KapaSection section;
+    if (rawFunc == NULL) {
+        section = section1;
+    } else {
+        section = section2;
+    }
+
+    if (rawFunc == NULL) KapaClearPlots(kapa1);
+    KapaInitGraph(&graphdata);
+
+    // Determine Plot Limits
+    pmVisualScaleGraphdata(&graphdata, Mag, lnMag, false);
+
+    // Make a line for the fit
+    float x[2] = {graphdata.xmin, graphdata.xmax};
+    float y[2] = {lumFunc->offset + x[0] * lumFunc->slope,
+                 lumFunc->offset + x[1] * lumFunc->slope};
+
+    // Plot Data
+    KapaSetSection(kapa1, &section);
+    KapaSetLimits(kapa1, &graphdata);
+
+    KapaSetFont (kapa1, "helvetica", 14);
+    KapaBox (kapa1, &graphdata);
+    KapaSendLabel (kapa1, "Magnitude", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "Log(N)", KAPA_LABEL_YM);
+    if (rawFunc == NULL)
+        KapaSendLabel (kapa1, "Raw Star Luminosity Function", KAPA_LABEL_XP);
+    else
+        KapaSendLabel (kapa1,
+                       "Reference Star Luminosity Function, Shifted Raw Fit, and Cutoff",
+                       KAPA_LABEL_XP);
+    graphdata.color = KapaColorByName("black");
+    graphdata.style = KAPA_PLOT_HISTOGRAM;
+    KapaPrepPlot (kapa1, lnMag->n, &graphdata);
+    KapaPlotVector(kapa1, lnMag->n,   Mag->data.F32, "x");
+    KapaPlotVector(kapa1, lnMag->n, lnMag->data.F32, "y");
+
+    // Overplot fit
+    graphdata.style = KAPA_PLOT_CONNECT;
+    KapaPrepPlot(kapa1,2,&graphdata);
+    KapaPlotVector(kapa1, 2, x, "x");
+    KapaPlotVector(kapa1, 2, y, "y");
+
+    // If rawFunc was supplied, overplot the raw star fit + cutoff
+    if( rawFunc != NULL) {
+        double mRef = 0.5*(lumFunc->mMin + lumFunc->mMax);
+        double logRho = mRef * lumFunc->slope + lumFunc->offset;
+        double mRaw = (logRho - rawFunc->offset) / rawFunc->slope;
+        double deltaM = mRef - mRaw;
+        double mRefMax = rawFunc->mMax + deltaM;
+
+        float xraw[2] = {rawFunc->mMin + deltaM, rawFunc->mMax + deltaM};
+        float yraw[2] = {rawFunc->offset + (rawFunc->slope) * rawFunc->mMin,
+                        rawFunc->offset + (rawFunc->slope) * rawFunc->mMax};
+        float x[2] = {mRefMax, mRefMax};
+        float y[2] = {graphdata.ymin, graphdata.ymax};
+        graphdata.color= KapaColorByName("red");
+        KapaPrepPlot(kapa1, 2, &graphdata);
+        KapaPlotVector(kapa1, 2, x, "x");
+        KapaPlotVector(kapa1, 2, y, "y");
+        KapaPrepPlot (kapa1, 2, &graphdata);
+        KapaPlotVector (kapa1, 2, xraw, "x");
+        KapaPlotVector (kapa1, 2, yraw, "y");
+
+        // pause and wait for user input:
+        pmVisualAskUser (&plotLumFunc);
+    }
+    return true;
+} // end of pmAstromVisualPlotLuminosityFunction
+
+
+bool pmAstromVisualPlotRemoveClumps (psArray *input, // Array containing the field stars
+                                    psImage *count, // A 2D histogram of the field star distribution
+                                    int scale,      // The pixel size of the histogram
+                                    float limit     // The minimum numuber of stars in a bin flagged as a clump
+                                    )
+{
+    if (!plotRemoveClumps) return true;
+    if (!pmVisualTestLevel("psastro.plot4", 1)) return true;
+    if (!pmVisualInitWindow (&kapa1, "psastro:plots")) return false;
+
+    KapaSection section;
+    Graphdata graphdata;
+
+    KapaClearSections (kapa1);
+    KapaInitGraph(&graphdata);
+    section.bg = KapaColorByName ("none"); // XXX probably should be 'none'
+
+    section.x = 0.0;
+    section.dx = 1;
+    section.y = 0.0;
+    section.dy = 1.0;
+    section.name = NULL;
+    psStringAppend( &section.name, "a0");
+    KapaSetSection(kapa1, &section);
+    psFree(section.name);
+
+    graphdata.ptype = KAPA_POINT_BOX_SOLID;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+    graphdata.color = KapaColorByName ("black");
+    KapaClearPlots(kapa1);
+
+    // set up plot vectors
+    float Xmin = +FLT_MAX;
+    float Xmax = -FLT_MAX;
+    float Ymin = +FLT_MAX;
+    float Ymax = -FLT_MAX;
+    psVector *xVec = psVectorAlloc (input->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (input->n, PS_TYPE_F32);
+
+    // determine boundaries for histogram bin calculation
+    int n = 0;
+    for (int i=0; i< input->n; i++) {
+        pmAstromObj *obj = (pmAstromObj *)input->data[i];
+        if (!isfinite(obj->FP->x)) continue;
+        if (!isfinite(obj->FP->y)) continue;
+        xVec->data.F32[n] = obj->FP->x;
+        yVec->data.F32[n] = obj->FP->y;
+        Xmin = PS_MIN (Xmin, xVec->data.F32[n]);
+        Xmax = PS_MAX (Xmax, xVec->data.F32[n]);
+        Ymin = PS_MIN (Ymin, yVec->data.F32[n]);
+        Ymax = PS_MAX (Ymax, yVec->data.F32[n]);
+        n++;
+    }
+    xVec->n = yVec->n = n;
+
+    // plot stars
+    graphdata.xmax = Xmax;
+    graphdata.xmin = Xmin;
+    graphdata.ymax = Ymax;
+    graphdata.ymin = Ymin;
+    KapaSetLimits (kapa1, &graphdata);
+    KapaSetFont (kapa1, "helvetica", 14);
+
+    KapaBox (kapa1, &graphdata);
+
+    KapaSendLabel (kapa1, "L (pixels)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "M (pixels)", KAPA_LABEL_YM);
+    KapaSendLabel (kapa1, "Regions Flagged as Clumps (Red Boxes)",
+                   KAPA_LABEL_XP);
+
+    KapaPrepPlot (kapa1, xVec->n, &graphdata);
+    KapaPlotVector (kapa1, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapa1, yVec->n, yVec->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.style = KAPA_PLOT_CONNECT;
+
+    // overplot clumpy regions excluded from analysis
+    for (int i = 0; i < count->numCols; i++) {
+        for (int j = 0; j < count->numRows; j++) {
+            if(count->data.U32[j][i] <= limit) continue; // not a clump
+            float Xbot = (i - 5) * scale + Xmin;
+            float Ybot = (j - 5) * scale + Ymin;
+            if(Xbot < graphdata.xmin || Xbot > graphdata.xmax ||
+               Ybot < graphdata.ymin || Ybot > graphdata.ymax) continue;
+            float x[5] = {Xbot, Xbot + scale, Xbot + scale, Xbot, Xbot};
+            float y[5] = {Ybot, Ybot, Ybot + scale, Ybot + scale, Ybot};
+            KapaPrepPlot (kapa1, 5, &graphdata);
+            KapaPlotVector (kapa1, 5, x, "x");
+            KapaPlotVector (kapa1, 5, y, "y");
+        }
+    }
+
+    // ask for user input and finish
+    pmVisualAskUser (&plotRemoveClumps);
+    psFree (xVec);
+    psFree (yVec);
+
+    return true;
+}
+
+
+bool pmAstromVisualPlotOneChipFit (psArray *rawstars, // stars detected in the image
+                                  psArray *refstars, // reference stars over the same region
+                                  psArray *match,    // contains which rawstars match to which refstars
+                                  psMetadata *recipe // data reduction recipe
+                                  )
+{
+    if (!plotOneChipFit) return true;
+    if (!pmVisualTestLevel("psastro.plot5", 1)) return true;
+    if (!pmVisualInitWindow(&kapa1, "psastro:plot1")) return false;
+    if (!pmVisualInitWindow(&kapa2, "psastro:plot2")) return false;
+    if (!pmVisualInitWindow(&kapa3, "psastro:plot3")) return false;
+
+    // plot the residuals
+    if (!residPlot(rawstars, refstars, match, recipe, "Single Chip Fit Residuals (Chip Coordinates)")) {
+        plotOneChipFit = false;
+        return false;
+    }
+
+    // ask for user input and finish
+    pmVisualAskUser(&plotOneChipFit);
+    return true;
+}
+
+
+bool pmAstromVisualPlotFixChips (pmFPAfile *input, // focal plane array file
+                                psVector *xOld, // old X location of chip cornerss
+                                psVector *yOld // old Y location of chip corners
+                                )
+{
+    if (!plotFixChips) return true;
+    if (!pmVisualTestLevel("psastro.plot6", 1)) return true;
+    if (!pmVisualInitWindow(&kapa1, "psastro:plots")) return false;
+
+    int colorNone = KapaColorByName ("none"); // XXX probably should be 'none'
+
+    KapaSection section = {"s1", 0.0, 0.0, 1.0, 1.0, colorNone};
+    Graphdata graphdata;
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa1);
+    graphdata.ptype = KAPA_POINT_CROSS;
+    graphdata.style = KAPA_PLOT_POINTS;
+
+    psVector *xNew = psVectorAllocEmpty (xOld->n, PS_TYPE_F32);
+    psVector *yNew = psVectorAllocEmpty (yOld->n, PS_TYPE_F32);
+
+    // copy of the code in psastroFixChips that generated xOld, yOld, but for xNew, yNew
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    pmChip *obsChip = NULL;
+    while ((obsChip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+        if (!obsChip->process || !obsChip->file_exists || !obsChip->data_exists) { continue; }
+
+        psRegion *region = pmChipPixels(obsChip);
+        psPlane ptCP, ptFP;
+
+        ptCP.x = region->x0; ptCP.y = region->y0;
+        psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+        psVectorAppend (xNew, ptFP.x);
+        psVectorAppend (yNew, ptFP.y);
+
+        ptCP.x = region->x0; ptCP.y = region->y1;
+        psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+        psVectorAppend (xNew, ptFP.x);
+        psVectorAppend (yNew, ptFP.y);
+
+        ptCP.x = region->x1; ptCP.y = region->y1;
+        psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+        psVectorAppend (xNew, ptFP.x);
+        psVectorAppend (yNew, ptFP.y);
+
+        ptCP.x = region->x1; ptCP.y = region->y0;
+        psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+        psVectorAppend (xNew, ptFP.x);
+        psVectorAppend (yNew, ptFP.y);
+
+        psFree (region);
+    }
+
+    // set up graph
+    pmVisualScaleGraphdata(&graphdata, xOld, yOld, true);
+    pmVisualInitGraph(kapa1, &section, &graphdata);
+    KapaSendLabel (kapa1, "L (FP)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "M (FP)", KAPA_LABEL_YM);
+    KapaSendLabel (kapa1, "Chip corners before (black) and after (red) FixChips", KAPA_LABEL_XP);
+    KapaPrepPlot (kapa1, xOld->n, &graphdata);
+    KapaPlotVector (kapa1, xOld->n, xOld->data.F32, "x");
+    KapaPlotVector (kapa1, xOld->n, yOld->data.F32, "y");
+
+    graphdata.ptype = KAPA_POINT_BOX_OPEN;
+    graphdata.color = KapaColorByName("red");
+    KapaPrepPlot (kapa1, xNew->n, &graphdata);
+    KapaPlotVector (kapa1, xNew->n, xNew->data.F32, "x");
+    KapaPlotVector (kapa1, xNew->n, yNew->data.F32, "y");
+
+    pmVisualAskUser(&plotFixChips);
+    psFree(xNew);
+    psFree(yNew);
+    psFree(view);
+    return true;
+}
+
+
+bool pmAstromVisualPlotAstromGuessCheck (psVector *cornerPo, // P coordinates of chip corners before fitting
+                                        psVector *cornerQo, // Q coordinates of chip corners before fitting
+                                        psVector *cornerPn, // P coordinates of chip corners after fitting
+                                        psVector *cornerQn, // Q coordinates of chip corners after fitting
+                                        psVector *cornerPd, // P coordinate residuals of fit from old to new coordinates
+                                        psVector *cornerQd  // Q coordinate residuals of fit from old to new coordinates
+                                        )
+{
+    if (!plotAstromGuessCheck) return true;
+    if (!pmVisualTestLevel("psastro.plot7", 1)) return true;
+    if (!pmVisualInitWindow (&kapa1, "psastro:plots")) return false;
+
+    Graphdata graphdata;
+    KapaSection section;
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa1);
+
+    section.bg = KapaColorByName ("none"); // XXX probably should be 'none'
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = KAPA_POINT_CROSS;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+
+    section.dx = 0.4;
+    section.dy = 0.4;
+
+    // Old Corners
+    section.x = 0.30;
+    section.y = 0.50;
+    section.name = NULL;
+    psStringAppend (&section.name, "a0");
+    KapaSetSection (kapa1, &section);
+    psFree(section.name);
+
+    pmVisualScaleGraphdata (&graphdata, cornerPo, cornerPo, true);
+    KapaSetLimits (kapa1, &graphdata);
+    KapaBox (kapa1, &graphdata);
+    KapaSendLabel (kapa1, "P (Pixels)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "Q (Pixels)", KAPA_LABEL_YM);
+    KapaSendLabel (kapa1,
+                   "Fiducial Points in the Tangent Plane. Black: Initial Astrometry. Red: Final Astrometry",
+                   KAPA_LABEL_XP);
+    KapaPrepPlot (kapa1, cornerPo->n, &graphdata);
+    KapaPlotVector (kapa1, cornerPo->n, cornerPo->data.F32, "x");
+    KapaPlotVector (kapa1, cornerQo->n, cornerQo->data.F32, "y");
+
+    // New Corners
+    graphdata.color = KapaColorByName("red");
+    graphdata.ptype = KAPA_POINT_BOX_SOLID;
+    graphdata.size = 1.5;
+    KapaPrepPlot (kapa1, cornerPn->n, &graphdata);
+    KapaPlotVector (kapa1, cornerPn->n, cornerPn->data.F32, "x");
+    KapaPlotVector (kapa1, cornerQn->n, cornerQn->data.F32, "y");
+
+    // Residuals
+    psVector *xResid = psVectorAlloc(cornerPn->n, PS_DATA_F32);
+    psVector *yResid = psVectorAlloc(cornerQn->n, PS_DATA_F32);
+    for(int i=0; i < cornerPn->n; i++) {
+        xResid->data.F32[i] = (cornerPd->data.F32[i]);
+        yResid->data.F32[i] = (cornerQd->data.F32[i]);
+    }
+
+    graphdata.color = KapaColorByName("black");
+    graphdata.size=0.5;
+    section.x = 0.3;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a1");
+    KapaSetSection (kapa1, &section);
+    psFree(section.name);
+
+    pmVisualScaleGraphdata (&graphdata, xResid, yResid, true);
+    KapaSetLimits (kapa1, &graphdata);
+    KapaBox (kapa1, &graphdata);
+    KapaSendLabel (kapa1, "dP", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "dQ", KAPA_LABEL_YM);
+    KapaSendLabel (kapa1,
+                   "Residual of the Fit from the Initial Astrometry to the Final Astrometry",
+                   KAPA_LABEL_XP);
+    KapaPrepPlot (kapa1, cornerPd->n, &graphdata);
+    KapaPlotVector (kapa1, cornerPd->n, xResid->data.F32, "x");
+    KapaPlotVector (kapa1, cornerQd->n, yResid->data.F32, "y");
+
+    psFree(xResid);
+    psFree(yResid);
+    pmVisualAskUser (&plotAstromGuessCheck);
+    return true;
+}
+
+
+bool pmAstromVisualPlotCommonScale (pmFPA *fpa,         // the fpa
+                                   psVector *oldScale  // the old pixel scale of each chip in the fpa
+                                   )
+{
+    if (!plotCommonScale) return true;
+    if (!pmVisualTestLevel("psastro.plot8", 1)) return true;
+    if (!pmVisualInitWindow(&kapa1, "psastro:plots")) return false;
+
+    int colorNone = KapaColorByName ("none");
+    KapaSection section = {"s1", 0.0, 0.0, 1.0, 1.0, colorNone};
+    Graphdata graphdata;
+
+    psPlane ptCH, ptFP;
+    ptCH.x = 0;
+    ptCH.y = 0;
+    psVector *xVec = psVectorAlloc (oldScale->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (oldScale->n, PS_TYPE_F32);
+
+    int nobj = 0;
+
+    // project each chip corner to the Focal Plane
+    for(int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!chip->process || !chip->file_exists) { continue; }
+        if (!chip->toFPA) { continue; }
+
+        psPlaneTransformApply (&ptFP, chip->toFPA, &ptCH);
+        xVec->data.F32[nobj] = ptFP.x;
+        yVec->data.F32[nobj] = ptFP.y;
+        nobj++;
+        if (nobj == oldScale->n) break;
+    }
+
+    // set up plot window
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa1);
+    KapaSetSection (kapa1, &section);
+    KapaSetFont (kapa1, "helvetica", 14);
+    pmVisualTriplePlot (kapa1, &graphdata, xVec, yVec, oldScale, false);
+    KapaSendLabel (kapa1, "L (FP)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "M (FP)", KAPA_LABEL_YM);
+    KapaSendLabel (kapa1, "Old Pixel Scale of FPA Chips (Not to Scale)", KAPA_LABEL_XP);
+
+    pmVisualAskUser (&plotCommonScale);
+
+    psFree(xVec);
+    psFree(yVec);
+
+    return true;
+}
+
+
+bool pmAstromVisualPlotMosaicOneChip (psArray *rawstars, psArray *refstars,
+                                     psArray *match, psMetadata *recipe)
+{
+    if (!plotMosaicOneChip) return true;
+    if (!pmVisualTestLevel("psastro.plot9", 1)) return true;
+    if (!pmVisualInitWindow(&kapa1, "psastro:plot1")) return false;
+    if (!pmVisualInitWindow(&kapa2, "psastro:plot2")) return false;
+    if (!pmVisualInitWindow(&kapa3, "psastro:plot3")) return false;
+
+    // plot the residuals
+    if (!residPlot(rawstars, refstars, match, recipe, "Single Chip Fit Residuals - Mosaic Mode")) {
+        pmVisualSetVisual(false);
+        return false;
+    }
+
+    // ask for user input and finish
+    pmVisualAskUser(&plotMosaicOneChip);
+
+    return true;
+}
+
+
+bool pmAstromVisualPlotMosaicMatches( psArray *rawstars, psArray *refstars,
+                                    psArray *match, int iteration,
+                                    psMetadata *recipe)
+{
+    if (!plotMosaicMatches) return true;
+    if (!pmVisualTestLevel("psastro.plot10", 1)) return true;
+    if (!pmVisualInitWindow(&kapa1, "psastro:plot1")) return false;
+    if (!pmVisualInitWindow(&kapa2, "psastro:plot2")) return false;
+    if (!pmVisualInitWindow(&kapa3, "psastro:plot3")) return false;
+
+    char title[60];
+    sprintf(title, "Matches found during psastroMosaicSetMatch iteration %d", iteration);
+
+    if (!residPlot(rawstars, refstars, match, recipe, title)){
+        pmVisualSetVisual(false);
+        return false;
+    }
+
+    // ask for user input
+    pmVisualAskUser (&plotMosaicMatches);
+    return true;
+}
+
+
+bool pmAstromVisualPlotGridMatch (const psArray *raw,
+                                  const psArray *ref,
+                                  psImage *gridNP,
+                                  double offsetX,
+                                  double offsetY,
+                                  double maxOffpix,
+                                  double Scale,
+                                  double Offset)
+{
+    if (!plotGridMatch) return true;
+    if (!pmVisualTestLevel("psastro.plot11", 1)) return true;
+    if (!pmVisualInitWindow(&kapa1, "psastro:plots")) return false;
+
+    int colorNone = KapaColorByName ("none");
+    KapaSection section  = {"s1", 0.00, 0.00, 0.75, 0.75, colorNone};
+    KapaSection sectionY = {"s2", 0.75, 0.00, 0.25, 0.75, colorNone};
+    KapaSection sectionX = {"s3", 0.00, 0.75, 0.75, 0.25, colorNone};
+
+    Graphdata graphdata;
+    int nplot = raw->n * ref->n;                      // number of points to plot
+    psVector *dXplot = psVectorAlloc (nplot, PS_TYPE_F32); // x data points
+    psVector *dYplot = psVectorAlloc (nplot, PS_TYPE_F32); // y data points
+
+    pmAstromObj *ob1; pmAstromObj *ob2;               // shortcuts to the data in raw and ref
+    psU32 **NP = gridNP->data.U32;                    // shortcut to the gridNP data
+    float vertHistSlice[gridNP->numRows];             // vertical histogram slice through peak
+    float horizHistSlice[gridNP->numCols];            // horizontal histogram slice through peak
+    float horizontalIndices[gridNP->numCols];         // the horizontal offset corresponding to each bin of horizHistSlice
+    float verticalIndices[gridNP->numRows];           // the vertical offset corresponding to each bin of vertHistSlice
+    int maxHorizontalSlice = 0;                       // peak value of horizHistSlice
+    int maxVerticalSlice = 0;                         // peak value of vertHistSlice
+    int peakXbin = (int) (offsetX / Scale + Offset);  // X bin index of peak
+    int peakYbin = (int) (offsetY / Scale + Offset);  // Y bin index of peak
+
+    // psVector *vertHistSlice = psVectorAlloc (gridNP->numRows, PS_TYPE_F32);
+
+    // set up plot information
+    KapaClearPlots(kapa1);
+    KapaInitGraph(&graphdata);
+    KapaSetSection(kapa1, &section);
+
+    graphdata.xmin = -1.0 * maxOffpix;
+    graphdata.xmax =  1.0 * maxOffpix;
+    graphdata.ymin = -1.0 * maxOffpix;
+    graphdata.ymax =  1.0 * maxOffpix;
+    KapaSetLimits(kapa1, &graphdata);
+
+    KapaSetFont(kapa1, "helvetica", 14);
+    KapaBox(kapa1, &graphdata);
+    KapaSendLabel (kapa1, "X offset (FP)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "Y offset (FP)", KAPA_LABEL_YM);
+    KapaSendLabel (kapa1, "pmAstromGridAngle residuals. Box: Correlation Peak.",
+                   KAPA_LABEL_XP);
+    graphdata.style = KAPA_PLOT_POINTS;
+    graphdata.ptype = KAPA_POINT_BOX_SOLID;
+    graphdata.size = 0.4;
+    graphdata.color = KapaColorByName ("black");
+
+    // calculate the plot points
+    float dX, dY;
+    for (int i = 0; i < raw->n; i++) {
+        ob1 = (pmAstromObj *)raw->data[i];
+        for (int j = 0; j < ref->n; j++) {
+            ob2 = (pmAstromObj *)ref->data[j];
+            dX = ob1->FP->x - ob2->FP->x;
+            dY = ob1->FP->y - ob2->FP->y;
+            dXplot->data.F32[(i * ref->n) + j] = dX;
+            dYplot->data.F32[(i * ref->n) + j] = dY;
+        }
+    }
+
+    // calculate the points for the profiles
+    for (int i = 0; i < gridNP->numRows; i++) {
+        vertHistSlice[i] = NP[i][peakXbin];
+        verticalIndices[i] = (i - Offset) * Scale;
+        if (vertHistSlice[i] > maxVerticalSlice) {
+            maxVerticalSlice = vertHistSlice[i];
+        }
+    }
+    for (int i = 0; i < gridNP->numCols; i++) {
+        horizHistSlice[i] = NP[peakYbin][i];
+        horizontalIndices[i] = (i - Offset) * Scale;
+        if (horizHistSlice[i] > maxHorizontalSlice) {
+            maxHorizontalSlice = horizHistSlice[i];
+        }
+    }
+
+    // Plot the offsets
+    KapaPrepPlot(kapa1, nplot, &graphdata);
+    KapaPlotVector (kapa1, nplot, dXplot->data.F32, "x");
+    KapaPlotVector (kapa1, nplot, dYplot->data.F32, "y");
+
+    // Overplot bounding box, peak of distribution
+    float xbound[5] = { -maxOffpix, maxOffpix, maxOffpix, -maxOffpix, -maxOffpix};
+    float ybound[5] = { -maxOffpix, -maxOffpix, maxOffpix, maxOffpix, -maxOffpix};
+    float xbin[5] = {offsetX - 0.5 * Scale, offsetX + 0.5 * Scale,
+                     offsetX + 0.5 * Scale, offsetX - 0.5 * Scale,
+                     offsetX - 0.5 * Scale};
+    float ybin[5] = {offsetY - 0.5 * Scale, offsetY - 0.5 * Scale,
+                     offsetY + 0.5 * Scale, offsetY + 0.5 * Scale,
+                     offsetY - 0.5 * Scale};
+    graphdata.color = KapaColorByName("red");
+    graphdata.style = KAPA_PLOT_CONNECT;
+    graphdata.size = 1.0;
+    KapaPrepPlot(kapa1, 5, &graphdata);
+    KapaPlotVector (kapa1, 5, xbound, "x");
+    KapaPlotVector (kapa1, 5, ybound, "y");
+    KapaPrepPlot(kapa1, 5, &graphdata);
+    KapaPlotVector (kapa1, 5, xbin, "x");
+    KapaPlotVector (kapa1, 5, ybin, "y");
+
+    // plot X profile
+    KapaSetSection(kapa1, &sectionX);
+    graphdata.color = KapaColorByName("black");
+    graphdata.ptype = KAPA_POINT_BOX_OPEN;
+    graphdata.style = KAPA_PLOT_HISTOGRAM;
+    graphdata.ymin = 0;
+    graphdata.ymax = maxHorizontalSlice + 0.5;
+    strcpy (graphdata.labels, "0200");
+    KapaSetLimits(kapa1, &graphdata);
+
+    KapaBox(kapa1, &graphdata);
+    KapaPrepPlot(kapa1, gridNP->numCols, &graphdata);
+    KapaPlotVector (kapa1, gridNP->numCols, horizontalIndices, "x");
+    KapaPlotVector (kapa1, gridNP->numCols, horizHistSlice, "y");
+
+    float xslice[2] = {offsetX - Scale / 2., offsetX - Scale / 2.};
+    float yslice[2] = {-5, 100};
+    graphdata.style = KAPA_PLOT_CONNECT;
+    graphdata.color = KapaColorByName("red");
+    KapaPrepPlot(kapa1, 2, &graphdata);
+    KapaPlotVector (kapa1, 2, xslice, "x");
+    KapaPlotVector (kapa1, 2, yslice, "y");
+
+    // plot Y profile
+    KapaSetSection(kapa1, &sectionY);
+    graphdata.color = KapaColorByName("black");
+    graphdata.ptype = KAPA_POINT_BOX_OPEN;
+    graphdata.style = KAPA_PLOT_HISTOGRAM;
+    graphdata.ymin = -maxOffpix;
+    graphdata.ymax = maxOffpix;
+    graphdata.xmin = -1.0 ;
+    graphdata.xmax = maxVerticalSlice + 0.5;
+    strcpy (graphdata.labels, "2000");
+    KapaSetLimits(kapa1, &graphdata);
+
+    KapaBox(kapa1, &graphdata);
+    KapaPrepPlot(kapa1, gridNP->numRows, &graphdata);
+    KapaPlotVector (kapa1, gridNP->numRows, vertHistSlice, "x");
+    KapaPlotVector (kapa1, gridNP->numRows, verticalIndices, "y");
+
+    yslice[0] = yslice[1] = offsetY - Scale / 2.;
+    xslice[0] = -5; xslice[1] = 100;
+    graphdata.style = KAPA_PLOT_CONNECT;
+    graphdata.color = KapaColorByName("red");
+    KapaPrepPlot(kapa1, 2, &graphdata);
+    KapaPlotVector (kapa1, 2, xslice, "x");
+    KapaPlotVector (kapa1, 2, yslice, "y");
+
+    pmVisualAskUser(&plotGridMatch);
+    psFree(dXplot);
+    psFree(dYplot);
+    return true;
+} // end of pmAstromVisualPlotGridMatch
+
+
+bool pmAstromVisualPlotGridMatchOverlay (const psArray *raw,
+					 const psArray *ref,
+					 const psPlane offset)
+{
+    if (!plotGridMatch) return true;
+    if (!pmVisualTestLevel("psastro.plot12", 1)) return true;
+    if (!pmVisualInitWindow(&kapa2, "psastro:plots")) return false;
+
+    Graphdata graphdata;
+    psVector *xPlot = psVectorAlloc (PS_MAX(raw->n, ref->n), PS_TYPE_F32); // x data points
+    psVector *yPlot = psVectorAlloc (PS_MAX(raw->n, ref->n), PS_TYPE_F32); // y data points
+    psVector *zPlot = psVectorAlloc (PS_MAX(raw->n, ref->n), PS_TYPE_F32); // y data points
+
+    // set up plot information
+    KapaClearPlots(kapa2);
+    KapaInitGraph(&graphdata);
+
+    KapaSetFont(kapa2, "helvetica", 14);
+    KapaBox(kapa2, &graphdata);
+    KapaSendLabel (kapa2, "X (FP)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa2, "Y (FP)", KAPA_LABEL_YM);
+    KapaSendLabel (kapa2, "pmAstromGridAngle red: raw, black: ref.", KAPA_LABEL_XP);
+
+    // plot the REF data.  (also calculate the plot ranges, accumulate the plot vectors)
+    graphdata.xmin = +INT_MAX;
+    graphdata.xmax = -INT_MAX;
+    graphdata.ymin = +INT_MAX;
+    graphdata.ymax = -INT_MAX;
+    for (int i = 0; i < ref->n; i++) {
+        pmAstromObj *obj = ref->data[i];
+	graphdata.xmin = PS_MIN(graphdata.xmin, obj->FP->x);
+	graphdata.xmax = PS_MAX(graphdata.xmax, obj->FP->x);
+	graphdata.ymin = PS_MIN(graphdata.ymin, obj->FP->y);
+	graphdata.ymax = PS_MAX(graphdata.ymax, obj->FP->y);
+	xPlot->data.F32[i] = obj->FP->x + offset.x;
+	yPlot->data.F32[i] = obj->FP->y + offset.y;
+	zPlot->data.F32[i] = obj->Mag;
+    }
+    xPlot->n = yPlot->n = zPlot->n = ref->n;
+    KapaSetLimits(kapa2, &graphdata);
+
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN | PS_STAT_MIN  | PS_STAT_MAX );
+    psVectorStats (stats, zPlot, NULL, NULL, 0);
+    float range = stats->max - stats->min;
+    range = PS_MAX (0.5, PS_MIN (6.0, range));
+    float zero = stats->sampleMedian + 0.25*range;
+
+    float maxZ = zPlot->data.F32[0], minZ = zPlot->data.F32[0];
+    for (int i = 0; i < zPlot->n; i++) {
+	maxZ = PS_MAX (maxZ, zPlot->data.F32[i]);
+	minZ = PS_MIN (minZ, zPlot->data.F32[i]);
+	float value = (zero - zPlot->data.F32[i]) / range;
+	zPlot->data.F32[i] = PS_MAX(0.0, PS_MIN(1.0, value));
+    }
+    fprintf (stderr, "ref mags: %f to %f (%f median)\n", minZ, maxZ, stats->sampleMedian);
+
+    // the point size will be scaled from the z vector
+    graphdata.style = KAPA_PLOT_POINTS;
+    graphdata.ptype = KAPA_POINT_CROSS;
+    graphdata.size = -1;
+    graphdata.color = KapaColorByName ("black");
+
+    KapaPrepPlot   (kapa2, xPlot->n, &graphdata);
+    KapaPlotVector (kapa2, xPlot->n, xPlot->data.F32, "x");
+    KapaPlotVector (kapa2, yPlot->n, yPlot->data.F32, "y");
+    KapaPlotVector (kapa2, zPlot->n, zPlot->data.F32, "z");
+
+    // plot the RAW data (keep previous limits)
+    for (int i = 0; i < raw->n; i++) {
+        pmAstromObj *obj = raw->data[i];
+	xPlot->data.F32[i] = obj->FP->x;
+	yPlot->data.F32[i] = obj->FP->y;
+	zPlot->data.F32[i] = obj->Mag;
+    }
+    xPlot->n = yPlot->n = zPlot->n = raw->n;
+
+    psStatsInit(stats);
+    psVectorStats (stats, zPlot, NULL, NULL, 0);
+    range = stats->max - stats->min;
+    range = PS_MAX (0.5, PS_MIN (6.0, range));
+    zero = stats->sampleMedian + 0.25*range;
+    // zero = stats->sampleMedian + 1.0;
+    // range = 6.0;
+
+    maxZ = zPlot->data.F32[0], minZ = zPlot->data.F32[0];
+    for (int i = 0; i < zPlot->n; i++) {
+	maxZ = PS_MAX (maxZ, zPlot->data.F32[i]);
+	minZ = PS_MIN (minZ, zPlot->data.F32[i]);
+	float value = (zero - zPlot->data.F32[i]) / range;
+	zPlot->data.F32[i] = PS_MAX(0.0, PS_MIN(1.0, value));
+    }
+    fprintf (stderr, "raw mags: %f to %f (%f median)\n", minZ, maxZ, stats->sampleMedian);
+
+    // the point size will be scaled from the z vector
+    graphdata.style = KAPA_PLOT_POINTS;
+    graphdata.ptype = KAPA_POINT_CIRCLE_OPEN;
+    graphdata.size = -1;
+    graphdata.color = KapaColorByName ("red");
+
+    KapaPrepPlot   (kapa2, xPlot->n, &graphdata);
+    KapaPlotVector (kapa2, xPlot->n, xPlot->data.F32, "x");
+    KapaPlotVector (kapa2, yPlot->n, yPlot->data.F32, "y");
+    KapaPlotVector (kapa2, zPlot->n, zPlot->data.F32, "z");
+
+    pmVisualAskUser(&plotGridMatch);
+    psFree(xPlot);
+    psFree(yPlot);
+    psFree(zPlot);
+    psFree(stats);
+    return true;
+}
+
+bool pmAstromVisualPlotTweak (psVector *xHist, // Smoothed Horizontal cut through the histogram
+                              psVector *yHist, // Smoothed Vertical cut throug the histogram
+                              int xBin,        // X Bin index of the histogram peak
+                              int yBin         // Y bin index of the histogram peak
+    )
+{
+    if (!plotTweak) return true;
+    if (!pmVisualTestLevel("psastro.plot13", 1)) return true;
+    if (!pmVisualInitWindow(&kapa3, "psastro:plots")) return false;
+
+    Graphdata graphdata;
+
+    int colorNone = KapaColorByName ("none"); // XXX probably should be 'none'
+    KapaSection section1 = {"s1", 0.0, 0.0, 1.0, 0.5, colorNone};
+    KapaSection section2 = {"s2", 0.0, 0.5, 1.0, 0.5, colorNone};
+    KapaSection section3 = {"s3", 0.0, 0.0, 1.0, 1.0, colorNone};
+
+    psVector *xIndices = psVectorAlloc (xHist->n, PS_TYPE_F32);
+    psVector *yIndices = psVectorAlloc (yHist->n, PS_TYPE_F32);
+
+    // populate the Indices vectors
+    for(int i = 0; i < xHist->n; i++) {
+        xIndices->data.F32[i] = i;
+    }
+    for(int i = 0; i < yHist->n; i++) {
+        yIndices->data.F32[i] = i;
+    }
+
+    // set up plot information
+    KapaClearPlots(kapa3);
+    KapaInitGraph(&graphdata);
+
+    // plot the X histogram
+    pmVisualScaleGraphdata(&graphdata, xIndices, xHist, false);
+    KapaSetSection(kapa3, &section1);
+    KapaSetLimits (kapa3, &graphdata);
+    KapaSetFont(kapa3, "helvetica", 14);
+    KapaBox(kapa3, &graphdata);
+    KapaSendLabel (kapa3, "X offset Bin", KAPA_LABEL_XM);
+    KapaSendLabel (kapa3, "Number of Sources", KAPA_LABEL_YM);
+    KapaSendLabel (kapa3, "Horizontal Profile",
+                   KAPA_LABEL_XP);
+    graphdata.style = KAPA_PLOT_HISTOGRAM;
+    graphdata.ptype = KAPA_POINT_BOX_SOLID;
+    graphdata.size = 0.4;
+    graphdata.color = KapaColorByName ("black");
+
+    KapaPrepPlot (kapa3, xHist->n, &graphdata);
+    KapaPlotVector (kapa3, xHist->n, xIndices->data.F32, "x");
+    KapaPlotVector (kapa3, xHist->n, xHist->data.F32, "y");
+
+    // overplot the peak
+    float x[2] = {xBin, xBin};
+    float y[2] = {-500, 500};
+    graphdata.color = KapaColorByName ("red");
+    KapaPrepPlot (kapa3, 2, &graphdata);
+    KapaPlotVector (kapa3, 2, x, "x");
+    KapaPlotVector (kapa3, 2, y, "y");
+
+    // plot the Y histogram
+    pmVisualScaleGraphdata(&graphdata, yIndices, yHist, false);
+    KapaSetSection(kapa3, &section2);
+    KapaSetLimits (kapa3, &graphdata);
+    KapaSetFont(kapa3, "helvetica", 14);
+    graphdata.color = KapaColorByName ("black");
+    KapaBox(kapa3, &graphdata);
+    KapaSendLabel (kapa3, "Y offset Bin", KAPA_LABEL_XM);
+    KapaSendLabel (kapa3, "Number of Sources", KAPA_LABEL_YM);
+    KapaSendLabel (kapa3, "Vertical Profile",
+                   KAPA_LABEL_XP);
+    graphdata.style = KAPA_PLOT_HISTOGRAM;
+    graphdata.ptype = KAPA_POINT_BOX_SOLID;
+    graphdata.size = 0.4;
+
+    KapaPrepPlot (kapa3, yHist->n, &graphdata);
+    KapaPlotVector (kapa3, yHist->n, yIndices->data.F32, "x");
+    KapaPlotVector (kapa3, yHist->n, yHist->data.F32, "y");
+
+    // overplot the peak
+    x[0] = x[1] = yBin;
+    graphdata.color = KapaColorByName ("red");
+    KapaPrepPlot (kapa3, 2, &graphdata);
+    KapaPlotVector (kapa3, 2, x, "x");
+    KapaPlotVector (kapa3, 2, y, "y");
+
+    // plot title
+    graphdata.color = KapaColorByName("black");
+
+    KapaSetSection( kapa3, &section3);
+    KapaSendLabel (kapa3, "Tweaking the Astrometry Grid Solution. Smoothed profiles + peak location",
+                   KAPA_LABEL_XP);
+
+    pmVisualAskUser(&plotTweak);
+
+    psFree(xIndices);
+    psFree(yIndices);
+    return true;
+} // end of pmAstromPlotTweak
+
+
+bool residPlot (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe,
+                        char *title) {
+
+
+    // initialize graph information
+    Graphdata graphdata;
+    KapaSection section;
+    section.bg = KapaColorByName ("none"); // XXX probably should be 'none'
+
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa1);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = KAPA_POINT_CIRCLE_OPEN;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+
+    section.dx = 0.4;
+    section.dy = 0.4;
+
+    // initialize and populate the plotting vectors
+    bool status = false;
+    float iMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MIN");
+    float iMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MAX");
+    float rMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MIN");
+    float rMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MAX");
+
+    psVector *xVec = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *zVec = psVectorAlloc (match->n, PS_TYPE_F32);
+
+    // X vs dX
+    section.x = 0.0;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a0");
+    KapaSetSection (kapa1, &section);
+    psFree (section.name);
+
+    int n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->x;
+        yVec->data.F32[n] = raw->chip->x - ref->chip->x;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa1, "X", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "dX", KAPA_LABEL_YM);
+    pmVisualTriplePlot (kapa1, &graphdata, xVec, yVec, zVec, false);
+
+    // X vs dY
+    section.x = 0.5;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a1");
+    KapaSetSection (kapa1, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->x;
+        yVec->data.F32[n] = raw->chip->y - ref->chip->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa1, "X", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "dY", KAPA_LABEL_YM);
+    pmVisualTriplePlot (kapa1, &graphdata, xVec, yVec, zVec, false);
+
+    // Y vs dX
+    section.x = 0.0;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a2");
+    KapaSetSection (kapa1, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->y;
+        yVec->data.F32[n] = raw->chip->x - ref->chip->x;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa1, "Y", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "dX", KAPA_LABEL_YM);
+    pmVisualTriplePlot (kapa1, &graphdata, xVec, yVec, zVec, false);
+
+    // Y vs dY
+    section.x = 0.5;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a3");
+    KapaSetSection (kapa1, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->y;
+        yVec->data.F32[n] = raw->chip->y - ref->chip->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa1, "Y", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "dY", KAPA_LABEL_YM);
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa1, &graphdata, xVec, yVec, zVec, false);
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+
+    section.x = 0.0;
+    section.y = 0.0;
+    section.dx = 0.95;
+    section.dy = 0.95;
+    section.name = NULL;
+    psStringAppend (&section.name, "a5");
+    KapaSetSection (kapa1, &section);
+    KapaSendLabel (kapa1, title, KAPA_LABEL_XP);
+    psFree (section.name);
+
+    // ***************************************
+    // second window
+
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa2);
+
+    KapaSendLabel (kapa2, "X", KAPA_LABEL_XM);
+    KapaSendLabel (kapa2, "Y", KAPA_LABEL_YM);
+    KapaSendLabel (kapa2, "Chip Coordinates. Black = Raw Stars. Red = Ref Stars. Blue = Matched Stars", KAPA_LABEL_XP);
+
+    // X vs Y by mag (ref)
+    xVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    yVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    zVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+
+    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;
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = KAPA_POINT_X;
+    graphdata.style = KAPA_PLOT_POINTS;
+
+    pmVisualTriplePlot (kapa2, &graphdata, xVec, yVec, zVec, false);
+
+    // rescale the graph to include all points
+    float xmin = graphdata.xmin;
+    float ymin = graphdata.ymin;
+    float xmax = graphdata.xmax;
+    float ymax = graphdata.ymax;
+    pmVisualScaleGraphdata(&graphdata, xVec, yVec, true);
+    graphdata.xmin = PS_MIN(xmin, graphdata.xmin);
+    graphdata.ymin = PS_MIN(ymin, graphdata.ymin);
+    graphdata.xmax = PS_MAX(xmax, graphdata.xmax);
+    graphdata.ymax = PS_MAX(ymax, graphdata.ymax);
+    KapaSetLimits (kapa2, &graphdata);
+
+    // bool plotTweak;
+    // pmVisualAskUser(&plotTweak);
+
+    // X vs Y by mag (raw)
+    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);
+
+    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;
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = KAPA_POINT_BOX_OPEN;
+    graphdata.style = KAPA_PLOT_POINTS;
+    pmVisualTripleOverplot (kapa2, &graphdata, xVec, yVec, zVec, false);
+
+    // overplot matched stars in blue
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+
+    xVec = psVectorAlloc (match->n, PS_TYPE_F32);
+    yVec = psVectorAlloc (match->n, PS_TYPE_F32);
+    zVec = psVectorAlloc (match->n, PS_TYPE_F32);
+
+    n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->x;
+        yVec->data.F32[n] = raw->chip->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    fprintf (stderr, "plotting %d matched stars (raw = blue)\n", n);
+
+    graphdata.ptype = KAPA_POINT_CROSS;
+    graphdata.color = KapaColorByName ("blue");
+    pmVisualTripleOverplot (kapa2, &graphdata, xVec, yVec, zVec, false);
+
+    if (0) {
+	graphdata.ptype = KAPA_POINT_X;
+	graphdata.color = KapaColorByName ("green");
+	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] = ref->chip->x;
+	    yVec->data.F32[n] = ref->chip->y;
+	    zVec->data.F32[n] = ref->Mag;
+	    n++;
+	}
+	xVec->n = yVec->n = zVec->n = n;
+	fprintf (stderr, "plotting %d matched stars (ref = green)\n", n);
+	pmVisualTripleOverplot (kapa2, &graphdata, xVec, yVec, zVec, false);
+    }
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+
+    // ***************************************
+    // third window, pt1
+
+    xVec = psVectorAlloc (match->n, PS_TYPE_F32);
+    yVec = psVectorAlloc (match->n, PS_TYPE_F32);
+
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa3);
+
+    // mag vs dX
+
+    section.x  = 0.0;
+    section.y  = 0.0;
+    section.dx = 0.5;
+    section.dy = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "s1");
+    KapaSetSection (kapa3, &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 (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->Mag;
+        yVec->data.F32[n] = raw->chip->y - ref->chip->y;
+        n++;
+    }
+    xVec->n = yVec->n = n;
+
+    // rescale the graph to include all points
+    pmVisualScaleGraphdata(&graphdata, xVec, yVec, true);
+    KapaSetLimits (kapa3, &graphdata);
+    KapaBox (kapa3, &graphdata);
+    KapaSendLabel(kapa1, "raw mag", KAPA_LABEL_XM);
+    KapaSendLabel(kapa1, "dY", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = KAPA_POINT_TRIANGLE_OPEN;
+    graphdata.style = KAPA_PLOT_POINTS;
+    graphdata.size = 2;
+
+    KapaPrepPlot (kapa3, xVec->n, &graphdata);
+    KapaPlotVector (kapa3, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapa3, yVec->n, yVec->data.F32, "y");
+
+    // ***************************************
+    // third window, pt2
+
+    section.x  = 0.5;
+    section.y  = 0.0;
+    section.dx = 0.5;
+    section.dy = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "s2");
+    KapaSetSection (kapa3, &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 (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = ref->Mag;
+        yVec->data.F32[n] = raw->chip->y - ref->chip->y;
+        n++;
+    }
+    xVec->n = yVec->n = n;
+
+    // rescale the graph to include all points
+    pmVisualScaleGraphdata(&graphdata, xVec, yVec, true);
+    KapaSetLimits (kapa3, &graphdata);
+    KapaBox (kapa3, &graphdata);
+    KapaSendLabel(kapa1, "ref mag", KAPA_LABEL_XM);
+    KapaSendLabel(kapa1, "dY", KAPA_LABEL_YM);
+
+    KapaPrepPlot (kapa3, xVec->n, &graphdata);
+    KapaPlotVector (kapa3, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapa3, yVec->n, yVec->data.F32, "y");
+
+    // ***************************************
+    // third window, pt3
+
+    section.x  = 0.0;
+    section.y  = 0.5;
+    section.dx = 0.5;
+    section.dy = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "s3");
+    KapaSetSection (kapa3, &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 (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->Mag;
+        yVec->data.F32[n] = raw->chip->x - ref->chip->x;
+        n++;
+    }
+    xVec->n = yVec->n = n;
+
+    // rescale the graph to include all points
+    pmVisualScaleGraphdata(&graphdata, xVec, yVec, true);
+    KapaSetLimits (kapa3, &graphdata);
+    KapaBox (kapa3, &graphdata);
+    KapaSendLabel(kapa1, "raw mag", KAPA_LABEL_XM);
+    KapaSendLabel(kapa1, "dX", KAPA_LABEL_YM);
+
+    KapaPrepPlot (kapa3, xVec->n, &graphdata);
+    KapaPlotVector (kapa3, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapa3, yVec->n, yVec->data.F32, "y");
+
+    // ***************************************
+    // third window, pt4
+
+    section.x  = 0.5;
+    section.y  = 0.5;
+    section.dx = 0.5;
+    section.dy = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "s4");
+    KapaSetSection (kapa3, &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 (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = ref->Mag;
+        yVec->data.F32[n] = raw->chip->x - ref->chip->x;
+        n++;
+    }
+    xVec->n = yVec->n = n;
+
+    // rescale the graph to include all points
+    pmVisualScaleGraphdata(&graphdata, xVec, yVec, true);
+    KapaSetLimits (kapa3, &graphdata);
+    KapaBox (kapa3, &graphdata);
+    KapaSendLabel(kapa1, "ref mag", KAPA_LABEL_XM);
+    KapaSendLabel(kapa1, "dX", KAPA_LABEL_YM);
+
+    KapaPrepPlot (kapa3, xVec->n, &graphdata);
+    KapaPlotVector (kapa3, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapa3, yVec->n, yVec->data.F32, "y");
+
+    return true;
+}
+
+
+
+
+# else
+
+bool pmAstromVisualClose() { return true; }
+bool pmAstromVisualPlotGridMatch (const psArray *raw, const psArray *ref, psImage *gridNP, double offsetX, double offsetY, double maxOffpix, double Scale, double Offset) { return true; }
+bool pmAstromVisualPlotTweak (psVector *xHist, psVector *yHist, int xBin, int yBin) {return true;}
+bool pmAstromVisualPlotLuminosityFunction (psVector *lnMag, psVector *Mag, pmLumFunc *lumFunc, pmLumFunc *rawFunc) {return true;}
+bool pmAstromVisualPlotRawStars (psArray *rawstars, pmFPA *fpa, pmChip *chip, psMetadata *recipe) {return true;}
+bool pmAstromVisualPlotRefStars (psArray *refstars, psMetadata *recipe) {return true;}
+bool pmAstromVisualPlotRemoveClumps (psArray *input, psImage *count, int scale, float limit) {return true;}
+bool pmAstromVisualPlotFixChips (pmFPAfile *input, psVector *xOld, psVector *yOld) {return true;}
+bool pmAstromVisualPlotOneChipFit (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe) {return true;}
+bool pmAstromVisualPlotAstromGuessCheck (psVector *cornerPo, psVector *cornerQo, psVector *cornerPn, psVector *cornerQn, psVector *cornerPd, psVector *cornerQd) {return true;}
+bool pmAstromVisualPlotMosaicOneChip (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe) {return true;}
+bool pmAstromVisualPlotCommonScale (pmFPA *fpa, psVector *oldScale) {return true;}
+bool pmAstromVisualPlotMosaicMatches (psArray *rawstars, psArray *refstars, psArray *match, int iteration, psMetadata *recipe) {return true;}
+
+# endif
Index: /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryVisual.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryVisual.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryVisual.h	(revision 42651)
@@ -0,0 +1,167 @@
+/*
+ * @file pmAstrometryVisual.h
+ * @author Chris Beaumont, IfA
+ * @brief A set of functions to display visual diagnostics from psastro
+ * Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_ASTROM_VISUAL_H
+#define PM_ASTROM_VISUAL_H
+
+
+/** A fit to the logN / logS curve for a set of stars
+ * logN = offset + slope * logS
+ */
+typedef struct {
+    double mMin;                        ///< minimum magnitude bin with data
+    double mMax;                        ///< maximum magnitude bin with data
+    double offset;                      ///< fitted line offset
+    double slope;                       ///< fitted line slope
+    double mPeak;                       ///< mag of peak bin
+    int nPeak;                          ///< # of stars in peak bin
+    int sPeak;                          ///< sum of stars to peak bin
+} pmLumFunc;
+
+
+/** Close plotting windows at the end of a run
+ * @return true for success */
+bool pmAstromVisualClose(void);
+
+
+/**
+ * Plot the offset between every pair of reference and raw source locations. The peak of this
+ * distribution nominally gives the offset, scale difference, and rotation of the two catalogs.
+ * Overplots the location of this peak as determined by pmAstromGridAngle, as well as some profiles
+ * along horizontal and vertical cuts through this peak.
+ */
+bool pmAstromVisualPlotGridMatch (const psArray *raw, ///< raw stars
+                                  const psArray *ref, ///< reference stars
+                                  psImage *gridNP,    ///< a 2D histogram of raw-ref star distances
+                                  double offsetX,     ///< The X location (FP coordinates) of the peak of gridNP
+                                  double offsetY,     ///< the Y location (FP coordinates) of the peak of gridNP
+                                  double maxOffpix,   ///< The half-width of gridNP in FP coordinates
+                                  double Scale,       ///< The pixel size of gridNP in histogram-bin-coordinates
+                                  double Offset       ///< The (x,y) location (histogram-bin coordinates) of the FP point (0,0) in gridNP
+                                  );
+
+
+bool pmAstromVisualPlotGridMatchOverlay (const psArray *raw,
+					 const psArray *ref,
+					 const psPlane offset);
+
+/**
+ * Plot the refinements made within pmAstromGridTweak.
+ * After pmAstromGridMatch finds the best rotaion/scale/offset between raw and reference stars
+ * within a coarse grid of rotations/scales, pmAstromGridTweak computes a higher precision
+ * estimate of the offset. It computes the 2 point correlation function between raw and ref
+ * stars along horizontal and vertical cuts through the first-guess offset. It finds the peak
+ * of these two profiles and adjusts the offset accordingly. This procedure plots the profiles.
+ */
+bool pmAstromVisualPlotTweak (psVector *xHist, ///< Smoothed Horizontal cut through the histogram
+                              psVector *yHist, ///< Smoothed Vertical cut throug the histogram
+                              int xBin,        ///< X Bin index of the histogram peak
+                              int yBin         ///< Y bin index of the histogram peak
+                              );
+
+
+/**
+ * Plot the two luminosity functions created within psastroRefStarSubset
+ * The luminosity functions are used to select a subset of reference stars,
+ * so we plot the cutoff that defines this subset
+ */
+bool pmAstromVisualPlotLuminosityFunction (psVector *lnMag,   ///< Log(n) for each magnitude bin
+                                          psVector *Mag,     ///< magnitude bins
+                                          pmLumFunc *lumFunc,///< Fit to the reference star luminosity function
+                                          pmLumFunc *rawFunc ///< Fit to the raw star luminoisty function
+                                           );
+
+
+/**
+ * Plot raw stars as determined from first pass astrometry fit
+ * Called within psastroAstromGeuss
+ */
+bool pmAstromVisualPlotRawStars (psArray *rawstars, ///< Stars detected in the fpa
+                                 pmFPA *fpa,  ///< structure describing the focal plane array
+                                 pmChip *chip,  ///< structure describing the chip
+                                 psMetadata *recipe ///< the recipe used in psastro
+                                 );
+
+
+/**
+ * plot the location of references stars over the entire fpa
+ * invoked during psastroChooseRefStars
+ */
+bool pmAstromVisualPlotRefStars (psArray *refstars, psMetadata *recipe);
+
+
+/**
+ * Plot the stars in a region, and indicate which stars are part of 'clumps'
+ * These stars are flagged during astrometric fitting, since dense regions are
+ * harder to cross-match than sparse ones. Called during psastroRemoveClumps.
+ */
+bool pmAstromVisualPlotRemoveClumps (psArray *input, ///< Array containing the field stars
+                                    psImage *count, ///< A 2D histogram of the field star distribution
+                                    int scale,      ///< The pixel size of the histogram
+                                    float limit     ///< The minimum numuber of stars in a bin flagged as a clump
+                                     );
+
+/**
+ * Plots the chip corners in the FP before and after chips with inconsistent solutions have been fixed.
+ * Invoked during psastroFixChips
+ */
+bool pmAstromVisualPlotFixChips (pmFPAfile *input, ///< focal plane array file
+                                 psVector *xOld, ///< old X location of chip cornerss
+                                 psVector *yOld ///< old Y location of chip corners
+                                 );
+
+
+/**
+ * Assess the goodness of fit for a signle chip by
+ * plotting the fit residuals
+ * invoked during psastroOneChipFit
+ */
+bool pmAstromVisualPlotOneChipFit (psArray *rawstars, ///< stars detected in the image
+                                  psArray *refstars, ///< reference stars over the same region
+                                  psArray *match,    ///< contains which rawstars match to which refstars
+                                  psMetadata *recipe ///< data reduction recipe
+                                   );
+
+/**
+ *  Plots the fpa chip corners projected on to the tangential plane before and after
+ *  the astrometry solution has been applied. In psastroAstromGuessCheck, the old corners
+ *  are then fit to the new corners to get a sense at how far off the initial WCS info was
+ *  in offset, rotation, and scale. This procedure also plots the residuals of the fit from
+ *  old to new coordinates
+ */
+bool pmAstromVisualPlotAstromGuessCheck (psVector *cornerPo, ///< P coordinates of chip corners before fitting
+                                        psVector *cornerQo, ///< Q coordinates of chip corners before fitting
+                                        psVector *cornerPn, ///< P coordinates of chip corners after fitting
+                                        psVector *cornerQn, ///< Q coordinates of chip corners after fitting
+                                        psVector *cornerPd, ///< P coordinate residuals of fit from old to new coordinates
+                                        psVector *cornerQd  ///< Q coordinate residuals of fit from old to new coordinates
+                                         );
+
+
+/**
+ *   plot the residuals between raw stars and ref stars after
+ *   fitting in psastroMosaicOneChip
+ */
+bool pmAstromVisualPlotMosaicOneChip (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe) ;
+
+
+/**
+ * Plots the pixel scales of the fpa before they are
+ * equalized in psastroMosaicCommonScale
+ */
+bool pmAstromVisualPlotCommonScale (pmFPA *fpa,         ///< the fpa
+                                   psVector *oldScale  ///< the old pixel scale of each chip in the fpa
+                                    );
+
+
+/** pmAstromVisualPlotMosaicMatches
+ * Plot the matches between raw and reference stars during pmAstromVisualMosaicSetMatch
+ */
+bool pmAstromVisualPlotMosaicMatches (psArray *rawstars, psArray *refstars, psArray *match, int iteration, psMetadata *recipe);
+
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryWCS.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryWCS.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryWCS.c	(revision 42651)
@@ -0,0 +1,1239 @@
+/** @file  pmAstrometryWCS.c
+ *
+ *  @brief functions to convert FITS WCS keywords to / from pmFPA structures
+ *
+ *  @ingroup Astrometry
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.35 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-10 20:55:47 $
+ *
+ *  Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <strings.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAExtent.h"
+#include "pmAstrometryWCS.h"
+#include "pmAstrometryUtils.h"
+#include "pmAstrometryRegions.h"
+
+// the following functions support coordinate transformations direcly related to the FITS WCS
+// keywords.  The FITS WCS allows for only a single level of transformation, thus it is not
+// appropriate for mosaic astrometry consisting of telescope distortion plus chip terms.
+// Below, we support the Elixir convention of using two connected FITS headers to define two
+// levels of coordinate transformation.  In the pmFPA structure, the projection, distortion,
+// and FPA-to-Chip transformations are carried independently.  NOTE: The FITS WCS keywords do
+// not represent a simple polynomial.  Instead, they have no constant term, and the coordinates
+// are corrected to a reference pixel before the polynomial transformation is applied.
+
+// interpret header WCS (only handles traditional WCS for the moment)
+// pixelScale is microns per pixel
+bool pmAstromReadWCS (pmFPA *fpa, pmChip *chip, const psMetadata *header, double pixelScale)
+{
+    pmAstromWCS *wcs = pmAstromWCSfromHeader (header);
+    if (!wcs) {
+        return false;
+    }
+
+    bool status = pmAstromWCStoFPA (fpa, chip, wcs, pixelScale);
+
+    psFree (wcs);
+    return status;
+}
+
+// convert toFPA / toSky components to pmAstromWCS
+// tolerance is convergence for inversion of non-linear terms in pixels
+bool pmAstromWriteWCS (psMetadata *header, const pmFPA *fpa, const pmChip *chip, double tol)
+{
+    pmAstromWCS *wcs = pmAstromWCSfromFPA(fpa, chip, tol);
+    if (!wcs) return false;
+
+    pmAstromWCStoHeader (header, wcs);
+
+    psFree (wcs);
+    return true;
+}
+
+// interpret chip header WCS as bilevel chip components
+bool pmAstromReadBilevelChip (pmChip *chip, const psMetadata *header)
+{
+    pmAstromWCS *wcs = pmAstromWCSfromHeader (header);
+    if (!wcs) {
+        return false;
+    }
+
+    bool status = pmAstromWCSBileveltoChip (chip, wcs);
+
+    psFree (wcs);
+    return status;
+}
+
+// convert toFPA / toSky components to traditional WCS
+// we require the header to have NAXIS1,NAXIS2, the field of the FPA
+// the center of the TPA/Sky projection is 0.5*(NAXIS1,NAXIS2)
+bool pmAstromReadBilevelMosaic (pmFPA *fpa, const psMetadata *header)
+{
+    pmAstromWCS *wcs = pmAstromWCSfromHeader (header);
+    if (!wcs) {
+        psError(PS_ERR_UNKNOWN, false, "failure to determine WCS terms from header");
+        return false;
+    }
+
+    bool status1 = false;
+    bool status2 = false;
+    int Nx = psMetadataLookupS32 (&status1, header, "NAXIS1");
+    int Ny = psMetadataLookupS32 (&status2, header, "NAXIS2");
+
+    if (!status1 || !status2) {
+        Nx = psMetadataLookupS32 (&status1, header, "IMNAXIS1");
+        Ny = psMetadataLookupS32 (&status2, header, "IMNAXIS2");
+    }
+
+    if (!status1 || !status2) {
+        Nx = psMetadataLookupS32 (&status1, header, "ZNAXIS1");
+        Ny = psMetadataLookupS32 (&status2, header, "ZNAXIS2");
+    }
+
+    if (!status1 || !status2) {
+        psFree (wcs);
+        psError(PS_ERR_UNKNOWN, false, "missing required FPA size in header");
+        return false;
+    }
+
+    psRegion region = psRegionSet (-0.5*Nx, +0.5*Nx, -0.5*Ny, +0.5*Ny);
+    bool status = pmAstromWCSBileveltoFPA (fpa, wcs, region);
+
+    psFree (wcs);
+    return status;
+}
+
+// convert chip->toFPA components to bilevel WCS
+bool pmAstromWriteBilevelChip (psMetadata *header, const pmChip *chip, double tol)
+{
+    pmAstromWCS *wcs = pmAstromWCSBilevelChipFromFPA (chip, tol);
+    if (!wcs) {
+        psError(PS_ERR_UNKNOWN, false, "failure to determine WCS terms from fpa");
+        return false;
+    }
+
+    pmAstromWCStoHeader (header, wcs);
+
+    psFree (wcs);
+    return true;
+}
+
+
+// convert fpa->toTPA, fpa->toSky components to bilevel WCS
+bool pmAstromWriteBilevelMosaic (psMetadata *header, const pmFPA *fpa, double tol)
+{
+    pmAstromWCS *wcs = pmAstromWCSBilevelMosaicFromFPA (fpa, tol);
+    if (!wcs) {
+        psError(PS_ERR_UNKNOWN, false, "failure to determine WCS terms from fpa");
+        return false;
+    }
+
+    // we need to specify the dimensions of the FPA
+    // if we have chips defined, we can do
+    psRegion *region = pmAstromFPAExtent (fpa);
+    int Nx = region->x1 - region->x0;
+    int Ny = region->y1 - region->y0;
+    psMetadataAddS32 (header, PS_LIST_TAIL, "IMNAXIS1", PS_META_REPLACE, "Mosaic Dimensions", Nx);
+    psMetadataAddS32 (header, PS_LIST_TAIL, "IMNAXIS2", PS_META_REPLACE, "Mosaic Dimensions", Ny);
+
+    pmAstromWCStoHeader (header, wcs);
+
+    psFree (region);
+    psFree (wcs);
+    return true;
+}
+
+// convert coordinates from chip to sky using a pmAstromWCS structure
+bool pmAstromWCStoSky (psSphere *sky, pmAstromWCS *wcs, psPlane *chip)
+{
+
+    if (chip == NULL)
+        return false;
+    if (sky == NULL)
+        return false;
+    if (wcs == NULL)
+        return false;
+
+    psPlane *Chip = psPlaneAlloc();
+    psPlane *FP = psPlaneAlloc();
+
+    Chip->x = chip->x - wcs->crpix1;
+    Chip->y = chip->y - wcs->crpix2;
+
+    psPlaneTransformApply (FP, wcs->trans, Chip);
+    psDeproject (sky, FP, wcs->toSky); // find the RA,DEC coord of the focal-plane coordinate
+
+    psFree (Chip);
+    psFree (FP);
+    return true;
+}
+
+// convert coordinates from sky to chip using a pmAstromWCS structure
+bool pmAstromWCStoChip (psPlane *chip, pmAstromWCS *wcs, psSphere *sky)
+{
+
+    if (chip == NULL)
+        return false;
+    if (sky == NULL)
+        return false;
+    if (wcs == NULL)
+        return false;
+
+    psError(PS_ERR_UNKNOWN, true, "not yet implemented: needs to invert the transformation");
+    return false;
+
+    psPlane *Chip = psPlaneAlloc();
+    psPlane *FP = psPlaneAlloc();
+
+    psProject (FP, sky, wcs->toSky); // find the RA,DEC coord of the focal-plane coordinate
+
+    // XXX I actually need the inverse of wcs->transform at this point
+    psPlaneTransformApply (Chip, wcs->trans, FP);
+
+    chip->x = Chip->x + wcs->crpix1;
+    chip->y = Chip->y + wcs->crpix2;
+
+    psFree (Chip);
+    psFree (FP);
+    return true;
+}
+
+// interpret header WCS keywords (valid for bilevel and traditional WCS)
+pmAstromWCS *pmAstromWCSfromHeader (const psMetadata *header)
+{
+    psProjectionType type;
+    bool status, pcKeys, cdKeys, isPoly;
+    char name[PS_SMALLWORD]; // used to store FITS keyword below (always < 8, so 16 should be safe!)
+
+    // interpret header data, convert to crval(i), etc
+    char *ctype = psMetadataLookupPtr (&status, header, "CTYPE2");
+    if (!status) {
+        psLogMsg ("psastro", 5, "warning: no WCS metadata in header\n");
+        return NULL;
+    }
+
+    // determine projection type
+    // XXX there are two indications for higher-order terms: the type (DIS,WRP,PLY,ZPL) and
+    // the value of NPLYTERM.
+    type = psProjectTypeFromString (ctype);
+    if (type == PS_PROJ_NONE) {
+        psLogMsg ("psastro", 2, "warning: unknown projection type %s\n", ctype);
+        return NULL;
+    }
+
+    // what type of WCS keywords are available?
+    // XXX add check for CROTA2
+    int fitOrder = psMetadataLookupS32 (&isPoly, header, "NPLYTERM");
+    psMetadataLookupF64 (&pcKeys, header, "PC001001");
+    psMetadataLookupF64 (&cdKeys, header, "CD1_1");
+
+    if (cdKeys && pcKeys) {
+        // XXX make this an option
+        psLogMsg ("psastro", 5, "warning: both CDi_j and PC00i00j defined in headers, using PC00i00j terms\n");
+    }
+    if (!cdKeys && !pcKeys) {
+        psError(PS_ERR_UNKNOWN, true, "missing both CDi_j and PC00i00j WCS terms");
+        // XXX we could default here to RA, DEC, ROTANGLE
+        return NULL;
+    }
+    if (isPoly) {
+        if (!pcKeys) {
+            psError(PS_ERR_UNKNOWN, true, "polynomial terms defined, but missing PC00i00j WCS terms");
+            return NULL;
+        }
+        if (fitOrder == 0)
+            fitOrder = 1;
+        if ((fitOrder > 3) || (fitOrder < 1)) {
+            psError(PS_ERR_UNKNOWN, true, "NPLYTERM value undefined: %d", fitOrder);
+            return NULL;
+        }
+    } else {
+        fitOrder = 1;
+    }
+
+    pmAstromWCS *wcs = pmAstromWCSAlloc (fitOrder, fitOrder);
+
+    // construct a transformation from X,Y in pixels to L,M in pixels
+    // NOTE that the WCS keywords convert X,Y to degrees first (using cdelt1,2)
+    // and then define a transformation from degrees to degrees
+
+    wcs->crval1 = psMetadataLookupF64 (&status, header, "CRVAL1");
+    wcs->crval2 = psMetadataLookupF64 (&status, header, "CRVAL2");
+    wcs->crpix1 = psMetadataLookupF64 (&status, header, "CRPIX1");
+    wcs->crpix2 = psMetadataLookupF64 (&status, header, "CRPIX2");
+    wcs->toSky = psProjectionAlloc (wcs->crval1*PM_RAD_DEG, wcs->crval2*PM_RAD_DEG, PM_RAD_DEG, PM_RAD_DEG, type);
+
+    // XXX if type == ZPN, look for PV2_%d elements:
+    if (type == PS_PROJ_ZPN) {
+        psVector *maxRadial = psVectorAlloc (21, PS_TYPE_F64);
+        for (int i = 0; i <= 20; i++) {
+            char name[PS_BIGWORD];
+            ps_snprintf_nowarn (name, PS_BIGWORD, "PV2_%d", i);
+
+            maxRadial->data.F64[i] = 0.0;
+            double value = psMetadataLookupF64 (&status, header, name);
+
+            if (status) {
+                maxRadial->data.F64[i] = value;
+                maxRadial->n = i;
+            }
+
+            // PV2_1 is implicit if not present
+            if ((i == 1) && !status) {
+                maxRadial->data.F64[i] = 1.0;
+                continue;
+            }
+        }
+        maxRadial->n ++;
+        wcs->toSky->radial = maxRadial;
+    }
+
+    // These aren't needed but having them empty is disconcerting
+    strncpy(wcs->ctype2, ctype, PM_ASTROM_WCS_TYPE_SIZE-1);
+    ctype = psMetadataLookupStr (&status, header, "CTYPE1");
+    strncpy(wcs->ctype1, ctype, PM_ASTROM_WCS_TYPE_SIZE-1);
+    wcs->ctype1[PM_ASTROM_WCS_TYPE_SIZE-1] = 0;
+    wcs->ctype2[PM_ASTROM_WCS_TYPE_SIZE-1] = 0;
+
+    // XXX I think this is wrong for linear proj
+
+    // test the CDELTi varient
+    if (pcKeys) {
+        wcs->wcsCDkeys = 0;
+        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;
+                    }
+                    ps_snprintf_nowarn (name, PS_SMALLWORD, "PCA1X%1dY%1d", i, j);
+                    wcs->trans->x->coeff[i][j] = pow(wcs->cdelt1, i) * pow(wcs->cdelt2, j) * psMetadataLookupF64 (&status, header, name);
+                    ps_snprintf_nowarn (name, PS_SMALLWORD, "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->wcsCDkeys = 1;
+
+        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[PS_SMALLWORD]; // used to store FITS keyword below (always < 8, so PS_SMALLWORD should be safe!)
+    char *type;
+
+    if (!wcs) return false;
+
+    type = psProjectTypeToString (wcs->toSky->type, "RA--");
+    psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE1", PS_META_REPLACE, "", type);
+    psFree (type);
+
+    type = psProjectTypeToString (wcs->toSky->type, "DEC-");
+    psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE2", PS_META_REPLACE, "", type);
+    psFree (type);
+
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRVAL1", PS_META_REPLACE, "", wcs->toSky->R*PS_DEG_RAD);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRVAL2", PS_META_REPLACE, "", wcs->toSky->D*PS_DEG_RAD);
+
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRPIX1", PS_META_REPLACE, "", wcs->crpix1);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRPIX2", PS_META_REPLACE, "", wcs->crpix2);
+
+    if (wcs->toSky->type == PS_PROJ_ZPN) {
+        psAssert (wcs->toSky->radial, "missing radial vector");
+        for (int i = 0; i < wcs->toSky->radial->n; i++) {
+            if (wcs->toSky->radial->data.F64[i] == 0.0) continue;
+            ps_snprintf_nowarn (name, PS_SMALLWORD, "PV2_%d", i);
+            psMetadataAddF64 (header, PS_LIST_TAIL, name, PS_META_REPLACE, "", wcs->toSky->radial->data.F64[i]);
+        }
+    }
+
+    // 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
+    if (!wcs->wcsCDkeys) {
+
+        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;
+                    ps_snprintf_nowarn (name, PS_SMALLWORD, "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));
+                    ps_snprintf_nowarn (name, PS_SMALLWORD, "PCA2X%1dY%1d", i, j);
+                    psMetadataAddF64 (header, PS_LIST_TAIL, name, PS_META_REPLACE, "", wcs->trans->y->coeff[i][j] / pow(cdelt1, i) / pow(cdelt2, j));
+                }
+            }
+            psMetadataAddS32 (header, PS_LIST_TAIL, "NPLYTERM", PS_META_REPLACE, "", fitOrder);
+        }
+
+        // remove any existing 'CDi_j style' wcs keywords
+        if (psMetadataLookup(header, "CD1_1")) {
+            psMetadataRemoveKey(header, "CD1_1");
+            psMetadataRemoveKey(header, "CD1_2");
+            psMetadataRemoveKey(header, "CD2_1");
+            psMetadataRemoveKey(header, "CD2_2");
+        }
+
+        // Remove 'CDi_jX' WCS keywords
+        psString cd11 = psStringCopy("CD1_1 ");
+        psString cd12 = psStringCopy("CD1_2 ");
+        psString cd21 = psStringCopy("CD2_1 ");
+        psString cd22 = psStringCopy("CD2_2 ");
+        for (char extra = 'A'; extra <= 'Z'; extra++) {
+            cd11[strlen(cd11)-1] = extra;
+            if (psMetadataLookup(header, cd11)) {
+                cd12[strlen(cd12)-1] = extra;
+                cd21[strlen(cd21)-1] = extra;
+                cd22[strlen(cd22)-1] = extra;
+                psMetadataRemoveKey(header, cd11);
+                psMetadataRemoveKey(header, cd12);
+                psMetadataRemoveKey(header, cd21);
+                psMetadataRemoveKey(header, cd22);
+            }
+        }
+        psFree(cd11);
+        psFree(cd12);
+        psFree(cd21);
+        psFree(cd22);
+
+
+    } else {
+
+        psMetadataAddF64 (header, PS_LIST_TAIL, "CD1_1", PS_META_REPLACE, "", wcs->trans->x->coeff[1][0]);
+        psMetadataAddF64 (header, PS_LIST_TAIL, "CD1_2", PS_META_REPLACE, "", wcs->trans->x->coeff[0][1]);
+        psMetadataAddF64 (header, PS_LIST_TAIL, "CD2_1", PS_META_REPLACE, "", wcs->trans->y->coeff[1][0]);
+        psMetadataAddF64 (header, PS_LIST_TAIL, "CD2_2", PS_META_REPLACE, "", wcs->trans->y->coeff[0][1]);
+
+        if (psMetadataLookup(header, "PC001001")) {
+            psMetadataRemoveKey(header, "PC001001");
+            psMetadataRemoveKey(header, "PC001002");
+            psMetadataRemoveKey(header, "PC002001");
+            psMetadataRemoveKey(header, "PC002002");
+        }
+    }
+
+    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;
+
+    int ExtraOrders = pmAstrometryGetExtraOrders();
+
+    // 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);
+    toSky->radial = psMemIncrRefCounter (wcs->toSky->radial);
+
+    if (fpa->toSky == NULL) {
+        psFree(fpa->toTPA);
+        psFree(fpa->fromTPA);
+        fpa->toTPA = psPlaneTransformIdentity (1);
+        fpa->fromTPA = psPlaneTransformIdentity (1);
+        fpa->toSky = toSky;
+    } else {
+
+        // this section allows the loaded chip to be included in an fpa structure in which
+        // other chips have already been loaded (ie, the fpa->toTPA, fpa->toSky components have
+        // already been defined).  we have to adjust to match the existing transformation.
+
+        if (fpa->toTPA == NULL)
+            psAbort("projection defined, tangent-plane not defined");
+        if (fpa->fromTPA == NULL)
+            psAbort("projection defined, tangent-plane not defined");
+
+        // convert from pixels on this chip to pixels on reference chip
+        // rX has units of refpixels / pixel
+        double rX = toSky->Xs / fpa->toSky->Xs;
+        double rY = toSky->Ys / fpa->toSky->Ys;
+
+        for (int i = 0; i <= toFPA->x->nX; i++) {
+            for (int j = 0; j <= toFPA->x->nY; j++) {
+                toFPA->x->coeff[i][j] *= rX;
+                toFPA->y->coeff[i][j] *= rY;
+            }
+        }
+
+        // apply the exiting fromTPA transformation to make the new toFPA consistent with the toTPA layter
+        // XXX this only works if toTPA is at most a linear transformation
+        psPlaneTransform *toFPAnew = psPlaneTransformAlloc(toFPA->x->nX, toFPA->x->nY, PS_POLYNOMIAL_ORD);
+        for (int i = 0; i <= toFPA->x->nX; i++) {
+            for (int j = 0; j <= toFPA->x->nY; j++) {
+                double f1 = toFPA->x->coeffMask[i][j] ? 0.0 : fpa->fromTPA->x->coeff[1][0]*toFPA->x->coeff[i][j];
+                double f2 = toFPA->y->coeffMask[i][j] ? 0.0 : fpa->fromTPA->x->coeff[0][1]*toFPA->y->coeff[i][j];
+                toFPAnew->x->coeff[i][j] = f1 + f2;
+
+                double g1 = toFPA->x->coeffMask[i][j] ? 0.0 : fpa->fromTPA->y->coeff[1][0]*toFPA->x->coeff[i][j];
+                double g2 = toFPA->y->coeffMask[i][j] ? 0.0 : fpa->fromTPA->y->coeff[0][1]*toFPA->y->coeff[i][j];
+                toFPAnew->y->coeff[i][j] = g1 + g2;
+            }
+        }
+        toFPAnew->x->coeff[0][0] += fpa->fromTPA->x->coeff[0][0];
+        toFPAnew->y->coeff[0][0] += fpa->fromTPA->y->coeff[0][0];
+
+        psFree (toFPA);
+        toFPA = toFPAnew;
+
+        // adjust reference pixel for new toSky reference coordinate
+        // find the FPA coordinate of 0,0 for this chip.
+        psPlane *fpOld = psPlaneAlloc();
+        psPlane *fpNew = psPlaneAlloc();
+        psPlane *tp = psPlaneAlloc();
+        psSphere *sky = psSphereAlloc();
+
+        sky->r = toSky->R;
+        sky->d = toSky->D;
+        psProject (tp, sky, fpa->toSky); // find the focal-plane coord of this RA,DEC coord using the ref chip projection
+        psPlaneTransformApply (fpOld, fpa->fromTPA, tp);
+
+        sky->r = fpa->toSky->R;
+        sky->d = fpa->toSky->D;
+        psProject (tp, sky, fpa->toSky); // find the focal-plane coord of this RA,DEC coord using the ref chip projection
+        psPlaneTransformApply (fpNew, fpa->fromTPA, tp);
+
+        toFPA->x->coeff[0][0] -= fpNew->x - fpOld->x;
+        toFPA->y->coeff[0][0] -= fpNew->y - fpOld->y;
+
+        psFree (sky);
+        psFree (tp);
+        psFree (fpOld);
+        psFree (fpNew);
+
+        psFree (toSky);
+    }
+
+    // free an existing toFPA structure
+    psFree (chip->toFPA);
+    assert (chip->toFPA == NULL);
+    chip->toFPA = toFPA;
+
+    // determine the inverse transformation: we need the chip pixels covered by this transform
+    psRegion *region = pmChipPixels (chip);
+
+    // as of r40806, psPlaneTransformInvert supplies the extra order (if non-linear)
+    psFree (chip->fromFPA);
+    chip->fromFPA = psPlaneTransformInvert(NULL, chip->toFPA, *region, 50, ExtraOrders);
+    psFree (region);
+
+    // XXX if the inversion fails, we probably do not have a valid transform anyway
+    if (!chip->fromFPA) {
+        psWarning ("failed to find a valid transformation");
+        psFree (chip->toFPA);
+        return false;
+    }
+
+    // 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;
+
+    fpa->wcsCDkeys = wcs->wcsCDkeys;
+
+    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)
+     */
+
+    int ExtraOrders = pmAstrometryGetExtraOrders();
+
+    psFree (chip->toFPA);
+    if ((fabs(wcs->crpix1) > 0.01) || (fabs(wcs->crpix2) > 0.01)) {
+      chip->toFPA = psPlaneTransformSetCenter (NULL, wcs->trans, -wcs->crpix1, -wcs->crpix2);
+    } else {
+      chip->toFPA = psPlaneTransformAlloc(wcs->trans->x->nX, wcs->trans->x->nY, PS_POLYNOMIAL_ORD);
+
+      // copy the toFPA x,y, transformations to the wcs version
+      chip->toFPA->x = psPolynomial2DCopy (chip->toFPA->x, wcs->trans->x);
+      chip->toFPA->y = psPolynomial2DCopy (chip->toFPA->y, wcs->trans->y);
+
+      // these need to be set based on crval1,2
+      chip->toFPA->x->coeff[0][0] = wcs->crval1;
+      chip->toFPA->y->coeff[0][0] = wcs->crval2;
+    }
+
+    // determine the inverse transformation: we need the chip pixels covered by this transform
+    psRegion *region = pmChipPixels (chip);
+
+    // as of r40806, psPlaneTransformInvert supplies the extra order (if non-linear)
+    psFree (chip->fromFPA);
+    chip->fromFPA = psPlaneTransformInvert(NULL, chip->toFPA, *region, 50, ExtraOrders);
+    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);
+    fpa->toSky->radial = psMemIncrRefCounter (wcs->toSky->radial);
+
+    // 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);
+    int ExtraOrders = pmAstrometryGetExtraOrders();  // This is the number of orders that should be added.
+    fpa->fromTPA = psPlaneTransformInvert(NULL, fpa->toTPA, region, 50, ExtraOrders);
+    return true;
+}
+
+// convert toFPA / toSky components to pmAstromWCS
+// tolerance is allowed error in center solution in pixels
+pmAstromWCS *pmAstromWCSfromFPA (const pmFPA *fpa, const pmChip *chip, double tol)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+    PS_ASSERT_PTR_NON_NULL(chip->toFPA, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa->toTPA, NULL);
+
+    // XXX require chip->toFPA->x->nX == chip->toFPA->x->nY
+    // XXX require chip->toFPA->y->nX == chip->toFPA->y->nY
+    // XXX require chip->toFPA->x->nX == chip->toFPA->y->nX
+    // XXX require chip->toFPA->nX == 1,2,3
+
+    // technically, we can have a plate scale here (fpa->toTPA:dx,dy != 1)
+    // XXX not really: toTPA needs to have unity scale for distortion fitting function
+    // if (!psPlaneTransformIsDiagonal (fpa->toTPA))
+    // psAbort("invalid TPA transformation");
+
+    // create a temporary transform which combines toTPA and toFPA.  allow toTPA to have 0th
+    // and 1st order terms
+
+    // XXX require fpa->toTPA->x->nX == fpa->toTPA->x->nY
+    // XXX require fpa->toTPA->y->nX == fpa->toTPA->y->nY
+    // XXX require fpa->toTPA->x->nX == fpa->toTPA->y->nX
+    // XXX require fpa->toTPA->x->coeffMask[1][1]
+    // XXX require fpa->toTPA->y->coeffMask[1][1]
+    // XXX require fpa->toTPA->nX == 1
+
+    psPlaneTransform *toTPA = psPlaneTransformAlloc(chip->toFPA->x->nX, chip->toFPA->x->nY, PS_POLYNOMIAL_ORD);
+
+    for (int i = 0; i <= toTPA->x->nX; i++) {
+        for (int j = 0; j <= toTPA->x->nY; j++) {
+            double f1 = chip->toFPA->x->coeffMask[i][j] ? 0.0 : fpa->toTPA->x->coeff[1][0]*chip->toFPA->x->coeff[i][j];
+            double f2 = chip->toFPA->y->coeffMask[i][j] ? 0.0 : fpa->toTPA->x->coeff[0][1]*chip->toFPA->y->coeff[i][j];
+            toTPA->x->coeff[i][j] = f1 + f2;
+
+            double g1 = chip->toFPA->x->coeffMask[i][j] ? 0.0 : fpa->toTPA->y->coeff[1][0]*chip->toFPA->x->coeff[i][j];
+            double g2 = chip->toFPA->y->coeffMask[i][j] ? 0.0 : fpa->toTPA->y->coeff[0][1]*chip->toFPA->y->coeff[i][j];
+            toTPA->y->coeff[i][j] = g1 + g2;
+        }
+    }
+    toTPA->x->coeff[0][0] += fpa->toTPA->x->coeff[0][0];
+    toTPA->y->coeff[0][0] += fpa->toTPA->y->coeff[0][0];
+
+    pmAstromWCS *wcs = pmAstromWCSAlloc(toTPA->x->nX, toTPA->x->nY);
+
+    // convert projection from FPA to SKY into wcs projection (degrees to radians)
+    wcs->toSky = psProjectionAlloc (fpa->toSky->R, fpa->toSky->D, PM_RAD_DEG, PM_RAD_DEG, fpa->toSky->type);
+    wcs->toSky->radial = psMemIncrRefCounter (fpa->toSky->radial);
+
+    wcs->crval1 = fpa->toSky->R*PS_DEG_RAD;
+    wcs->crval2 = fpa->toSky->D*PS_DEG_RAD;
+
+    // generate a transform that has 0.0 rotation:
+    // get the current posangle of the ref chip
+    // XXX average angles for x and y...
+    float angle = atan2 (toTPA->y->coeff[1][0], toTPA->x->coeff[1][0]);
+    // fprintf (stderr, "angle: %f\n", angle*PS_DEG_RAD);
+    psPlaneTransform *tpa1 = psPlaneTransformRotate (NULL, toTPA, angle);
+
+    // given transformation, solve for coordinates which yields output coordinates of 0,0
+    psPlane *center = psPlaneTransformGetCenter (tpa1, tol);
+    if (!center) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to solve for TPA center.");
+        psFree (toTPA);
+        psFree (tpa1);
+        psFree (wcs);
+        return NULL;
+    }
+
+    // generate transform with the original orientation (does this rotate about 'center'?)
+    psPlaneTransform *tpa2 = psPlaneTransformRotate (NULL, tpa1, -1.0*angle);
+
+    // prove that the center coordinates give 0,0:
+    // float Xo = psPolynomial2DEval (tpa1->x, center->x, center->y);
+    // float Yo = psPolynomial2DEval (tpa1->x, center->x, center->y);
+    // fprintf (stderr, "tpa1: Xo, Yo: %f, %f\n", Xo, Yo);
+
+    // prove that the center coordinates give 0,0:
+    // Xo = psPolynomial2DEval (tpa2->x, center->x, center->y);
+    // Yo = psPolynomial2DEval (tpa2->x, center->x, center->y);
+    // fprintf (stderr, "tpa2: Xo, Yo: %f, %f\n", Xo, Yo);
+
+    // create wcs transform from toFPA, resulting transformation has units of microns/pixel
+    // adjust wcs transform to use center as reference coordinate
+    psPlaneTransformSetCenter (wcs->trans, tpa2, center->x, center->y);
+
+    // calculated center is crpix1,2
+    wcs->crpix1 = center->x;
+    wcs->crpix2 = center->y;
+    psFree (center);
+    psFree (tpa1);
+    psFree (tpa2);
+
+    // pdelt1,2 has units of degrees/micron
+    double pdelt1 = fpa->toSky->Xs * PS_DEG_RAD;
+    double pdelt2 = fpa->toSky->Ys * PS_DEG_RAD;
+
+    // convert wcs->trans to a matrix with units of degrees/pixel
+    for (int i = 0; i <= wcs->trans->x->nX; i++) {
+        for (int j = 0; j <= wcs->trans->x->nY; j++) {
+            wcs->trans->x->coeff[i][j] *= pdelt1;
+            wcs->trans->y->coeff[i][j] *= pdelt2;
+        }
+    }
+
+    // cdelt1,2 has units of degrees/pixel
+    wcs->cdelt1 = hypot (wcs->trans->x->coeff[1][0], wcs->trans->x->coeff[0][1]);
+    wcs->cdelt2 = hypot (wcs->trans->y->coeff[1][0], wcs->trans->y->coeff[0][1]);
+
+    wcs->wcsCDkeys = fpa->wcsCDkeys;
+    psFree (toTPA);
+
+    return wcs;
+}
+
+/* the bilevel astrometry description consists of a polynomial warping from
+   chip coordinates to FPA coordinates (coords->ctype = LIN---WRP), followed
+   by a polynomial representation of the telescope distortion + the projection
+   (coords->ctype = RA---DIS).
+*/
+
+// convert the chip-level toFPA to a wcs polynomial transformation.  the pmAstromWCS
+// structure represents a single layer transformation (e.g., RA-TAN, RA-WRP).  Here we are
+// converting the chip-level to a WRP projection in the structure.  Later, this will be
+// converted to the WCS keywords
+
+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);
+
+    // copy the toFPA x,y, transformations to the wcs version
+    wcs->trans->x = psPolynomial2DCopy (wcs->trans->x, chip->toFPA->x);
+    wcs->trans->y = psPolynomial2DCopy (wcs->trans->y, chip->toFPA->y);
+
+    // 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);
+
+    // reference pixel (CRPIX1,2) is (0.0, 0.0):
+    wcs->crpix1 = 0.0;
+    wcs->crpix2 = 0.0;
+
+    // we need to set CRVAL1,2 for the 0,0 pixel:
+    wcs->crval1 = psPolynomial2DEval (chip->toFPA->x, 0.0, 0.0);
+    wcs->crval2 = psPolynomial2DEval (chip->toFPA->y, 0.0, 0.0);
+
+    wcs->toSky->R = wcs->crval1*PM_RAD_DEG;
+    wcs->toSky->D = wcs->crval2*PM_RAD_DEG;
+
+    // these need to be set to 0.0 since they have been moved to crpix1,crpix2
+    wcs->trans->x->coeff[0][0] = 0.0;
+    wcs->trans->y->coeff[0][0] = 0.0;
+    
+    // output coordinates are in microns : CDELT1,2 has units of microns/pixel
+    wcs->cdelt1 = hypot (wcs->trans->x->coeff[1][0], wcs->trans->x->coeff[0][1]);
+    wcs->cdelt2 = hypot (wcs->trans->y->coeff[1][0], wcs->trans->y->coeff[0][1]);
+
+    return wcs;
+}
+
+// convert the fpa-level toTPA, toSky to a wcs polynomial transformation
+pmAstromWCS *pmAstromWCSBilevelMosaicFromFPA (const pmFPA *fpa, double tol)
+{
+    // XXX require fpa->toTPA->x->nX == fpa->toTPA->x->nY
+    // XXX require fpa->toTPA->y->nX == fpa->toTPA->y->nY
+    // XXX require fpa->toTPA->x->nX == fpa->toTPA->y->nX
+    // XXX require fpa->toTPA->nX == 1,2,3
+    // XXX require fpa->toSky->type == PS_PROJ_TAN
+
+    // convert fpa->toTPA + fpa->toSky to wcs format (DIS)
+    pmAstromWCS *wcs = pmAstromWCSAlloc(fpa->toTPA->x->nX, fpa->toTPA->x->nY);
+
+    // convert projection from TPA to SKY into wcs projection (degrees to radians)
+    wcs->toSky = psProjectionAlloc (fpa->toSky->R, fpa->toSky->D, PM_RAD_DEG, PM_RAD_DEG, PS_PROJ_DIS);
+    wcs->crval1 = fpa->toSky->R*PS_DEG_RAD;
+    wcs->crval2 = fpa->toSky->D*PS_DEG_RAD;
+
+    // given transformation, solve for coordinates which yields output coordinates of 0,0
+    psPlane *center = psPlaneTransformGetCenter (fpa->toTPA, tol);
+    if (!center) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to solve for TPA center.");
+        psFree (wcs);
+        return NULL;
+    }
+
+    // adjust wcs transform to use center as reference coordinate
+    // resulting transformation has units of unity (microns/micron)
+    psPlaneTransformSetCenter (wcs->trans, fpa->toTPA, center->x, center->y);
+
+    // calculated center is crpix1,2
+    wcs->crpix1 = center->x;
+    wcs->crpix2 = center->y;
+    psFree (center);
+
+    // pdelt1,2 has units of degrees/micron
+    double pdelt1 = fpa->toSky->Xs * PS_DEG_RAD;
+    double pdelt2 = fpa->toSky->Ys * PS_DEG_RAD;
+
+    // convert wcs->trans to units of degree/micron
+    for (int i = 0; i <= wcs->trans->x->nX; i++) {
+        for (int j = 0; j <= wcs->trans->x->nY; j++) {
+            wcs->trans->x->coeff[i][j] *= pdelt1;
+            wcs->trans->y->coeff[i][j] *= pdelt2;
+        }
+    }
+
+    // cdelt1,2 has units of degrees/micron
+    wcs->cdelt1 = hypot (wcs->trans->x->coeff[1][0], wcs->trans->x->coeff[0][1]);
+    wcs->cdelt2 = hypot (wcs->trans->y->coeff[1][0], wcs->trans->y->coeff[0][1]);
+
+    return wcs;
+}
+
+static psPlaneTransform *
+linearFitToTransform(psPlaneTransform *trans, psRegion *bounds)
+{
+    int     nSamples = 10;  // 10 samples in each dimension
+
+    double   deltaX = (bounds->x1 - bounds->x0);
+    double   deltaY = (bounds->y1 - bounds->y0);
+
+    psArray *src = psArrayAlloc(nSamples * nSamples);
+    psArray *dst = psArrayAlloc(nSamples * nSamples);
+
+    int k=0;
+    for (int j=0; j<nSamples; j++) {
+        double y = bounds->y0 + (j * deltaY / nSamples);
+        for (int i=0; i<nSamples; i++) {
+            psPlane *s = psPlaneAlloc();
+            s->x = bounds->x0 + (i * deltaX / nSamples);
+            s->y = y;
+            psArraySet(src, k, s);
+            psPlane *d = psPlaneTransformApply(NULL, trans, s);
+            psArraySet(dst, k, d);
+            psFree(s);  // drop our refs to s and d
+            psFree(d);
+            ++k;
+        }
+    }
+
+    psPlaneTransform *newTrans = psPlaneTransformAlloc(1, 1, PS_POLYNOMIAL_ORD);
+
+    if (!psPlaneTransformFit(newTrans, src, dst, 0, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "linear fit to transform failed");
+        return NULL;
+    }
+
+#define noCOMPARE_TRANS
+#ifdef COMPARE_TRANS
+    // compare the computed coordintes from this transform with the original
+    psPlane *new = psPlaneAlloc();
+    printf("   i     chip_x  tpa_x     tpa_x_fit     dx         chip_y    tpa_y     tpa_y_fit     dy     dx > 0.5 || dy > 0.5\n");
+    for (int i=0; i<psArrayLength(dst); i++) {
+        psPlane *d = (psPlane *) psArrayGet(dst, i);
+        psPlane *s = (psPlane *) psArrayGet(src, i);
+
+        new = psPlaneTransformApply(new, newTrans, s);
+
+        double xerr = new->x - d->x;
+        double yerr = new->y - d->y;
+        bool bigerr = (fabs(xerr) > .5) || (fabs(yerr) > .5);
+        printf("%4d %9.2f %9.2f %9.2f %9.4f     %9.2f %9.2f %9.2f %9.4f   %s\n"
+               , i, s->x, new->x, d->x, xerr, s->y, new->y, d->y, yerr, bigerr ? "BIGERR" : "");
+    }
+    psFree(new);
+#endif
+    psArrayElementsFree(src);
+    psFree(src);
+    psArrayElementsFree(dst);
+    psFree(dst);
+
+    return newTrans;
+}
+
+bool pmAstromLinearizeTransforms(pmFPA *inFPA, pmChip *inChip, pmFPA *outFPA, pmChip *outChip, psRegion *outputBounds, double offset_x, double offset_y)
+{
+    PS_ASSERT_PTR_NON_NULL(inFPA, NULL);
+    PS_ASSERT_PTR_NON_NULL(inChip, NULL);
+
+    int ExtraOrders = pmAstrometryGetExtraOrders();
+
+    if (outFPA == NULL) {
+        outFPA = inFPA;
+    }
+    if (outChip == NULL) {
+        outChip = inChip;
+    }
+    if (outputBounds == NULL) {
+        outputBounds = pmChipPixels(outChip);
+    }
+
+    // First combine the "chip to FPA" and "FPA to TPA" into a single transformation
+    psPlaneTransform *chipToTPA = psPlaneTransformCombine(NULL, inChip->toFPA, inFPA->toTPA, *outputBounds, 50);
+    if (!chipToTPA) {
+        psError(PS_ERR_UNKNOWN, false, "failed to create chipToTPA");
+        return false;
+    }
+
+    // Next do the linear fit within the output boundary pixels
+    psPlaneTransform *chipToFPA = linearFitToTransform(chipToTPA, outputBounds);
+    psFree(chipToTPA);
+    if (!chipToFPA) {
+        psError(PS_ERR_UNKNOWN, false, "linear fit of chip to TPA transform failed");
+        return false;
+    }
+
+    // if requested,  change the center
+    psPlaneTransform *outToFPA;
+    if (offset_x != 0. && offset_y != 0.) {
+        outToFPA = psPlaneTransformSetCenter(NULL, chipToFPA, offset_x, offset_y);
+        psFree(chipToFPA);
+    } else {
+        outToFPA = chipToFPA;
+    }
+
+    // NOTE: the extraOrders value (4) should be ignored since outToFPA is specified to be linear
+    psPlaneTransform *outFromFPA = psPlaneTransformInvert(NULL, outToFPA, *outputBounds, 50, ExtraOrders);
+    if (!outFromFPA) {
+        psFree(outToFPA);
+        psError(PS_ERR_UNKNOWN, false, "inversion of fit of output chip toFPA failed");
+        return false;
+    }
+
+    // Success. Now set the fpa's toTPA and fromTPA to identity and replace the chip's transforms.
+
+    psFree(outFPA->toTPA);
+    outFPA->toTPA =  psPlaneTransformIdentity(1);
+
+    psFree(outFPA->fromTPA);
+    outFPA->fromTPA = psPlaneTransformIdentity(1);
+
+    psFree(outChip->toFPA);
+    outChip->toFPA = outToFPA;
+
+    psFree(outChip->fromFPA);
+    outChip->fromFPA = outFromFPA;
+
+    // Finally, change the type for the projection.
+    outFPA->toSky->type = PS_PROJ_TAN;
+
+    return true;
+}
+
+bool pmAstromLinearizeToSky(pmFPA *inFPA, pmChip *inChip, pmFPA *outFPA, pmChip *outChip, psRegion *bounds)
+{
+    PS_ASSERT_PTR_NON_NULL(inFPA, NULL);
+    PS_ASSERT_PTR_NON_NULL(inChip, NULL);
+    PS_ASSERT_PTR_NON_NULL(outFPA, NULL);
+    PS_ASSERT_PTR_NON_NULL(outChip, NULL);
+    PS_ASSERT_PTR_NON_NULL(bounds, NULL);
+
+    // outFPA projection must be defined as the goal
+
+    // the output transformations are:
+    // chip -> FPA : standard linear trans with needed rotation, etc
+    // FPA  -> TPA : identidy
+
+    // NOTE: streaksremove's linearizeTransform function passes pointers to skeleton outFPA and outChip structs
+    // Only outFPA->toSky is valid. No other memebers in these structs should be read. The resulting output transforms
+    // are copied to inFPA and inChip by the caller.
+
+    int nSamples = 10;  // 10 samples in each dimension
+
+    double deltaX = (bounds->x1 - bounds->x0);
+    double deltaY = (bounds->y1 - bounds->y0);
+
+    psArray *src = psArrayAllocEmpty(nSamples * nSamples);
+    psArray *dst = psArrayAllocEmpty(nSamples * nSamples);
+
+    psPlane srcFP, srcTP;
+
+    for (int j = 0; j < nSamples; j++) {
+        double y = bounds->y0 + (j * deltaY / nSamples);
+        for (int i =  0; i < nSamples; i++) {
+
+            psSphere srcSky;
+            psPlane *srcChip = psPlaneAlloc();
+            psPlane *dstTP = psPlaneAlloc();
+
+            srcChip->x = bounds->x0 + (i * deltaX / nSamples);
+            srcChip->y = y;
+
+            psPlaneTransformApply (&srcFP, inChip->toFPA, srcChip);
+            psPlaneTransformApply (&srcTP, inFPA->toTPA, &srcFP);
+            psDeproject (&srcSky, &srcTP, inFPA->toSky);
+
+            // fprintf (stderr, "%f %f | %f %f | %f %f | %f %f\n", srcChip->x, srcChip->y, srcFP.x, srcFP.y, srcTP.x, srcTP.y, srcSky.r*PS_DEG_RAD, srcSky.d*PS_DEG_RAD);
+
+            psProject (dstTP, &srcSky, outFPA->toSky);
+
+            srcChip->x -= bounds->x0;
+            srcChip->y -= bounds->y0;
+            psArrayAdd (src, 100, srcChip);
+            psArrayAdd (dst, 100, dstTP);
+
+            psFree(srcChip);  // drop our refs to s and d
+            psFree(dstTP);
+        }
+    }
+
+    psPlaneTransform *newToFPA = psPlaneTransformAlloc(1, 1, PS_POLYNOMIAL_ORD);
+    newToFPA->x->coeffMask[1][1] = 1;
+    newToFPA->y->coeffMask[1][1] = 1;
+
+    if (!psPlaneTransformFit(newToFPA, src, dst, 0, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "linear fit to transform failed");
+        psFree(src);
+        psFree(dst);
+        return NULL;
+    }
+
+# if (0)
+    for (int i = 0; i < src->n; i++) {
+
+        psSphere srcSky, dstSky;
+        psPlane *srcChip = src->data[i];
+        psPlane *dstTP   = dst->data[i];
+
+        psPlaneTransformApply (&srcFP, newToFPA, srcChip);
+        psDeproject (&srcSky, &srcFP, outFPA->toSky);
+        psDeproject (&dstSky, dstTP, outFPA->toSky);
+
+        double dX = (srcSky.r*PS_DEG_RAD - dstSky.r*PS_DEG_RAD)*3600.0;
+        double dY = (srcSky.d*PS_DEG_RAD - dstSky.d*PS_DEG_RAD)*3600.0;
+        fprintf (stderr, "%f %f | %f %f | %f %f | %f %f | %f %f | %f %f\n", dX, dY, srcChip->x, srcChip->y, srcFP.x, srcFP.y, dstTP->x, dstTP->y, srcSky.r*PS_DEG_RAD, srcSky.d*PS_DEG_RAD, dstSky.r*PS_DEG_RAD, dstSky.d*PS_DEG_RAD);
+
+    }
+# endif
+
+    psFree(src);
+    psFree(dst);
+
+    // this is a linear transformation, no extra orders are needed
+    psPlaneTransform *newFromFPA = psPlaneTransformInvert(NULL, newToFPA, *bounds, 1, 0);
+    if (!newFromFPA) {
+        psFree(newToFPA);
+        psError(PS_ERR_UNKNOWN, false, "inversion of fit of output chip toFPA failed");
+        return false;
+    }
+
+    // Success. Now set the fpa's toTPA and fromTPA to identity and replace the chip's transforms.
+    psFree(outChip->toFPA);
+    outChip->toFPA = newToFPA;
+
+    psFree(outChip->fromFPA);
+    outChip->fromFPA = newFromFPA;
+
+    psFree(outFPA->toTPA);
+    outFPA->toTPA =  psPlaneTransformIdentity(1);
+
+    psFree(outFPA->fromTPA);
+    outFPA->fromTPA = psPlaneTransformIdentity(1);
+
+    return true;
+}
+
+static void pmAstromWCSFree (pmAstromWCS *wcs)
+{
+
+    if (!wcs)
+        return;
+    psFree (wcs->trans);
+    psFree (wcs->toSky);
+}
+
+pmAstromWCS *pmAstromWCSAlloc (int nXorder, int nYorder)
+{
+
+    pmAstromWCS *wcs = (pmAstromWCS *) psAlloc(sizeof(pmAstromWCS));
+    psMemSetDeallocator(wcs, (psFreeFunc) pmAstromWCSFree);
+
+    // note: WCS transforms are always defined as chip to sky
+    wcs->trans = psPlaneTransformAlloc (nXorder, nYorder, PS_POLYNOMIAL_ORD);
+    wcs->toSky = NULL;
+    wcs->wcsCDkeys = 0;
+
+    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_branches/psModules.20240412/src/astrom/pmAstrometryWCS.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryWCS.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmAstrometryWCS.h	(revision 42651)
@@ -0,0 +1,82 @@
+/* @file  pmAstrometryDistortion.h
+ * @brief functions to convert FITS WCS keywords to / from pmFPA structures
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.12 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-01-24 01:08:17 $
+ * 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;
+    bool wcsCDkeys;
+    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);
+
+bool pmAstromLinearizeTransforms(pmFPA *inFPA, pmChip *inChip, pmFPA *outFPA, pmChip *outChip, psRegion *outputBounds, double offset_x, double offset_y);
+bool pmAstromLinearizeToSky(pmFPA *inFPA, pmChip *inChip, pmFPA *outFPA, pmChip *outChip, psRegion *bounds);
+
+// move to pslib
+psPlaneDistort *psPlaneDistortIdentity (int order);
+bool psPlaneDistortIsDiagonal (psPlaneDistort *distort);
+
+// XXX probably should remove these and just use the PS_ version in the code
+# define PM_DEG_RAD PS_DEG_RAD
+# define PM_RAD_DEG PS_RAD_DEG
+
+/// @}
+#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_branches/psModules.20240412/src/astrom/pmKHcorrect.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmKHcorrect.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmKHcorrect.c	(revision 42651)
@@ -0,0 +1,252 @@
+/** @file  pmKHcorrect.c
+ *  @brief Functions to read (and write?) Koppenhoefer correction file
+ *
+ *  The Koppenhoefer correction is needed for some chips of gpc1 before the camera voltages were adjusted 2011/05/11.
+ *  The correction is a modification of the X (and possibly Y) coordinate of a star which depends on the instrumental 
+ *  surface brightness, defined as -2.5 log_10 (DN) + 5.0 log_10 fwhm_maj [XXX be careful about the definition of fwhm_maj]
+ *
+ *  @ingroup AstroImage
+ *  @author EAM, IfA
+ *
+ *  Copyright 2014 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+/******************************************************************************/
+/*  INCLUDE FILES                                                             */
+/******************************************************************************/
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <unistd.h>   // for unlink
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPAExtent.h"
+#include "pmFPAfileFitsIO.h"
+#include "pmConcepts.h"
+#include "pmKHcorrect.h"
+
+static void KHcorrectDataFree (KHcorrectData *spline) {
+
+    if (!spline) return;
+
+    psFree (spline->xk);
+    psFree (spline->yk);
+    psFree (spline->y2);
+    return;
+}
+
+KHcorrectData *KHcorrectDataAlloc (int Nrow) {
+
+    // allocate xk[Nrow], etc
+
+    KHcorrectData *spline = (KHcorrectData *) psAlloc(sizeof(KHcorrectData));
+    psMemSetDeallocator(spline, (psFreeFunc) KHcorrectDataFree);
+
+    spline->N  = Nrow;
+    spline->xk = (float *) psAlloc(Nrow*sizeof(float));
+    spline->yk = (float *) psAlloc(Nrow*sizeof(float));
+    spline->y2 = (float *) psAlloc(Nrow*sizeof(float));
+
+    return spline;
+}
+
+// the KH correction is a function of the instrumental surface brightness, SBinst
+float KHcorrectApply (KHcorrectData *spline, float X) {
+
+    int N = spline->N;
+
+    // saturate correction at high and low ends
+    if (X < spline->xk[  0]) return spline->yk[  0];
+    if (X > spline->xk[N-1]) return spline->yk[N-1];
+
+    float *xk = spline->xk;
+    float *yk = spline->yk;
+    float *y2 = spline->y2;
+
+    /* find correct element in array (x must be sorted) */
+    int lo = 0;
+    int hi = N-1;
+    while (hi - lo > 1) {
+	int i = 0.5*(hi+lo);
+	if (xk[i] > X) {
+	    hi = i;
+	} else {
+	    lo = i;
+	}
+    }
+
+    /* error condition: duplicate abssisca */
+    float dx = xk[hi] - xk[lo];
+    if (dx == 0.0) {
+	return (0.0);
+    }
+
+    /* evaluate spline */
+    float a = (xk[hi] - X) / dx;
+    float b = (X - xk[lo]) / dx;
+
+    float value = a*yk[lo] + b*yk[hi] + ((a*a - 1.0)*a*y2[lo] + (b*b - 1.0)*b*y2[hi])*(dx*dx) / 6.0;
+    return (value);
+}
+
+/********************* CheckDataStatus functions *****************************/
+
+bool pmKHcorrectCheckDataStatusForView (const pmFPAview *view, pmFPAfile *file) {
+    psError(PS_ERR_IO, false, "Check Data Status not defined");
+    return false;
+}
+
+bool pmKHcorrectCheckDataStatusForFPA (const pmFPA *fpa) {
+    psError(PS_ERR_IO, false, "Check Data Status not defined");
+    return false;
+}
+
+bool pmKHcorrectCheckDataStatusForChip (const pmChip *chip) {
+    psError(PS_ERR_IO, false, "Check Data Status not defined");
+    return false;
+}
+
+/********************* Write Data functions *****************************/
+
+// NOTE : these are not exposed because I don't think we need them (we do not create KHcorrect
+// in psModules based tool, but in DVO.
+
+bool pmKHcorrectWriteFPA (pmFPAfile *file, const pmFPA *fpa);
+bool pmKHcorrectWriteForView (const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    // write the full model in one pass: require the level to be FPA
+    if (view->chip != -1) {
+        psError(PS_ERR_IO, false, "Koppenhoefer Correction must be written at the FPA level");
+        return false;
+    }
+
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // Suitable FPA for writing
+
+    if (!pmKHcorrectWriteFPA(file, fpa)) {
+        psError(PS_ERR_IO, false, "Failed to write KH Correction for fpa");
+        psFree(fpa);
+        return false;
+    }
+
+    psFree(fpa);
+
+    return true;
+}
+
+// write out all chip-level KH Correction data for this FPA
+bool pmKHcorrectWriteFPA (pmFPAfile *file, const pmFPA *fpa)
+{
+    psError(PS_ERR_IO, false, "output for KH Correction is not defined");
+    return false;
+}
+
+/********************* Read Data functions *****************************/
+
+bool pmKHcorrectReadForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+    {
+        // read the full model in one pass: require the level to be FPA
+        if (view->chip != -1) {
+            psError(PS_ERR_IO, false, "KH Correction must be read at the FPA level");
+            return false;
+        }
+
+        if (!pmKHcorrectReadFPA (file)) {
+            psError(PS_ERR_IO, false, "Failed to read KH Correction for fpa");
+            return false;
+        }
+        return true;
+    }
+
+// read in all chip-level KH Correction data for this FPA
+bool pmKHcorrectReadFPA (pmFPAfile *file) {
+
+    if (!pmKHcorrectReadChips (file)) {
+        psError(PS_ERR_IO, false, "Failed to read KH Correction for chips");
+        return false;
+    }
+
+    return true;
+}
+
+// read the set of tables, one for each chip
+bool pmKHcorrectReadChips (pmFPAfile *file) {
+
+    bool haveData, status;
+
+    // loop over the extensions
+    // for each extension, use the extname (eg, XY01.DX.T0) to assign to a chip
+
+    // move to the start of the file
+    haveData = psFitsMoveExtNum (file->fits, 1, false);
+    if (!haveData) {
+        psError(PS_ERR_IO, false, "Failed to read even the first extension?");
+        return false;
+    }
+
+    while (haveData) {
+
+	// load the header
+	psMetadata *header = psFitsReadHeader(NULL, file->fits); // The FITS header
+	if (!header) psAbort("cannot read model header");
+
+	// load the full model in one shot
+	psArray *model = psFitsReadTable (file->fits);
+	if (!model) psAbort("cannot read model");
+	
+	// determine the chip:
+	char *extname = psMetadataLookupStr (&status, header, "EXTNAME");
+	psLogMsg ("psModules.astrom", 4, "read %ld rows from Koppenhoefer correction file, extname %s\n", model->n, extname);
+
+	// I expect to find a name of the form: chipName.dir.tset (eg, XY01.DX.T0)
+	// where chipName like 'XY01'
+	// dir = 'DX' 
+	// tset = 'T0'
+	psAssert (strlen(extname) == 10, "invalid extension %s", extname);
+	psAssert (extname[5] == 'D', "invalid extension %s", extname);
+	psAssert (extname[6] == 'X', "invalid extension %s", extname);
+	psAssert (extname[8] == 'T', "invalid extension %s", extname);
+	psAssert (extname[9] == '0', "invalid extension %s", extname);
+
+	char chipName[5];
+	strncpy (chipName, extname, 4);
+	chipName[4] = 0;
+
+	pmChip *chip = pmConceptsChipFromName (file->fpa, chipName);
+	if (!chip) psAbort ("invalid chip?");
+
+	KHcorrectData *spline = KHcorrectDataAlloc (model->n);
+
+	// parse the model entries
+	for (int i = 0; i < model->n; i++) {
+	    psMetadata *row = model->data[i];
+
+	    spline->xk[i] = psMetadataLookupF32(&status, row, "X_KNOT");
+	    spline->yk[i] = psMetadataLookupF32(&status, row, "Y_KNOT");
+	    spline->y2[i] = psMetadataLookupF32(&status, row, "DY2_DX");
+	}
+	psMetadataAddUnknown (chip->analysis, PS_LIST_TAIL, "KH.CORRECT", PS_META_REPLACE, "", spline);
+	psFree (spline);
+
+	psFree (model);
+	psFree (header);
+
+	// move to the next extension
+	haveData = psFitsMoveExtNum (file->fits, 1, true);
+    }
+
+    return true;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/astrom/pmKHcorrect.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/astrom/pmKHcorrect.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/astrom/pmKHcorrect.h	(revision 42651)
@@ -0,0 +1,35 @@
+/** @file  pmKHcorrect.h
+ *  @brief Functions to read (and write?) Koppenhoefer correction file
+ *
+ *  The Koppenhoefer correction is needed for some chips of gpc1 before the camera voltages were adjusted 2011/05/11.
+ *  The correction is a modification of the X (and possibly Y) coordinate of a star which depends on the instrumental 
+ *  surface brightness, defined as -2.5 log_10 (DN) + 5.0 log_10 fwhm_maj [XXX be careful about the definition of fwhm_maj]
+ *
+ *  @ingroup AstroImage
+ *  @author EAM, IfA
+ *
+ *  Copyright 2014 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_KH_CORRECT_H
+#define PM_KH_CORRECT_H
+
+/// @addtogroup Astrometry
+/// @{
+
+typedef struct {
+    int N;
+    float *xk;
+    float *yk;
+    float *y2;
+} KHcorrectData;
+
+KHcorrectData *KHcorrectDataAlloc (int Nrow);
+float KHcorrectApply (KHcorrectData *spline, float X);
+
+bool pmKHcorrectReadForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmKHcorrectReadFPA (pmFPAfile *file);
+bool pmKHcorrectReadChips (pmFPAfile *file);
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/camera/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/Makefile.am	(revision 42651)
@@ -0,0 +1,65 @@
+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 \
+	pmFPAfileFringeIO.c \
+	pmFPAFlags.c \
+	pmFPALevel.c \
+	pmFPAExtent.c \
+	pmCellSquish.c \
+	pmReadoutStack.c \
+	pmReadoutFake.c \
+	pmFPABin.c \
+	pmFPAExpNumIO.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 \
+	pmFPAfileFringeIO.h \
+	pmFPAFlags.h \
+	pmFPALevel.h \
+	pmFPAExtent.h \
+	pmCellSquish.h \
+	pmReadoutStack.h \
+	pmReadoutFake.h \
+	pmFPABin.h \
+	pmFPAExpNumIO.h
+
+CLEANFILES = *~
Index: /branches/eam_branches/psModules.20240412/src/camera/pmCellSquish.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmCellSquish.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmCellSquish.c	(revision 42651)
@@ -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, psImageMaskType 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_IMAGE_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_IMAGE_MASK_DATA[ySquish][xSquish] |= mask->data.PS_TYPE_IMAGE_MASK_DATA[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_branches/psModules.20240412/src/camera/pmCellSquish.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmCellSquish.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmCellSquish.h	(revision 42651)
@@ -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
+                  psImageMaskType maskVal,   ///< Value to be masked
+                  bool useShifts        ///< Use the shifts when squishing?
+    );
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPA.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPA.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPA.c	(revision 42651)
@@ -0,0 +1,473 @@
+#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->variance);
+    psFree(readout->covariance);
+    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);
+
+    psFree(chip->toFPA);
+    psFree(chip->fromFPA);
+}
+
+
+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(fpa->camera);
+    psFree(fpa->hdu);
+
+    psFree(fpa->fromTPA);
+    psFree(fpa->toTPA);
+    psFree(fpa->toSky);
+}
+
+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->variance);
+    psFree(readout->covariance);
+    psFree(readout->bias);
+
+    psTrace("psModules.camera", 3, "Freeing readout data for %zd\n", (size_t) readout);
+
+    readout->image = NULL;
+    readout->variance = NULL;
+    readout->covariance = NULL;
+    readout->mask = NULL;
+
+    readout->bias = psListAlloc(NULL);
+
+    readout->col0 = 0;
+    readout->row0 = 0;
+
+    readout->thisImageScan = 0;
+    readout->thisMaskScan = 0;
+    readout->thisVarianceScan = 0;
+
+    readout->lastImageScan = 0;
+    readout->lastMaskScan = 0;
+    readout->lastVarianceScan = 0;
+}
+
+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->variances);
+        psFree(cell->hdu->masks);
+        // psFree(cell->hdu->header);
+
+        cell->hdu->images = NULL;
+        cell->hdu->variances = 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->variances);
+        psFree(chip->hdu->masks);
+        // psFree(chip->hdu->header);
+
+        chip->hdu->images = NULL;
+        chip->hdu->variances = 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->variances);
+        psFree(fpa->hdu->masks);
+        // psFree(fpa->hdu->header);
+
+        fpa->hdu->images = NULL;
+        fpa->hdu->variances = 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->variance = NULL;
+    tmpReadout->covariance = 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;
+
+    tmpReadout->thisImageScan = 0;
+    tmpReadout->thisMaskScan = 0;
+    tmpReadout->thisVarianceScan = 0;
+
+    tmpReadout->lastImageScan = 0;
+    tmpReadout->lastMaskScan = 0;
+    tmpReadout->lastVarianceScan = 0;
+
+    tmpReadout->forceScan = false;
+
+    return(tmpReadout);
+}
+
+bool psMemCheckReadout(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) readoutFree);
+}
+
+
+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();
+    if (!psMetadataAddStr(tmpCell->concepts, PS_LIST_HEAD, "CELL.NAME", 0, NULL, name)) {
+        psErrorClear();
+        psWarning("Could not add CELL.NAME to metadata.");
+    }
+    tmpCell->conceptsRead = PM_CONCEPT_SOURCE_NONE;
+    pmConceptsBlankCell(tmpCell);
+
+    return tmpCell;
+}
+
+bool psMemCheckCell(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) cellFree);
+}
+
+
+pmChip *pmChipAlloc(
+    pmFPA *fpa,
+    const char *name)
+{
+    pmChip *tmpChip = (pmChip*)psAlloc(sizeof(pmChip));
+    psMemSetDeallocator(tmpChip, (psFreeFunc) chipFree);
+
+    tmpChip->toFPA = NULL;
+    tmpChip->fromFPA = NULL;
+
+    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();
+    if (!psMetadataAddStr(tmpChip->concepts, PS_LIST_HEAD, "CHIP.NAME", 0, NULL, name)) {
+        psErrorClear();
+        psWarning("Could not add CHIP.NAME %s to concepts.", name);
+    }
+    tmpChip->conceptsRead = PM_CONCEPT_SOURCE_NONE;
+    pmConceptsBlankChip(tmpChip);
+    return tmpChip;
+}
+
+bool psMemCheckChip(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) chipFree);
+}
+
+pmFPA *pmFPAAlloc(const psMetadata *camera, const char *cameraName)
+{
+    pmFPA *tmpFPA = (pmFPA *) psAlloc(sizeof(pmFPA));
+    psMemSetDeallocator(tmpFPA, (psFreeFunc) FPAFree);
+
+    tmpFPA->fromTPA = NULL;
+    tmpFPA->toTPA = NULL;
+    tmpFPA->toSky = NULL;
+    tmpFPA->wcsCDkeys = false;
+
+    tmpFPA->analysis = psMetadataAlloc();
+    tmpFPA->camera = psMemIncrRefCounter((psPtr) camera);
+    tmpFPA->chips = psArrayAlloc(0);
+    tmpFPA->hdu = NULL;
+
+    tmpFPA->concepts = psMetadataAlloc();
+    if (!psMetadataAddStr(tmpFPA->concepts, PS_LIST_TAIL, "FPA.CAMERA", PS_META_REPLACE,
+                          "Camera name (according to configuration)", cameraName)) {
+        psErrorClear();
+        psWarning("Could not add FPA.CAMERA %s to concepts.", cameraName);
+    }
+    tmpFPA->conceptsRead = PM_CONCEPT_SOURCE_NONE;
+    pmConceptsBlankFPA(tmpFPA);
+
+    // this may be somewhat pedantic, but it makes these things consistent
+    if (!psMetadataAddStr(tmpFPA->concepts, PS_LIST_TAIL, "FPA.NAME", PS_META_REPLACE,
+                          "name of FPA level", "fpa")) {
+        psErrorClear();
+        psWarning("Could not add FPA.NAME to concepts.");
+    }
+
+    return tmpFPA;
+}
+
+bool psMemCheckFPA(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) FPAFree);
+}
+
+
+// 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, false);
+
+    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_branches/psModules.20240412/src/camera/pmFPA.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPA.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPA.h	(revision 42651)
@@ -0,0 +1,251 @@
+/* @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.26 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-06 02:31:24 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_H
+#define PM_FPA_H
+
+#include <pslib.h>
+#include <pmHDU.h>
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+// 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 {
+    // 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
+    bool wcsCDkeys;
+    // 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 {
+    // Astrometric transformations
+    psPlaneTransform *toFPA;            ///< Transformation from chip to FPA coordinates, or NULL
+    psPlaneTransform *fromFPA;          ///< Transformation from FPA to chip coordinates, or NULL
+    // 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 variance
+/// 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 *variance;                  ///< Variance of input image (corresponds to CELL.TRIMSEC region)
+    psKernel *covariance;               ///< Covariance pseudo-matrix (covariance factors for single pixel)
+    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)?
+    int thisImageScan;                  ///< start scan for next/current read of image
+    int lastImageScan;                  ///< start scan of the last read of image
+    int thisMaskScan;                   ///< start scan for next/current read of mask
+    int lastMaskScan;                   ///< start scan of the last read of mask
+    int thisVarianceScan;               ///< start scan for next/current read of variance
+    int lastVarianceScan;               ///< start scan of the last read of variance
+    bool forceScan;                     ///< Force pmFPARead to obey the above commanded this and last scans.
+} 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
+bool psMemCheckReadout(psPtr ptr);
+
+/// 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
+bool psMemCheckCell(psPtr ptr);
+
+
+/// 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
+bool psMemCheckChip(psPtr ptr);
+
+
+/// Allocate an FPA
+pmFPA *pmFPAAlloc(const psMetadata *camera, ///< Camera configuration (to store in FPA)
+                  const char *cameraName ///< Name of camera (for FPA.CAMERA concept)
+    );
+bool psMemCheckFPA(psPtr ptr);
+
+/// 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
+
+
+/// Assertions
+
+/// Check that the fundamentals of a readout are set
+#define PM_ASSERT_READOUT_NON_NULL(READOUT, RETVAL) { \
+    if (!(READOUT) || !(READOUT)->bias || !(READOUT)->analysis) { \
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Readout %s or one of its components is NULL.", #READOUT); \
+        return RETVAL; \
+    } \
+    int numCols = 0, numRows = 0; /* Size of readout images */ \
+    psImage *image = (READOUT)->image; /* Image pixels */ \
+    if (image) { \
+        PS_ASSERT_IMAGE_TYPE((READOUT)->image, PS_TYPE_F32, RETVAL); \
+        numCols = image->numCols; \
+        numRows = image->numRows; \
+    } \
+    psImage *mask = (READOUT)->mask; /* Mask pixels */ \
+    if (mask) { \
+        PS_ASSERT_IMAGE_NON_NULL((READOUT)->mask, RETVAL); \
+        if ((numCols != 0 || numRows != 0) && (mask->numCols != numCols || mask->numRows != numRows)) { \
+            psError(PS_ERR_BAD_PARAMETER_SIZE, true, "Mask in readout %s has wrong size (%dx%d vs %dx%d)", \
+                    #READOUT, mask->numCols, mask->numRows, numCols, numRows); \
+            return RETVAL; \
+        } else { \
+            numCols = mask->numCols; \
+            numRows = mask->numRows; \
+        } \
+    } \
+    psImage *variance = (READOUT)->variance; /* Variance map pixels */ \
+    if (variance) { \
+        PS_ASSERT_IMAGE_NON_NULL((READOUT)->variance, RETVAL); \
+        if ((numCols != 0 || numRows != 0) && \
+            (variance->numCols != numCols || variance->numRows != numRows)) { \
+            psError(PS_ERR_BAD_PARAMETER_SIZE, true, \
+                    "Variance in readout %s has wrong size (%dx%d vs %dx%d)", \
+                    #READOUT, variance->numCols, variance->numRows, numCols, numRows); \
+            return RETVAL; \
+        } \
+    } \
+}
+
+/// Assert that a readout contains an image
+#define PM_ASSERT_READOUT_IMAGE(READOUT, RETVAL) \
+    PS_ASSERT_IMAGE_NON_NULL((READOUT)->image, RETVAL);
+
+/// Assert that a readout contains a mask
+#define PM_ASSERT_READOUT_MASK(READOUT, RETVAL) \
+    PS_ASSERT_IMAGE_NON_NULL((READOUT)->mask, RETVAL);
+
+/// Assert that a readout contains a variance map
+#define PM_ASSERT_READOUT_VARIANCE(READOUT, RETVAL) \
+    PS_ASSERT_IMAGE_NON_NULL((READOUT)->variance, RETVAL);
+
+/// Assert that a readout contains a covariance matrix
+#define PM_ASSERT_READOUT_COVARIANCE(READOUT, RETVAL) \
+    PS_ASSERT_KERNEL_NON_NULL((READOUT)->covariance, RETVAL);
+
+/// @}
+#endif // #ifndef PM_FPA_H
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAAstrometry.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAAstrometry.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAAstrometry.c	(revision 42651)
@@ -0,0 +1,521 @@
+#include <stdio.h>
+#include "pslib.h"
+#include "pmFPAAstrometry.h"
+#include "pmFPA.h"
+
+/********** 
+
+	    EAM : this file is not included in the psModules build
+	    alternative versions are defined by psastroMaskUtils.c
+
+ *********/
+
+
+/*****************************************************************************
+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);
+}
+
+
+// EAM : this function does not handle non-linear transformations.  is it used?
+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);
+}
+
+// EAM : this function does not handle non-linear transformations.  is it used?
+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_branches/psModules.20240412/src/camera/pmFPAAstrometry.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAAstrometry.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAAstrometry.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/camera/pmFPABin.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPABin.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPABin.c	(revision 42651)
@@ -0,0 +1,137 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmFPA.h"
+#include "pmFPABin.h"
+
+bool pmReadoutRebin(pmReadout *out, const pmReadout *in, psImageMaskType maskVal, int xBin, int yBin)
+{
+    PM_ASSERT_READOUT_NON_NULL(out, false);
+    PM_ASSERT_READOUT_NON_NULL(in, false);
+
+    psImage *inImage = in->image, *inMask = in->mask; // Input image
+    int numColsIn = inImage->numCols, numRowsIn = inImage->numRows; // Size of input image
+
+    psImageBinning *binning = psImageBinningAlloc(); // Binning instructions
+    binning->nXbin = xBin;
+    binning->nYbin = yBin;
+    binning->nXfine = numColsIn;
+    binning->nYfine = numRowsIn;
+    binning->nXskip = 0;
+    binning->nYskip = 0;
+    psImageBinningSetRuffSize(binning, PS_IMAGE_BINNING_CENTER);
+
+    int numColsOut = binning->nXruff, numRowsOut = binning->nYruff; // Size of output image
+
+
+    psImage *outImage;                  // Output image
+    if (out->image && out->image->numCols >= numColsOut && out->image->numRows >= numRowsOut) {
+	outImage = out->image;
+    } else {
+	outImage = out->image = psImageRecycle(out->image,  numColsOut, numRowsOut, PS_TYPE_F32);
+    }
+
+    psImage *outMask;                   // Output mask
+    if (out->mask && out->mask->numCols >= numColsOut && out->mask->numRows >= numRowsOut) {
+	outMask = out->mask;
+    } else {
+	outMask = out->mask = psImageRecycle(out->mask,  numColsOut, numRowsOut, PS_TYPE_IMAGE_MASK);
+    }
+
+    int Nbits = (int) (ceil(log(maskVal)/log(2)) + 1);
+    int *bitcounter = malloc(sizeof(int) * Nbits);
+    int pxlcount;
+
+    int xLast = numColsIn - 1, yLast = numRowsIn - 1; // Last index
+    int yStart = psImageBinningGetFineY(binning, 0); // Starting input y for binning
+    for (int yOut = 0; yOut < numRowsOut; yOut++) {
+	int yStop = psImageBinningGetFineY(binning, yOut + 1); // Stopping input y for binning
+	yStop = PS_MIN(yStop, yLast);
+	int xStart = psImageBinningGetFineX(binning, 0); // Starting input x for binning
+	for (int xOut = 0; xOut < numColsOut; xOut++) {
+	    int xStop = psImageBinningGetFineX(binning, xOut + 1); // Stopping input x for binning
+	    xStop = PS_MIN(xStop, xLast);
+
+	    float sum = 0.0;            // Sum of pixels
+	    int numPix = 0;             // Number of pixels
+
+	    for (int j = 0; j < Nbits; j++) { // Reset bit counter
+		bitcounter[j] = 0;
+	    }
+	    pxlcount = 0;
+	    
+	    for (int y = yStart; y < yStop; y++) {
+		for (int x = xStart; x < xStop; x++) {
+		    if (false && inMask && (inMask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] != 0)) {
+			for (int j = 0; j < Nbits; j++) {
+			    psImageMaskType M = (psImageMaskType) pow(2,j);
+			    if (inMask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & M) {
+				bitcounter[j]++;
+			    }
+			}
+		    }
+		  
+		    if (inMask && (inMask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskVal)) {
+			continue;
+		    }
+		    if (!isfinite(inImage->data.F32[y][x])) {
+			continue;
+		    }
+		    sum += inImage->data.F32[y][x];
+		    numPix++;
+
+
+		}
+	    }
+	    
+	    // Values to set
+	    float imageValue;
+	    psImageMaskType maskValue;
+	    if (numPix > 0) {
+		imageValue = sum / numPix;
+		maskValue = 0;
+	    } else {
+		imageValue = NAN;
+		maskValue = maskVal;
+	    }
+	    outImage->data.F32[yOut][xOut] = imageValue;
+	    if (true) {
+		outMask->data.PS_TYPE_IMAGE_MASK_DATA[yOut][xOut] = maskValue;
+	    } else {
+		outMask->data.PS_TYPE_IMAGE_MASK_DATA[yOut][xOut] = 0;
+	    }
+	    // this loop is pointless if pxlcount == 0 (all masked)
+	    if (false) {
+		if (pxlcount) {
+		    for (int j = 0; j < Nbits; j++) {
+			if (bitcounter[j] > 0.5 * pxlcount) {
+			    outMask->data.PS_TYPE_IMAGE_MASK_DATA[yOut][xOut] |= (1 << j);
+			}
+		    }
+		} else {
+		    outMask->data.PS_TYPE_IMAGE_MASK_DATA[yOut][xOut] = maskValue;
+		}
+	    }
+	    xStart = xStop;
+	}
+	yStart = yStop;
+    }
+
+    psFree(binning);
+
+    out->data_exists = true;
+    if (out->parent) {
+	pmCell *outCell = out->parent;  // Output cell
+	outCell->data_exists = outCell->parent->data_exists = true;
+
+	// We would copy the concepts from the input cell, except that is done by pmFPACopy,
+	// pmChipCopyStructure, etc.  This function just does the mechanics of binning.
+	// We don't even update the CELL.XBIN, CELL.YBIN because that would apply the correction twice.
+    }
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPABin.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPABin.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPABin.h	(revision 42651)
@@ -0,0 +1,17 @@
+#ifndef PM_FPA_BIN_H
+#define PM_FPA_BIN_H
+
+#include <pslib.h>
+
+#include <pmFPA.h>
+
+/// Rebin a readout
+bool pmReadoutRebin(pmReadout *out,     ///< Output readout
+                    const pmReadout *in,///< Input readout
+                    psImageMaskType maskVal, ///< Value to mask
+                    int xBin, int yBin  ///< Binning factors in x and y
+    );
+
+
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPACalibration.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPACalibration.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPACalibration.c	(revision 42651)
@@ -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_branches/psModules.20240412/src/camera/pmFPACalibration.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPACalibration.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPACalibration.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/camera/pmFPAConstruct.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAConstruct.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAConstruct.c	(revision 42651)
@@ -0,0 +1,1579 @@
+#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 "pmConceptsRead.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) {
+        psWarning("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
+    int num = values->n; // number of parsed content elements
+
+    // extend the arrays if they exist, create new ones if they don't
+    if (first && !*first) {
+        *first = psArrayAllocEmpty(values->n);
+    }
+    if (second && !*second) {
+        *second = psArrayAllocEmpty(values->n);
+    }
+    if (third && !*third) {
+        *third = psArrayAllocEmpty(values->n);
+    }
+
+    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:
+            psArrayAdd(*third, 8, fst->data[2]);
+          case 2:
+            psArrayAdd(*second, 8, fst->data[1]);
+          case 1:
+            psArrayAdd(*first, 8, fst->data[0]);
+            break;
+        default:
+          psAbort("Should never get here.");
+        }
+        psFree(fst);
+    }
+    psFree(valuesIter);
+    psFree(values);
+
+    return num;
+}
+
+// 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);
+
+    // XXXX here is the issue : we need to avoid raising an error here
+    if (fpa->hdu) {
+        // Something's already here
+        if (fpa->hdu != hdu) {
+# define TEST1 1
+# if (TEST1)
+	    psError(PS_ERR_IO, true, "Unable to add HDU since FPA already has one.\n");
+# else
+	    psWarning ("Unable to add HDU since FPA already has one.\n");
+# endif
+        }
+# if (TEST1)
+        return false;
+# else
+        return true;
+# endif
+    }
+    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) {
+            psWarning("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 psMetadataItem *chipContents, // Contents of chip, cellName:cellType pairs (either in a string or a metadata)
+                        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
+
+    int nParsed = 0;
+
+    switch (chipContents->type) {
+      case PS_DATA_STRING: {
+          nParsed = parseContent(&cellNames, &cellTypes, NULL, chipContents->data.str);
+          break;
+      }
+      case PS_DATA_METADATA: {
+          psMetadataIterator *iter = psMetadataIteratorAlloc(chipContents->data.md, PS_LIST_HEAD, NULL); // Iterator
+          psMetadataItem *item;           // Item from iteration
+          while ((item = psMetadataGetAndIncrement(iter))) {
+              if (item->type != PS_DATA_STRING) {
+                  psWarning ("Item %s in camera format chip table is not of type STR.", item->name);
+                  continue;
+              }
+              nParsed += parseContent(&cellNames, &cellTypes, NULL, item->data.str);
+          }
+          psFree (iter);
+          break;
+      }
+      default:
+        psWarning ("Item %s in camera format chip table is not of type STR.", chipContents->name);
+        break;
+    }
+
+    if (nParsed == 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 psMetadataItem *chipContents = psMetadataLookup(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
+        if (!content) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to determine content of file.");
+            return false;
+        }
+
+        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))) {
+        pmHDU *hdu = pmHDUAlloc(contentItem->name); // 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, 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
+        if (!content) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to find CONTENT entry in header");
+            return false;
+        }
+
+        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?
+    psMetadataItem *chipContents = psMetadataLookup(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 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
+
+    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)) {
+		      psError(PS_ERR_UNKNOWN, false, "Unable to add HDU to FPA.");
+                      psFree(phdu);
+                      psFree(view);
+                      return NULL;
+		  }		  
+                  if (!addSource_FPA_CHIP(fpa, format)) {
+                      psError(PS_ERR_UNKNOWN, false, "Unable to add source.");
+                      psFree(phdu);
+                      psFree(view);
+                      return NULL;
+                  }
+                  if (!pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_PHU, true, NULL)) {
+                      psError(PS_ERR_UNKNOWN, false, "Unable to read concepts.");
+                      psFree(phdu);
+                      psFree(view);
+                      return NULL;
+                  }
+	      }
+              psFree(phdu);
+              return view;
+            case PM_FPA_LEVEL_CELL:
+	      phdu->blankPHU = true;
+              if (install) {
+                  if (!addHDUtoFPA(fpa, phdu)) {
+                      psError(PS_ERR_UNKNOWN, false, "Unable to add HDU to FPA.");
+                      psFree(phdu);
+                      psFree(view);
+                      return NULL;
+                  }
+                  if (!addSource_FPA_CELL(fpa, format)) {
+                      psError(PS_ERR_UNKNOWN, false, "Unable to add source.");
+                      psFree(phdu);
+                      psFree(view);
+                      return NULL;
+                  }
+                  if (!pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_PHU, true, NULL)) {
+                      psError(PS_ERR_UNKNOWN, false, "Unable to read concepts.");
+                      psFree(phdu);
+                      psFree(view);
+                      return NULL;
+                  }
+              }
+              psFree(phdu);
+              return view;
+            case PM_FPA_LEVEL_NONE:
+	      phdu->blankPHU = false;
+              if (install) {
+                  if (!addHDUtoFPA(fpa, phdu)) {
+                      psError(PS_ERR_UNKNOWN, false, "Unable to add HDU to FPA.");
+                      psFree(phdu);
+                      psFree(view);
+                      return NULL;
+                  }
+                  if (!addSource_FPA_NONE(fpa, format)) {
+                      psError(PS_ERR_UNKNOWN, false, "Unable to add source.");
+                      psFree(phdu);
+                      psFree(view);
+                      return NULL;
+                  }
+                  if (!pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_PHU, true, NULL)) {
+                      psError(PS_ERR_UNKNOWN, false, "Unable to read concepts.");
+                      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, const char *cameraName)
+{
+    PS_ASSERT_PTR_NON_NULL(camera, NULL);
+
+    pmFPA *fpa = pmFPAAlloc(camera, cameraName);    // 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) {
+            psWarning("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 pmFPAAddSourceFromFormat(pmFPA *fpa, const psMetadata *format)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_METADATA_NON_NULL(format, false);
+
+    // Generate the correct structure
+    pmFPALevel phuLevel = pmFPAPHULevel(format); // Level at which PHU goes
+    pmFPAview *view = pmFPAviewAlloc(0);// View for current level
+    if (phuLevel == PM_FPA_LEVEL_FPA) {
+        if (!pmFPAAddSourceFromView(fpa, view, format)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to add PHU to FPA.");
+            psFree(view);
+            return false;
+        }
+    } else {
+        pmChip *chip;                       // Chip from FPA
+        while ((chip = pmFPAviewNextChip(view, fpa, 1))) {
+            if (phuLevel == PM_FPA_LEVEL_CHIP) {
+                if (!pmFPAAddSourceFromView(fpa, view, format)) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to add PHU to FPA.");
+                    psFree(view);
+                    return false;
+                }
+            } else {
+                pmCell *cell;                   // Cell from chip
+                while ((cell = pmFPAviewNextCell(view, fpa, 1))) {
+                    if (phuLevel == PM_FPA_LEVEL_CELL) {
+                        if (!pmFPAAddSourceFromView(fpa, view, format)) {
+                            psError(PS_ERR_UNKNOWN, false, "Unable to add PHU to FPA.");
+                            psFree(view);
+                            return false;
+                        }
+                    }
+                }
+            }
+        }
+    }
+    psFree(view);
+
+    return true;
+}
+
+bool pmFPAAddSourceFromView(pmFPA *fpa, 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, 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;
+    }
+
+    pmFPAview *view = addSource(fpa, NULL, phu, format, true); // View of PHU, to return
+
+    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, 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 *variance = readout->variance; // The variance
+                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 (variance) {
+                    INDENT(fd, 7);
+                    fprintf(fd, "Variance: [%d:%d,%d:%d] (%dx%d)\n",
+                            variance->col0, variance->col0 + variance->numCols,
+                            variance->row0, variance->row0 + variance->numRows,
+                            variance->numCols, variance->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_branches/psModules.20240412/src/camera/pmFPAConstruct.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAConstruct.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAConstruct.h	(revision 42651)
@@ -0,0 +1,76 @@
+/* @file pmFPAConstruct.h
+ * @brief Functions to create an FPA, and add data sources to it.
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.11 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-06-05 01:31:33 $
+ * 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
+                      const char *cameraName ///< Name of the camera (for FPA.CAMERA concept)
+                     );
+
+/// Add a source to the focal plane hierarchy, specified by a camera format
+///
+/// This is suitable for generating an output FPA given the desired format.
+bool pmFPAAddSourceFromFormat(pmFPA *fpa, ///< The FPA
+                              const psMetadata *format ///< Format of file
+    );
+
+/// 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 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_branches/psModules.20240412/src/camera/pmFPACopy.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPACopy.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPACopy.c	(revision 42651)
@@ -0,0 +1,689 @@
+#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 "pmConceptsCopy.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, // Image to which to copy
+                                 const psImage *source, // Image from which to copy
+                                 psImageBinning *binning, // New binning
+                                 bool xFlip, bool yFlip, // Flip in x or y?
+                                 bool pixels // Copy the 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);
+    psImageInit (*target, 0.0);
+    return;
+}
+
+// Update the output analysis metadata, adding stuff in the input
+//
+// This is probably very similar to psMetadataCopy, but we want to explicitly deal with arrays (especially
+// since astronomical sources live in them)
+static psMetadata *updateAnalysis(psMetadata *out, psMetadata *in)
+{
+    psAssert(in, "Require input");
+    if (!out) {
+        out = psMetadataAlloc();
+    }
+
+    psMetadataIterator *iter = psMetadataIteratorAlloc(in, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item;               // Item from iteration
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        psMetadataItem *original = psMetadataLookup(in, item->name); // Checking for MULTI
+        psMetadataItem *extant = psMetadataLookup(out, item->name); // Existing item?
+        if ((original && original->type == PS_DATA_METADATA_MULTI) ||
+            (extant && extant->type == PS_DATA_METADATA_MULTI)) {
+            psMetadataAddItem(out, item, PS_LIST_TAIL, PS_META_DUPLICATE_OK);
+            continue;
+        }
+
+        switch (item->type) {
+          case PS_DATA_ARRAY: {
+              // Concatenate arrays if they already exist
+              psMetadataItem *extant = psMetadataLookup(out, item->name); // Existing array?
+              if (extant && extant->type == PS_DATA_ARRAY) {
+                  psArray *new = item->data.V; // New array
+                  psArray *old = extant->data.V; // Old array
+                  long numNew = new->n, numOld = old->n; // Number of values in each
+                  extant->data.V = old = psArrayRealloc(old, old->n + numNew);
+                  for (long i = 0; i < numNew; i++) {
+                      old->data[numOld + i] = psMemIncrRefCounter(new->data[i]);
+                  }
+                  old->n = numOld + numNew;
+              } else {
+                  psMetadataAddItem(out, item, PS_LIST_TAIL, PS_META_REPLACE);
+              }
+              break;
+            default:
+              // If in doubt, there's not much we can do except replace
+              psMetadataAddItem(out, item, PS_LIST_TAIL, PS_META_REPLACE);
+              break;
+          }
+        }
+    }
+    psFree(iter);
+
+    return out;
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// 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 on 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 for source
+    // 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 (xParityTarget == 0) {
+        psMetadataItem *item = psMetadataLookup(target->concepts, "CELL.XPARITY"); // Item with parity
+        xParityTarget = item->data.S32 = xParitySource;
+    }
+
+    psAssert (abs(xParitySource) == 1, "CELL.XPARITY not set for source");
+    psAssert (abs(xParityTarget) == 1, "CELL.XPARITY not set for target");
+    if (abs(xParitySource) != 1) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "CELL.XPARITY is not set for source (%d)",
+                xParitySource);
+        psFree(binning);
+        return false;
+    } else {
+        // Use the source parity
+        COPY_CONCEPT(target->concepts, source->concepts, "CELL.XPARITY", S32);
+        xParityTarget = xParitySource;
+    }
+    if (xParityTarget != xParitySource) {
+        xFlip = true;
+    }
+
+    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 (yParityTarget == 0) {
+        psMetadataItem *item = psMetadataLookup(target->concepts, "CELL.YPARITY"); // Item with parity
+        yParityTarget = item->data.S32 = yParitySource;
+    }
+
+    psAssert (abs(yParitySource) == 1, "CELL.YPARITY not set for source");
+    psAssert (abs(yParityTarget) == 1, "CELL.YPARITY not set for target");
+    if (abs(yParitySource) != 1) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "CELL.YPARITY is not set for source (%d)",
+                yParitySource);
+        psFree(binning);
+        return false;
+    } else {
+        // Use the source parity
+        COPY_CONCEPT(target->concepts, source->concepts, "CELL.YPARITY", S32);
+        yParityTarget = yParitySource;
+    }
+    if (yParityTarget != yParitySource) {
+        yFlip = true;
+    }
+    psTrace("psModules.camera", 3, "xFlip: %d; yFlip: %d\n", xFlip, yFlip);
+
+    // Blow away extant readouts
+    for (int i = 0; i < target->readouts->n; i++) {
+        psFree(target->readouts->data[i]);
+        target->readouts->data[i] = NULL;
+    }
+    target->readouts->n = 0;
+
+    // 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, variance)
+        readoutCopyComponent(&targetReadout->image, sourceReadout->image, binning, xFlip, yFlip, pixels);
+        readoutCopyComponent(&targetReadout->mask, sourceReadout->mask, binning, xFlip, yFlip, pixels);
+        readoutCopyComponent(&targetReadout->variance, sourceReadout->variance, binning, xFlip, yFlip,
+                             pixels);
+        // Copy covariance matrix: doesn't care about flips, etc.
+        if (sourceReadout->covariance) {
+            if (targetReadout->covariance) {
+                psFree(targetReadout->covariance);
+            }
+            targetReadout->covariance = psKernelCopy(sourceReadout->covariance);
+#if 0
+            if (binning) {
+                // XXX This isn't strictly correct, but we don't have a function that bins covariance matrices
+                // with unequal binning factors.
+                psKernel *covar = psImageCovarianceBin(PS_MAX(binning->nXbin, binning->nYbin),
+                                                       targetReadout->covariance);
+                psFree(targetReadout->covariance);
+                targetReadout->covariance = covar;
+            }
+#endif
+        }
+
+        // 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);
+
+        // Copy the analysis metadata
+        targetReadout->analysis = updateAnalysis(targetReadout->analysis, sourceReadout->analysis);
+
+        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
+
+        psAssert (abs(xParity) == 1, "CELL.XPARITY not set for 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;
+
+    // Update the analysis metadata
+    target->analysis = updateAnalysis(target->analysis, source->analysis);
+
+    // Copy any headers
+    pmHDU *targetHDU = pmHDUFromCell(target); // The target HDU
+    if (targetHDU) {
+        pmHDU *sourceHDU = pmHDUFromCell(source); // The source HDU
+        if (sourceHDU && 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 attributes
+	    targetCell->file_exists = sourceCell->file_exists;
+	    targetCell->data_exists = sourceCell->data_exists;
+	    targetCell->process     = sourceCell->process;
+        }
+    }
+
+    // Update the analysis metadata
+    target->analysis = updateAnalysis(target->analysis, source->analysis);
+
+    // Update the concepts
+    psMetadataItem *chipName = psMemIncrRefCounter(psMetadataLookup(target->concepts, "CHIP.NAME"));
+    pmConceptsCopyChip(target, source, false);
+
+    // update the attributes
+    target->file_exists = source->file_exists;
+    target->data_exists = source->data_exists;
+    target->process     = source->process;
+
+    psMetadataAddItem(target->concepts, chipName, PS_LIST_TAIL, PS_META_REPLACE);
+    psFree(chipName);
+    pmConceptsCopyFPA(target->parent, source->parent, false, false);
+
+    // Update the astrometric parameters
+    // free any previous versions
+    psFree (target->toFPA);   target->toFPA   = psMemIncrRefCounter (source->toFPA);
+    psFree (target->fromFPA); target->fromFPA = psMemIncrRefCounter (source->fromFPA);
+
+    // Update the parent fpa astrometry parameters, or check that they match
+    pmFPA *targetFPA = target->parent;
+    pmFPA *sourceFPA = source->parent;
+
+    // XXX should we require that both of these exist?
+    if (targetFPA && sourceFPA) {
+	psFree(targetFPA->toSky);   targetFPA->toSky = psMemIncrRefCounter (sourceFPA->toSky);
+	psFree(targetFPA->toTPA);   targetFPA->toTPA = psMemIncrRefCounter (sourceFPA->toTPA);
+	psFree(targetFPA->fromTPA); targetFPA->fromTPA = psMemIncrRefCounter (sourceFPA->fromTPA);
+    }
+
+    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
+        psAssert (abs(xParityTarget) == 1, "CELL.XPARITY not set for target");
+        psMetadataAddS32 (targetCell->concepts, PS_LIST_TAIL, "CELL.XPARITY", PS_META_REPLACE, "", xParityTarget);
+        int yParityTarget = psMetadataLookupS32(&status, sourceCell->concepts, "CELL.YPARITY"); // Target y parity
+        psAssert (abs(xParityTarget) == 1, "CELL.YPARITY not set for target");
+        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 analysis metadata
+    targetChip->analysis = updateAnalysis(targetChip->analysis, sourceChip->analysis);
+
+    // Update the concepts
+    pmConceptsCopyChip(targetChip, sourceChip, false);
+
+    // 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 analysis metadata
+    target->analysis = updateAnalysis(target->analysis, source->analysis);
+
+    // Update the concepts
+    pmConceptsCopyFPA(target, source, false, false);
+
+    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_branches/psModules.20240412/src/camera/pmFPACopy.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPACopy.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPACopy.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/camera/pmFPAExpNumIO.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAExpNumIO.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAExpNumIO.c	(revision 42651)
@@ -0,0 +1,450 @@
+#include <stdio.h>
+#include <pslib.h>
+#include <string.h>
+
+#include "pmHDU.h"
+#include "pmHDUUtils.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPAfileFitsIO.h"
+#include "pmFPAHeader.h"
+#include "pmConceptsRead.h"
+#include "pmConceptsWrite.h"
+
+#include "pmFPAExpNumIO.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+static bool pmReadoutReadExpNum(pmReadout *ro, psFits *fits)
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    pmCell *cell = ro->parent;          // Cell of interest
+    if (!cell) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "No cell associated with readout.");
+        return false;
+    }
+    pmChip *chip = cell->parent;    // Chip of interest
+    pmFPA *fpa = chip->parent;      // FPA of interest
+    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // HDU for readout
+    if (!hdu) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "No HDU associated with readout.");
+        return false;
+    }
+
+#ifdef notdef
+    if (!psFitsMoveExtName(fits, hdu->extname)) {
+        psError(PS_ERR_IO, false, "Unable to move to EXPNUM image.");
+        return false;
+    }
+#endif
+    psMetadata *header = psFitsReadHeader(NULL, fits); // Header
+    if (!header) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to read header for EXPNUM image.");
+        return false;
+    }
+
+    bool data = false;                  // Did we find any data?
+
+    int naxis = psMetadataLookupS32(NULL, header, "NAXIS"); // Number of axes
+    if (naxis > 0) {
+        psImage *expnum = psFitsReadImage(fits, psRegionSet(0, 0, 0, 0), 0);
+        psMetadataAddImage(ro->analysis, PS_LIST_TAIL, "EXPNUM", PS_META_REPLACE,
+                           "EXPNUM", expnum);
+        psFree(expnum);
+        data = true;
+    }
+
+    if (data) {
+        ro->data_exists = true;
+        ro->parent->data_exists = true;
+        ro->parent->parent->data_exists = true;
+    }
+
+    psFree(header);
+
+    return true;
+}
+
+static bool pmCellReadExpNum(pmCell *cell, const pmFPAview *view,
+                              pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(cell->readouts, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    // Create a readout if none exists
+    if (!cell->readouts || cell->readouts->n == 0) {
+        pmReadout *readout = pmReadoutAlloc(cell); // New readout
+        psFree(readout);                // Drop reference
+    }
+
+    cell->data_exists = false;
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        thisView->readout = i;
+        if (!pmReadoutReadExpNum(readout, file->fits)) {
+            psError(PS_ERR_IO, false, "Unable to read pattern correction.");
+            return false;
+        }
+    }
+    psFree(thisView);
+
+    if (!pmCellReadHeader(cell, file->fits, config)) {
+        psError(PS_ERR_IO, false, "Unable to read header for cell.");
+        return false;
+    }
+    // load in the concept information for this cell
+    if (!pmConceptsReadCell(cell, PM_CONCEPT_SOURCE_HEADER, true, NULL)) {
+        psErrorClear();
+        psWarning("Difficulty reading concepts for cell; attempting to proceed.");
+    }
+
+    return true;
+}
+
+static bool pmChipReadExpNum(pmChip *chip, const pmFPAview *view,
+                              pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(chip->cells, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    chip->data_exists = false;
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        thisView->cell = i;
+        pmCellReadExpNum(cell, thisView, file, config);
+        if (!cell->data_exists) {
+            continue;
+        }
+        chip->data_exists = true;
+    }
+    psFree(thisView);
+
+    if (!pmChipReadHeader(chip, file->fits, config)) {
+        psError(PS_ERR_IO, false, "Unable to read header for cell.");
+        return false;
+    }
+    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;
+}
+
+static bool pmFPAReadExpNum(pmFPA *fpa, const pmFPAview *view,
+                             pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa->chips, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        thisView->chip = i;
+        pmChipReadExpNum(chip, thisView, file, config);
+    }
+    psFree(thisView);
+
+    if (!pmFPAReadHeader(fpa, file->fits, config)) {
+        psError(PS_ERR_IO, false, "Unable to read header for cell.");
+        return false;
+    }
+    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;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmExpNumRead(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        pmFPAReadExpNum(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) {
+        pmChipReadExpNum(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) {
+        pmCellReadExpNum(cell, view, file, config);
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    return pmReadoutReadExpNum(readout, file->fits);
+}
+#ifdef notyet
+// Currently only ppStack writes an EXPNUM image and it uses file type MASK
+bool pmReadoutWriteExpNum(pmReadout *ro, psFits *fits)
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    bool gotRow, gotCell;
+    psImage *rowCorr = psMetadataLookupPtr(&gotRow, ro->analysis, PM_PATTERN_ROW_CORRECTION); // Row correction
+    float cellCorr = psMetadataLookupF32(&gotCell, ro->analysis, PM_PATTERN_CELL_CORRECTION); // Cell corr.
+
+    pmCell *cell = ro->parent;          // Cell of interest
+    if (!cell) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "No cell associated with readout.");
+        return false;
+    }
+    pmChip *chip = cell->parent;    // Chip of interest
+    pmFPA *fpa = chip->parent;      // FPA of interest
+    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // HDU for readout
+    if (!hdu) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "No HDU associated with readout.");
+        return false;
+    }
+
+    psMetadata *header = psMetadataCopy(NULL, hdu->header); // Header for output
+
+    if (gotCell) {
+        psMetadataAddF32(header, PS_LIST_TAIL, PM_PATTERN_CELL_CORRECTION, PS_META_REPLACE,
+                         "ExpNum cell correction value", cellCorr);
+    }
+
+    if (gotRow) {
+        if (!psFitsWriteImage(fits, header, rowCorr, 0, hdu->extname)) {
+            psError(PS_ERR_IO, false, "Unable to write pattern row correction.");
+            psFree(header);
+            return false;
+        }
+    } else {
+        if (!psFitsWriteBlank(fits, header, hdu->extname)) {
+            psError(PS_ERR_IO, false, "Unable to write pattern cell correction.");
+            psFree(header);
+            return false;
+        }
+    }
+
+    psFree(header);
+    return true;
+}
+
+static bool pmCellWriteExpNum(pmCell *cell, const pmFPAview *view,
+                               pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(cell->readouts, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    if (!pmConceptsWriteCell(cell, true, config)) {
+        psError(PS_ERR_IO, false, "Unable to write concepts for cell.");
+        return false;
+    }
+
+    // Only do the FIRST readout --- don't want to write lots of headers
+    pmReadout *readout = cell->readouts->data[0];
+    if (!pmReadoutWriteExpNum(readout, file->fits)) {
+        psError(PS_ERR_IO, false, "Failed to write readout");
+        return false;
+    }
+    return true;
+}
+
+static bool pmChipWriteExpNum(pmChip *chip, const pmFPAview *view,
+                               pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(chip->cells, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    if (!pmConceptsWriteChip(chip, true, true, config)) {
+        psError(PS_ERR_IO, false, "Unable to write concepts for chip.\n");
+        return false;
+    }
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        thisView->cell = i;
+        if (!pmCellWriteExpNum(cell, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write %dth cell", i);
+            psFree(thisView);
+            return false;
+        }
+    }
+    psFree(thisView);
+    return true;
+}
+
+static bool pmFPAWriteExpNum(pmFPA *fpa, const pmFPAview *view,
+                              pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fpa->chips, false);
+
+    if (!pmConceptsWriteFPA(fpa, true, config)) {
+        psError(PS_ERR_IO, false, "Unable to write concepts for FPA.\n");
+        return false;
+    }
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        thisView->chip = i;
+        if (!pmChipWriteExpNum(chip, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write %dth chip", i);
+            psFree(thisView);
+            return false;
+        }
+    }
+    psFree(thisView);
+    return true;
+}
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Currently only ppStack writes an EXPNUM image and it uses file type MASK
+bool pmExpNumWrite(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // Suitable FPA for writing
+
+    if (view->chip == -1) {
+        if (!pmFPAWriteExpNum(fpa, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write pattern correction from fpa");
+            psFree(fpa);
+            return false;
+        }
+        psFree(fpa);
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        psError(PS_ERR_UNKNOWN, false, "Writing chip == %d (>= chips->n == %ld)", view->chip, fpa->chips->n);
+        psFree(fpa);
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        if (!pmChipWriteExpNum(chip, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write pattern correction from chip");
+            psFree(fpa);
+            return false;
+        }
+        psFree(fpa);
+        return true;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        psError(PS_ERR_UNKNOWN, false, "Writing cell == %d (>= cells->n == %ld)",
+                view->cell, chip->cells->n);
+        psFree(fpa);
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        if (!pmCellWriteExpNum(cell, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write pattern correction from cell");
+            psFree(fpa);
+            return false;
+        }
+        psFree(fpa);
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        psError(PS_ERR_UNKNOWN, false, "Writing readout == %d (>= readouts->n == %ld)",
+                view->readout, cell->readouts->n);
+        psFree(fpa);
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    if (!pmReadoutWriteExpNum(readout, file->fits)) {
+        psError(PS_ERR_IO, false, "Failed to write pattern correction from readout %d", view->readout);
+        psFree(fpa);
+        return false;
+    }
+
+    psFree(fpa);
+    return true;
+}
+
+bool pmExpNumWritePHU(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    if (file->wrote_phu) {
+        return true;
+    }
+
+    // find the FPA phu
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // Suitable FPA for writing
+    pmHDU *phu = psMemIncrRefCounter(pmFPAviewThisPHU(view, fpa));
+    psFree(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);
+    }
+    psFree(phu);
+
+    pmConfigConformHeader(outhead, file->format);
+
+    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;
+}
+#endif // notyet
+
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAExpNumIO.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAExpNumIO.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAExpNumIO.h	(revision 42651)
@@ -0,0 +1,36 @@
+#ifndef PM_EXPNUM_IO_H
+#define PM_EXPNUM_IO_H
+
+#include <pslib.h>
+
+#include <pmHDU.h>
+#include <pmFPA.h>
+
+
+bool pmExpNumRead(const pmFPAview *view, ///< View into which to read
+                   pmFPAfile *file, ///< File from which to read
+                   pmConfig *config ///< Configuration
+    );
+
+#ifdef notyet
+// Currently we don't need write functions for the EXPNUM images. 
+// Only ppStack creates them and it uses a MASK file type
+
+/// Write pattern correction within a readout to a FITS file
+bool pmReadoutWritePattern(
+    pmReadout *readout,                 ///< Readout for which to write pattern correction (in analysis MD)
+    psFits *fits                        ///< FITS file to which to write
+    );
+bool pmPatternWrite(const pmFPAview *view, ///< View from which to write
+                    pmFPAfile *file, ///< File to which to write
+                    pmConfig *config ///< Configuration
+    );
+
+bool pmPatternWritePHU(const pmFPAview *view, // View to PHU
+                       pmFPAfile *file, ///< File to which to write
+                       pmConfig *config ///< Configuration
+    );
+
+#endif // notyet
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAExtent.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAExtent.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAExtent.c	(revision 42651)
@@ -0,0 +1,192 @@
+#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->variance;
+    }
+
+    int xSize = 0;
+    int ySize = 0;
+
+    if (!image) {
+        pmHDU *hdu = pmHDUFromReadout (readout);
+        if (hdu && hdu->header) {
+            bool status;
+            xSize = psMetadataLookupS32(&status, hdu->header, "NAXIS1");
+            if (!status) {
+                xSize = psMetadataLookupS32(&status, hdu->header, "IMNAXIS1");
+                if (!status) return NULL;
+            }
+            ySize = psMetadataLookupS32(&status, hdu->header, "NAXIS2");
+            if (!status) {
+                ySize = psMetadataLookupS32(&status, hdu->header, "IMNAXIS2");
+                if (!status) return NULL;
+            }
+        } 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);
+    }
+
+    // Don't have anything to base the true extent on, so have to give the hardwired value (largest possible extent)
+    if (readouts->n == 0) {
+        int xSize = psMetadataLookupS32(NULL, cell->concepts, "CELL.XSIZE");
+        int ySize = psMetadataLookupS32(NULL, cell->concepts, "CELL.YSIZE");
+        cellExtent->x0 = 0;
+        cellExtent->x1 = xSize;
+        cellExtent->y0 = 0;
+        cellExtent->y1 = ySize;
+    }
+
+    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;
+    }
+
+    int xParityCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.XPARITY");
+    int yParityCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.YPARITY");
+
+    // CELL.X0,Y0 are the coordinate of the amp on the chip, subtract size if parity flipped
+    if (xParityCell > 0) {
+        cellExtent->x0 += x0;
+        cellExtent->x1 += x0;
+    } else {
+        float x0Cell = x0 - cellExtent->x1;
+        float x1Cell = x0 - cellExtent->x0;
+        cellExtent->x0 = x0Cell;
+        cellExtent->x1 = x1Cell;
+    }
+    if (yParityCell > 0) {
+        cellExtent->y0 += y0;
+        cellExtent->y1 += y0;
+    } else {
+        float y0Cell = y0 - cellExtent->y1;
+        float y1Cell = y0 - cellExtent->y0;
+        cellExtent->y0 = y0Cell;
+        cellExtent->y1 = y1Cell;
+    }
+
+    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_branches/psModules.20240412/src/camera/pmFPAExtent.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAExtent.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAExtent.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/camera/pmFPAFlags.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAFlags.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAFlags.c	(revision 42651)
@@ -0,0 +1,484 @@
+#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 pmReadoutSetFileStatus(pmReadout *readout, bool status)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+
+    readout->file_exists = status;
+    return true;
+}
+
+bool pmFPAviewSetFileStatus (pmFPA *fpa, const pmFPAview *view, bool status) {
+
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    if (view->chip == -1) {
+	bool set = pmFPASetFileStatus (fpa, status);
+        return set;
+    }
+
+    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 set = pmChipSetFileStatus (chip, status);
+        return set;
+    }
+
+    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 set = pmCellSetFileStatus (cell, status);
+        return set;
+    }
+
+    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 set = pmReadoutSetFileStatus (readout, status);
+    return set;
+}
+
+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];
+        pmReadoutSetDataStatus(readout, status);
+    }
+    return true;
+}
+
+bool pmReadoutSetDataStatus (pmReadout *readout, bool status)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+
+    readout->data_exists = status;
+    return true;
+}
+
+bool pmFPAviewSetDataStatus (pmFPA *fpa, const pmFPAview *view, bool status) {
+
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    if (view->chip == -1) {
+	bool set = pmFPASetDataStatus (fpa, status);
+        return set;
+    }
+
+    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 set = pmChipSetDataStatus (chip, status);
+        return set;
+    }
+
+    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 set = pmCellSetDataStatus (cell, status);
+        return set;
+    }
+
+    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 set = pmReadoutSetDataStatus (readout, status);
+    return set;
+}
+
+// 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;
+}
+
+
+// XXX this function should probably be re-defined to merge with 'setCellsProcess'
+bool pmChipSelectCells(pmChip *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 *cell = cells->data[i];
+        if (!cell) {
+            continue;
+        }
+	cell->process = true;
+    }
+    return true;
+}
+
+
+int pmFPAExcludeChip(pmFPA *fpa, int chipNum)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, -1);
+
+    psArray *chips = fpa->chips;        // Component chips
+    if (chips == NULL) {
+        psWarning("WARNING: fpa->chips == NULL\n");
+        return(0);
+    }
+    if ((chipNum >= chips->n) || (NULL == (pmChip *) chips->data[chipNum])) {
+        psWarning("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);
+}
+
+
+// turn off all chips
+bool pmFPAExcludeChips(pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+
+    psArray *chips = fpa->chips;        // Component chips
+    if (chips == NULL) {
+        psWarning("WARNING: fpa->chips == NULL\n");
+        return false;
+    }
+
+    for (int i = 0 ; i < chips->n ; i++) {
+        pmChip *tmpChip = (pmChip *) chips->data[i]; // Chip of interest
+        if (tmpChip != NULL) {
+	  tmpChip->process = false;
+	  setCellsProcess(tmpChip, false); // Wipe out the cell as well
+        }
+    }
+
+    return true;
+}
+
+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_branches/psModules.20240412/src/camera/pmFPAFlags.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAFlags.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAFlags.h	(revision 42651)
@@ -0,0 +1,135 @@
+/*  @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
+                        );
+
+bool pmReadoutSetFileStatus(pmReadout *readout, bool status);
+
+bool pmFPAviewSetFileStatus (pmFPA *fpa, const pmFPAview *view, bool status);
+
+// 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
+                        );
+
+bool pmReadoutSetDataStatus (pmReadout *readout, bool status);
+
+bool pmFPAviewSetDataStatus (pmFPA *fpa, const pmFPAview *view, bool status);
+
+// 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 all chips within an FPA from processing
+bool pmFPAExcludeChips(pmFPA *fpa        ///< FPA containing the chip of interest
+                    );
+
+bool pmChipSelectCells(pmChip *chip);
+
+/// 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_branches/psModules.20240412/src/camera/pmFPAHeader.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAHeader.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAHeader.c	(revision 42651)
@@ -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 "pmConceptsRead.h"
+#include "pmFPAHeader.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmCellReadHeader(pmCell *cell, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    if (!cell->hdu) {
+        return pmChipReadHeader(cell->parent, fits, config);
+    }
+    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 | PM_CONCEPT_SOURCE_DATABASE, false, config);
+}
+
+
+bool pmChipReadHeader(pmChip *chip, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    if (!chip->hdu) {
+        return pmFPAReadHeader(chip->parent, fits, config);
+    }
+    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 | PM_CONCEPT_SOURCE_DATABASE, true, true, config)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read concepts for chip.\n");
+        return false;
+    }
+
+    return true;
+}
+
+
+bool pmFPAReadHeader(pmFPA *fpa, psFits *fits, pmConfig *config)
+{
+    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 | PM_CONCEPT_SOURCE_DATABASE, true, config)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read concepts for FPA.\n");
+        return false;
+    }
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAHeader.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAHeader.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAHeader.h	(revision 42651)
@@ -0,0 +1,44 @@
+/*  @file pmFPAHeader.h
+ *  @brief Functions read FITS headers for FPA components
+ *
+ *  @author Paul Price, IfA
+ *
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-06-17 22:16:38 $
+ *  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
+                     pmConfig *config   ///< Configuration
+                    );
+
+/// 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
+                     pmConfig *config   ///< Configuration
+                     );
+
+/// 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
+                     pmConfig *config   ///< Configuration
+                     );
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPALevel.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPALevel.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPALevel.c	(revision 42651)
@@ -0,0 +1,67 @@
+#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
+static const char *nameCHUNK = "CHUNK"; ///< Name for PM_FPA_LEVEL_CHUNK
+
+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;
+    case PM_FPA_LEVEL_CHUNK:
+        return nameCHUNK;
+    default:
+        psAbort("You can't get here; level = %d", level);
+    }
+    return NULL;
+}
+
+pmFPALevel pmFPALevelFromName(const char *name)
+{
+    if (name == NULL) {
+        return PM_FPA_LEVEL_NONE;
+    }
+    if (!strcasecmp(name, nameFPA)) {
+        return PM_FPA_LEVEL_FPA;
+    }
+    if (!strcasecmp(name, nameCHIP)) {
+        return PM_FPA_LEVEL_CHIP;
+    }
+    if (!strcasecmp(name, nameCELL)) {
+        return PM_FPA_LEVEL_CELL;
+    }
+    if (!strcasecmp(name, nameREADOUT)) {
+        return PM_FPA_LEVEL_READOUT;
+    }
+    if (!strcasecmp(name, nameCHUNK)) {
+        return PM_FPA_LEVEL_CHUNK;
+    }
+    if (!strcasecmp(name, nameNONE)) {
+        return PM_FPA_LEVEL_NONE;
+    }
+
+    psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unrecognised FPA level name: %s", name);
+    return PM_FPA_LEVEL_NONE;
+}
+
+
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPALevel.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPALevel.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPALevel.h	(revision 42651)
@@ -0,0 +1,37 @@
+/* @file pmFPALevel.h
+ * @brief Defines enum and string representations for the FPA levels
+ *
+ * @author Eugene Magnier, IfA
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-02-07 00:10:08 $
+ * Copyright 2005-2008 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
+    PM_FPA_LEVEL_CHUNK,                 ///< Level corresponds to a chunk
+} 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_branches/psModules.20240412/src/camera/pmFPAMaskWeight.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAMaskWeight.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAMaskWeight.c	(revision 42651)
@@ -0,0 +1,640 @@
+#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"
+
+#define PIXELS_BUFFER 100               // Size of buffer for allocating pixel lists
+#define RENORM_NUM_SIGMA 3.0            // Number of standard deviations for Gaussian kernel
+#define RENORM_PEAK 2.0                 // Number of standard deviations of noise for fake source peak flux
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// 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_IMAGE_MASK);
+        psImageInit(masks->data[i], 0);
+    }
+
+    return;
+}
+
+// Create the parent variance images that reside in the HDU
+static void createParentVariances(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 *variances = hdu->variances;    // Array of variance images
+    if (!variances) {
+        variances = psArrayAlloc(images->n);
+        hdu->variances = variances;
+    }
+
+    for (long i = 0; i < images->n; i++) {
+        psImage *image = images->data[i]; // The image for this readout
+        if (!image || variances->data[i]) {
+            continue;
+        }
+        variances->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, psImageMaskType satMask, psImageMaskType 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;
+        psWarning("CELL.SATURATION is not set --- completely masking cell.\n");
+        saturation = NAN;
+    }
+    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;
+        psWarning("CELL.BAD is not set --- completely masking cell.\n");
+        bad = NAN;
+    }
+    psTrace("psModules.camera", 5, "Saturation: %f, bad: %f\n", saturation, bad);
+
+    // if CELL.GAIN or CELL.READNOISE are not set, then the variance will be set to NAN;
+    // in this case, we have to set the mask as well
+    float gain = psMetadataLookupF32(&mdok, cell->concepts, "CELL.GAIN"); // Cell gain
+    if (!mdok) { gain = NAN; }
+    float readnoise = psMetadataLookupF32(&mdok, cell->concepts, "CELL.READNOISE"); // Cell read noise
+    if (!mdok) { readnoise = NAN; }
+
+    // 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_IMAGE_MASK);
+    }
+    psImage *mask = readout->mask;      // The mask pixels
+
+    // completely mask if SATURATION or BAD are invalid
+    if (isnan(saturation) || isnan(bad) || isnan(gain) || isnan(readnoise)) {
+        psImageInit(mask, badMask);
+        return true;
+    }
+
+    psImageInit(mask, 0);
+
+    // Dereference pointers for speed
+    psF32 **imageData = image->data.F32;// The image
+    psImageMaskType **maskData = mask->data.PS_TYPE_IMAGE_MASK_DATA;  // 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, psImageMaskType satMask, psImageMaskType 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 pmReadoutSetVariance(pmReadout *readout, const psImage *noiseMap, bool poisson)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    // check that the noiseMap (if it exists) matches the readout variance size)
+
+    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 variance.\n");
+        // return false;
+        psWarning("CELL.GAIN is not set --- setting variance to NAN\n");
+        gain = NAN;
+    }
+    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 variance.\n");
+        // return false;
+        psWarning("CELL.READNOISE is not set --- setting variance to NAN\n");
+        readnoise = NAN;
+    }
+    // if we have a non-NAN readnoise, then we need to ensure it has been updated (not necessary if NAN)
+    if (!isnan(gain) && psMetadataLookup(cell->concepts, "CELL.READNOISE.UPDATE")) {
+        psError(PS_ERR_IO, true, "CELL.READNOISE has not yet been updated for the gain");
+        return false;
+    }
+
+    // for invalid input data, set the readout variance to NAN
+    if (isnan(gain) || isnan(readnoise)) {
+        if (!readout->variance) {
+            // generate the image if needed
+            readout->variance = psImageAlloc(readout->image->numCols, readout->image->numRows, PS_TYPE_F32);
+        }
+        // XXX need to set the mask, if defined
+        psImageInit(readout->variance, NAN);
+        return true;
+    }
+
+    if (poisson) {
+        // Set variance image to the variance in ADU = f/g + rn^2
+        psImage *image = readout->image;    // The image pixels
+        readout->variance = (psImage*)psBinaryOp(readout->variance, image, "/", psScalarAlloc(gain, PS_TYPE_F32));
+
+        // a negative variance is non-sensical. if the image value drops below 1, the variance must be 1.
+        // XXX this calculation is wrong: limit is 1 e-, but this is in DN
+        readout->variance = (psImage*)psUnaryOp(readout->variance, readout->variance, "abs");
+        readout->variance = (psImage*)psBinaryOp(readout->variance, readout->variance, "max",
+                                               psScalarAlloc(1, PS_TYPE_F32));
+    } else {
+        // Just use the read noise
+        if (!readout->variance) {
+            readout->variance = psImageAlloc(readout->image->numCols, readout->image->numRows, PS_TYPE_F32);
+        }
+        psImageInit(readout->variance, 0.0);
+    }
+
+    // apply a supplied readnoise map (NOTE: in DN, not electrons):
+    if (noiseMap) {
+        psImage *rdVar = (psImage*)psBinaryOp(NULL, (const psPtr) noiseMap, "*", (const psPtr) noiseMap);
+        readout->variance = (psImage*)psBinaryOp(readout->variance, readout->variance, "+", rdVar);
+        psFree (rdVar);
+    } else {
+        readout->variance = (psImage*)psBinaryOp(readout->variance, readout->variance, "+", psScalarAlloc(readnoise*readnoise/gain/gain, PS_TYPE_F32));
+    }
+
+    return true;
+}
+
+// this function creates the variance pixels, or uses the existing variance pixels.  it will set
+// the noise pixel values only if the variance image is not supplied
+bool pmReadoutGenerateVariance(pmReadout *readout, const psImage *noiseMap, 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 variance image if required
+    if (readout->variance)
+        return true;
+
+    psRegion *trimsec = psMetadataLookupPtr(&mdok, cell->concepts, "CELL.TRIMSEC"); // Trim section
+    if (!mdok || psRegionIsNaN(*trimsec)) {
+      // if trimsec is not defined, use the full image
+      trimsec = psRegionAlloc(0,0,0,0);
+      // psError(PS_ERR_IO, true, "CELL.TRIMSEC is not set --- unable to set variance.\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;
+    }
+
+    createParentVariances(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 *variance = psImageSubset(hdu->variances->data[index], *trimsec); // The variance pixels
+    if (!variance) {
+        psString trimsecString = psRegionToString(*trimsec);
+        psError(PS_ERR_UNKNOWN, false, "Unable to set variance from HDU with trimsec: %s.\n",
+                trimsecString);
+        psFree(trimsecString);
+        return false;
+    }
+    psImageInit(variance, 0);
+    readout->variance = variance;
+
+    return pmReadoutSetVariance(readout, noiseMap, poisson);
+}
+
+bool pmReadoutGenerateMaskVariance(pmReadout *readout, psImageMaskType satMask, psImageMaskType badMask, const psImage *noiseMap, bool poisson)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+
+    bool success = true;                // Was everything successful?
+
+    success &= pmReadoutGenerateMask(readout, satMask, badMask);
+    success &= pmReadoutGenerateVariance(readout, noiseMap, poisson);
+
+    return success;
+}
+
+bool pmCellGenerateMaskVariance(pmCell *cell, psImageMaskType satMask, psImageMaskType badMask, const psImage *noiseMap, 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
+        success &= pmReadoutGenerateMaskVariance(readout, satMask, badMask, noiseMap, poisson);
+    }
+
+    return success;
+}
+
+
+bool pmReadoutVarianceRenormalise(const pmReadout *readout, psImageMaskType maskVal,
+                                  int sample, float minValid, float maxValid)
+{
+    PM_ASSERT_READOUT_NON_NULL(readout, false);
+    PM_ASSERT_READOUT_IMAGE(readout, false);
+    PM_ASSERT_READOUT_VARIANCE(readout, false);
+
+    psImage *image = readout->image, *mask = readout->mask, *variance = readout->variance; // Readout parts
+    int numCols = image->numCols, numRows = image->numRows; // Size of image
+
+    int xMin, xMax, yMin, yMax;         // Bounds of image
+    if (mask) {
+        xMin = numCols;
+        xMax = 0;
+        yMin = numRows;
+        yMax = 0;
+        for (int y = 0; y < numRows; y++) {
+            for (int x = 0; x < numCols; x++) {
+                if (mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskVal) {
+                    continue;
+                }
+                xMin = PS_MIN(xMin, x);
+                xMax = PS_MAX(xMax, x);
+                yMin = PS_MIN(yMin, y);
+                yMax = PS_MAX(yMax, y);
+            }
+        }
+    } else {
+        xMin = 0;
+        xMax = numCols;
+        yMin = 0;
+        yMax = numRows;
+    }
+
+    int xNum = xMax - xMin, yNum = yMax - yMin; // Number of pixels
+
+    int numPix = xNum * yNum;                                  // Number of pixels
+    int num = PS_MIN(sample, numPix);                          // Number we care about
+    psVector *signoise = psVectorAllocEmpty(num, PS_TYPE_F32);   // Signal-to-noise values
+
+    if (num >= numPix) {
+        // We have an image smaller than Nsubset, so just loop over the image pixels
+        int index = 0;                  // Index for vector
+        for (int y = yMin; y < yMax; y++) {
+            for (int x = xMin; x < xMax; x++) {
+                if ((mask && mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskVal) ||
+                    !isfinite(image->data.F32[y][x]) || !isfinite(variance->data.F32[y][x])) {
+                    continue;
+                }
+
+                signoise->data.F32[index] = image->data.F32[y][x] / sqrtf(variance->data.F32[y][x]);
+                index++;
+            }
+        }
+        signoise->n = index;
+    } else {
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS); // Random number generator
+        int index = 0;                  // Index for vector
+        for (long i = 0; i < num; i++) {
+            // Pixel coordinates
+            int pixel = numPix * psRandomUniform(rng);
+            int x = xMin + pixel % xNum;
+            int y = yMin + pixel / xNum;
+
+            if ((mask && mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskVal) ||
+                !isfinite(image->data.F32[y][x]) || !isfinite(variance->data.F32[y][x])) {
+                continue;
+            }
+
+            signoise->data.F32[index] = image->data.F32[y][x] / sqrtf(variance->data.F32[y][x]);
+            index++;
+        }
+        signoise->n = index;
+        psFree(rng);
+    }
+
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_STDEV); // Statistics
+
+    if (!psVectorStats(stats, signoise, NULL, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to measure statistics on S/N image");
+        psFree(signoise);
+        return false;
+    }
+    psFree(signoise);
+
+    float covar = sqrtf(psImageCovarianceFactor(readout->covariance)); // Covariance factor
+    float correction = stats->robustStdev / covar; // Correction factor
+    psFree(stats);
+    psLogMsg("psModules.camera", PS_LOG_DETAIL, "Variance renormalisation factor is %f", correction);
+
+    // Check valid range of correction factor
+    if ((isfinite(minValid) && correction < minValid) || (isfinite(maxValid) && correction > maxValid)) {
+	psError(PS_ERR_UNKNOWN, true, "Variance renormalisation is outside valid range: %f vs %f:%f --- no correction made", correction, minValid, maxValid);
+	psMetadataAddF32(readout->analysis, PS_LIST_TAIL, PM_READOUT_ANALYSIS_RENORM, 0, "Renormalisation of variance", PS_SQR(correction));
+        return false;
+    }
+
+    psImage *subImage = psImageSubset(variance, psRegionSet(xMin, xMax, yMin, yMax)); // Smaller image
+    psBinaryOp(subImage, subImage, "*", psScalarAlloc(PS_SQR(correction), PS_TYPE_F32));
+    psFree(subImage);
+
+    pmHDU *hdu = pmHDUFromReadout(readout); // HDU for readout
+    if (hdu)  {
+        psString history = NULL;
+        psStringAppend(&history, "Rescaled variance by %6.4f (stdev by %6.4f)",
+                       PS_SQR(correction), correction);
+        psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, NULL, history);
+        psFree(history);
+    }
+
+    return psMetadataAddF32(readout->analysis, PS_LIST_TAIL, PM_READOUT_ANALYSIS_RENORM, 0,
+                            "Renormalisation of variance", PS_SQR(correction));
+}
+
+// find any pixels which are not already masked (with maskTest) which are not valid and raise maskSet bits
+bool pmReadoutMaskInvalid (const pmReadout *readout, psImageMaskType maskTest, psImageMaskType maskSet) {
+
+    if (!readout) return true;
+
+    psImage *image = readout->image;
+    psImage *mask  = readout->mask;
+    psImage *variance = readout->variance;
+    for (int y = 0; y < image->numRows; y++) {
+        for (int x = 0; x < image->numCols; x++) {
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskTest) continue;
+            bool valid = false;
+            valid = isfinite(image->data.F32[y][x]);
+            if (valid && variance) {
+                valid &= isfinite(variance->data.F32[y][x]);
+            }
+            if (valid) continue;
+            mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= maskSet;
+        }
+    }
+
+    return true;
+}
+
+// raise maskVal for any invalid pixels
+bool pmReadoutMaskNonfinite(pmReadout *readout, psImageMaskType maskVal)
+{
+    PM_ASSERT_READOUT_NON_NULL(readout, false);
+    PM_ASSERT_READOUT_IMAGE(readout, false);
+
+    psImage *image = readout->image;    // Readout's image
+    psImage *variance = readout->variance;  // Readout's variance
+    int numCols = image->numCols, numRows = image->numRows; // Size of image
+
+    if (!readout->mask) {
+        readout->mask = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK);
+    }
+    psImage *mask = readout->mask;      // Readout's mask
+
+    for (int y = 0; y < numRows; y++) {
+        for (int x = 0; x < numCols; x++) {
+            if (!isfinite(image->data.F32[y][x]) || (variance && !isfinite(variance->data.F32[y][x]))) {
+                mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= maskVal;
+            }
+        }
+    }
+
+    return true;
+}
+
+
+
+bool pmReadoutMaskApply(pmReadout *readout, psImageMaskType maskVal)
+{
+    PM_ASSERT_READOUT_NON_NULL(readout, false);
+    PM_ASSERT_READOUT_IMAGE(readout, false);
+    PM_ASSERT_READOUT_MASK(readout, false);
+
+    int numCols = readout->image->numCols, numRows = readout->image->numRows; // Size of image
+    psImageMaskType **maskData = readout->mask->data.PS_TYPE_IMAGE_MASK_DATA; // Dereference mask
+    psF32 **imageData = readout->image->data.F32;// Dereference image
+    psF32 **varianceData = readout->variance ? readout->variance->data.F32 : NULL; // Dereference variance map
+    float maskFrac = 0.0;
+    for (int y = 0; y < numRows; y++) {
+        for (int x = 0; x < numCols; x++) {
+            if (maskData[y][x] & maskVal) {
+  	        maskFrac += 1;
+                imageData[y][x] = NAN;
+                if (varianceData) {
+                    varianceData[y][x] = NAN;
+                }
+            }
+        }
+    }
+    maskFrac = maskFrac / (1.0 * numRows * numCols);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "READOUT.MASK.FRAC", PS_META_REPLACE,
+		     "fraction of pixels masked by pmReadoutMaskApply",maskFrac);
+    return true;
+}
+
+
+bool pmReadoutInterpolateBadPixels(pmReadout *readout, psImageMaskType maskVal, psImageInterpolateMode mode,
+                                   float poorFrac, psImageMaskType maskPoor, psImageMaskType maskBad)
+{
+    PM_ASSERT_READOUT_NON_NULL(readout, false);
+    PM_ASSERT_READOUT_IMAGE(readout, false);
+    PM_ASSERT_READOUT_MASK(readout, false);
+    if (!maskVal) {
+        return true;
+    }
+
+    psImage *image = readout->image;    // Image
+    psImage *mask = readout->mask;      // Mask
+    psImage *variance = readout->variance;  // Variance map
+
+    psImageInterpolation *interp = psImageInterpolationAlloc(mode, image, variance, mask, maskVal,
+                                                             NAN, NAN, maskBad, maskPoor, poorFrac, 0);
+    interp->shifting = false;           // Turn off "exact shifts" so we get proper interpolation
+
+    int numCols = mask->numCols, numRows = mask->numRows; // Size of image
+
+    psPixels *pixels = psPixelsAllocEmpty(PIXELS_BUFFER); // Pixels that have been interpolated
+    psVector *imagePix = psVectorAllocEmpty(PIXELS_BUFFER, PS_TYPE_F32); // Corresponding values for image
+    psVector *variancePix = psVectorAllocEmpty(PIXELS_BUFFER, PS_TYPE_F32); // Corresponding values for variance
+    psVector *maskPix = psVectorAllocEmpty(PIXELS_BUFFER, PS_TYPE_IMAGE_MASK); // Corresponding values for mask
+    // NOTE: maskPix carries the actual image mask values -- do NOT use
+    // PS_TYPE_VECTOR_MASK here; it is storage, and is not treated as a vector mask
+
+    long numBad = 0;                    // Number of bad pixels interpolated
+    for (int y = 0; y < numRows; y++) {
+        for (int x = 0; x < numCols; x++) {
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskVal) {
+                double imageValue, varianceValue; // Image and variance value from interpolation
+                psImageMaskType maskValue = 0; // Mask value from interpolation
+
+		// interpolate to pixel center (index + 0.5)
+                psImageInterpolateStatus status = psImageInterpolate(&imageValue, &varianceValue, &maskValue, x + 0.5, y + 0.5, interp);
+                if (status == PS_INTERPOLATE_STATUS_ERROR || status == PS_INTERPOLATE_STATUS_OFF) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to interpolate readout at %d,%d", x, y);
+                    psFree(interp);
+                    psFree(pixels);
+                    psFree(imagePix);
+                    psFree(variancePix);
+                    psFree(maskPix);
+                    return false;
+                }
+                if (status == PS_INTERPOLATE_STATUS_BAD) {
+                    // It's still bad: couldn't interpolate enough
+                    continue;
+                }
+
+                pixels = psPixelsAdd(pixels, PIXELS_BUFFER, x, y);
+                imagePix = psVectorExtend(imagePix, PIXELS_BUFFER, 1);
+                variancePix = psVectorExtend(variancePix, PIXELS_BUFFER, 1);
+                maskPix = psVectorExtend(maskPix, PIXELS_BUFFER, 1);
+                imagePix->data.F32[numBad] = imageValue;
+                variancePix->data.F32[numBad] = varianceValue;
+                maskPix->data.PS_TYPE_IMAGE_MASK_DATA[numBad] = maskValue;
+                numBad++;
+            }
+        }
+    }
+
+    psFree(interp);
+
+    for (long i = 0; i < numBad; i++) {
+        int x = pixels->data[i].x, y = pixels->data[i].y; // Coordinates of pixel
+        image->data.F32[y][x] = imagePix->data.F32[i];
+        variance->data.F32[y][x] = variancePix->data.F32[i];
+        mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] = maskPix->data.PS_TYPE_IMAGE_MASK_DATA[i];
+    }
+
+    psFree(pixels);
+    psFree(imagePix);
+    psFree(variancePix);
+    psFree(maskPix);
+
+    psLogMsg("psModules.camera", PS_LOG_INFO, "Interpolated over %ld pixels", numBad);
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAMaskWeight.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAMaskWeight.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAMaskWeight.h	(revision 42651)
@@ -0,0 +1,125 @@
+/* @file pmFPAHeader.h
+ * @brief Functions read FITS headers for FPA components
+ *
+ * @author Paul Price, IfA
+ * @author Eugene Magnier, IfA
+ *
+ * @version $Revision: 1.18 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-06 02:31:24 $
+ * 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
+/// @{
+
+#define PM_READOUT_ANALYSIS_RENORM "READOUT.RENORM" // Name on analysis metadata for renormalisation
+
+/// 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
+                      psImageMaskType satMask, ///< Mask value to give saturated pixels
+                      psImageMaskType badMask  ///< Mask value to give bad (low) pixels
+    );
+
+
+/// Set a temporary readout variance map using CELL.GAIN and CELL.READNOISE
+///
+/// Calculates 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 pmReadoutSetVariance(pmReadout *readout, ///< Readout for which to set variance
+                          const psImage *noiseMap, ///< 2D image of the read noise in DN
+                          bool poisson    ///< Include poisson variance (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
+                           psImageMaskType sat, ///< Mask value to give saturated pixels
+                           psImageMaskType bad ///< Mask value to give bad (low) pixels
+    );
+
+/// Generate a variance map (suitable for output) using CELL.GAIN and CELL.READNOISE
+///
+/// Calculates variances for each pixel using photon statistics and the cell gain (CELL.GAIN) and read noise
+/// (CELL.READNOISE).  The variance map that is produced within the readout is suitable for output (complete
+/// with HDU entry).  This is intended for most operations.
+bool pmReadoutGenerateVariance(pmReadout *readout, ///< Readout for which to generate variance
+                          const psImage *noiseMap, ///< 2D image of the read noise in DN
+                               bool poisson    ///< Include poisson variance (in addition to read noise)?
+    );
+
+/// Generate mask and variance map for a readout
+///
+/// Calls pmReadoutGenerateMask and pmReadoutGenerateVariance for the readout
+bool pmReadoutGenerateMaskVariance(pmReadout *readout, ///< Readout for which to generate mask and variance
+                                   psImageMaskType sat, ///< Mask value to give saturated pixels
+                                   psImageMaskType bad, ///< Mask value to give bad (low) pixels
+                                   const psImage *noiseMap, ///< 2D image of the read noise in DN
+                                   bool poisson ///< Include poisson variance (in addition to read noise)?
+    );
+
+/// Generate mask and variance maps for all readouts within a cell
+///
+/// Calls pmReadoutGenerateMaskVariance for each readout within the cell.
+bool pmCellGenerateMaskVariance(pmCell *cell, ///< Cell for which to generate mask and variance
+                                psImageMaskType sat, ///< Mask value to give saturated pixels
+                                psImageMaskType bad, ///< Mask value to give bad (low) pixels
+                                const psImage *noiseMap, ///< 2D image of the read noise in DN
+                                bool poisson ///< Include poisson variance (in addition to read noise)?
+    );
+
+/// Renormalise the variance map to match the actual pixel variance
+///
+/// The variance map is adjusted so that the mean matches the actual pixel variance in the image
+bool pmReadoutVarianceRenormalise(
+    const pmReadout *readout,           ///< Readout to normalise
+    psImageMaskType maskVal,            ///< Value to mask
+    int sample,                         ///< Sample size
+    float minValid,                     ///< Minimum valid renormalisation, or NAN
+    float maxValid                      ///< Maximum valid renormalisation, or NAN
+    );
+
+/// Explicitly mask non-finite pixels
+///
+/// Since unmasked non-finite pixels can occur (e.g., by out-of-range in quantisation), it is sometimes
+/// necessary to mask them explicitly.  Non-finite pixels in the image or variance have their mask OR-ed with
+/// the provided value.
+bool pmReadoutMaskNonfinite(pmReadout *readout, ///< Readout to mask
+                            psImageMaskType maskVal ///< Mask value to give non-finite pixels
+    );
+
+// find any pixels which are not already masked (with maskTest) which are not valid and raise maskSet bits
+bool pmReadoutMaskInvalid (const pmReadout *readout, psImageMaskType maskTest, psImageMaskType maskSet);
+
+/// Apply a mask to the image and variance map
+///
+/// Unfortunately, image subtraction may result in a bi-modal image in masked areas, which can upset image
+/// statistics (very important for quantising images so that a product can be written out!).  This function
+/// sets masked areas to NAN in the image and variance.
+bool pmReadoutMaskApply(pmReadout *readout, ///< Readout to mask
+                        psImageMaskType maskVal ///< Mask value for which to apply mask
+    );
+
+/// Interpolate over bad pixels
+///
+/// Scan the mask image for bad pixels, and interpolate over them using the nominated options
+bool pmReadoutInterpolateBadPixels(pmReadout *readout, ///< Readout to work on
+                                   psImageMaskType maskVal, ///< Value to mask
+                                   psImageInterpolateMode mode, ///< Interpolation mode
+                                   float poorFrac, ///< Maximum bad fraction of kernel for "poor" status
+                                   psImageMaskType maskPoor, ///< Mask value to give poor pixels
+                                   psImageMaskType maskBad ///< Mask value to give bad pixels
+    );
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAMosaic.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAMosaic.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAMosaic.c	(revision 42651)
@@ -0,0 +1,1434 @@
+#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., //
+
+#define BLANK_VALUE NAN                 // 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). -- still true??
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// 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++;
+
+        // 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(U8);
+                COPY_WITH_PARITY_DIFFERENCE(U16);
+                COPY_WITH_PARITY_DIFFERENCE(U32);
+                COPY_WITH_PARITY_DIFFERENCE(U64);
+                COPY_WITH_PARITY_DIFFERENCE(S8);
+                COPY_WITH_PARITY_DIFFERENCE(S16);
+                COPY_WITH_PARITY_DIFFERENCE(S32);
+                COPY_WITH_PARITY_DIFFERENCE(S64);
+                COPY_WITH_PARITY_DIFFERENCE(F32);
+                COPY_WITH_PARITY_DIFFERENCE(F64);
+              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(U8);
+            FILL_IN(U16);
+            FILL_IN(U32);
+            FILL_IN(U64);
+            FILL_IN(S8);
+            FILL_IN(S16);
+            FILL_IN(S32);
+            FILL_IN(S64);
+            FILL_IN(F32);
+            FILL_IN(F64);
+          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 *variances,   // Array of variances
+                    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;
+    }
+
+    if (cell->readouts->n > 1) {
+        psWarning("Skipping video cell for mosaic.\n");
+        return true;
+    }
+
+    // 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);
+        variances = psArrayRealloc(variances, 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;
+    variances->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?
+
+    const char *cellName = psMetadataLookupStr(NULL, cell->concepts, "CELL.NAME"); // Name of cell
+    const char *chipName = psMetadataLookupStr(NULL, cell->parent->concepts, "CHIP.NAME"); // Name of chip
+
+    // 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 %s,%s is not set.\n", chipName, cellName);
+        good = false;
+    }
+    int y0Cell = psMetadataLookupS32(&mdok, cell->concepts, "CELL.Y0");
+    if (!mdok) {
+        psError(PS_ERR_UNKNOWN, true, "CELL.Y0 for cell %s,%s is not set.\n", chipName, cellName);
+        good = false;
+    }
+    psTrace("psModules.camera", 5, "Cell %s,%s (%ld): x0=%d y0=%d\n",
+            chipName, cellName, 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 %s is not set.\n", chipName);
+            good = false;
+        }
+        y0Chip = psMetadataLookupS32(&mdok, chip->concepts, "CHIP.Y0");
+        if (!mdok) {
+            psError(PS_ERR_UNKNOWN, true, "CHIP.Y0 for chip %s is not set.\n", chipName);
+            good = false;
+        }
+    }
+    if (!good) {
+	// XXX do something to address this?
+    }
+
+    // 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 %s,%s is not set.\n", chipName, cellName);
+        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 %s,%s is not set.\n", chipName, cellName);
+        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 %s,%s is not set.\n", chipName, cellName);
+        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 %s,%s is not set.\n", chipName, cellName);
+        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 %s is not set.\n", chipName);
+            return false;
+        }
+        yParityChip = psMetadataLookupS32(&mdok, chip->concepts, "CHIP.YPARITY");
+        if (!mdok || (yParityChip != 1 && yParityChip != -1)) {
+            psError(PS_ERR_UNKNOWN, true, "CHIP.YPARITY for chip %s is not set.\n", chipName);
+            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
+    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);
+    variances->data[index] = psMemIncrRefCounter(readout->variance);
+    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 **mosaicVariance, // The mosaic variance, 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
+                       psImageMaskType blank // Mask value to give blank pixels
+                      )
+{
+    assert(mosaicImage);
+    assert(mosaicMask);
+    assert(mosaicVariance);
+    assert(xBinChip);
+    assert(yBinChip);
+    assert(chip);
+    assert(targetCell);
+
+    psArray *images = psArrayAlloc(0); // Array of images that will be mosaicked
+    psArray *variances = psArrayAlloc(0); // Array of variance 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) {
+        psWarning("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) {
+        psWarning("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)) {
+        psWarning("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)) {
+        psWarning("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, variances, 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);
+        *mosaicVariance = imageMosaic(variances, 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(variances);
+    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 **mosaicVariance, // The mosaic variance, 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
+                      psImageMaskType blank  // Mask value to give blank pixels
+                     )
+{
+    assert(mosaicImage);
+    assert(mosaicMask);
+    assert(mosaicVariance);
+    assert(xBinFPA);
+    assert(yBinFPA);
+    assert(fpa);
+    assert(targetChip);
+    assert(targetCell);
+
+    psArray *images = psArrayAlloc(0); // Array of images that will be mosaicked
+    psArray *variances = psArrayAlloc(0); // Array of variance 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) {
+        psWarning("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) {
+        psWarning("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) {
+        psWarning("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) {
+        psWarning("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)) {
+        psWarning("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)) {
+        psWarning("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)) {
+        psWarning("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)) {
+        psWarning("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, variances, 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);
+        *mosaicVariance = imageMosaic(variances, 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(variances);
+    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, psImageMaskType 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 *mosaicVariance = NULL;      // The mosaic variances
+
+    // 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->variances) {
+            mosaicVariance = psImageSubset(hdu->variances->data[0], bounds);
+            if (!mosaicVariance) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to select variance 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, &mosaicVariance, &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
+
+    // Average the covariances
+    psList *covariances = psListAlloc(NULL); // Input covariance matrices
+    for (int i = 0; i < source->cells->n; i++) {
+        pmCell *cell = source->cells->data[i]; // Cell of interest
+        if (!cell || !cell->data_exists) {
+            continue;
+        }
+        pmReadout *ro = cell->readouts->data[0]; // Readout of interest
+        if (!ro || !ro->covariance) {
+            continue;
+        }
+        psListAdd(covariances, PS_LIST_TAIL, ro->covariance);
+    }
+    psKernel *mosaicCovariance = NULL;  // Covariance for mosaic
+    if (psListLength(covariances) > 0) {
+        psArray *covarArray = psListToArray(covariances); // Array with covariances
+        mosaicCovariance = psImageCovarianceAverage(covarArray);
+        psFree(covarArray);
+    }
+    psFree(covariances);
+
+    // Now make a new readout to go in the target cell
+    pmReadout *newReadout = pmReadoutAlloc(targetCell); // New readout
+    newReadout->image  = mosaicImage;
+    newReadout->mask   = mosaicMask;
+    newReadout->variance = mosaicVariance;
+    newReadout->covariance = mosaicCovariance;
+    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, psImageMaskType 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 *mosaicVariances = NULL;      // The mosaic variances
+
+    // 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->variances) {
+            mosaicVariances = psImageSubset(hdu->variances->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, &mosaicVariances, &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);
+
+    // Average the covariances
+    psList *covariances = psListAlloc(NULL); // Input covariance matrices
+    for (int i = 0; i < covariances->n; i++) {
+        pmChip *chip = chips->data[i];  // Chip of interest
+        if (!chip || !chip->data_exists) {
+            continue;
+        }
+        psArray *cells = chip->cells;   // Cells in chip
+        for (long j = 0; j < cells->n; j++) {
+            pmCell *cell = cells->data[i]; // Cell of interest
+            if (!cell || !cell->data_exists) {
+                continue;
+            }
+            pmReadout *ro = cell->readouts->data[0]; // Readout of interest
+            if (!ro || !ro->covariance) {
+                continue;
+            }
+            psListAdd(covariances, PS_LIST_TAIL, ro->covariance);
+        }
+    }
+    psKernel *mosaicCovariances = NULL; // Covariance for mosaic
+    if (psListLength(covariances) > 0) {
+        psArray *covarArray = psListToArray(covariances); // Array with covariances
+        mosaicCovariances = psImageCovarianceAverage(covarArray);
+        psFree(covarArray);
+    }
+    psFree(covariances);
+
+    // Now make a new readout to go in the new cell
+    pmReadout *newReadout = pmReadoutAlloc(targetCell); // New readout
+    newReadout->image  = mosaicImage;
+    newReadout->mask   = mosaicMask;
+    newReadout->variance = mosaicVariances;
+    newReadout->covariance = mosaicCovariances;
+    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) {
+        psWarning("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) {
+        psWarning("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_branches/psModules.20240412/src/camera/pmFPAMosaic.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAMosaic.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAMosaic.h	(revision 42651)
@@ -0,0 +1,41 @@
+/* @file pmFPAMosaic.h
+ * @brief Functions to mosaic FPA components into a single entity
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-01-27 06:39:38 $
+ * 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)
+                  psImageMaskType 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)
+                 psImageMaskType blank       ///< Mask value to give blank pixels
+                );
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPARead.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPARead.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPARead.c	(revision 42651)
@@ -0,0 +1,1449 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <strings.h>
+#include <assert.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmConfigMask.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAFlags.h"
+#include "pmHDUUtils.h"
+#include "pmConceptsRead.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_VARIANCE,             // Read variance map
+    FPA_READ_TYPE_HEADER                // Read header
+} fpaReadType;
+
+// Desired type for pixels; the index corresponds to the fpaReadType, above.
+static psElemType pixelTypes[] = {
+    PS_TYPE_F32,
+    PS_TYPE_IMAGE_MASK,
+    PS_TYPE_F32,
+    0
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Get the "thisXXXScan" value in the readout for the appropriate image type
+static int readoutGetThisScan(pmReadout *readout, // Readout of interest
+                              fpaReadType type // Type of image
+    )
+{
+    switch (type) {
+      case FPA_READ_TYPE_IMAGE:
+        return readout->thisImageScan;
+      case FPA_READ_TYPE_MASK:
+        return readout->thisMaskScan;
+      case FPA_READ_TYPE_VARIANCE:
+        return readout->thisVarianceScan;
+      default:
+        psAbort("Unknown read type: %x\n", type);
+    }
+}
+
+// Set the "thisXXXScan" value in the readout for the appropriate image type
+static void readoutSetThisScan(pmReadout *readout, // Readout of interest
+                              fpaReadType type, // Type of image
+                              int thisScan // Starting scan number
+    )
+{
+    switch (type) {
+      case FPA_READ_TYPE_IMAGE:
+        readout->thisImageScan = thisScan;
+        return;
+      case FPA_READ_TYPE_MASK:
+        readout->thisMaskScan = thisScan;
+        return;
+      case FPA_READ_TYPE_VARIANCE:
+        readout->thisVarianceScan = thisScan;
+        return;
+      default:
+        psAbort("Unknown read type: %x\n", type);
+    }
+}
+
+// Get the "lastXXXScan" value in the readout for the appropriate image type
+static int readoutGetLastScan(pmReadout *readout, // Readout of interest
+                              fpaReadType type // Type of image
+    )
+{
+    switch (type) {
+      case FPA_READ_TYPE_IMAGE:
+        return readout->lastImageScan;
+      case FPA_READ_TYPE_MASK:
+        return readout->lastMaskScan;
+      case FPA_READ_TYPE_VARIANCE:
+        return readout->lastVarianceScan;
+      default:
+        psAbort("Unknown read type: %x\n", type);
+    }
+}
+
+// Set the "lastXXXScan" value in the readout for the appropriate image type
+static void readoutSetLastScan(pmReadout *readout, // Readout of interest
+                              fpaReadType type, // Type of image
+                              int lastScan // Last scan number
+    )
+{
+    switch (type) {
+      case FPA_READ_TYPE_IMAGE:
+        readout->lastImageScan = lastScan;
+        return;
+      case FPA_READ_TYPE_MASK:
+        readout->lastMaskScan = lastScan;
+        return;
+      case FPA_READ_TYPE_VARIANCE:
+        readout->lastVarianceScan = lastScan;
+        return;
+      default:
+        psAbort("Unknown read type: %x\n", type);
+    }
+}
+
+// Return pointer to appropriate image
+static psImage **readoutImageByType(pmReadout *readout, // Readout of interest
+                                    fpaReadType type // Type of image
+    )
+{
+    switch (type) {
+      case FPA_READ_TYPE_IMAGE:
+        return &readout->image;
+      case FPA_READ_TYPE_MASK:
+        return &readout->mask;
+      case FPA_READ_TYPE_VARIANCE:
+        return &readout->variance;
+      default:
+        psAbort("Unknown read type: %x\n", type);
+    }
+}
+
+// Determine number of readouts in the FITS file
+// In the process, reads the header and concepts
+static int cellNumReadouts(pmCell *cell,    // Cell of interest
+                            psFits *fits,    // FITS file
+                            pmConfig *config // Configuration
+    )
+{
+    assert(cell);
+    assert(fits);
+
+    // Get the HDU and read the header
+    pmHDU *hdu = pmHDUFromCell(cell);   // The HDU
+    if (!hdu || hdu->blankPHU) {
+        psError(PS_ERR_IO, true, "Unable to find HDU");
+        return 0;
+    }
+    if (!pmCellReadHeader(cell, fits, config)) {
+        psError(PS_ERR_IO, false, "Unable to read header for cell!\n");
+        return 0;
+    }
+    if (!pmConceptsReadCell(cell, PM_CONCEPT_SOURCE_HEADER | PM_CONCEPT_SOURCE_CELLS, true, NULL)) {
+        psError(PS_ERR_IO, false, "Failed to read concepts for cell.\n");
+        return 0;
+    }
+
+    // Get the size of the third dimension
+    bool mdok;                          // Status of MD lookup
+    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 0;
+    }
+
+    if (naxis == 0) {
+        // No pixels to read
+        psError(PS_ERR_IO, true, "No pixels in extension %s.", hdu->extname);
+        return 0;
+    }
+    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 0;
+    }
+    int naxis3;                     // 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 0;
+        }
+    } else {
+        naxis3 = 1;
+    }
+
+    return naxis3;
+}
+
+// Determine whether a FITS file contains covariance matrices
+static bool hduCovariance(pmHDU *hdu,   // Header data unit
+                          psFits *fits  // FITS file
+    )
+{
+    if (hdu->extname && !psFitsMoveExtName(fits, hdu->extname)) {
+        psError(PS_ERR_IO, false, "Unable to move to extension %s", hdu->extname);
+        return false;
+    }
+    // Need to explicitly read the header, since the HDU may not contain the variance header
+    psMetadata *header = psFitsReadHeader(NULL, fits); // Header
+    if (!header) {
+        psError(PS_ERR_IO, false, "Unable to read variance header.");
+        return false;
+    }
+    bool mdok;                          // Status of MD lookup
+    bool covar = psMetadataLookupBool(&mdok, header, PM_HDU_COVARIANCE_KEYWORD); // Got covariance?
+    psFree(header);
+    return covar;
+}
+
+// Does the current readout, with scans set for a new read, represent any real data, or is it beyond the end?
+// Requires that cellNumReadouts() has been called before (for header and concepts to have been read)
+// In the process, adjusts the TRIMSEC
+static bool readoutHaveMoreScans(int *start, // Start of scan
+                                 int *last, // Last possible scan (defined by TRIMSEC)
+                                 pmReadout *readout, // Readout of interest
+                                 int numScans, // Number of scans to read at a time
+                                 fpaReadType type, // Type of image
+                                 pmConfig *config // Configuration
+                                 )
+{
+    assert(start);
+    assert(last);
+    assert(readout);
+
+    if (!pmConceptsReadCell(readout->parent, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_DATABASE,
+                            true, config)) {
+        psError(PS_ERR_IO, false, "Failed to read concepts for cell.");
+        return false;
+    }
+    // Header and concepts have been read by a call to cellNumReadouts(), so we can just assume they're there.
+
+    // Get the trim and bias sections
+    pmCell *cell = readout->parent;     // Parent cell
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    pmHDU *hdu = pmHDUFromCell(cell);   // HDU for data
+
+    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;
+    }
+    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 2.\n");
+        return false;
+    }
+
+    // Rationalize trimsec against naxis1, naxis2:  valid range for trimsec is 1-Nx,1-Ny
+    if (trimsec->x1 < 1) {
+        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;
+        }
+        trimsec->x1 = naxis1 + trimsec->x1;
+    }
+    if (trimsec->y1 < 1) {
+        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;
+        }
+        trimsec->y1 = naxis2 + trimsec->y1;
+    }
+
+    *last = (readdir == 1) ? trimsec->y1 : trimsec->x1; // Maximum possible scan number
+
+    // Calculate the segment offset and upper limit
+    if (numScans == 0) {
+        // Read entire image.  In that case, we never call this funtion unless the data has not yet been read.
+        // thus, only if the delta is should we return false (ie, trimsec defines an empty region)
+        *start = (readdir == 1) ? trimsec->y0 : trimsec->x0;
+    } else if (readout->forceScan) {
+        // We're forced to read what we're told
+        *start = readoutGetThisScan(readout, type);
+    } else {
+        // Progressive scans
+        psImage *image = *readoutImageByType(readout, type); // Appropriate image from readout
+        *start = image ? readoutGetLastScan(readout, type) : 0;
+    }
+
+    return true;
+}
+
+static bool readoutMore(pmReadout *readout, // Readout of interest
+                        psFits *fits,    // FITS file
+                        int z,          // Plane number to read
+                        int *zMax,      // Max plane number in this cell
+                        int numScans,   // Number of scans to read at a time
+                        fpaReadType type, // Type of image
+                        pmConfig *config// Configuration
+    )
+{
+    assert(readout);
+    assert(fits);
+
+    psImage *image = *readoutImageByType(readout, type);
+
+    // XXX this may not be the valid test in a multithread environment. consider a fileGroup of
+    // N readouts, but numScans set to 0.  only the first should report that it requires data,
+    // even if all readouts lack the image pointer.
+    if (numScans == 0) {
+      if (!image) {
+        return true;
+      } else {
+        return false;
+      }
+    }
+
+    pmCell *cell = readout->parent;     // Parent cell
+    if (!cell) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find parent cell.");
+        return false;
+    }
+    *zMax = cellNumReadouts(cell, fits, config); // Number of planes
+    if (z >= *zMax) {
+        // No more to read
+        return false;
+    }
+
+    int start, last;                    // Start and last scans
+    if (!readoutHaveMoreScans(&start, &last, readout, numScans, type, config)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to determine readout properties.");
+        return false;
+    }
+
+    return start < last;
+}
+
+// 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 // Type of image
+                        )
+{
+    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
+    psImage **target = readoutImageByType(readout, type); // Target image
+    if (*target) {
+        psFree(*target);
+    }
+    *target = psImageSubset(image, region);
+
+    // Get the list of overscans: only for IMAGE types (no overscan for MASK and VARIANCE)
+    if (type == FPA_READ_TYPE_IMAGE) {
+        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);
+                psFree(iter);
+                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
+                                     psElemType type // Expected type for image
+    )
+{
+    assert(fits);
+    assert(fullImage);
+    assert((readdir == 1) || (readdir == 2));
+
+    int nRead = 0;                      // Number of scans read
+    int nScans = max - min;             // Number of scans desired
+    assert(nScans > 0);
+
+    psRegion toRead = *fullImage;  // full image region
+
+    int dX = 0, dY = 0;                 // Offset from image in FITS file to lower left corner of what's read
+    int nX = 0, nY = 0;                 // Size of region to read
+
+    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);
+
+    // Ensure the pixel type corresponds to what we desire
+    if (image->type.type != type) {
+        psImage *temp = psImageCopy(NULL, image, type);
+        psFree(image);
+        image = temp;
+    }
+
+    // Resize the image so that it matches the number of scans requested
+    // XXX this modification is not carried back up stream: it affects readout->row0,col0
+    //
+    // XXXXX Do we really want to do this???  Why???
+    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 a chunk of a readout (or the whole lot)
+static bool readoutReadChunk(pmReadout *readout, // Readout into which to read
+                             psFits *fits, // FITS file
+                             int z,     // Desired image plane
+                             int *zMax, // Max plane number in this cell
+                             int numScans, // Number of scans (row or col depends on CELL.READDIR); 0 for all
+                             int overlap, // Number of scans (row/col) to overlap between scans
+                             fpaReadType type, // Type of image
+                             pmConfig *config   // Configuration
+    )
+{
+    assert(readout);
+    assert(fits);
+    assert(z >= 0);
+    assert(numScans >= 0);
+    assert(overlap >= 0);
+
+    psImage **image = readoutImageByType(readout, type); // Pointer to the image of interest
+    if (*image && numScans == 0) {
+        psError(PS_ERR_UNKNOWN, true, "Already read entire image --- won't clobber.");
+        return false;
+    }
+
+    pmCell *cell = readout->parent;     // The parent cell
+    if (!cell) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find parent cell.");
+        return false;
+    }
+
+    int naxis3 = cellNumReadouts(cell, fits, config); // Number of image planes
+    if (zMax) *zMax = naxis3;
+
+    if (z >= naxis3) {
+        psError(PS_ERR_IO, false, "Desired image plane (%d) exceeds available number (%d).",
+                z, naxis3);
+        return false;
+    }
+
+    int thisScan;                       // Starting scan for this read
+    int maxScan;                        // Maximum scan number
+    if (!readoutHaveMoreScans(&thisScan, &maxScan, readout, numScans, type, config)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to determine readout properties.");
+        return false;
+    }
+    if (thisScan >= maxScan) {
+        psError(PS_ERR_IO, true, "No more of the readout to read.");
+        return false;
+    }
+
+    pmHDU *hdu = pmHDUFromCell(cell);   // The HDU
+    assert(hdu && !hdu->blankPHU);      // Checked by cellNumReadouts()
+
+    bool mdok;                          // Status of MD lookup
+    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;
+    }
+
+    // Need to set the invalid (unread) pixels appropriately, and to read the mask bits
+    float bad = 0;                      // Bad level
+    switch (type) {
+      case FPA_READ_TYPE_MASK: {
+          // Need to explicitly read the header, since what's in the pmHDU may not correspond to the mask
+          psMetadata *header = psFitsReadHeader(NULL, fits);
+          if (!header) {
+              psError(PS_ERR_IO, false, "Unable to read mask header.");
+              return false;
+          }
+          if (!pmConfigMaskReadHeader(config, header)) {
+              psError(PS_ERR_IO, false, "Unable to determine mask bits");
+              psFree(header);
+              return false;
+          }
+          psFree(header);
+          bad = pmConfigMaskGet("LOW", config);
+          if (!bad) {
+              // XXX look up old name for compatability
+              bad = pmConfigMaskGet("BAD", config);
+          }
+          break;
+      }
+      case FPA_READ_TYPE_IMAGE:
+      case FPA_READ_TYPE_VARIANCE:
+        bad = psMetadataLookupF32(&mdok, cell->concepts, "CELL.BAD");
+        break;
+      default:
+        psAbort("Unrecognised type: %x", type);
+    }
+
+    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;
+    }
+
+    // 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
+        psError(PS_ERR_IO, true, "No pixels in extension %s.", hdu->extname);
+        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 origThisScan = thisScan;        // Original value of thisScan (starting point for read)
+    if (thisScan == 0) {
+        overlap = 0;
+    }
+    thisScan -= overlap;
+    if (thisScan < 0) {
+        thisScan = 0;
+    }
+
+    // Calculate limits, adjust readout->row0,col0
+    // XXX Should row0,col0 be adjusted, since they are used for astrometry???
+    if (readdir == 1) {
+        // Reading rows
+        readout->row0 = thisScan;
+        readout->col0 = trimsec->x0;
+        if (numScans == 0) {
+            numScans = trimsec->y1 - trimsec->y0;
+        }
+    } else {
+        // Reading cols
+        readout->col0 = thisScan;
+        readout->row0 = trimsec->y0;
+        if (numScans == 0) {
+            numScans = trimsec->x1 - trimsec->x0;
+        }
+    }
+    int lastScan = origThisScan + numScans; // Last scan to read
+
+    readoutSetThisScan(readout, type, thisScan);
+    readoutSetLastScan(readout, type, lastScan);
+
+    // Blow away existing data.
+    // Do this before returning, so that we're not returning data from a previous read
+//    psFree(*image);
+//    *image = NULL;
+    *image = readoutReadComponent(*image, fits, trimsec, readdir, thisScan, lastScan, z, bad, pixelTypes[type]);
+
+    // Read overscans only for "image" type --- variances and masks shouldn't record overscans
+    if (type == FPA_READ_TYPE_IMAGE) {
+        // Blow away existing data
+        while (readout->bias->n > 0) {
+            psListRemove(readout->bias, PS_LIST_HEAD);
+        }
+
+        // Get the new bias sections
+        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;
+        }
+        psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, false); // Iterator
+        psRegion *biassec = NULL;           // Bias section from iteration
+        while ((biassec = psListGetAndIncrement(biassecsIter))) {
+            psImage *bias = readoutReadComponent(NULL, fits, biassec, readdir, thisScan, lastScan, z, bad, pixelTypes[type]); // The bias
+            psListAdd(readout->bias, PS_LIST_TAIL, bias);
+            psFree(bias);                   // Drop reference
+        }
+        psFree(biassecsIter);
+    }
+
+    return true;
+}
+
+// Read into an cell; this is the engine for pmCellRead, pmCellReadMask, pmCellReadVariance
+// 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
+                     pmConfig *config,  // Configuration
+                     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_VARIANCE:
+        hduReadFunc = pmHDUReadVariance;
+        dataPointer = hdu->variances;
+        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 | PM_CONCEPT_SOURCE_DATABASE, true, config)) {
+        //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 = pixelTypes[type]; // Expected type for image
+    switch (type) {
+      case FPA_READ_TYPE_IMAGE:
+        imageArray = hdu->images;
+        break;
+      case FPA_READ_TYPE_MASK:
+        imageArray = hdu->masks;
+        break;
+      case FPA_READ_TYPE_VARIANCE:
+        imageArray = hdu->variances;
+        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
+        PS_ASSERT_IMAGE_NON_NULL(source, false);
+
+        // 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, pmChipReadVariance
+// Iterates over component cells, reading each
+static bool chipRead(pmChip *chip,      // Chip into which to read
+                     psFits *fits,      // FITS file from which to read
+                     pmConfig *config,  // Configuration
+                     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, config, type);
+    }
+    if (success) {
+        if (!pmConceptsReadChip(chip, PM_CONCEPT_SOURCE_HEADER | PM_CONCEPT_SOURCE_DATABASE,
+                                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, pmFPAReadVariance
+// Iterates over component chips, reading each
+static bool fpaRead(pmFPA *fpa,         // FPA into which to read
+                    psFits *fits,       // FITS file from which to read
+                    pmConfig *config,   // Configuration
+                    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, config, type);
+    }
+    if (success) {
+        if (!pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_HEADER | PM_CONCEPT_SOURCE_DATABASE, 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
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// pmReadoutReadNext is maintained here (for now) to maintain backwards compatibility.
+// pmReadoutReadNext has been replaced by pmReadoutRead, pmReadoutReadChunk, pmReadoutMore
+bool pmReadoutReadNext(bool *status, pmReadout *readout, psFits *fits, int z, int numScans, pmConfig *config)
+{
+    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, config)) {
+        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 | PM_CONCEPT_SOURCE_DATABASE, 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) {
+        psWarning("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;
+
+    // XX not used int maxSize;                        // Number of cols,rows in image
+    // XX not used if (readdir == 1) {
+    // XX not used     maxSize = PS_MIN(naxis2, trimsec->y1 - trimsec->y0);
+    // XX not used } else {
+    // XX not used     maxSize = PS_MIN(naxis1, trimsec->x1 - trimsec->x0);
+    // XX not used }
+
+    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,
+                                          PS_TYPE_F32); // 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,
+                                             PS_TYPE_F32); // The bias
+        psListAdd(readout->bias, PS_LIST_TAIL, bias);
+        psFree(bias);                   // Drop reference
+    }
+    psFree(biassecsIter);
+
+    *status = true;
+    return true;
+}
+
+
+
+bool pmReadoutMore(pmReadout *readout, psFits *fits, int z, int *zMax, int numScans, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return readoutMore(readout, fits, z, zMax, numScans, FPA_READ_TYPE_IMAGE, config);
+}
+
+bool pmReadoutReadChunk(pmReadout *readout, psFits *fits, int z, int *zMax, int numScans, int overlap, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    PS_ASSERT_INT_NONNEGATIVE(z, false);
+    PS_ASSERT_INT_NONNEGATIVE(numScans, false);
+
+    return readoutReadChunk(readout, fits, z, zMax, numScans, overlap, FPA_READ_TYPE_IMAGE, config);
+}
+
+bool pmReadoutRead(pmReadout *readout, psFits *fits, int z, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return readoutReadChunk(readout, fits, z, NULL, 0, 0, FPA_READ_TYPE_IMAGE, config);
+}
+
+int pmCellNumReadouts(pmCell *cell, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return cellNumReadouts(cell, fits, config);
+}
+
+bool pmCellRead(pmCell *cell, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return cellRead(cell, fits, config, FPA_READ_TYPE_IMAGE);
+}
+
+bool pmChipRead(pmChip *chip, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return chipRead(chip, fits, config, FPA_READ_TYPE_IMAGE);
+}
+
+bool pmFPARead(pmFPA *fpa, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return fpaRead(fpa, fits, config, FPA_READ_TYPE_IMAGE);
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Reading the mask
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmReadoutMoreMask(pmReadout *readout, psFits *fits, int z, int *zMax, int numScans, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return readoutMore(readout, fits, z, zMax, numScans, FPA_READ_TYPE_MASK, config);
+}
+
+bool pmReadoutReadChunkMask(pmReadout *readout, psFits *fits, int z, int *zMax, int numScans, int overlap,
+                            pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    PS_ASSERT_INT_NONNEGATIVE(z, false);
+    PS_ASSERT_INT_NONNEGATIVE(numScans, false);
+
+    return readoutReadChunk(readout, fits, z, zMax, numScans, overlap, FPA_READ_TYPE_MASK, config);
+}
+
+bool pmReadoutReadMask(pmReadout *readout, psFits *fits, int z, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return readoutReadChunk(readout, fits, z, NULL, 0, 0, FPA_READ_TYPE_MASK, config);
+}
+
+bool pmCellReadMask(pmCell *cell, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return cellRead(cell, fits, config, FPA_READ_TYPE_MASK);
+}
+
+bool pmChipReadMask(pmChip *chip, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return chipRead(chip, fits, config, FPA_READ_TYPE_MASK);
+}
+
+bool pmFPAReadMask(pmFPA *fpa, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return fpaRead(fpa, fits, config, FPA_READ_TYPE_MASK);
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Reading the variance map
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmReadoutMoreVariance(pmReadout *readout, psFits *fits, int z, int *zMax, int numScans, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return readoutMore(readout, fits, z, zMax, numScans, FPA_READ_TYPE_VARIANCE, config);
+}
+
+bool pmReadoutReadChunkVariance(pmReadout *readout, psFits *fits, int z, int *zMax, int numScans, int overlap,
+                              pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    PS_ASSERT_INT_NONNEGATIVE(z, false);
+    PS_ASSERT_INT_NONNEGATIVE(numScans, false);
+
+    return readoutReadChunk(readout, fits, z, zMax, numScans, overlap, FPA_READ_TYPE_VARIANCE, config);
+}
+
+bool pmReadoutReadVariance(pmReadout *readout, psFits *fits, int z, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return readoutReadChunk(readout, fits, z, NULL, 0, 0, FPA_READ_TYPE_VARIANCE, config);
+}
+
+bool pmCellReadVariance(pmCell *cell, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    if (!cellRead(cell, fits, config, FPA_READ_TYPE_VARIANCE)) {
+        return false;
+    }
+    pmHDU *hdu = pmHDUFromCell(cell);   // Header data unit
+    if (hduCovariance(hdu, fits)) {
+        return pmCellReadCovariance(cell, fits);
+    }
+    return true;
+}
+
+bool pmChipReadVariance(pmChip *chip, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    if (!chipRead(chip, fits, config, FPA_READ_TYPE_VARIANCE)) {
+        return false;
+    }
+    pmHDU *hdu = pmHDUFromChip(chip);   // Header data unit
+    if (hduCovariance(hdu, fits)) {
+        return pmChipReadCovariance(chip, fits);
+    }
+    return true;
+}
+
+bool pmFPAReadVariance(pmFPA *fpa, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    if (!fpaRead(fpa, fits, config, FPA_READ_TYPE_VARIANCE)) {
+        return false;
+    }
+    pmHDU *hdu = pmHDUFromFPA(fpa);     // Header data unit
+    if (hduCovariance(hdu, fits)) {
+        return pmFPAReadCovariance(fpa, fits);
+    }
+    return true;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Reading the image header
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmCellReadHeaderSet(pmCell *cell, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return cellRead(cell, fits, config, FPA_READ_TYPE_HEADER);
+}
+
+bool pmChipReadHeaderSet(pmChip *chip, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return chipRead(chip, fits, config, FPA_READ_TYPE_HEADER);
+}
+
+bool pmFPAReadHeaderSet(pmFPA *fpa, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    return fpaRead(fpa, fits, config, 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_FITS_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_FITS_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_FITS_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;
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Reading covariance matrices
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmCellReadCovariance(pmCell *cell, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    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, "COVAR_%s_%s", chipName, cellName);
+
+    if (!psFitsMoveExtName(fits, extname)) {
+        psError(PS_ERR_IO, false, "Unable to move to extension %s\n", extname);
+        psFree(extname);
+        return false;
+    }
+    psFree(extname);
+
+    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(header);
+        return false;
+    }
+
+    bool mdok;                          // Status of MD lookup
+    int x0 = psMetadataLookupS32(&mdok, header, "COVARIANCE.CENTRE.X"); // Centre of matrix in x
+    if (!mdok) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unable to read covariance centre");
+        psFree(header);
+        return false;
+    }
+    int y0 = psMetadataLookupS32(&mdok, header, "COVARIANCE.CENTRE.Y"); // Centre of matrix in y
+    if (!mdok) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unable to read covariance centre");
+        psFree(header);
+        return false;
+    }
+    psFree(header);
+
+    psArray *images = psFitsReadImageCube(fits, psRegionSet(0,0,0,0)); // Covariance matrices
+    if (!images) {
+        psError(PS_ERR_IO, false, "Unable to read covariance matrices for chip %s, cell %s",
+                chipName, cellName);
+        return false;
+    }
+
+    psArray *readouts = cell->readouts; // Readouts of cell
+    if (images->n != readouts->n) {
+        psError(PS_ERR_BAD_PARAMETER_SIZE, true,
+                "Number of covariance matrices (%ld) doesn't match number of readouts (%ld)",
+                images->n, readouts->n);
+        psFree(images);
+        return false;
+    }
+
+    for (int i = 0; i < readouts->n; i++) {
+        pmReadout *ro = readouts->data[i]; // Readout of interest
+        psImage *image = images->data[i]; // Image of interest
+        if (ro->covariance) {
+            psWarning("Clobbering extant covariance matrix in chip %s, cell %s, readout %d",
+                      chipName, cellName, i);
+            psFree(ro->covariance);
+        }
+        ro->covariance = psKernelAllocFromImage(image, x0, y0);
+    }
+    psFree(images);
+
+    return true;
+}
+
+
+bool pmChipReadCovariance(pmChip *chip, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    psArray *cells = chip->cells;       // Array of cells
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // Cell of interest
+        if (!pmCellReadCovariance(cell, fits)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+
+bool pmFPAReadCovariance(pmFPA *fpa, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    psArray *chips = fpa->chips;        // Array of chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // Chip of interest
+        if (!pmChipReadCovariance(chip, fits)) {
+            return false;
+        }
+    }
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPARead.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPARead.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPARead.h	(revision 42651)
@@ -0,0 +1,272 @@
+/* @file pmFPARead.h
+ * @brief Functions to read FPA components from a FITS file
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.16 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-06 02:31:24 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_READ_H
+#define PM_FPA_READ_H
+
+#include <pmConfig.h>
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// Check to see if there is more to read when reading a readout incrementally
+bool pmReadoutMore(pmReadout *readout,  ///< Readout of interest
+                   psFits *fits,        ///< FITS file from which to read
+                   int z,               ///< Readout number/plane; zero-offset indexing
+		   int *zMax,		///< Max plane number in this cell
+                   int numScans,        ///< Number of scans (rows/cols) to read
+                   pmConfig *config     ///< Configuration
+    );
+
+/// Read a chunk of a readout.
+///
+/// Allows reading the readout incrementally
+bool pmReadoutReadChunk(pmReadout *readout, ///< Readout of interest
+                        psFits *fits,   ///< FITS file from which to read
+                        int z,          ///< Readout number/plane; zero-offset indexing
+		   int *zMax,		///< Max plane number in this cell
+                        int numScans,   ///< Number of scans (rows/cols) to read
+                        int overlap,    ///< Overlap between consecutive reads
+                        pmConfig *config ///< Configuration
+    );
+
+/// Read the entire readout
+bool pmReadoutRead(pmReadout *readout,  ///< Readout of interest
+                   psFits *fits,        ///< FITS file from which to read
+                   int z,               ///< Readout number/plane; zero-offset indexing
+                   pmConfig *config     ///< Configuration
+    );
+
+/// Read a readout incrementally --- this is maintained temporarily only for backwards compatibility; it has
+/// been replaced by pmReadoutRead, pmReadoutReadChunk and pmReadoutMore.
+///
+/// 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
+                       pmConfig *config
+    );
+
+/// Return the number of readouts within a cell
+///
+/// This function is type-independent (doesn't matter if you are interested in the image/mask/variance).
+int pmCellNumReadouts(pmCell *cell, psFits *fits, pmConfig *config);
+
+/// 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
+                pmConfig *config        // Configuration
+    );
+
+/// 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
+                pmConfig *config        // Configuration
+    );
+
+/// 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
+               pmConfig *config         // Configuration
+    );
+
+// Mask functions follow
+
+/// Check to see if there is more to read when reading a readout incrementally into the mask
+bool pmReadoutMoreMask(pmReadout *readout, ///< Readout of interest
+                       psFits *fits,    ///< FITS file from which to read
+                       int z,           ///< Readout number/plane; zero-offset indexing
+		       int *zMax,	///< Max plane number in this cell
+                       int numScans,    ///< Number of scans (rows/cols) to read
+                       pmConfig *config ///< Configuration
+    );
+
+/// Read a chunk of a readout into the mask
+///
+/// Allows reading the readout incrementally
+bool pmReadoutReadChunkMask(pmReadout *readout, ///< Readout of interest
+                            psFits *fits, ///< FITS file from which to read
+                            int z,      ///< Readout number/plane; zero-offset indexing
+			    int *zMax,		///< Max plane number in this cell
+                            int numScans, ///< Number of scans (rows/cols) to read
+                            int overlap, ///< Overlap between consecutive reads
+                            pmConfig *config ///< Configuration
+    );
+
+/// Read the entire readout into the mask
+bool pmReadoutReadMask(pmReadout *readout, ///< Readout of interest
+                       psFits *fits,    ///< FITS file from which to read
+                       int z,           ///< Readout number/plane; zero-offset indexing
+                       pmConfig *config ///< Configuration
+    );
+
+/// 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
+                    pmConfig *config    // Configuration
+    );
+
+/// 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
+                    pmConfig *config    // Configuration
+    );
+
+/// 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
+                   pmConfig *config     // Configuration
+    );
+
+// Variance functions follow
+
+/// Check to see if there is more to read when reading a readout incrementally into the variance
+bool pmReadoutMoreVariance(pmReadout *readout, ///< Readout of interest
+                           psFits *fits, ///< FITS file from which to read
+                           int z,       ///< Readout number/plane; zero-offset indexing
+			   int *zMax,	///< Max plane number in this cell
+                           int numScans, ///< Number of scans (rows/cols) to read
+                           pmConfig *config ///< Configuration
+    );
+
+/// Read a chunk of a readout into the variance
+///
+/// Allows reading the readout incrementally
+bool pmReadoutReadChunkVariance(pmReadout *readout, ///< Readout of interest
+                                psFits *fits, ///< FITS file from which to read
+                                int z,    ///< Readout number/plane; zero-offset indexing
+		   int *zMax,		///< Max plane number in this cell
+                                int numScans, ///< Number of scans (rows/cols) to read
+                                int overlap, ///< Overlap between consecutive reads
+                                pmConfig *config ///< Configuration
+    );
+
+/// Read the entire readout into the variance
+bool pmReadoutReadVariance(pmReadout *readout, ///< Readout of interest
+                           psFits *fits,  ///< FITS file from which to read
+                           int z,         ///< Readout number/plane; zero-offset indexing
+                           pmConfig *config ///< Configuration
+    );
+
+/// Read an entire cell into the variance
+///
+/// Same as pmCellRead, but reads into the variance element of the readouts.
+bool pmCellReadVariance(pmCell *cell,     // Cell to read into
+                        psFits *fits,     // FITS file from which to read
+                        pmConfig *config  // Configuration
+    );
+
+/// Read an entire chip into the variance
+///
+/// Same as pmChipRead, but reads into the variance element of the readouts.
+bool pmChipReadVariance(pmChip *chip,     // Chip to read into
+                        psFits *fits,     // FITS file from which to read
+                        pmConfig *config  // Configuration
+    );
+
+/// Read an entire FPA into the variance
+///
+/// Same as pmFPARead, but reads into the variance element of the readouts.
+bool pmFPAReadVariance(pmFPA *fpa,        // FPA to read into
+                       psFits *fits,      // FITS file from which to read
+                       pmConfig *config   // Configuration
+    );
+
+/// 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
+                         pmConfig *config // Configuration
+    );
+
+/// 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
+                      pmConfig *config  // Configuration
+                     );
+
+/// 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
+                        pmConfig *config // Configuration
+    );
+
+/// 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 to read 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,       ///< Chip for which to read table
+                    psFits *fits,       ///< FITS file from which to read 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,          ///< FPA for which to read table
+                   psFits *fits,        ///< FITS file from which to read the table
+                   const char *name     ///< Specifies the extension name, and target in the analysis metadata
+                  );
+
+/// Read covariance matrices for a cell
+bool pmCellReadCovariance(pmCell *cell, ///< Cell for which to read covariance matrices
+                          psFits *fits  ///< FITS file from which to read
+    );
+
+/// Read covariance matrices for a chip
+bool pmChipReadCovariance(pmChip *chip, ///< Chip for which to read covariance matrices
+                          psFits *fits  ///< FITS file from which to read
+    );
+
+/// Read covariance matrices for a cell
+bool pmFPAReadCovariance(pmFPA *fpa,    ///< FPA for which to read covariance matrices
+                         psFits *fits   ///< FITS file from which to read
+    );
+
+
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAUtils.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAUtils.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAUtils.c	(revision 42651)
@@ -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_branches/psModules.20240412/src/camera/pmFPAUtils.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAUtils.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAUtils.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/camera/pmFPAWrite.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAWrite.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAWrite.c	(revision 42651)
@@ -0,0 +1,787 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmConfigMask.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 "pmConceptsWrite.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_VARIANCE             // Write variance map
+} fpaWriteType;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static (private) functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// 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_VARIANCE:
+        return &hdu->variances;
+    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
+                                 const pmConfig *config, // Configuration
+                                 fpaWriteType type // Type to write
+                                )
+{
+    switch (type) {
+    case FPA_WRITE_TYPE_IMAGE:
+        return pmHDUWrite(hdu, fits, config);
+    case FPA_WRITE_TYPE_MASK:
+        return pmHDUWriteMask(hdu, fits, config);
+    case FPA_WRITE_TYPE_VARIANCE:
+        return pmHDUWriteVariance(hdu, fits, config);
+    default:
+        psAbort("Unknown write type: %x\n", type);
+    }
+    return false;
+}
+
+// Indicate whether a covariance matrix is defined
+static bool readoutSearchCovariances(pmReadout *ro)
+{
+    return ro->covariance ? true : false;
+}
+
+// Search for a covariance matrix
+#define SEARCH_COVARIANCES(NAME, PARENT, CHILD, CHILDREN, TESTFUNC) \
+static bool NAME(PARENT *parent) \
+{ \
+    if (!parent || !parent->CHILDREN) { \
+        return false; \
+    } \
+    psArray *children = parent->CHILDREN; /* Array of children */ \
+    for (int i = 0; i < children->n; i++) { \
+        CHILD *child = children->data[i]; /* Child of interest */ \
+        if (child && TESTFUNC(child)) { \
+            return true; \
+        } \
+    } \
+    return false; \
+}
+
+SEARCH_COVARIANCES(cellSearchCovariances, pmCell, pmReadout, readouts, readoutSearchCovariances);
+SEARCH_COVARIANCES(chipSearchCovariances, pmChip, pmCell,    cells,    cellSearchCovariances);
+SEARCH_COVARIANCES(fpaSearchCovariances,  pmFPA,  pmChip,    chips,    chipSearchCovariances);
+
+// Some type-specific additions to the header
+static bool writeUpdateHeader(pmFPA *fpa, // FPA of interest
+                              pmChip *chip, // Chip of interest, or NULL
+                              pmCell *cell, // Cell of interest, or NULL
+                              fpaWriteType type, // Type to write
+                              pmConfig *config // Configuration
+                              )
+{
+    switch (type) {
+      case FPA_WRITE_TYPE_MASK: {
+          pmHDU *phu = pmHDUGetHighest(fpa, chip, cell); // Primary header
+          if (!pmConfigMaskWriteHeader(config, phu->header)) {
+              psError(PS_ERR_UNKNOWN, false, "Unable to set the mask names in the PHU header");
+              return false;
+          }
+          break;
+      }
+      case FPA_WRITE_TYPE_VARIANCE: {
+          bool covar = false;           // Are covariances present?
+          if ((cell && cellSearchCovariances(cell)) ||
+              (!cell && ((chip && chipSearchCovariances(chip)) ||
+                         (!chip && fpa && fpaSearchCovariances(fpa))))) {
+              covar = true;
+          }
+
+          pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // Header being written
+          psMetadataAddBool(hdu->header, PS_LIST_TAIL, PM_HDU_COVARIANCE_KEYWORD, PS_META_REPLACE,
+                            "Is a covariance matrix present?", covar);
+          break;
+      }
+      default:
+        break;
+    }
+
+    return true;
+}
+
+
+// Write a cell image/mask/variance
+static bool cellWrite(pmCell *cell,     // Cell to write
+                      psFits *fits,     // FITS file to which to write
+                      pmConfig *config, // Configuration
+                      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
+
+    // XXX detect missing variance & mask images...
+
+    // 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) {
+        if (!pmHDUGenerateForCell(cell)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to generate HDU for cell --- likely programming error.");
+            return false;
+        }
+        if (!*imageArray) {
+            if (type == FPA_WRITE_TYPE_IMAGE) {
+                psError(PS_ERR_UNKNOWN, false, "Expected to write an image, but it is missing...programming error?.");
+                return false;
+            }
+            if (type == FPA_WRITE_TYPE_MASK) {
+                psWarning("No mask image for this cell; skipping");
+            }
+            if (type == FPA_WRITE_TYPE_VARIANCE) {
+                psWarning("No variance image for this cell; skipping");
+            }
+            return true;
+        }
+    }
+
+    // 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) {
+        if (!pmConceptsWriteCell(cell, true, config)) {
+            psError(PS_ERR_IO, false, "Unable to write concepts for cell.");
+            return false;
+        }
+        if (!writeUpdateHeader(NULL, NULL, cell, type, config)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to update header for writing");
+            return false;
+        }
+        if (!appropriateWriteFunc(hdu, fits, config, 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/variance
+static bool chipWrite(pmChip *chip,     // Chip to write
+                      psFits *fits,     // FITS file to which to write
+                      pmConfig *config, // Configuration
+                      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) {
+            if (!pmConceptsWriteChip(chip, true, true, config)) {
+                psError(PS_ERR_IO, false, "Unable to write concepts for chip.\n");
+                return false;
+            }
+
+            if (!writeUpdateHeader(NULL, chip, NULL, type, config)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to update header for writing");
+                return false;
+            }
+
+            if (!appropriateWriteFunc(hdu, fits, config, 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, config, false, type)) {
+                psError(PS_ERR_IO, false, "Unable to write Chip.\n");
+                return false;
+            }
+        }
+    }
+
+    return true;
+}
+
+
+// Write an FPA image/mask/variance
+static bool fpaWrite(pmFPA *fpa,        // FPA to write
+                     psFits *fits,      // FITS file to which to write
+                     pmConfig *config,  // Configuration
+                     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) {
+            if (!pmConceptsWriteFPA(fpa, true, config)) {
+                psError(PS_ERR_IO, false, "Unable to write concepts for FPA.\n");
+                return false;
+            }
+            if (!writeUpdateHeader(fpa, NULL, NULL, type, config)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to update header for writing");
+                return false;
+            }
+            if (!appropriateWriteFunc(hdu, fits, config, 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, config, false, true, type)) {
+                psError(PS_ERR_IO, false, "Unable to write FPA.\n");
+                return false;
+            }
+        }
+    }
+
+    return true;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Update the CHIP.NAME and CELL.NAME in the FITS header, if required
+bool pmFPAUpdateNames(pmFPA *fpa, pmChip *chip, pmCell *cell, psS64 imageId, psS64 sourceId)
+{
+    pmHDU *hduHigh = pmHDUGetHighest(fpa, chip, cell); // Highest HDU, i.e., the PHU
+    if (!hduHigh) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find PHU.\n");
+        return false;
+    }
+    if (!hduHigh->header) {
+        hduHigh->header = psMetadataAlloc();
+    }
+    if (!pmHDUWriteIdentifiers(hduHigh, imageId, sourceId)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to write identifiers to header.");
+        return false;
+    }
+
+    pmHDU *hduLow = pmHDUGetLowest(fpa, chip, cell); // Lowest HDU, i.e., the extension
+    if (!hduLow) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find HDU.\n");
+        return false;
+    }
+    if (!hduLow->header) {
+        hduLow->header = psMetadataAlloc();
+    }
+    if (hduLow != hduHigh && !pmHDUWriteIdentifiers(hduLow, imageId, sourceId)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to write identifiers to header.");
+        return false;
+    }
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *fileData = psMetadataLookupMetadata(&mdok, hduHigh->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 && !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(hduHigh->header, PS_LIST_TAIL, contentKey, PS_META_REPLACE,
+                         "Content of file", content);
+        psFree(content);                // Drop reference
+    }
+
+    return true;
+}
+
+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
+    //    psImage *mask = readout->mask;        // Corresponding mask image
+    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, pmConfig *config, bool blank)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    return cellWrite(cell, fits, config, blank, FPA_WRITE_TYPE_IMAGE);
+}
+
+bool pmChipWrite(pmChip *chip, psFits *fits, pmConfig *config, bool blank, bool recurse)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    return chipWrite(chip, fits, config, blank, recurse, FPA_WRITE_TYPE_IMAGE);
+}
+
+bool pmFPAWrite(pmFPA *fpa, psFits *fits, pmConfig *config, bool blank, bool recurse)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    return fpaWrite(fpa, fits, config, blank, recurse, FPA_WRITE_TYPE_IMAGE);
+}
+
+
+bool pmCellWriteMask(pmCell *cell, psFits *fits, pmConfig *config, bool blank)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    return cellWrite(cell, fits, config, blank, FPA_WRITE_TYPE_MASK);
+}
+
+bool pmChipWriteMask(pmChip *chip, psFits *fits, pmConfig *config, bool blank, bool recurse)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    return chipWrite(chip, fits, config, blank, recurse, FPA_WRITE_TYPE_MASK);
+}
+
+bool pmFPAWriteMask(pmFPA *fpa, psFits *fits, pmConfig *config, bool blank, bool recurse)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    return fpaWrite(fpa, fits, config, blank, recurse, FPA_WRITE_TYPE_MASK);
+}
+
+
+bool pmCellWriteVariance(pmCell *cell, psFits *fits, pmConfig *config, bool blank)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    if (!cellWrite(cell, fits, config, blank, FPA_WRITE_TYPE_VARIANCE)) {
+        return false;
+    }
+    if (!pmCellWriteCovariance(fits, cell)) {
+        return false;
+    }
+    return true;
+}
+
+bool pmChipWriteVariance(pmChip *chip, psFits *fits, pmConfig *config, bool blank, bool recurse)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    if (!chipWrite(chip, fits, config, blank, recurse, FPA_WRITE_TYPE_VARIANCE)) {
+        return false;
+    }
+    if (!pmChipWriteCovariance(fits, chip)) {
+        return false;
+    }
+    return true;
+}
+
+bool pmFPAWriteVariance(pmFPA *fpa, psFits *fits, pmConfig *config, bool blank, bool recurse)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    if (!fpaWrite(fpa, fits, config, blank, recurse, FPA_WRITE_TYPE_VARIANCE)) {
+        return false;
+    }
+    if (!pmFPAWriteCovariance(fits, fpa)) {
+        return false;
+    }
+    return true;
+}
+
+
+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;
+}
+
+bool pmCellWriteCovariance(psFits *fits, const pmCell *cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    int numCovar = 0;
+    psArray *readouts = cell->readouts; // Array of readouts
+    for (int i = 0; i < readouts->n; i++) {
+        pmReadout *readout = readouts->data[i]; // The readout of interest
+        if (readout && readout->covariance) {
+            numCovar++;
+        }
+    }
+    if (numCovar == 0) {
+        return true;
+    }
+    if (numCovar != readouts->n) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Number of covariances (%d) doesn't match number of readouts (%ld)",
+                numCovar, readouts->n);
+        return false;
+    }
+
+    // Check size of covariances
+    int xMinCovar = INT_MAX, xMaxCovar = INT_MIN, yMinCovar = INT_MAX, yMaxCovar = INT_MIN; // Size
+    for (int i = 0; i < readouts->n; i++) {
+        pmReadout *readout = readouts->data[i]; // The readout of interest
+        psAssert(readout, "Should be defined.");
+        psKernel *covar = readout->covariance; // Covariance matrix
+        psAssert(covar, "Should be defined.");
+        xMinCovar = PS_MIN(xMinCovar, covar->xMin);
+        xMaxCovar = PS_MAX(xMaxCovar, covar->xMax);
+        yMinCovar = PS_MIN(yMinCovar, covar->yMin);
+        yMaxCovar = PS_MAX(yMaxCovar, covar->yMax);
+    }
+
+    // Correct covariances to common size
+    psArray *images = psArrayAlloc(numCovar); // Array of images
+    for (int i = 0; i < readouts->n; i++) {
+        pmReadout *readout = readouts->data[i]; // The readout of interest
+        psAssert(readout, "Should be defined.");
+        psKernel *covar = readout->covariance; // Covariance matrix
+        psAssert(covar, "Should be defined.");
+        int xMin = covar->xMin, xMax = covar->xMax, yMin = covar->yMin, yMax = covar->yMax;// Size
+        if (xMin == xMinCovar && xMax == xMaxCovar && yMin == yMinCovar && yMax == yMaxCovar) {
+            images->data[i] = psMemIncrRefCounter(covar->image);
+        } else {
+            psImage *new = psImageAlloc(xMaxCovar - xMinCovar + 1, yMaxCovar - yMinCovar + 1, PS_TYPE_F32);
+            psImageInit(new, 0);
+            psImageOverlaySection(new, covar->image, xMinCovar - xMin, yMinCovar - yMin, "=");
+            images->data[i] = new;
+        }
+    }
+
+    // Determine extension name
+    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, "COVAR_%s_%s", chipName, cellName);
+
+    // Generate header
+    pmHDU *hdu = pmHDUFromCell(cell);   // HDU for cell
+    psMetadata *header = psMetadataCopy(NULL, hdu->header); // Header to write
+
+    psMetadataAddS32(header, PS_LIST_TAIL, "COVARIANCE.CENTRE.X", PS_META_REPLACE,
+                     "Centre of covariance matrix in x", -xMinCovar);
+    psMetadataAddS32(header, PS_LIST_TAIL, "COVARIANCE.CENTRE.Y", PS_META_REPLACE,
+                     "Centre of covariance matrix in y", -yMinCovar);
+
+    // Turn off compression
+    int bitpix = fits->options ? fits->options->bitpix : 0; // Desired bits per pixel
+    psFitsScaling scaling = fits->options ? fits->options->scaling : 0; // Current scaling method.
+    psFitsCompression *compress = psFitsCompressionGet(fits); // Current compression options
+    
+/*     fprintf(stderr,"Attempting to write chip %s cell %s extension %s with scaling %d\n", */
+/* 	    chipName,cellName,extname,fits->options->scaling); */
+    if (!psFitsSetCompression(fits, PS_FITS_COMPRESS_NONE, NULL, 0, 0, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to set FITS compression to NONE");
+        psFree(extname);
+        psFree(header);
+        psFree(images);
+        psFree(compress);
+        return false;
+    }
+    if (fits->options) {
+        fits->options->bitpix = 0;
+    }
+    if (fits->options) {
+        fits->options->scaling = psFitsScalingFromString("STDEV_POSITIVE"); // This is a bit of a hack. We don't really have a default value.
+    }
+
+    // Write images
+    if (!psFitsWriteImageCube(fits, header, images, extname)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to write covariances from chip %s, cell %s to extension %s",
+                chipName, cellName, extname);
+        psFree(extname);
+        psFree(header);
+        psFree(images);
+        if (fits->options) {
+            fits->options->bitpix = bitpix;
+        }
+        psFitsCompressionApply(fits, compress);
+        psFree(compress);
+        return 0;
+    }
+    psFree(extname);
+    psFree(header);
+    psFree(images);
+
+    // Restore compression
+    if (fits->options) {
+        fits->options->bitpix = bitpix;
+    }
+    if (fits->options) {
+        fits->options->scaling = scaling;
+    }
+    if (!psFitsCompressionApply(fits, compress)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to restore FITS compression");
+        psFree(compress);
+        return false;
+    }
+    psFree(compress);
+
+    return true;
+}
+
+
+bool pmChipWriteCovariance(psFits *fits, const pmChip *chip)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    psArray *cells = chip->cells;       // Array of cells
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // Cell of interest
+        if (!pmCellWriteCovariance(fits, cell)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+
+bool pmFPAWriteCovariance(psFits *fits, const pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    psArray *chips = fpa->chips;        // Array of chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // Chip of interest
+        if (!pmChipWriteCovariance(fits, chip)) {
+            return false;
+        }
+    }
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAWrite.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAWrite.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAWrite.h	(revision 42651)
@@ -0,0 +1,195 @@
+/* @file pmFPAWrite.h
+ * @brief Write FPA components to a FITS file
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-06 02:31:24 $
+ *
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_WRITE_H
+#define PM_FPA_WRITE_H
+
+#include <pmConfig.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
+                 pmConfig *config,      ///<  Configuration
+                 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
+                 pmConfig *config,      ///<  Configuration
+                 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
+                pmConfig *config,       ///<  Configuration
+                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
+                     pmConfig *config,  ///<  Configuration
+                     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
+                     pmConfig *config,  ///<  Configuration
+                     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
+                    pmConfig *config,   ///<  Configuration
+                    bool blank,         ///<  Write a blank PHU?
+                    bool recurse        ///<  Recurse to lower levels?
+    );
+
+/// Write a cell variance to a FITS file
+///
+/// Generates CELL.TRIMSEC, CELL.BIASSEC and the HDU variance 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 variance 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 pmCellWriteVariance(pmCell *cell,    ///<  Cell to write
+                         psFits *fits,    ///<  FITS file to which to write
+                         pmConfig *config, ///<  Configuration
+                         bool blank       ///<  Write a blank PHU?
+    );
+
+/// Write a chip variance to a FITS file
+///
+/// Generates CELL.TRIMSEC, CELL.BIASSEC and the HDU variance 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 variance 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 pmChipWriteVariance(pmChip *chip,    ///<  Chip to write
+                         psFits *fits,    ///<  FITS file to which to write
+                         pmConfig *config, ///<  Configuration
+                         bool blank,      ///<  Write a blank PHU?
+                         bool recurse     ///<  Recurse to lower levels?
+    );
+
+/// Write an FPA variance to a FITS file
+///
+/// Generates CELL.TRIMSEC, CELL.BIASSEC and the HDU variance 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 variance 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 pmFPAWriteVariance(pmFPA *fpa,       ///<  FPA to write
+                        psFits *fits,     ///<  FITS file to which to write
+                        pmConfig *config, ///<  Configuration
+                        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
+    );
+
+/// Write covariance matrix to a FITS file
+///
+/// The covariance matrices for a cell are written to an independent extension, named after the chip and cell
+/// name.
+bool pmCellWriteCovariance(psFits *fits,///< FITS file to which to write
+                           const pmCell *cell ///< Cell for which to write covariance
+    );
+
+bool pmChipWriteCovariance(psFits *fits,///< FITS file to which to write
+                           const pmChip *chip ///< Chip for which to write covariance
+    );
+
+bool pmFPAWriteCovariance(psFits *fits,///< FITS file to which to write
+                          const pmFPA *fpa ///< FPA for which to write covariance
+    );
+
+// Update the header before writing to be consistent
+//
+// XXX Would like a better name
+bool pmFPAUpdateNames(pmFPA *fpa,       ///< FPA
+                      pmChip *chip,     ///< Chip, or NULL
+                      pmCell *cell,     ///< Cell, or NULL
+                      psS64 imageId,    ///< Image identifier
+                      psS64 sourceId    ///< Source identifier
+    );
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPA_JPEG.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPA_JPEG.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPA_JPEG.c	(revision 42651)
@@ -0,0 +1,260 @@
+/** @file  pmFPA_JPEG.c
+ *
+ * This file contains functions to write JPEG images.
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.27 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-01-27 06:39:38 $
+ *
+ *  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 "pmConfigMask.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, "File is not of type JPEG");
+        return false;
+    }
+
+    psTrace("psModules.camera", 3, "writing jpeg for readout %p\n", readout);
+
+    psMetadata *recipe = psMetadataLookupMetadata(&status, config->recipes, "JPEG");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find JPEG recipe");
+        return false;
+    }
+
+    psMetadata *options = psMetadataLookupMetadata(&status, recipe, file->name);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find options for file %s in JPEG recipe", file->name);
+        return false;
+    }
+
+    // measure the image statistics for scaling
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS);
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN);
+    stats->nSubsample = 10000;
+    psImageMaskType maskVal = pmConfigMaskGet("MASK.VALUE", config); // Value to mask
+    float mean = 0, delta = 0;          //
+    if (!psImageBackground(stats, NULL, readout->image, readout->mask, maskVal, rng)) {
+        psStats *statsAlt = psStatsAlloc(PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+        stats->nSubsample = 10000;
+        if (!psImageBackground(stats, NULL, readout->image, readout->mask, maskVal, rng)) {
+            psLogMsg("psModules.jpeg", PS_LOG_WARN,
+                     "Unable to measure statistics for image; writing blank jpeg");
+            mean = 0;
+            delta = 1;
+        } else {
+            mean = statsAlt->sampleMean;
+            delta = 2.0 * statsAlt->sampleStdev;
+        }
+        psFree(statsAlt);
+    } else {
+        mean = stats->robustMedian;
+        delta = stats->robustUQ - stats->robustLQ;
+    }
+    psFree(rng);
+    psFree(stats);
+
+    // default options are: no flip in X or Y, scale bar on bottom
+    psImageJpegOptions *jpegOptions = psImageJpegOptionsAlloc();
+
+    char *colormapName = psMetadataLookupStr(NULL, options, "COLORMAP"); // Name of colour map
+    if (!colormapName) {
+        colormapName = "-greyscale";
+    }
+    psImageJpegColormapSet(jpegOptions, colormapName);
+
+    // set up the scale options
+    char *mode = psMetadataLookupStr(NULL, options, "SCALE.MODE"); // Mode for scaling image
+    if (!mode) {
+        mode = "RANGE";
+    }
+    float fmin = psMetadataLookupF32(&status, options, "SCALE.MIN"); // Minimum value/faction for scaling
+    if (!status) {
+        fmin = -3.0;
+    }
+    float fmax = psMetadataLookupF32(&status, options, "SCALE.MAX"); // Maximum value/faction for scaling
+    if (!status) {
+        fmax = +6.0;
+    }
+    if (!strcasecmp(mode, "RANGE")) {
+        jpegOptions->min = mean + fmin*delta;
+        jpegOptions->max = mean + fmax*delta;
+    } else if (!strcasecmp(mode, "FRACTION")) {
+        jpegOptions->min = fmin*mean;
+        jpegOptions->max = fmax*mean;
+    } else if (!strcasecmp(mode, "VALUE")) {
+	jpegOptions->min = fmin;
+        jpegOptions->max = fmax;
+    } else {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unrecognised scaling mode: %s", mode);
+        return false;
+    }
+    if (!isfinite(jpegOptions->min) || !isfinite(jpegOptions->max)) {
+        psLogMsg("psModules.jpeg", PS_LOG_WARN,
+                 "The stretch parameters are not both finite --- writing blank jpeg");
+        jpegOptions->min = 0;
+        jpegOptions->max = 1;
+    }
+
+    jpegOptions->showScale = PS_JPEG_SHOWSCALE_NONE;
+    jpegOptions->xFlip = false;
+    jpegOptions->yFlip = false;
+    
+    char *userOptions = psMetadataLookupStr(&status, options, "OPTIONS"); // Mode for scaling image
+    if (userOptions) {
+	// just use strstr : strcasestr is non-standard an not always available.  replace with our own?
+	if (strstr(userOptions, "+SB")) {
+	    jpegOptions->showScale = PS_JPEG_SHOWSCALE_BOTTOM;
+	} 
+	if (strstr(userOptions, "-X")) {
+	    jpegOptions->xFlip = true;
+	} 
+	if (strstr(userOptions, "-Y")) {
+	    jpegOptions->yFlip = true;
+	} 
+	// lowercase versions 
+	if (strstr(userOptions, "+sb")) {
+	    jpegOptions->showScale = PS_JPEG_SHOWSCALE_BOTTOM;
+	} 
+	if (strstr(userOptions, "-x")) {
+	    jpegOptions->xFlip = true;
+	} 
+	if (strstr(userOptions, "-y")) {
+	    jpegOptions->yFlip = true;
+	} 
+    }
+
+    // NOTE: we can overlay the location of the stars from the readout by passing a bDrawBuffer to this function
+
+    if (!psImageJpeg(jpegOptions, readout->image, NULL, file->filename)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to write JPEG image");
+        psFree(jpegOptions);
+        return false;
+    }
+
+    psFree(jpegOptions);
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPA_JPEG.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPA_JPEG.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPA_JPEG.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/camera/pmFPA_MANAPLOT.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPA_MANAPLOT.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPA_MANAPLOT.c	(revision 42651)
@@ -0,0 +1,176 @@
+/** @file  pmFPA_MANAPLOT.c
+ *
+ * This file contains functions to write MANAPLOT images.
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-06-10 20:58:28 $
+ *
+ *  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, false);
+    psFree (tmpName);
+
+    tmpName = pmFPAfileNameFromRule (file->filerule, file, view);
+    psString output = pmConfigConvertFilename (tmpName, config, create, false);
+    psFree (tmpName);
+
+    tmpName = pmFPAfileNameFromRule (file->extname, file, view);
+    psString script = pmConfigConvertFilename (tmpName, config, create, false);
+    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_branches/psModules.20240412/src/camera/pmFPA_MANAPLOT.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPA_MANAPLOT.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPA_MANAPLOT.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/camera/pmFPAfile.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAfile.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAfile.c	(revision 42651)
@@ -0,0 +1,691 @@
+#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 "pmHDUUtils.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPACopy.h"
+#include "pmConceptsCopy.h"
+
+static int fileNum = 0;                 // Number of file
+
+static bool fpaFileFreeStrict = true;   // Strict checking when file has been freed?
+
+bool pmFPAfileFreeSetStrict(bool new)
+{
+    bool old = fpaFileFreeStrict;       // Old value, to return
+    fpaFileFreeStrict = new;
+    return old;
+}
+
+static void pmFPAfileFree(pmFPAfile *file)
+{
+    if (!file) {
+        return;
+    }
+    psTrace ("pmFPAfileFree", 5, "freeing %s %p\n", file->name, file->fits);
+    psAssert(!fpaFileFreeStrict || file->fits == NULL, "File %s wasn't closed.", file->name);
+
+    int testFile = FALSE;							
+    testFile = testFile || !strcmp(file->name, "GDIFF.OUTPUT.SOURCES");	
+    testFile = testFile || !strcmp(file->name, "GDIFF.POS1.SOURCES");	
+    testFile = testFile || !strcmp(file->name, "GDIFF.POS2.SOURCES");	
+    if (FALSE && testFile) {							
+      fprintf (stderr, "%s : %d : %d", file->name, file->state, file->mode);
+      fprintf (stderr, "\n");
+    }
+
+    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);
+
+    psFree(file->compression);
+    psFree(file->options);
+
+    psFree (file->filerule);
+
+    psFree (file->filesrc);
+    psFree (file->detrend);
+
+    psFree (file->filename);
+    psFree (file->origname);
+    psFree (file->extname);
+
+    return;
+}
+
+pmFPAfile *pmFPAfileAlloc(void)
+{
+    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->options = NULL;
+    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->origname = NULL;
+    file->extname  = NULL;
+
+    file->filesrc = NULL;
+    file->detrend = NULL;
+
+    file->xBin = 1;
+    file->yBin = 1;
+    file->src = NULL;
+
+    file->save = false;
+
+    file->fileIndex = fileNum++;
+    file->fileID = 0;
+
+    file->imageId = 0;
+    file->sourceId = 0;
+
+    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);
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    psString newName = NULL;            // New name, to be returned
+    newName = psStringCopy(rule);
+
+    bool status = false;
+    if (strstr(newName, "{FPA.OBS}")) {
+        char *name = psMetadataLookupStr(&status, fpa->concepts, "FPA.OBS");
+        if (name) {
+            psStringSubstitute(&newName, name, "{FPA.OBS}");
+        } 
+    }
+    if (strstr(newName, "{FPA.NAME}")) {
+        char *name = psMetadataLookupStr(NULL, fpa->concepts, "FPA.NAME");
+        if (name) {
+            psStringSubstitute(&newName, name, "{FPA.NAME}");
+        }
+    }
+    if (strstr(newName, "{CHIP.NAME}")) {
+        const char *name = NULL;        // Name of chip
+        pmChip *chip = pmFPAviewThisChip(view, fpa);
+        if (chip) {
+            name = psMetadataLookupStr(NULL, chip->concepts, "CHIP.NAME");
+            psAssert(name, "All chips should have a name");
+        } else {
+            name = "fpa";
+        }
+        psStringSubstitute(&newName, name, "{CHIP.NAME}");
+
+    }
+    if (strstr(newName, "{CHIP.ID}")) {
+        pmChip *chip = pmFPAviewThisChip(view, fpa);
+        if (chip) {
+            char *name = psMetadataLookupStr(NULL, chip->concepts, "CHIP.ID");
+            if (name) {
+                psStringSubstitute(&newName, name, "{CHIP.ID}");
+            }
+        }
+    }
+    if (strstr(newName, "{CHIP.N}")) {
+        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, "{CHIP.NUM}")) {
+        char *name = NULL;
+        if (view->chip < 0) {
+            psStringAppend(&name, "XX");
+        } else {
+            int chipNum = view->chip;   // Number of chip
+            // Potential correction for fortran (unit-indexed) numbering
+            pmChip *chip = pmFPAviewThisChip(view, fpa); // Chip of interest
+            pmHDU *hdu = pmHDUFromChip(chip); // Corresponding HDU
+            bool mdok;                  // Status of MD lookup
+            psMetadata *formats = psMetadataLookupMetadata(&mdok, hdu->format, "FORMATS"); // Special formats
+            if (mdok && formats) {
+                const char *format = psMetadataLookupStr(&mdok, formats, "CHIP.NUM"); // Format for CHIP.NUM
+                if (mdok && format && strcmp(format, "FORTRAN") == 0) {
+                    chipNum++;
+                }
+            }
+            psStringAppend(&name, "%d", chipNum);
+        }
+        psStringSubstitute(&newName, name, "{CHIP.NUM}");
+        psFree(name);
+    }
+    if (strstr(newName, "{CELL.NAME}")) {
+        const char *name = NULL;        // Name of cell
+        pmCell *cell = pmFPAviewThisCell(view, fpa);
+        if (cell) {
+            name = psMetadataLookupStr(NULL, cell->concepts, "CELL.NAME");
+            psAssert(name, "All cells should have a name");
+        } else {
+            name = "chip";
+        }
+        psStringSubstitute(&newName, name, "{CELL.NAME}");
+    }
+    if (strstr(newName, "{CELL.N}")) {
+        char *name = NULL;
+        if (view->cell < 0) {
+            psStringAppend(&name, "XX");
+        } else {
+            psStringAppend(&name, "%02d", view->cell);
+        }
+        psStringSubstitute(&newName, name, "{CELL.N}");
+    }
+    if (strstr(newName, "{CELL.NUM}")) {
+        char *name = NULL;
+        if (view->cell < 0) {
+            psStringAppend(&name, "XX");
+        } else {
+            int cellNum = view->cell;   // Number of cell
+            // Potential correction for fortran (unit-indexed) numbering
+            pmCell *cell = pmFPAviewThisCell(view, fpa); // Cell of interest
+            pmHDU *hdu = pmHDUFromCell(cell); // Corresponding HDU
+            bool mdok;                  // Status of MD lookup
+            psMetadata *formats = psMetadataLookupMetadata(&mdok, hdu->format, "FORMATS"); // Special formats
+            if (mdok && formats) {
+                const char *format = psMetadataLookupStr(&mdok, formats, "CELL.NUM"); // Format for CELL.NUM
+                if (mdok && format && strcmp(format, "FORTRAN") == 0) {
+                    cellNum++;
+                }
+            }
+            psStringAppend(&name, "%d", cellNum);
+        }
+        psStringSubstitute(&newName, name, "{CELL.NUM}");
+    }
+    if (strstr(newName, "{EXTNAME}")) {
+        pmHDU *hdu = pmFPAviewThisHDU(view, fpa);
+        if (hdu->extname && *hdu->extname) {
+            psStringSubstitute(&newName, hdu->extname, "{EXTNAME}");
+        }
+    }
+    if (strstr(newName, "{FILTER}") && fpa) {
+        char *name = psMetadataLookupStr(NULL, fpa->concepts, "FPA.FILTER");
+        if (name && *name) {
+            psStringSubstitute(&newName, name, "{FILTER}");
+        }
+    }
+    if (strstr(newName, "{FILTER.ID}") && fpa) {
+        char *name = psMetadataLookupStr(NULL, fpa->concepts, "FPA.FILTERID");
+        if (name && *name) {
+            psStringSubstitute(&newName, name, "{FILTER.ID}");
+        }
+    }
+    if (strstr(newName, "{CAMERA}") && fpa) {
+        char *name = psMetadataLookupStr(NULL, fpa->concepts, "FPA.INSTRUMENT");
+        if (name && *name) {
+            psStringSubstitute(&newName, name, "{CAMERA}");
+        }
+    }
+    if (strstr(newName, "{INSTRUMENT}") && fpa) {
+        char *name = psMetadataLookupStr(NULL, fpa->concepts, "FPA.INSTRUMENT");
+        if (name && *name) {
+            psStringSubstitute(&newName, name, "{INSTRUMENT}");
+        }
+    }
+    if (strstr(newName, "{DETECTOR}") && fpa) {
+        char *name = psMetadataLookupStr(NULL, fpa->concepts, "FPA.DETECTOR");
+        if (name && *name) {
+            psStringSubstitute(&newName, name, "{DETECTOR}");
+        }
+    }
+    if (strstr(newName, "{TELESCOPE}") && fpa) {
+        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}");
+        }
+    }
+
+    if (strstr(newRule, "{FILE.INDEX}")) {
+        // Number of the file in list
+        psString num = NULL;            // Number to use
+        psStringAppend(&num, "%d", file->fileIndex);
+        psStringSubstitute(&newRule, num, "{FILE.INDEX}");
+        psFree(num);
+    }
+
+    if (strstr(newRule, "{FILE.ID}")) {
+        // Number of the file in list
+        psString num = NULL;            // Number to use
+        psStringAppend(&num, "%" PRId64, file->fileID);
+        psStringSubstitute(&newRule, num, "{FILE.ID}");
+        psFree(num);
+    }
+
+    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, const 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
+    pmConceptsCopyFPA(out, in, true, true);
+
+    // 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, "CFF"))     {
+        return PM_FPA_FILE_CFF;
+    }
+    if (!strcasecmp(type, "WCS"))     {
+        return PM_FPA_FILE_WCS;
+    }
+    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, "VARIANCE"))     {
+        return PM_FPA_FILE_VARIANCE;
+    }
+    if (!strcasecmp(type, "FRINGE")) {
+        return PM_FPA_FILE_FRINGE;
+    }
+    if (!strcasecmp(type, "DARK"))     {
+        return PM_FPA_FILE_DARK;
+    }
+    if (!strcasecmp(type, "HEADER"))     {
+        return PM_FPA_FILE_HEADER;
+    }
+    if (!strcasecmp(type, "LINEARITY"))  {
+      return PM_FPA_FILE_LINEARITY;
+    }
+    if (!strcasecmp(type, "NEWNONLIN"))  {
+      return PM_FPA_FILE_NEWNONLIN;
+    }
+    if (!strcasecmp(type, "ASTROM"))     {
+      return PM_FPA_FILE_ASTROM_MODEL;
+    }
+    if (!strcasecmp(type, "ASTROM.MODEL"))     {
+        return PM_FPA_FILE_ASTROM_MODEL;
+    }
+    if (!strcasecmp(type, "ASTROM.REFSTARS"))     {
+        return PM_FPA_FILE_ASTROM_REFSTARS;
+    }
+    if (!strcasecmp(type, "KH.CORRECT"))     {
+        return PM_FPA_FILE_KH_CORRECT;
+    }
+    if (!strcasecmp(type, "PATTERN.ROW.AMP"))     {
+        return PM_FPA_FILE_PATTERN_ROW_AMP;
+    }
+    if (!strcasecmp(type, "PATTERN.DEAD.CELLS"))     {
+        return PM_FPA_FILE_PATTERN_DEAD_CELLS;
+    }
+    if (!strcasecmp(type, "SUBKERNEL"))     {
+        return PM_FPA_FILE_SUBKERNEL;
+    }
+    if (!strcasecmp(type, "PATTERN")) {
+        return PM_FPA_FILE_PATTERN;
+    }
+    if (!strcasecmp(type, "EXPNUM")) {
+        return PM_FPA_FILE_EXPNUM;
+    }
+
+    return PM_FPA_FILE_NONE;
+}
+
+const 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_CFF:
+        return ("CFF");
+      case PM_FPA_FILE_WCS:
+        return ("WCS");
+      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_VARIANCE:
+        return ("VARIANCE");
+      case PM_FPA_FILE_FRINGE:
+        return ("FRINGE");
+      case PM_FPA_FILE_DARK:
+        return("DARK");
+      case PM_FPA_FILE_HEADER:
+        return ("HEADER");
+      case PM_FPA_FILE_ASTROM_MODEL:
+        return ("ASTROM.MODEL");
+      case PM_FPA_FILE_ASTROM_REFSTARS:
+        return ("ASTROM.REFSTARS");
+      case PM_FPA_FILE_KH_CORRECT:
+        return ("KH.CORRECT");
+      case PM_FPA_FILE_PATTERN_ROW_AMP:
+        return ("PATTERN.ROW.AMP");
+      case PM_FPA_FILE_PATTERN_DEAD_CELLS:
+        return ("PATTERN.DEAD.CELLS");
+      case PM_FPA_FILE_SUBKERNEL:
+        return ("SUBKERNEL");
+      case PM_FPA_FILE_PATTERN:
+        return "PATTERN";
+      case PM_FPA_FILE_EXPNUM:
+        return "EXPNUM";
+      default:
+        return ("NONE");
+    }
+    return ("NONE");
+}
+
+
+psArray *pmFPAfileSelect(psMetadata *files, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(files, NULL);
+
+    psList *list = psListAlloc(NULL);   // List of files selected
+
+    psString regex = NULL;              // Regular expression
+    if (name) {
+        if (!psMetadataLookup(files, name)) {
+            psFree (list);
+            return NULL;
+        }
+        psStringAppend(&regex, "^%s$", name);
+    }
+    psMetadataIterator *iter = psMetadataIteratorAlloc(files, PS_LIST_HEAD, regex); // Iterator
+    psFree(regex);
+    psMetadataItem *item;               // Item from iteration
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        pmFPAfile *file = item->data.V; // File of iterest
+        psListAdd(list, PS_LIST_TAIL, file);
+    }
+    psFree(iter);
+
+    psArray *array = psListToArray(list); // Array generated from list
+    psFree(list);
+
+    return array;
+}
+
+pmFPAfile *pmFPAfileSelectSingle(psMetadata *files, const char *name, int num)
+{
+    PS_ASSERT_PTR_NON_NULL(files, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(num, NULL);
+
+    psString regex = NULL;              // Regular expression
+    if (name) {
+        if (!psMetadataLookup(files, name)) {
+            // No files
+            return NULL;
+        }
+        psStringAppend(&regex, "^%s$", name);
+    }
+
+    psMetadataIterator *iter = psMetadataIteratorAlloc(files, PS_LIST_HEAD, regex); // Iterator
+    psFree(regex);
+    psMetadataItem *item;               // Item from iteration
+    int i = 0;                          // Counter
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        if (i++ == num) {
+            psFree(iter);
+            return item->data.V;
+        }
+    }
+    psFree(iter);
+
+    psLogMsg("psModules.camera", PS_LOG_MINUTIA, "Unable to find instance %d of file %s", num, name);
+    return NULL;
+}
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAfile.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAfile.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAfile.h	(revision 42651)
@@ -0,0 +1,180 @@
+/* @file  pmFPAview.h
+ * @brief Tools to manipulate the FPA structure elements.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.35 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-06 02:31:24 $
+ *
+ * Copyright 2004-2005 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_FILE_H
+#define PM_FPA_FILE_H
+
+#include <pslib.h>
+
+#include <pmFPALevel.h>
+#include <pmFPA.h>
+#include <pmFPAview.h>
+#include <pmDetrendDB.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_CFF,
+    PM_FPA_FILE_WCS,
+    PM_FPA_FILE_RAW,
+    PM_FPA_FILE_IMAGE,
+    PM_FPA_FILE_MASK,
+    PM_FPA_FILE_VARIANCE,
+    PM_FPA_FILE_FRINGE,
+    PM_FPA_FILE_DARK,
+    PM_FPA_FILE_PSF,
+    PM_FPA_FILE_JPEG,
+    PM_FPA_FILE_KAPA,
+    PM_FPA_FILE_HEADER,
+    PM_FPA_FILE_ASTROM_MODEL,
+    PM_FPA_FILE_ASTROM_REFSTARS,
+    PM_FPA_FILE_KH_CORRECT,
+    PM_FPA_FILE_SUBKERNEL,
+    PM_FPA_FILE_SRCTEXT,
+    PM_FPA_FILE_PATTERN,
+    PM_FPA_FILE_PATTERN_ROW_AMP,
+    PM_FPA_FILE_PATTERN_DEAD_CELLS,
+    PM_FPA_FILE_LINEARITY,
+    PM_FPA_FILE_NEWNONLIN,
+    PM_FPA_FILE_EXPNUM,
+} 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
+    psFitsOptions *options;             // FITS I/O options
+
+    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 *origname;                     // the original name (before mangling) 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
+
+    int fileIndex;			// Index of file
+    psS64 fileID;		        // internal sequence number
+
+    psS64 imageId, sourceId;            // Image and source identifiers
+} pmFPAfile;
+
+// allocate an empty pmFPAfile structure
+pmFPAfile *pmFPAfileAlloc(void);
+
+// 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, const 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
+const char *pmFPAfileStringFromType(pmFPAfileType type);
+
+/// Select files with the same name from the list of files
+///
+/// Returns all files if name is NULL.
+psArray *pmFPAfileSelect(psMetadata *files, ///< All files
+                         const char *name ///< Name of file(s) to return, or NULL for all
+    );
+
+/// Select a specific instance of a file from the list of files
+///
+/// Returns the num-th instance of all files if name is NULL.
+pmFPAfile *pmFPAfileSelectSingle(psMetadata *files, ///< All files
+                                 const char *name, ///< Name of file
+                                 int num ///< Instance number of specific instance
+    );
+
+/// Set strict checking when freeing file
+///
+/// If true (default), freeing a pmFPAfile involves asserting that the FITS filehandle has been closed.
+bool pmFPAfileFreeSetStrict(bool new    // New state
+    );
+
+
+/// @}
+# endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileDefine.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileDefine.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileDefine.c	(revision 42651)
@@ -0,0 +1,1482 @@
+#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 "pmConfigMask.h"
+#include "pmConfigRun.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"
+
+#include "pmConceptsCopy.h"
+
+# define FPA_TEST_ASSERT(A){ \
+        assert(A->format == NULL); \
+        assert(A->formatName == NULL); \
+        assert(A->filerule == NULL); \
+        assert(A->filesrc == NULL); }
+
+// 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;
+}
+
+// Parse an option from a metadata, returning the appropriate float value
+static float parseOptionFloat(const psMetadata *md, // Metadata containing the option
+                              const char *name, // Option name
+                              const char *source // Description of source, for warning messages
+                              )
+{
+    psMetadataItem *item = psMetadataLookup(md, name); // Item with the value of interest
+    if (!item) {
+        psWarning("Unable to find value for %s in %s", name, source);
+        return NAN;
+    }
+    float value = psMetadataItemParseF32(item); // Value of interst
+    return value;
+}
+
+// Parse an option from a metadata, returning the appropriate double value
+static double parseOptionDouble(const psMetadata *md, // Metadata containing the option
+                                const char *name, // Option name
+                                const char *source // Description of source, for warning messages
+                                )
+{
+    psMetadataItem *item = psMetadataLookup(md, name); // Item with the value of interest
+    if (!item) {
+        psWarning("Unable to find value for %s in %s", name, source);
+        return NAN;
+    }
+    double value = psMetadataItemParseF64(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, char *cameraName, 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(psErrorCodeLast(), false, "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");
+    if (!type) {
+        psError(PM_ERR_CONFIG, true, "FILE.TYPE is not defined for %s\n", name);
+        psFree(file);
+        return NULL;
+    }
+
+    file->type = pmFPAfileTypeFromString(type);
+    if (file->type == PM_FPA_FILE_NONE) {
+        psError(PM_ERR_CONFIG, true, "FILE.TYPE %s is not registered in pmFPAfile.c:pmFPAfileTypeFromString\n", type);
+        psFree(file);
+        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(PM_ERR_CONFIG, 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;
+
+    if (fpa) {
+        file->fpa = psMemIncrRefCounter(fpa);
+        file->camera = psMemIncrRefCounter((psMetadata *)fpa->camera);
+        file->cameraName = cameraName ? psMemIncrRefCounter(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(PM_ERR_CONFIG, 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->system, "CAMERAS"); // Known cameras
+        if (!mdok || !cameras) {
+            psError(PM_ERR_CONFIG, true, "Unable to find CAMERAS in the system configuration.\n");
+            return NULL;
+        }
+        camera = psMetadataLookupMetadata(&mdok, cameras, cameraName); // Camera configuration of interest
+        if (!mdok || !camera) {
+            psError(PM_ERR_CONFIG, true,
+                    "Unable to find automatically generated camera configuration %s in system configuration.",
+                    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 *filerule = pmConfigFileRule(config, camera, name); // File rule
+    if (!filerule) {
+        psError(psErrorCodeLast(), false, "Can't find file rule %s!", name);
+        return NULL;
+    }
+
+    pmFPAfile *file = pmFPAfileAlloc();
+
+    // save the name of this pmFPAfile
+    file->name = psStringCopy(name);
+
+    // this is the filename rule
+    file->filerule = psMemIncrRefCounter(psMetadataLookupStr(&status, filerule, "FILENAME.RULE"));
+
+    const char *type = psMetadataLookupStr(&status, filerule, "FILE.TYPE");
+    file->type = pmFPAfileTypeFromString(type);
+    if (file->type == PM_FPA_FILE_NONE) {
+        psError(PM_ERR_CONFIG, 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);
+
+    // Copy the file id valuves if they have been set in the config
+    if (config->sourceId) {
+        file->sourceId = config->sourceId;
+    }
+    if (config->imageId) {
+        file->imageId = config->imageId;
+    }
+
+    // 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, filerule, "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(PM_ERR_CONFIG, 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, file->cameraName);
+    }
+
+    // Get FITS output scheme
+    const char *fitsType = psMetadataLookupStr(&status, filerule, "FITS.TYPE"); // Name of FITS scheme to use
+    if (fitsType && strcasecmp(fitsType, "NONE") != 0) {
+
+        // load the FITSTYPE scheme for this file
+        psMetadata *scheme = pmConfigFitsType(config, camera, fitsType); // File rule
+        if (!scheme) {
+            // XXX change to a config error?
+            psWarning("Unable to find %s in FITS in camera configuration --- will use defaults.", fitsType);
+            goto FITS_OPTIONS_DONE;
+        }
+        psLogMsg ("psModules.camera", PS_LOG_INFO, "using FITS.TYPE %s for %s.\n", fitsType, file->name);
+
+        psString source = NULL;     // Source of options
+        psStringAppend(&source, "%s in FITS in camera configuration", fitsType);
+
+        psFitsOptions *options = file->options = psFitsOptionsAlloc(); // FITS I/O options
+
+        // Custom floating-point
+        bool mdok;                      // Status of MD lookup
+        const char *floatName = psMetadataLookupStr(&mdok, scheme, "FLOAT"); // Name of custom float
+        if (mdok && floatName) {
+            psString fullName = NULL;   // Full name of custom floating-point
+            psStringAppend(&fullName, "FLOAT_%s", floatName);
+            options->floatType = psFitsFloatTypeFromString(fullName);
+            psFree(fullName);
+        }
+
+        options->bitpix = parseOptionInt(scheme, "BITPIX", source, 0); // Bits per pixel
+
+        // Scaling options
+        const char *scalingString = psMetadataLookupStr(&mdok, scheme, "SCALING"); // Scaling name
+        if (scalingString) {
+            options->scaling = psFitsScalingFromString(scalingString); // Scaling method
+
+            switch (options->scaling) {
+              case PS_FITS_SCALE_NONE:
+              case PS_FITS_SCALE_RANGE:
+	      case PS_FITS_SCALE_LOG_RANGE:
+	      case PS_FITS_SCALE_ASINH_RANGE:
+                // No options required
+                break;
+              case PS_FITS_SCALE_STDEV_POSITIVE:
+              case PS_FITS_SCALE_STDEV_NEGATIVE:
+	      case PS_FITS_SCALE_LOG_STDEV_POSITIVE:
+  	      case PS_FITS_SCALE_LOG_STDEV_NEGATIVE:
+	      case PS_FITS_SCALE_ASINH_STDEV_POSITIVE:
+  	      case PS_FITS_SCALE_ASINH_STDEV_NEGATIVE:
+                options->stdevNum = parseOptionFloat(scheme, "STDEV.NUM", source); // Padding to edge
+                if (!isfinite(options->stdevNum)) {
+                    psError(PM_ERR_CONFIG, true, "Bad value for STDEV.NUM for %s", source);
+                    psFree(source);
+                    psFree(file);
+                    return NULL;
+                }
+                // Flow through
+              case PS_FITS_SCALE_STDEV_BOTH:
+	      case PS_FITS_SCALE_LOG_STDEV_BOTH:
+	      case PS_FITS_SCALE_ASINH_STDEV_BOTH:
+                options->stdevBits = parseOptionInt(scheme, "STDEV.BITS", source, 0); // Bits for stdev
+                if (options->stdevBits <= 0) {
+                    psError(PM_ERR_CONFIG, true, "Bad value for STDEV.BITS (%d) for %s",
+                            options->stdevBits, source);
+                    psFree(source);
+                    psFree(file);
+                    return NULL;
+                }
+                break;
+              case PS_FITS_SCALE_MANUAL:
+                options->bscale = parseOptionDouble(scheme, "BSCALE", source); // Scaling
+                options->bzero = parseOptionDouble(scheme, "BZERO", source); // Zero point
+                break;
+	      case PS_FITS_SCALE_LOG_MANUAL:
+		options->bscale = parseOptionDouble(scheme, "BSCALE", source); // Scaling
+		options->bzero = parseOptionDouble(scheme, "BZERO", source); // Zero point
+		options->boffset = parseOptionDouble(scheme, "BOFFSET", source); // Log offset
+	      case PS_FITS_SCALE_ASINH_MANUAL:
+		options->bscale = parseOptionDouble(scheme, "BSCALE", source); // Scaling
+		options->bzero = parseOptionDouble(scheme, "BZERO", source); // Zero point
+		options->boffset = parseOptionDouble(scheme, "BOFFSET", source); // Log offset
+		options->bsoften = parseOptionDouble(scheme, "BSOFTEN", source); // Softening parameter
+		break;	      
+	      default:
+                psAbort("Should never get here.");
+            }
+        }
+
+        psMetadataItem *fuzz = psMetadataLookup(scheme, "FUZZ"); // Quantisation fuzz?
+        if (fuzz) {
+            if (fuzz->type != PS_DATA_BOOL) {
+                psWarning("FUZZ in compression scheme %s isn't boolean.", fitsType);
+                goto FITS_OPTIONS_DONE;
+            }
+            options->fuzz = fuzz->data.B;
+        }
+
+        // Compression options
+        const char *compressString = psMetadataLookupStr(&mdok, scheme, "COMPRESSION"); // Compression type
+        if (mdok && compressString) {
+            psFitsCompressionType type = psFitsCompressionTypeFromString(compressString); // Compression
+            psVector *tile = psVectorAlloc(3, PS_TYPE_S32); // Tile sizes
+            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 = 0, hsmooth = 0;// Scaling and smoothing for HCOMPRESS
+            if (type == PS_FITS_COMPRESS_HCOMPRESS) {
+                hscale = parseOptionInt(scheme, "HSCALE", source, 0);
+                hsmooth = parseOptionInt(scheme, "HSMOOTH", source, 0);
+            }
+
+            file->compression = psFitsCompressionAlloc(type, tile, noise, hscale, hsmooth);
+            psFree(tile);
+        }
+
+        psFree(source);
+    }
+ FITS_OPTIONS_DONE:
+
+    file->fileLevel = pmFPAPHULevel(format);
+    if (file->fileLevel == PM_FPA_LEVEL_NONE) {
+        psError(PM_ERR_CONFIG, true, "Unable to determine file level for %s\n", name);
+        psFree(file);
+        return NULL;
+    }
+
+    file->dataLevel = pmFPALevelFromName(psMetadataLookupStr(&status, filerule, "DATA.LEVEL"));
+    if (file->dataLevel == PM_FPA_LEVEL_NONE) {
+        psError(PM_ERR_CONFIG, 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, "Output file name", outname);
+
+    // place the resulting file in the config system
+    psMetadataAddPtr(config->files, PS_LIST_TAIL, name, PS_DATA_UNKNOWN | PS_META_DUPLICATE_OK,
+                     "Output file", 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);
+}
+
+// given a filename, convert to UNIX namespace and read the PHU 
+psMetadata *readPHUfromFilename (char *filename, pmConfig *config) {
+
+    // Need to generate an FPA
+    psString realName = pmConfigConvertFilename(filename, config, false, false);
+    if (!realName) {
+	psError(psErrorCodeLast(), false, "Failed to convert file name %s", filename);
+	return NULL;
+    }
+
+    // load the header of the first image
+    // EXTWORD (fits->extword) is not relevant to the PHU
+    psFits *fits = psFitsOpen(realName, "r"); // FITS file
+    if (!fits) {
+	psError(psErrorCodeLast(), false, "Failed to open file %s", realName);
+	psFree(realName);
+	return NULL;
+    }
+
+    psMetadata *phu = psFitsReadHeader (NULL, fits); // Primary header
+    if (!phu) {
+	psError(psErrorCodeLast(), false, "Failed to read file header %s", realName);
+	psFree(realName);
+	return NULL;
+    }
+
+    if (!psFitsClose(fits)) {
+	psError(psErrorCodeLast(), false, "Failed to close file %s", realName);
+	psFree(realName);
+	psFree(phu);
+	return NULL;
+    }
+
+    psFree(realName);
+    return phu;
+}
+
+// this this function wants to return:
+// pmFPA, PHU, fileLevel, outConfig
+// camera, cameraName, formatName
+typedef struct {
+    pmFPA *fpa;
+    psMetadata *phu;
+    psMetadata *format;
+    pmFPALevel fileLevel;
+    psString cameraName;
+    psString formatName;
+} pmFPAfromFilenameOutput;
+
+// for the given filename, read PHU and determine camera format; build an FPA for the file
+bool pmFPAfromFilename (pmFPAfromFilenameOutput *output, pmConfig **outConfig, pmConfig *sysConfig, char *filename){
+
+    // Need to generate an FPA
+    psMetadata *phu = readPHUfromFilename (filename, sysConfig);
+    if (!phu) {
+	psError(psErrorCodeLast(), false, "Failed to read PHU for %s", filename);
+	return false;
+    }
+
+    // if we expect the loaded FPA to differ in configuration from the current system configuration
+    // generate an output config for this FPA
+    pmConfig *config = NULL;
+    if (outConfig) {
+	config = pmConfigAlloc();
+	config->user = psMemIncrRefCounter(sysConfig->user);
+	config->system = psMemIncrRefCounter(sysConfig->system);
+
+	psFree (config->files);
+	config->files = psMemIncrRefCounter(sysConfig->files);
+	psFree (config->arguments);
+	config->arguments = psMemIncrRefCounter(sysConfig->arguments);
+
+	*outConfig = config;
+    } else {
+	config = sysConfig;
+    }
+
+    // values which are returned to calling function
+    psString formatName = NULL;	// Name of camera format
+    psString cameraName = NULL;	// Name of camera
+    psMetadata *camera = NULL;	// Camera configuration
+
+    // Determine the current format from the header; determine camera if not specified already.
+    psMetadata *format = pmConfigCameraFormatFromHeader(&camera, &cameraName, &formatName, config, phu, true);
+    if (!format) {
+	psError(psErrorCodeLast(), false, "Failed to determine camera format for %s", filename);
+	psFree(camera);
+	psFree(formatName);
+	psFree(phu);
+	return false;
+    }
+
+    pmFPALevel fileLevel = pmFPAPHULevel(format);
+    if (fileLevel == PM_FPA_LEVEL_NONE) {
+	psError(PM_ERR_CONFIG, true, "Unable to determine file level for %s", filename);
+	psFree(camera);
+	psFree(formatName);
+	psFree(phu);
+	return false;
+    }
+
+    // build the template fpa, set up the basic view
+    // we supply the metaCamera name (if NULL, baseCamera name is used)
+    pmFPA *fpa = pmFPAConstruct(camera, cameraName);
+    psFree(camera);
+
+    if (!fpa) {
+	psError(psErrorCodeLast(), false, "Failed to construct FPA from %s", filename);
+	psFree(formatName);
+	psFree(format);
+	psFree(phu);
+	return NULL;
+    }
+
+    output->fpa = fpa;
+    output->phu = phu;
+    output->format = format;
+    output->fileLevel = fileLevel;
+    output->cameraName = cameraName;
+    output->formatName = formatName;
+
+    return true;
+
+}
+
+/// Define a file from an array of filenames
+static pmFPAfile *fpaFileDefineFromArray(pmConfig **outConfig, // output configuration
+					 pmConfig *sysConfig, // global configuration
+                                         pmFPAfile *bind, // File to bind to, or NULL
+                                         const char *name, // Name of file
+                                         const psArray *filenames // Array of file names
+    )
+{
+    PS_ASSERT_PTR_NON_NULL(sysConfig, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+
+    pmFPA *fpa = NULL;                  // FPA for file
+    psMetadata *format = NULL;          // Camera format configuration
+    psString formatName = NULL;         // Name of camera format
+    psString cameraName = NULL;         // Name of camera
+    pmFPALevel fileLevel = PM_FPA_LEVEL_NONE; // Level for files
+    psMetadata *phu = NULL;             // Primary header
+
+    if (bind) {
+        // Use the FPA we're binding to
+        fpa = psMemIncrRefCounter(bind->fpa);
+        fileLevel = bind->fileLevel;
+    } else {
+	pmFPAfromFilenameOutput output;
+	if (!pmFPAfromFilename (&output, outConfig, sysConfig, filenames->data[0])) {
+	    return NULL;
+	}
+	fpa = output.fpa;
+	phu = output.phu;
+	format = output.format;
+	fileLevel = output.fileLevel;
+	cameraName = output.cameraName;
+	formatName = output.formatName;
+    }
+
+    pmConfig *config = outConfig ? *outConfig : sysConfig;
+
+    // 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, cameraName, name); // File, to return
+    if (!file) {
+        psError(psErrorCodeLast(), false, "File %s not defined", name);
+        psFree(formatName);
+        psFree(format);
+        psFree(fpa);
+        psFree(phu);
+        return NULL;
+    }
+    psFree(cameraName);
+    psFree(fpa);                        // Drop reference
+
+    file->format = format;
+    file->formatName = formatName;
+    file->fileLevel = fileLevel;
+
+    // We use the filerule and filesrc to identify the files in the file->names data
+    psFree(file->filerule); // this is set in pmFPAfileDefineInput
+    file->filerule = psStringCopy("@FILES");
+    file->filesrc = psStringCopy("{CHIP.NAME}.{CELL.NAME}");
+
+    // Examine the list of input files and validate their cameras
+    // Associate each filename with an element of the FPA
+    // Save the association on file->names
+    for (int i = 0; i < filenames->n; i++) {
+        // Check that the file corresponds to the same camera and format
+        if (!phu) {
+	    phu = readPHUfromFilename (filenames->data[i], config);
+            if (!phu) {
+                psError(psErrorCodeLast(), false, "Failed to read PHU for %s", (char *)filenames->data[i]);
+                return NULL;
+            }
+        }
+
+        if (i == 0 && file->type == PM_FPA_FILE_MASK) {
+            if (!pmConfigMaskReadHeader(config, phu)) {
+                psError(psErrorCodeLast(), false, "Error reading mask bits");
+                psFree(phu);
+                return NULL;
+            }
+        }
+
+        if (bind || i > 0) {
+            if (format) {
+                bool valid = false;
+                if (!pmConfigValidateCameraFormat(&valid, format, phu)) {
+                    psError(psErrorCodeLast(), false, "Error in config scripts\n");
+                    psFree(phu);
+                    return NULL;
+                }
+                if (!valid) {
+                    psError(psErrorCodeLast(), false, "File %s is not from the required camera",
+                            (char*)filenames->data[i]);
+                    psFree(phu);
+                    return NULL;
+                }
+            } else {
+                format = pmConfigCameraFormatFromHeader(NULL, NULL, NULL, config, phu, true);
+                if (!format) {
+                    psError(psErrorCodeLast(), false, "Failed to determine camera format from %s",
+                            (char*)filenames->data[i]);
+                    psFree(phu);
+                    return NULL;
+                }
+            }
+        }
+
+        // Ensure the format is set
+        if (!file->format) {
+            file->format = format;
+        }
+        if (!file->formatName) {
+            file->formatName = formatName;
+        }
+
+        // Set the view to the corresponding entry for this phu
+        pmFPAview *view = NULL;         // View to PHU
+        if (bind) {
+            view = pmFPAIdentifySourceFromHeader(bind->fpa, phu, format);
+        } else {
+            view = pmFPAAddSourceFromHeader(fpa, phu, format);
+        }
+        psFree(phu);
+        phu = NULL;
+        if (!view) {
+            psError(PM_ERR_CONFIG, true, "Unable to determine source for %s", name);
+            return NULL;
+        }
+
+        // Associate the filename with the FPA element
+        psString location = pmFPAfileNameFromRule(file->filesrc, file, view);
+        psFree(view);
+        psMetadataAddStr(file->names, PS_LIST_TAIL, location, 0, "Location of file", filenames->data[i]);
+        psFree(location);
+    }
+
+    return file;
+}
+
+// find the file associated with the argname & generate a pmFPAfile for it based on the filerule
+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);
+
+    // Search the argument data for the named fileset (argname)
+    bool status;                        // Status of MD lookup
+    psArray *filenames = psMetadataLookupPtr(&status, config->arguments, argname); // Filenames for file
+    if (!status) {
+        if (success) {
+            *success = true;
+        }
+        return NULL;
+    }
+    if (filenames->n == 0) {
+        psError(PM_ERR_CONFIG, true, "No files in array in %s in arguments", argname);
+        if (success) {
+            *success = false;
+        }
+        return NULL;
+    }
+
+    pmFPAfile *file = fpaFileDefineFromArray(NULL, config, NULL, filename, filenames); // File of interest
+
+    if (success) {
+        *success = file ? true : false;
+    }
+
+    return file;
+}
+
+// find the file associated with the argname & bind it to the given pmFPAfile for it based on the filerule
+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);
+
+    // Search the argument data for the named fileset (argname)
+    bool status;                        // Status of MD lookup
+    psArray *filenames = psMetadataLookupPtr(&status, config->arguments, argname); // Filenames for file
+    if (!status) {
+        if (success) {
+            *success = true;
+        }
+        return NULL;
+    }
+    if (filenames->n == 0) {
+        psError(PM_ERR_CONFIG, true, "No files in array in %s in arguments", argname);
+        if (success) {
+            *success = false;
+        }
+        return NULL;
+    }
+
+    pmFPAfile *file = fpaFileDefineFromArray(NULL, config, input, filename, filenames); // File of interest
+
+    if (success) {
+        *success = file ? true : false;
+    }
+
+    return file;
+}
+
+// find the specific file associated with the argname & generate a pmFPAfile for it based on the filerule
+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);
+
+    // Search the argument data for the named fileset (argname)
+    bool status;                        // Status from MD lookup
+    psArray *filenames = psMetadataLookupPtr(&status, config->arguments, argname); // Filenames for file
+    if (!status) {
+        if (success) {
+            *success = true;
+        }
+        return NULL;
+    }
+    if (filenames->n <= entry) {
+        psError(PM_ERR_CONFIG, true, "Insufficient files (%ld) in array in %s in arguments",
+                filenames->n, argname);
+        if (success) {
+            *success = false;
+        }
+        return NULL;
+    }
+
+    psArray *single = psArrayAlloc(1);  // Array of single filename of interest
+    single->data[0] = psMemIncrRefCounter(filenames->data[entry]);
+    pmFPAfile *file = fpaFileDefineFromArray(NULL, config, NULL, filename, single); // File of interest
+    psFree(single);
+
+    if (success) {
+        *success = file ? true : false;
+    }
+
+    return file;
+}
+
+// find the file in the config list & generate a pmFPAfile for it based on the filerule
+// return values (return, status):
+// (not NULL, true) : file defined on RUN and can be loaded
+// (NULL, true) : file not defined on RUN
+// (NULL, false) : file defined on RUN, cannot be loaded
+pmFPAfile *pmFPAfileDefineFromRun(bool *success, pmFPAfile *bind, pmConfig *config, const char *filename)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+
+    psArray *filenames = pmConfigRunFileGet(config, filename); // Filenames used, or NULL
+    if (!filenames) {
+        if (success) {
+            *success = true;
+        }
+        return NULL;
+    }
+
+    pmFPAfile *file = fpaFileDefineFromArray(NULL, config, bind, filename, filenames); // File of interest
+    psFree(filenames);
+
+    if (success) {
+        *success = file ? true : false;
+    }
+
+    return file;
+}
+
+// find the files in the config list & generate an array of pmFPAfiles for them based on the filerule
+psArray *pmFPAfileDefineMultipleFromRun(bool *success, psArray *bind, pmConfig *config, const char *filename)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+
+    if (success) {
+        *success = false;
+    }
+
+    psArray *files = pmConfigRunFileGet(config, filename); // Filenames used, to return
+    if (!files || files->n == 0) {
+        if (success) {
+            *success = true;
+        }
+        return NULL;
+    }
+    if (bind && files->n != bind->n) {
+        psError(PM_ERR_CONFIG, true,
+                "Length of filenames (%ld) and bind files (%ld) does not match.",
+                files->n, bind->n);
+        psFree(files);
+        return NULL;
+    }
+
+    psArray *dummy = psArrayAlloc(1);   // Dummy array of single filename
+    for (int i = 0; i < files->n; i++) {
+        psFree(dummy->data[0]);
+        dummy->data[0] = files->data[i];
+        pmFPAfile *bindFile = bind ? bind->data[i] : NULL; // File to which to bind
+        files->data[i] = psMemIncrRefCounter(fpaFileDefineFromArray(NULL, config, bindFile, filename, dummy));
+        if (!files->data[i]) {
+            psError(psErrorCodeLast(), false, "Unable to define file %s %d", filename, i);
+            psFree(dummy);
+            psFree(files);
+            return NULL;
+        }
+    }
+    psFree(dummy);
+
+    if (success) {
+        *success = true;
+    }
+
+    return files;
+}
+
+// find the file associated with the argname & generate a pmFPAfile for it based on the filerule
+pmFPAfile *pmFPAfileDefineNewConfig(bool *success, pmConfig **outConfig, pmConfig *sysConfig, const char *filename, const char *argname)
+{
+    PS_ASSERT_PTR_NON_NULL(outConfig, NULL);
+    PS_ASSERT_PTR_NON_NULL(sysConfig, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(argname, NULL);
+
+    // Search the argument data for the named fileset (argname)
+    bool status;                        // Status of MD lookup
+    psArray *filenames = psMetadataLookupPtr(&status, sysConfig->arguments, argname); // Filenames for file
+    if (!status) {
+        if (success) {
+            *success = true;
+        }
+        return NULL;
+    }
+    if (filenames->n == 0) {
+        psError(PM_ERR_CONFIG, true, "No files in array in %s in arguments", argname);
+        if (success) {
+            *success = false;
+        }
+        return NULL;
+    }
+
+    pmFPAfile *file = fpaFileDefineFromArray(outConfig, sysConfig, NULL, filename, filenames); // File of interest
+
+    if (success) {
+        *success = file ? true : false;
+    }
+
+    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(PM_ERR_PROG, true, "camera is not defined");
+        return NULL;
+    }
+
+    // build the template fpa, set up the basic view
+    pmFPA *fpa = pmFPAConstruct(config->camera, config->cameraName);
+    if (!fpa) {
+        psError(psErrorCodeLast(), 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, NULL, filename);
+    psFree (fpa);
+    if (!file) {
+        psError(psErrorCodeLast(), false, "file %s not defined\n", filename);
+        return NULL;
+    }
+
+    // image names may not come from file->names
+    if (!strcasecmp(file->filerule, "@FILES")) {
+        psError(PM_ERR_CONFIG, true, "supplied filerule uses illegal value @FILES");
+        // XXX remove the file from config->files
+        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) XXX potentially free the fpa...
+        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, 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 (type == PM_DETREND_TYPE_NONE) {
+        return NULL;
+    }
+
+    if (success) {
+        *success = false;
+    }
+
+    // a camera config is needed (as source of file rule)
+    if (config->camera == NULL) {
+        psError(PM_ERR_PROG, 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, config->cameraName);
+        if (!fpa) {
+            psError(psErrorCodeLast(), 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, NULL, filename);
+        if (!file) {
+            psError(psErrorCodeLast(), false, "file %s not defined\n", filename);
+            psFree(fpa);
+            return NULL;
+        }
+
+        // XXX A TEST: this is a provisional fpa until we read the first header for this pmFPAfile
+        // we are going to replace it when we determine the true file.  blow this away here...
+        psFree (file->fpa);
+        file->fpa = 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");
+    }
+
+    // XXX careful about this: is this set correctly in the camera.config files?
+    char *cameraName = psMetadataLookupStr(NULL, input->concepts, "FPA.CAMERA");
+    pmDetrendSelectOptions *options = pmDetrendSelectOptionsAlloc(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(PM_ERR_CONFIG, 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 (psErrorCodeLast(), false, "no matching detrend data");
+            return NULL;
+        }
+        file->detrend = results;
+        file->fileLevel = pmFPALevelFromName(results->level);
+        if (file->fileLevel == PM_FPA_LEVEL_NONE) {
+            psError (PM_ERR_CONFIG, false, "invalid file level for selected detrend data");
+            return NULL;
+        }
+    }
+
+    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, psMetadataLookupStr(NULL, src->concepts, "FPA.CAMERA"));
+    // XXX should this use DefineOutputForFormat?
+    pmFPAfile *file = pmFPAfileDefineOutput (config, fpa, filename);
+    if (!file) {
+        psError(psErrorCodeLast(), 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);
+    if (!file) {
+        psError(psErrorCodeLast(), false, "file %s not defined\n", filename);
+        return NULL;
+    }
+    file->src = psMemIncrRefCounter(src->fpa); // inherit output elements from this source pmFPA
+    file->xBin = xBin;
+    file->yBin = yBin;
+
+    // inherit the concepts from the src fpa:
+    pmConceptsCopyFPA(file->fpa, file->src, true, true);
+
+    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(psErrorCodeLast(), false, "file %s not defined\n", filename);
+        return NULL;
+    }
+    if (!file->camera) {
+        psError(PM_ERR_CONFIG, false, "file %s does not define a new camera\n", filename);
+        return NULL;
+    }
+    file->fpa = pmFPAConstruct(file->camera, file->cameraName);
+
+    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(PM_ERR_CONFIG, true, "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(PM_ERR_CONFIG, true, "file %s not defined\n", filename);
+        return NULL;
+    }
+
+    file->src = psMemIncrRefCounter(src); // inherit output elements from this source pmFPA
+    if (src) {
+        if (!pmConceptsCopyFPA(file->fpa, src, true, false)) {
+            psError(psErrorCodeLast(), false, "Unable to copy concepts from source to new FPA");
+            return NULL;
+        }
+    }
+
+    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(PM_ERR_CONFIG, true, "file %s not defined\n", filename);
+        return NULL;
+    }
+
+    file->src = psMemIncrRefCounter(src); // inherit output elements from this source pmFPA
+    if (src) {
+        if (!pmConceptsCopyFPA(file->fpa, src, false, false)) {
+            psError(psErrorCodeLast(), false, "Unable to copy concepts from source to new FPA");
+            return NULL;
+        }
+    }
+
+    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);
+
+    // free a previously existing readout
+    psFree(file->readout);
+    file->readout = readout;
+
+    // allow for multiple entries
+    // XXX handle replace vs multiple?
+    psMetadataAddPtr(files, PS_LIST_TAIL, name, PS_DATA_UNKNOWN | PS_META_DUPLICATE_OK, "", 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(PM_ERR_CONFIG, 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;
+}
+
+// Select or construct the requested readout.  If the named entry does not exist, generate it based
+// on the specified fpa and binning.  We have 4 possibilities: (INTERNAL or I/O file) and (exists or
+// not).  This call is used after all user-requested pmFPAfiles have been generated.  A missing
+// pmFPAfile is being used internally.
+pmReadout *pmFPAGenerateReadout(const pmConfig *config, // configuration information
+                                const pmFPAview *view, // select background for this entry
+                                const char *name, // name of internal/external file
+                                const pmFPA *fpa, // use this fpa to generate
+                                const psImageBinning *binning,
+                                int index) {
+  pmReadout *readout = NULL;
+
+  pmFPAfile *file = pmFPAfileSelectSingle(config->files, name, index);
+
+  // if the file does not exist, it is not being used as an I/O file: define an internal version
+  if (file == NULL) {
+      // XXX currently, we do not guarantee that the defined file lands on entry 'index'
+      psAssert (binning, "internal files must be supplied a psImageBinning for the output images size"); 
+      readout = pmFPAfileDefineInternal (config->files, name, binning->nXruff, binning->nYruff, PS_TYPE_F32);
+      return readout;
+  }
+
+  // if the mode is INTERNAL, it has been defined in a previous call.  XXX This seems to require
+  // that the readout have the same dimensions for all entries.
+  if (file->mode == PM_FPA_MODE_INTERNAL) {
+    readout = file->readout;
+    return readout;
+  }
+
+  // we are using this pmFPAfile as an I/O file: select readout or create
+  readout = pmFPAviewThisReadout (view, file->fpa);
+  if (readout == 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
+    if (binning == NULL) {
+      pmFPAfileCopyStructureView (file->fpa, fpa, 1, 1, view);
+      readout = pmFPAviewThisReadout (view, file->fpa);
+    } else {
+      pmFPAfileCopyStructureView (file->fpa, fpa, binning->nXbin, binning->nYbin, view);
+      readout = pmFPAviewThisReadout (view, file->fpa);
+      PS_ASSERT (binning->nXruff == readout->image->numCols, false);
+      PS_ASSERT (binning->nYruff == readout->image->numRows, false);
+    }
+  }
+
+  return readout;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileDefine.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileDefine.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileDefine.h	(revision 42651)
@@ -0,0 +1,189 @@
+/* @file  pmFPAview.h
+ * @brief Tools to manipulate the FPA structure elements.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.18 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-11-11 00:03:46 $
+ * 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, char *cameraName, 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.
+///
+/// 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 *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);
+
+/// Define a file based on the filenames in the RUN metadata in the configuration
+///
+/// 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 *pmFPAfileDefineFromRun(
+    bool *found,                        ///< Found files?
+    pmFPAfile *bind,                    ///< File to which to bind, or NULL
+    pmConfig *config,                   ///< Configuration
+    const char *filename                ///< Name of file
+    );
+
+/// Define multiple files based on the filenames in the RUN metadata in the configuration
+///
+/// An array of the files defined is returned
+psArray *pmFPAfileDefineMultipleFromRun(
+    bool *found,                        ///< Found files?
+    psArray *bind,                      ///< Files to which to bind, or NULL
+    pmConfig *config,                   ///< Configuration
+    const char *filename                ///< Name of file
+    );
+
+// find the file associated with the argname & generate a pmFPAfile for it based on the filerule
+pmFPAfile *pmFPAfileDefineNewConfig(
+    bool *success,                      ///< Found files?
+    pmConfig **outConfig, 		///< output configuration for this file
+    pmConfig *sysConfig, 		///< existing system config info
+    const char *filename, 		///< name of filerule
+    const char *argname			///< argument entry
+    );
+
+
+// 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
+///
+/// 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 *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)
+///
+/// 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.
+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);
+
+// Select or construct the requested readout.  If the named entry does not exist, generate it based
+// on the specified fpa and binning.  We have 4 possibilities: (INTERNAL or I/O file) and (exists or
+// not).  This call is used after all user-requested pmFPAfiles have been generated.  A missing
+// pmFPAfile is being used internally.
+pmReadout *pmFPAGenerateReadout(const pmConfig *config, // configuration information
+                                const pmFPAview *view, // select background for this entry
+                                const char *name, // name of internal/external file
+                                const pmFPA *fpa, // use this fpa to generate
+                                const psImageBinning *binning,
+				int index
+    );
+
+/// @}
+# endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileFitsIO.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileFitsIO.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileFitsIO.c	(revision 42651)
@@ -0,0 +1,625 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmConfigMask.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"
+#include "pmDark.h"
+#include "pmConceptsWrite.h"
+
+// Get a suitable FPA for the file; generate it if necessary
+static pmFPA *suitableFPA(const pmFPAfile *file, // File for which to get FPA
+                          const pmFPAview *view, // View at which to produce the FPA
+                          pmConfig *config, // Configuration (for concepts update)
+                          bool pixels   // Worry about copying pixels?
+    )
+{
+    psAssert(file, "It's supposed to be here");
+    psAssert(view, "It's supposed to be here");
+    psAssert(config, "It's supposed to be here");
+
+    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 *copy = pmFPAConstruct(file->camera, file->cameraName);  // FPA to return
+    if (!pmFPAAddSourceFromView(copy, 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 ((pixels && !pmFPACopy(copy, file->fpa)) ||
+            (!pixels && !pmFPACopyStructure(copy, file->fpa, 1, 1))) {
+            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 ((pixels && !pmChipCopy(chip, srcChip)) ||
+              (!pixels && !pmChipCopyStructure(chip, srcChip, 1, 1))) {
+              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 ((pixels && !pmCellCopy(cell, srcCell)) ||
+              (!pixels && !pmCellCopyStructure(cell, srcCell, 1, 1))) {
+              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");
+    }
+
+    // Unreachable
+    return NULL;
+}
+
+
+pmFPA *pmFPAfileSuitableFPA(const pmFPAfile *file, const pmFPAview *view, pmConfig *config, bool pixels)
+{
+    PS_ASSERT_PTR_NON_NULL(file, NULL);
+    PS_ASSERT_PTR_NON_NULL(view, NULL);
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    pmFPA *fpa = suitableFPA(file, view, config, pixels); // A suitable FPA for writing
+    if (!fpa) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to produce suitable FPA.");
+        return NULL;
+    }
+
+    // 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).
+    switch (file->type) {
+      case PM_FPA_FILE_IMAGE:
+      case PM_FPA_FILE_MASK:
+      case PM_FPA_FILE_VARIANCE:
+      case PM_FPA_FILE_HEADER:
+      case PM_FPA_FILE_FRINGE:
+      case PM_FPA_FILE_DARK:
+      case PM_FPA_FILE_LINEARITY:
+      case PM_FPA_FILE_CMP:
+      case PM_FPA_FILE_CMF:
+      case PM_FPA_FILE_PSF:
+      case PM_FPA_FILE_ASTROM_MODEL:
+      case PM_FPA_FILE_ASTROM_REFSTARS: 
+      case PM_FPA_FILE_KH_CORRECT:
+      case PM_FPA_FILE_PATTERN_ROW_AMP:
+      case PM_FPA_FILE_PATTERN_DEAD_CELLS:
+	{
+          pmHDU *hdu = pmFPAviewThisHDU(view, fpa);
+          if (hdu) {
+              if (!hdu->header) {
+                  hdu->header = psMetadataAlloc();
+              }
+
+              pmConfigConformHeader(hdu->header, file->format);
+
+              // whenever we write out a mask image, we should define the bits which represent mask concepts
+              if (file->type == PM_FPA_FILE_MASK) {
+                  assert (hdu->header);
+                  if (!pmConfigMaskWriteHeader(config, hdu->header)) {
+                      psError(PS_ERR_UNKNOWN, false,
+                              "failed to set the bitmask names in the PHU header for Image %s (%s)\n",
+                              file->filename, file->name);
+                      return false;
+                  }
+              }
+          }
+
+          pmChip *chip = pmFPAviewThisChip(view, fpa); // Chip of interest, or NULL
+          pmCell *cell = pmFPAviewThisCell(view, fpa); // Cell of interest, or NULL
+          if (cell) {
+              if (!pmConceptsWriteCell(cell, true, config)) {
+                  psError(PS_ERR_IO, false, "Unable to write concepts for cell.\n");
+                  return false;
+              }
+          } else if (chip) {
+              if (!pmConceptsWriteChip(chip, true, true, config)) {
+                  psError(PS_ERR_IO, false, "Unable to write concepts for chip.\n");
+                  return false;
+              }
+          } else if (!pmConceptsWriteFPA(fpa, true, config)) {
+              psError(PS_ERR_IO, false, "Unable to write concepts for FPA.\n");
+              return false;
+          }
+
+          if (!pmFPAUpdateNames(fpa, chip, cell, file->imageId, file->sourceId)) {
+              psError(PS_ERR_UNKNOWN, false, "Unable to update names in header.");
+              return false;
+          }
+          break;
+      }
+      default:
+        // No action
+        break;
+    }
+
+    return fpa;
+}
+
+// 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, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // 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
+                                 pmConfig *config, // Configuration
+                                 bool (*fpaReadFunc)(pmFPA*, psFits*, pmConfig*), // Function to read FPA
+                                 bool (*chipReadFunc)(pmChip*, psFits*, pmConfig*), // Function to read chip
+                                 bool (*cellReadFunc)(pmCell*, psFits*, pmConfig*) // 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, config);
+    }
+
+    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, config);
+    }
+
+    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, config);
+    }
+    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, config);
+    } else {
+        pmReadoutReadSegment (readout, fits, view->nRows, view->iRows, NULL, NULL);
+    }
+    return true;
+    #endif
+}
+
+
+bool pmFPAviewReadFitsImage(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    return fpaViewReadFitsImage(view, file, config, pmFPARead, pmChipRead, pmCellRead);
+}
+
+bool pmFPAviewReadFitsMask(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    return fpaViewReadFitsImage(view, file, config, pmFPAReadMask, pmChipReadMask, pmCellReadMask);
+}
+
+bool pmFPAviewReadFitsVariance(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    return fpaViewReadFitsImage(view, file, config, pmFPAReadVariance, pmChipReadVariance,
+                                pmCellReadVariance);
+}
+
+bool pmFPAviewReadFitsDark(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    return fpaViewReadFitsImage(view, file, config, pmFPAReadDark, pmChipReadDark, pmCellReadDark);
+}
+
+bool pmFPAviewReadFitsHeaderSet(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    return fpaViewReadFitsImage(view, file, config, 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
+                                  pmConfig *config, // Configuration
+                                  bool (*fpaWriteFunc)(pmFPA*, psFits*, pmConfig*, bool, bool), // Func FPA
+                                  bool (*chipWriteFunc)(pmChip*, psFits*, pmConfig*, bool, bool),// Func chip
+                                  bool (*cellWriteFunc)(pmCell*, psFits*, pmConfig*, bool) // Func cell
+                                 )
+{
+    assert(view);
+    assert(file);
+
+    psFits *fits = file->fits;          // FITS file
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, true); // FPA to write
+
+    switch (pmFPAviewLevel(view)) {
+    case PM_FPA_LEVEL_FPA: {
+            bool success = fpaWriteFunc(fpa, fits, config, false, true);
+            psFree(fpa);
+            return success;
+        }
+    case PM_FPA_LEVEL_CHIP: {
+            pmChip *chip = pmFPAviewThisChip(view, fpa); // Chip of interest
+            bool success = chipWriteFunc(chip, fits, config, false, true);
+            psFree(fpa);
+            return success;
+        }
+    case PM_FPA_LEVEL_CELL: {
+            pmCell *cell = pmFPAviewThisCell(view, fpa); // Cell of interest
+            bool success = cellWriteFunc(cell, fits, config, 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, 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, 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 pmFPAviewWriteFitsVariance(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    return fpaViewWriteFitsImage(view, file, config, pmFPAWriteVariance, pmChipWriteVariance,
+                                 pmCellWriteVariance);
+}
+
+bool pmFPAviewWriteFitsDark(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    return fpaViewWriteFitsImage(view, file, config, pmFPAWriteDark, pmChipWriteDark, pmCellWriteDark);
+}
+
+// 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, 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, false);
+    pmHDU *phu = pmFPAviewThisHDU(view, fpa);
+    if (!phu || !phu->blankPHU) {
+        // No PHU to write!
+        psFree(fpa);
+        return true;
+    }
+
+    // whenever we write out a mask image, we should define the bits which represent mask concepts
+    if (file->type == PM_FPA_FILE_MASK) {
+        assert (phu->header);
+        if (!pmConfigMaskWriteHeader (config, phu->header)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to set the bitmask names in the PHU header for Image %s (%s)\n", file->filename, file->name);
+            return false;
+        }
+    }
+
+    switch (file->fileLevel) {
+      case PM_FPA_LEVEL_FPA:
+        status = pmFPAWrite(fpa, file->fits, config, true, false);
+        break;
+      case PM_FPA_LEVEL_CHIP: {
+          pmChip *chip = pmFPAviewThisChip(view, fpa);
+          status = pmChipWrite(chip, file->fits, config, true, false);
+          break;
+      }
+      case PM_FPA_LEVEL_CELL: {
+          pmCell *cell = pmFPAviewThisCell(view, fpa);
+          status = pmCellWrite(cell, file->fits, config, 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_branches/psModules.20240412/src/camera/pmFPAfileFitsIO.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileFitsIO.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileFitsIO.h	(revision 42651)
@@ -0,0 +1,106 @@
+/* @file  pmFPAview.h
+ * @brief Tools to manipulate the FPA structure elements.
+ *
+ * @author EAM, IfA
+ * @author PAP, IfA
+ *
+ * @version $Revision: 1.16 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-06 02:31:24 $
+ * 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
+                            pmConfig *config
+                           );
+
+/// 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
+                            pmConfig *config
+                          );
+/// Read a variance map into the current view
+bool pmFPAviewReadFitsVariance(const pmFPAview *view,  ///< View specifying level of interest
+                             pmFPAfile *file, ///< FPA file into which to read
+                            pmConfig *config
+                            );
+
+/// Read a dark into the current view
+bool pmFPAviewReadFitsDark(const pmFPAview *view,  ///< View specifying level of interest
+                           pmFPAfile *file, ///< FPA file into which to read
+                            pmConfig *config
+    );
+
+/// 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
+                            pmConfig *config
+    );
+
+/// Write the image for the specified view
+bool pmFPAviewWriteFitsImage(const pmFPAview *view, ///< View specifying level of interest
+                             pmFPAfile *file, ///< FPA file to write
+                             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
+                            pmConfig *config ///< Configuration
+                           );
+
+/// Write the variance map for the specified view
+bool pmFPAviewWriteFitsVariance(const pmFPAview *view, ///< View specifying level of interest
+                              pmFPAfile *file, ///< FPA file to write
+                              pmConfig *config ///< Configuration
+                             );
+
+/// Write the dark for the specified view
+bool pmFPAviewWriteFitsDark(const pmFPAview *view, ///< View specifying level of interest
+                            pmFPAfile *file, ///< FPA file to write
+                            pmConfig *config ///< Configuration
+    );
+
+/// Write a PHU for a fits image if needed
+bool pmFPAviewFitsWritePHU (const pmFPAview *view, pmFPAfile *file, 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
+                             pmConfig *config ///< Configuration
+                            );
+
+/// 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
+                            pmConfig *config, ///< Configuration
+                            bool pixels ///< Worry about copying the pixels?
+                           );
+
+/// @}
+
+# endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileFringeIO.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileFringeIO.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileFringeIO.c	(revision 42651)
@@ -0,0 +1,238 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+//#include "pmConfigMask.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 "pmFPAfileFringeIO.h"
+#include "pmFPAfileFitsIO.h"
+//#include "pmFPACopy.h"
+//#include "pmFPAConstruct.h"
+//#include "pmConceptsWrite.h"
+#include "pmFringeStats.h"
+
+# define FRINGE_TABLE "FRINGE.MEASUREMENTS"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Reading Fringe data
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// given an already-opened fits file, read the table corresponding to the specified view
+bool pmFPAviewReadFringes(const pmFPAview *view, pmFPAfile *file)
+{
+    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 pmFPAReadFringes(fpa, fits) > 0;
+    }
+
+    if (view->cell == -1) {
+        pmChip *chip = pmFPAviewThisChip(view, fpa); // Chip of interest
+        return pmChipReadFringes(chip, fits) > 0;
+    }
+
+    pmCell *cell = pmFPAviewThisCell(view, fpa); // Cell of interest
+    return pmCellReadFringes(cell, fits) > 0;
+}
+
+int pmFPAReadFringes(pmFPA *fpa, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, 0);
+    PS_ASSERT_FITS_NON_NULL(fits, 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 += pmChipReadFringes(chip, fits);
+    }
+
+    return numRead;
+}
+
+int pmChipReadFringes(pmChip *chip, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, 0);
+    PS_ASSERT_FITS_NON_NULL(fits, 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 += pmCellReadFringes(cell, fits);
+    }
+
+    return numRead;
+}
+
+int pmCellReadFringes(pmCell *cell, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, 0);
+    PS_ASSERT_FITS_NON_NULL(fits, 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
+
+    // Fringe Table Extension name
+    psString extname = NULL;
+    psStringAppend(&extname, "FRINGE_%s_%s", chipName, cellName);
+
+    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;
+    }
+
+    psArray *table = psFitsReadTable(fits); // The table
+    if (!table) {
+	psError(PS_ERR_UNKNOWN, false, "Unable to add read the fringe table data from the file");
+	psFree (header);
+	psFree (extname);
+	return 0;
+    }
+
+    psArray *fringes = pmFringesParseTable(table, header);
+    psFree(table);
+    psFree(header);
+
+    if (!fringes) {
+	psError(PS_ERR_UNKNOWN, false, "Unable to add parse the fringe table data");
+        psFree(extname);
+	return 0;
+    }
+
+    if (!psMetadataAdd(cell->analysis, PS_LIST_TAIL, FRINGE_TABLE, PS_DATA_ARRAY, "Fringes", fringes)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to add fringe from extension %s to analysis metadata "
+                "for chip %s, cell %s\n", extname, chipName, cellName);
+        psFree(fringes);
+        psFree(extname);
+        return 0;
+    }
+
+    psFree(fringes); // drop local reference
+    psFree(extname);
+
+    return 1;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Writing fringe data
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// given an already-opened fits file, write the table corresponding to the specified view
+bool pmFPAviewWriteFringes(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // FPA of interest
+    psFits *fits = file->fits;          // FITS file
+
+    if (view->chip == -1) {
+        return pmFPAWriteFringes(fits, fpa) > 0;
+    }
+
+    if (view->cell == -1) {
+        pmChip *chip = pmFPAviewThisChip(view, fpa); // Chip of interest
+        return pmChipWriteFringes(fits, chip) > 0;
+    }
+
+    pmCell *cell = pmFPAviewThisCell(view, fpa); // Cell of interest
+    return pmCellWriteFringes(fits, cell) > 0;
+}
+
+int pmFPAWriteFringes(psFits *fits, const pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, 0);
+    PS_ASSERT_PTR_NON_NULL(fits, 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 += pmChipWriteFringes(fits, chip);
+    }
+
+    return numWrite;
+}
+
+int pmChipWriteFringes(psFits *fits, const pmChip *chip)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, 0);
+    PS_ASSERT_PTR_NON_NULL(fits, 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 += pmCellWriteFringes(fits, cell);
+    }
+
+    return numWrite;
+}
+
+int pmCellWriteFringes(psFits *fits, const pmCell *cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, 0);
+    PS_ASSERT_PTR_NON_NULL(fits, 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 *fringes = psMetadataLookupPtr(NULL, cell->analysis, FRINGE_TABLE); // The FITS table
+    if (!fringes) {
+        // We wrote everything we could find
+        return 0;
+    }
+
+    psMetadata *header = psMetadataAlloc();
+    psArray *table = pmFringesFormatTable(header, fringes);
+    if (!table) {
+        // We wrote everything we could find
+	psFree(header);
+        return 0;
+    }
+
+    psString extname = NULL;            // Extension name
+    psStringAppend(&extname, "FRINGE_%s_%s", chipName, cellName);
+
+    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);
+	psFree(header);
+        return 0;
+    }
+
+    psFree(extname);
+    psFree(header);
+    return 1;
+}
+
+
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileFringeIO.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileFringeIO.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileFringeIO.h	(revision 42651)
@@ -0,0 +1,30 @@
+/* @file  pmFPAfileFringeIO.h
+ * @brief Read & Write Fringe tables
+ *
+ * @author EAM, IfA
+ * @author PAP, IfA
+ *
+ * @version $Revision: 1.16 $
+ * @date $Date: 2009-02-06 02:31:24 $
+ * Copyright 2004-2005 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_FILE_FRINGE_IO_H
+#define PM_FPA_FILE_FRINGE_IO_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// Read an fringes into the current view
+bool pmFPAviewReadFringes(const pmFPAview *view, pmFPAfile *file);
+int pmFPAReadFringes(pmFPA *fpa, psFits *fits);
+int pmChipReadFringes(pmChip *chip, psFits *fits);
+int pmCellReadFringes(pmCell *cell, psFits *fits);
+bool pmFPAviewWriteFringes(const pmFPAview *view, pmFPAfile *file, pmConfig *config);
+int pmFPAWriteFringes(psFits *fits, const pmFPA *fpa);
+int pmChipWriteFringes(psFits *fits, const pmChip *chip);
+int pmCellWriteFringes(psFits *fits, const pmCell *cell);
+
+/// @}
+
+# endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileIO.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileIO.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileIO.c	(revision 42651)
@@ -0,0 +1,1176 @@
+#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 "pmConfigMask.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 "pmFPAfileFitsIO.h"
+#include "pmFPAfileFringeIO.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+
+#include "pmSourceIO.h"
+#include "pmPSF_IO.h"
+
+#include "pmKHcorrect.h"
+#include "pmAstrometryModel.h"
+#include "pmAstrometryRefstars.h"
+#include "pmFPA_JPEG.h"
+#include "pmSourcePlots.h"
+#include "pmFPAConstruct.h"
+#include "pmSubtractionIO.h"
+#include "pmPatternIO.h"
+#include "pmFPAExpNumIO.h"
+#include "pmConcepts.h"
+#include "pmConfigRun.h"
+
+#include "pmFPAfileIO.h"
+
+void CatchTestFile (pmFPAfile *file, const char *function) {
+
+  int testFile = FALSE;							
+  testFile = testFile || !strcmp(file->name, "GDIFF.OUTPUT.SOURCES");	
+  testFile = testFile || !strcmp(file->name, "GDIFF.POS1.SOURCES");	
+  testFile = testFile || !strcmp(file->name, "GDIFF.POS2.SOURCES");	
+  if (FALSE && testFile) {							
+    fprintf (stderr, "%s : %d : %d (%s)", file->name, file->state, file->mode, function);
+    fprintf (stderr, "\n");
+  }
+}
+
+// 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;
+}
+
+// list all defined pmFPAfiles
+bool pmFPAfileIOList (pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_PTR_NON_NULL(config->files, 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;
+	
+	fprintf (stderr, "%s : %d %d %d : %d %d %d %d\n", file->name, file->type, file->mode, file->state,
+		 file->fileLevel, file->dataLevel, file->freeLevel, file->mosaicLevel);
+    }
+    psFree (iter);
+    return true;
+}
+
+// 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);
+
+    // 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;
+    }
+
+    // an active 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->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;
+    }
+
+    if (!pmConfigRunFileAddRead(config, file)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to add file to run-time information");
+        return false;
+    }
+
+    // select a reading method
+    bool status = true;
+    switch (file->type) {
+      case PM_FPA_FILE_IMAGE:
+        status = pmFPAviewReadFitsImage(view, file, config);
+        break;
+      case PM_FPA_FILE_MASK:
+        status = pmFPAviewReadFitsMask(view, file, config);
+        break;
+      case PM_FPA_FILE_VARIANCE:
+        status = pmFPAviewReadFitsVariance(view, file, config);
+        break;
+      case PM_FPA_FILE_HEADER:
+        status = pmFPAviewReadFitsHeaderSet(view, file, config);
+        break;
+      case PM_FPA_FILE_DARK:
+        status = pmFPAviewReadFitsDark(view, file, config);
+        break;
+      case PM_FPA_FILE_FRINGE:
+        status = pmFPAviewReadFitsImage(view, file, config);
+        if (status) {
+            if (!pmFPAviewReadFringes(view, file)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to read fringe data from %s.\n", file->filename);
+                return false;
+            }
+        }
+        break;
+      case PM_FPA_FILE_SUBKERNEL:
+        status = pmSubtractionReadKernels(view, file, config);
+        break;
+      case PM_FPA_FILE_PATTERN:
+        status = pmPatternRead(view, file, config);
+        break;
+      case PM_FPA_FILE_PATTERN_ROW_AMP:
+        status = pmPatternRowAmpRead(view, file, config);
+        break;
+      case PM_FPA_FILE_PATTERN_DEAD_CELLS:
+        status = pmPatternDeadCellsRead(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_CMF:
+      case PM_FPA_FILE_CFF:
+      case PM_FPA_FILE_WCS:
+      case PM_FPA_FILE_SRCTEXT:
+        status = pmFPAviewReadObjects (view, file, config);
+        break;
+      case PM_FPA_FILE_PSF:
+        status = pmPSFmodelReadForView (view, file, config);
+        break;
+      case PM_FPA_FILE_ASTROM_MODEL:
+        status = pmAstromModelReadForView (view, file, config);
+        break;
+      case PM_FPA_FILE_KH_CORRECT:
+        status = pmKHcorrectReadForView (view, file, config);
+        break;
+      case PM_FPA_FILE_EXPNUM:
+        status = pmExpNumRead(view, file, config);
+        break;
+      case PM_FPA_FILE_ASTROM_REFSTARS:
+      case PM_FPA_FILE_JPEG:
+      case PM_FPA_FILE_KAPA:
+      case PM_FPA_FILE_LINEARITY:
+      case PM_FPA_FILE_NEWNONLIN:
+        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;
+    }
+
+    // an active internal file should not be returned to here
+    PS_ASSERT(file->mode != PM_FPA_MODE_INTERNAL, false);
+
+    if (file->mode != PM_FPA_MODE_WRITE) {
+        psTrace("psModules.camera", 6, "skip create for non-write file %s", file->name);
+        return true;
+    }
+
+    // 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_VARIANCE:
+      case PM_FPA_FILE_FRINGE:
+      case PM_FPA_FILE_DARK:
+      case PM_FPA_FILE_PATTERN:
+      case PM_FPA_FILE_EXPNUM:
+        {
+            // create FPA structure component based on view
+            psMetadata *format = file->format; // Camera format configuration
+            if (!format) {
+                format = config->format;
+            }
+
+            pmFPAAddSourceFromView(file->fpa, 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_SUBKERNEL:
+      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_CFF:
+      case PM_FPA_FILE_WCS:
+      case PM_FPA_FILE_PSF:
+      case PM_FPA_FILE_ASTROM_MODEL:
+      case PM_FPA_FILE_ASTROM_REFSTARS:
+      case PM_FPA_FILE_KH_CORRECT:
+      case PM_FPA_FILE_PATTERN_ROW_AMP:
+      case PM_FPA_FILE_PATTERN_DEAD_CELLS:
+      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, file is inactive", file->name);
+        return true;
+    }
+
+    // an ACTIVE internal file should not be sent here
+    PS_ASSERT(file->mode != PM_FPA_MODE_INTERNAL, false);
+
+    if (file->mode != PM_FPA_MODE_WRITE) {
+        psTrace("psModules.camera", 6, "skip write for %s, mode is not WRITE", file->name);
+        return true;
+    }
+
+    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_CMF) {
+	if (!pmFPAviewCheckDataStatusForSources (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_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_ASTROM_MODEL) {
+      if (!pmAstromModelCheckDataStatusForView (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_ASTROM_REFSTARS) {
+      if (!pmAstromRefstarsCheckDataStatusForView (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_KH_CORRECT) {
+      psTrace("psModules.camera", 6, "skip write for %s, no write function defined", file->name);
+      return true;
+    }
+    if (file->type == PM_FPA_FILE_PATTERN_ROW_AMP) {
+      psTrace("psModules.camera", 6, "skip write for %s, no write function defined", file->name);
+      return true;
+    }
+    if (file->type == PM_FPA_FILE_PATTERN_DEAD_CELLS) {
+      psTrace("psModules.camera", 6, "skip write for %s, no write function defined", 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;
+        }
+    }
+
+    if (!pmConfigRunFileAddWrite(config, file)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to add file to run-time information");
+        return false;
+    }
+
+    // IMPORTANT: If adding a FITS-based file, make sure your write function uses an FPA produced by
+    // pmFPAfileSuitableFPA.  This ensures the HDUs are at the correct level for your output format, and sets
+    // the headers correctly.
+
+    // 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_VARIANCE:
+        status = pmFPAviewWriteFitsVariance(view, file, config);
+        break;
+      case PM_FPA_FILE_HEADER:
+        psAbort ("no HEADER write functions defined");
+        break;
+      case PM_FPA_FILE_DARK:
+        status = pmFPAviewWriteFitsDark(view, file, config);
+        break;
+      case PM_FPA_FILE_FRINGE:
+        status = pmFPAviewWriteFitsImage (view, file, config);
+        if (status) {
+            if (!pmFPAviewWriteFringes(view, file, config)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to write fringe data from %s.\n", file->filename);
+                return false;
+            }
+        }
+        break;
+      case PM_FPA_FILE_SUBKERNEL:
+        status = pmSubtractionWriteKernels(view, file, config);
+        break;
+      case PM_FPA_FILE_PATTERN:
+        status = pmPatternWrite(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_CMF:
+      case PM_FPA_FILE_CFF:
+        status = pmFPAviewWriteObjects (view, file, config);
+        break;
+
+      case PM_FPA_FILE_PSF:
+        status = pmPSFmodelWriteForView (view, file, config);
+        break;
+
+      case PM_FPA_FILE_ASTROM_MODEL:
+        status = pmAstromModelWriteForView (view, file, config);
+        break;
+
+      case PM_FPA_FILE_ASTROM_REFSTARS:
+        status = pmAstromRefstarsWriteForView (view, file, config);
+        break;
+
+      case PM_FPA_FILE_KH_CORRECT:
+        psError(PS_ERR_IO, true, "cannot write type KH.CORRECT (%s)", file->name);
+        break;
+
+      case PM_FPA_FILE_PATTERN_ROW_AMP:
+        psError(PS_ERR_IO, true, "cannot write type PATTERN.ROW.AMP (%s)", file->name);
+        break;
+
+      case PM_FPA_FILE_PATTERN_DEAD_CELLS:
+        psError(PS_ERR_IO, true, "cannot write type PATTERN.DEAD.CELLS (%s)", file->name);
+        break;
+
+      case PM_FPA_FILE_JPEG:
+        status = pmFPAviewWriteJPEG (view, file, config);
+        break;
+
+      case PM_FPA_FILE_KAPA:
+        status = pmFPAviewWriteSourcePlot (view, file, config);
+        break;
+
+      case PM_FPA_FILE_EXPNUM:
+        // when ppStack output's EXPNUM file it uses a file rule where the file type is MASK
+        psError(PS_ERR_IO, true, "cannot write type EXPNUM (%s)", file->name);
+        return false;
+
+      case PM_FPA_FILE_WCS:
+        psError(PS_ERR_IO, true, "cannot write type WCS (%s)", file->name);
+        return false;
+
+      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);
+
+    // 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;
+    }
+
+    // an active internal file should not be sent here (should not be left on config->files)
+    PS_ASSERT(file->mode != PM_FPA_MODE_INTERNAL, false);
+
+    // 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_VARIANCE:
+      case PM_FPA_FILE_HEADER:
+      case PM_FPA_FILE_FRINGE:
+      case PM_FPA_FILE_DARK:
+      case PM_FPA_FILE_SUBKERNEL:
+      case PM_FPA_FILE_PATTERN:
+      case PM_FPA_FILE_CMF:
+      case PM_FPA_FILE_CFF:
+      case PM_FPA_FILE_WCS:
+      case PM_FPA_FILE_PSF:
+      case PM_FPA_FILE_ASTROM_MODEL:
+      case PM_FPA_FILE_ASTROM_REFSTARS:
+      case PM_FPA_FILE_KH_CORRECT:
+      case PM_FPA_FILE_PATTERN_ROW_AMP:
+      case PM_FPA_FILE_PATTERN_DEAD_CELLS:
+      case PM_FPA_FILE_LINEARITY:
+      case PM_FPA_FILE_NEWNONLIN:
+      case PM_FPA_FILE_EXPNUM:
+        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);
+
+    if (file->state & PM_FPA_STATE_INACTIVE) {
+        psTrace("psModules.camera", 6, "skip free for %s, files is inactive", file->name);
+        return true;
+    }
+
+    // an active internal file should not be sent here (should not be left on config->files)
+    PS_ASSERT(file->mode != PM_FPA_MODE_INTERNAL, false);
+
+    // 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_VARIANCE:
+      case PM_FPA_FILE_HEADER:
+      case PM_FPA_FILE_FRINGE:
+      case PM_FPA_FILE_DARK:
+	//
+        status = pmFPAviewFreeData(view, file);
+        break;
+      case PM_FPA_FILE_SUBKERNEL:
+      case PM_FPA_FILE_PATTERN:
+      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_CFF:
+      case PM_FPA_FILE_WCS:
+      case PM_FPA_FILE_PSF:
+      case PM_FPA_FILE_ASTROM_MODEL:
+      case PM_FPA_FILE_ASTROM_REFSTARS:
+      case PM_FPA_FILE_KH_CORRECT:
+      case PM_FPA_FILE_PATTERN_ROW_AMP:
+      case PM_FPA_FILE_PATTERN_DEAD_CELLS:
+      case PM_FPA_FILE_EXPNUM:
+        psTrace ("psModules.camera", 6, "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:
+      case PM_FPA_FILE_LINEARITY:
+      case PM_FPA_FILE_NEWNONLIN:
+        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;
+}
+
+psString pmFPAfileName(const pmFPAfile *file, const pmFPAview *view, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(file, NULL);
+    PS_ASSERT_PTR_NON_NULL(view, NULL);
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    psString filename = pmFPAfileNameFromRule(file->filerule, file, view); // Filename, based on rule
+    if (!filename) {
+        psError(PS_ERR_IO, true, "Cannot determine file name from rule");
+        return false;
+    }
+
+    // indirect filenames: these come from a list on the command line or elsewhere
+    if (!strcasecmp(filename, "@FILES")) {
+        psString filesrc = pmFPAfileNameFromRule(file->filesrc, file, view); // Source of file name
+        if (!filesrc) {
+            psError(PS_ERR_IO, false, "error converting filesrc to name %s", file->filesrc);
+            return false;
+        }
+        psFree(filename);
+        filename = psMemIncrRefCounter(psMetadataLookupStr(NULL, file->names, filesrc));
+        if (!filename) {
+            psError(PS_ERR_IO, false, "filename lookup error (@FILES) for %s : %s", file->filesrc, filesrc);
+            psFree(filesrc);
+            return false;
+        }
+        psFree(filesrc);
+    }
+
+    // get name from detrend database
+    // file->detrend->detID contains the desired -det_id detID -iteration iter string
+    if (!strcasecmp(filename, "@DETDB")) {
+        if (!file->detrend) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find information about selected detrend.");
+            return false;
+        }
+        psMetadata *menu = psMetadataLookupMetadata(NULL, file->camera, "CLASSID"); // Menu of class IDs
+        if (!menu) {
+            psError(PS_ERR_IO, false, "Unable to find CLASSID metadata in camera configuration");
+            return false;
+        }
+        const char *rule = psMetadataLookupStr(NULL, menu, file->detrend->level); // Rule for class_id
+        if (!rule || strlen(rule) == 0) {
+            psError(PS_ERR_IO, false, "Unable to find %s in CLASSID in camera configuration",
+                    file->detrend->level);
+            return false;
+        }
+        psString classId = pmFPAfileNameFromRule(rule, file, view); // The class identifier, for pmDetrendFile
+        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(filename);
+        filename = pmDetrendFile(file->detrend->detID, classId, config);
+        if (!filename) {
+            psError(PS_ERR_IO, false, "failed to find a valid detrend image for detID %s : classID %s",
+                    file->detrend->detID, classId);
+            psFree(classId);
+            return false;
+        }
+
+        psTrace("psModules.camera", 6, "got detrend file %s", filename);
+        psFree(classId);
+    }
+
+    return filename;
+}
+
+// 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;
+    }
+
+    // an ACTIVE internal file should not be sent here
+    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 = pmFPAfileName(file, view, config);
+    if (!file->filename) {
+        psError(PS_ERR_IO, true, "Unable to determine filename for file %s", file->name);
+        return false;
+    }
+
+    // apply filename mangling rules (file://, path://, neb://)
+    bool create = file->mode == PM_FPA_MODE_WRITE ? true : false;
+    psString realName = pmConfigConvertFilename(file->filename, config, create, false);
+    if (!realName) {
+        psError(psErrorCodeLast(), false, "failed to determine real name from template for %s\n",
+                file->filename);
+        return false;
+    }
+    psFree(file->origname);
+    file->origname = file->filename;
+    file->filename = realName;
+
+    switch (file->type) {
+        // open the FITS types:
+      case PM_FPA_FILE_IMAGE:
+      case PM_FPA_FILE_MASK:
+      case PM_FPA_FILE_VARIANCE:
+      case PM_FPA_FILE_HEADER:
+      case PM_FPA_FILE_FRINGE:
+      case PM_FPA_FILE_DARK:
+      case PM_FPA_FILE_SUBKERNEL:
+      case PM_FPA_FILE_PATTERN:
+      case PM_FPA_FILE_CMF:
+      case PM_FPA_FILE_CFF:
+      case PM_FPA_FILE_WCS:
+      case PM_FPA_FILE_PSF:
+      case PM_FPA_FILE_ASTROM_MODEL:
+      case PM_FPA_FILE_ASTROM_REFSTARS:
+      case PM_FPA_FILE_KH_CORRECT:
+      case PM_FPA_FILE_PATTERN_ROW_AMP:
+      case PM_FPA_FILE_PATTERN_DEAD_CELLS:
+      case PM_FPA_FILE_LINEARITY:
+      case PM_FPA_FILE_NEWNONLIN:
+      case PM_FPA_FILE_EXPNUM:
+        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;
+
+        file->fits->options = psMemIncrRefCounter(file->options);
+
+        // in most cases, we have already open and read the phu and determined the format.
+        // in some cases, (eg DetDB images), we have only just determined the filename.
+        // we need to check the file format before we can work with the file
+        if (!file->format) {
+          psMetadata *phu = psFitsReadHeader (NULL, file->fits);
+          if (!phu) {
+            psError(PS_ERR_IO, false, "Failed to read file header %s\n", file->filename);
+            return false;
+          }
+
+          // determine the current format from the header
+          // determine camera if not specified already
+          // XXX can I actually reach this with camera not specified??
+          psMetadata *camera = NULL;
+          psString formatName = NULL;
+          psString cameraName = NULL;
+          file->format = pmConfigCameraFormatFromHeader(&camera, &cameraName, &formatName, config, phu, true);
+          if (!file->format) {
+            psError(PS_ERR_IO, false, "Failed to read CCD format from %s\n", file->filename);
+            psFree(phu);
+            return false;
+          }
+          psFree(phu);
+
+          pmFPA *newFPA = pmFPAConstruct (camera, formatName);
+          if (!newFPA) {
+              psError(PS_ERR_IO, false, "Failed to construct FPA from %s for %s", file->filename, formatName);
+              psFree(camera);
+              psFree(formatName);
+              return NULL;
+          }
+          psFree(camera);
+          psFree(formatName);
+          psFree(cameraName);
+
+          // XXX this is really dangerous...
+          psFree (file->fpa);
+          file->fpa = newFPA;
+        }
+
+        // if needed, set the optional EXTWORD field based on the camera value
+        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);
+        }
+
+        // XXX these are probably only needed for WRITE files
+        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.  this function is only called
+// for files for which the PHU is not already loaded (files passed via the db or files after
+// the first in a multifile dataset)
+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) {
+        // determine the format (camera is already known); do not load the recipe
+        file->format = pmConfigCameraFormatFromHeader (NULL, NULL, &file->formatName, 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;
+        }
+    } 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, 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_VARIANCE:
+      case PM_FPA_FILE_DARK:
+      case PM_FPA_FILE_FRINGE:
+        status = pmFPAviewFitsWritePHU (view, file, config);
+        break;
+      case PM_FPA_FILE_SUBKERNEL:
+        status = pmSubtractionWritePHU(view, file, config);
+        break;
+      case PM_FPA_FILE_PATTERN:
+        status = pmPatternWritePHU(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_ASTROM_REFSTARS:
+        status = pmAstromRefstarsWritePHU (view, file, config);
+        break;
+      case PM_FPA_FILE_EXPNUM:
+      case PM_FPA_FILE_ASTROM_MODEL:
+      case PM_FPA_FILE_KH_CORRECT:
+      case PM_FPA_FILE_PATTERN_ROW_AMP:
+      case PM_FPA_FILE_PATTERN_DEAD_CELLS:
+      case PM_FPA_FILE_SX:
+      case PM_FPA_FILE_RAW:
+      case PM_FPA_FILE_OBJ:
+      case PM_FPA_FILE_CMP:
+      case PM_FPA_FILE_WCS:
+      case PM_FPA_FILE_CFF:
+      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(s) to active (state == true) or inactive
+// if name is NULL, set the state for all pmFPAfiles
+bool pmFPAfileActivate(psMetadata *files, bool state, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(files, false);
+
+    // return false if the requested file is not in the list (not an error, but informational)
+    psArray *selected = pmFPAfileSelect(files, name);
+    if (!selected) {
+        return false;
+    }
+    for (int i = 0; i < selected->n; i++) {
+        pmFPAfile *file = selected->data[i]; // File of interest
+        if (!file) {
+            continue;
+        }
+        if (state) {
+            file->state &= PS_NOT_U8(PM_FPA_STATE_INACTIVE);
+        } else {
+            file->state |= PM_FPA_STATE_INACTIVE;
+        }
+    }
+    psFree(selected);
+
+    return true;
+}
+
+
+pmFPAfile *pmFPAfileActivateSingle(psMetadata *files, bool state, const char *name, int num)
+{
+    PS_ASSERT_PTR_NON_NULL(files, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(num, NULL);
+
+    pmFPAfile *file = pmFPAfileSelectSingle(files, name, num);
+    if (!file) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to select instance %d of file %s", num, name);
+        return NULL;
+    }
+    if (state) {
+        file->state &= PS_NOT_U8(PM_FPA_STATE_INACTIVE);
+    } else {
+        file->state |= PM_FPA_STATE_INACTIVE;
+    }
+
+    return file;
+}
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileIO.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileIO.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAfileIO.h	(revision 42651)
@@ -0,0 +1,60 @@
+/* @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: 2008-06-30 00:53:45 $
+ * 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
+/// @{
+
+// Determine appropriate file name
+psString pmFPAfileName(const pmFPAfile *file, const pmFPAview *view, pmConfig *config);
+
+// 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, const char *name);
+
+/// Set the state of a single pmFPAfile (in the case of multiple files with the same name)
+///
+/// Returns file activated
+pmFPAfile *pmFPAfileActivateSingle(psMetadata *files, ///< Files to activate
+                                   bool state, ///< State to set
+                                   const char *name, ///< Name of file to activate
+                                   int num    ///< Sequence numbner of file to activate
+    );
+
+// 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, pmConfig *config);
+bool pmFPAfileReadPHU (pmFPAfile *file, const pmFPAview *view, pmConfig *config);
+
+bool pmFPAfileIOList (pmConfig *config);
+
+/// @}
+# endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAview.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAview.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAview.c	(revision 42651)
@@ -0,0 +1,429 @@
+/** @file  pmFPAview.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.16 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-09-04 03:09:21 $
+ *
+ *  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 psMemCheckFPAview(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmFPAviewFree);
+}
+
+
+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;
+    }
+    PS_ASSERT_PTR_NON_NULL(fpa->chips, 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);
+    PS_ASSERT_PTR_NON_NULL(cell->readouts, 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;
+}
+
+pmFPAview *pmFPAviewTop(const pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    pmFPAview *view = pmFPAviewAlloc(0);// View to top
+
+    view->chip = -1;
+    view->cell = -1;
+    if (!fpa->hdu) {
+        int numChips = 0;                   // Number of active chips
+        psArray *chips = fpa->chips;        // Chips of interest
+        for (int i = 0; i < chips->n; i++) {
+            pmChip *chip = chips->data[i];  // Chip of interest
+            if (!chip) {
+                continue;
+            }
+            if (chip->hdu) {
+                numChips++;
+                view->chip = i;
+            } else {
+                int numCells = 0;               // Number of active cells
+                psArray *cells = chip->cells;   // Cells of interest
+                for (int j = 0; j < cells->n; j++) {
+                    pmCell *cell = cells->data[j]; // Cell of interest
+                    if (!cell) {
+                        continue;
+                    }
+                    if (cell->hdu) {
+                        numCells++;
+                        view->cell = j;
+                    }
+                }
+
+                if (numCells > 1) {
+                    if (numCells != cells->n) {
+                        psWarning("More than one, but not all cells are active.");
+                    }
+                    view->cell = -1;
+                }
+            }
+        }
+
+        if (numChips > 1) {
+            if (numChips != chips->n) {
+                psWarning("More than one, but not all chips are active.");
+            }
+            view->chip = -1;
+            view->cell = -1;
+        }
+    }
+
+    return view;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/camera/pmFPAview.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmFPAview.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmFPAview.h	(revision 42651)
@@ -0,0 +1,131 @@
+/* @file pmFPA.h
+ * @brief Tools to manipulate the FPA structure elements.
+ *
+ * @author Eugene Magnier, IfA
+ *
+ * @version $Revision: 1.12 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-08-25 22:05:58 $
+ * 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
+bool psMemCheckFPAview(psPtr ptr);
+
+/// 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
+    );
+
+
+/// Determine the view suitable for the top level of the provided FPA
+pmFPAview *pmFPAviewTop(const pmFPA *fpa ///< FPA of interest
+    );
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmHDU.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmHDU.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmHDU.c	(revision 42651)
@@ -0,0 +1,291 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmConfigMask.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->variances);
+    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->variances = NULL;
+    hdu->masks   = NULL;
+
+    return hdu;
+}
+
+bool psMemCheckHDU(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) hduFree);
+}
+
+
+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;
+    }
+
+    psTrace("psModules.camera", 5, "Reading the header...\n");
+
+    // The header may already exist (e.g., from doing concept writing at the PHU level) so we need to be
+    // careful.  We read into a separate container and copy that over the top of anything that's already read.
+    psMetadata *header = psFitsReadHeader(NULL, fits);
+    if (!header) {
+        psError(PS_ERR_IO, false, "Unable to read header for extension %s\n", hdu->extname);
+        return false;
+    }
+
+    if (!hdu->header) {
+        hdu->header = header;
+        return true;
+    }
+
+    psMetadataIterator *iter = psMetadataIteratorAlloc(header, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item;           // Item from iteration
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        const char *name = item->name; // Name of item
+        if (psMetadataLookup(hdu->header, name)) {
+            // It exists; clobber
+            psMetadataRemoveKey(hdu->header, name);
+        }
+        psMetadataAddItem(hdu->header, item, PS_LIST_TAIL, 0);
+    }
+    psFree(iter);
+    psFree(header);
+
+    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) {
+        psWarning("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 pmHDUReadVariance(pmHDU *hdu, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(hdu, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return hduRead(hdu, &hdu->variances, fits);
+}
+
+// Write an HDU to a FITS file
+static bool hduWrite(pmHDU *hdu,        // HDU to write
+                     const psArray *images, // Images to write
+                     const psArray *masks, // Masks to use when writing
+                     psImageMaskType maskVal,// Value to mask
+                     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) {
+        psWarning("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 (!psFitsWriteImageCubeWithMask(fits, hdu->header, images, masks, maskVal, 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, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(hdu, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    psImageMaskType maskVal = pmConfigMaskGet("MASK.VALUE", config); // Value to mask
+    return hduWrite(hdu, hdu->images, hdu->masks, maskVal, fits);
+}
+
+bool pmHDUWriteMask(pmHDU *hdu, psFits *fits, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(hdu, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    // We don't supply a mask because we're writing the mask!
+    return hduWrite(hdu, hdu->masks, NULL, 0, fits);
+}
+
+bool pmHDUWriteVariance(pmHDU *hdu, psFits *fits, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(hdu, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    psImageMaskType maskVal = pmConfigMaskGet("MASK.VALUE", config); // Value to mask
+    return hduWrite(hdu, hdu->variances, hdu->masks, maskVal, fits);
+}
+
+bool pmHDUWriteIdentifiers(pmHDU *hdu, psS64 imageId, psS64 sourceId)
+{
+    PS_ASSERT_PTR_NON_NULL(hdu, false);
+
+    // XXX Get header keyword name from camera configuration
+
+    if (imageId) {
+        psMetadataAddS64(hdu->header, PS_LIST_TAIL, "IMAGEID", PS_META_REPLACE, "Image identifier", imageId);
+    }
+    if (sourceId) {
+        psMetadataAddS64(hdu->header, PS_LIST_TAIL, "SOURCEID", PS_META_REPLACE, "Source identifier", sourceId);
+    }
+    return true;
+}
+
+bool pmHDUReadIdentifiers(psS64 *imageId, psS64 *sourceId, const pmHDU *hdu)
+{
+    PS_ASSERT_PTR_NON_NULL(hdu, false);
+
+    // XXX Get header keyword name from camera configuration
+
+    bool imageOK, sourceOK;             // Status of MD lookups
+    *imageId = psMetadataLookupS64(&imageOK, hdu->header, "IMAGEID");
+    *sourceId = psMetadataLookupS64(&sourceOK, hdu->header, "SOURCEID");
+
+    return imageOK && sourceOK;
+}
Index: /branches/eam_branches/psModules.20240412/src/camera/pmHDU.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmHDU.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmHDU.h	(revision 42651)
@@ -0,0 +1,102 @@
+/* @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.11 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-06 02:31:24 $
+ *
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_HDU_H
+#define PM_HDU_H
+
+#include <pslib.h>
+#include "pmConfig.h"
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+#define PM_HDU_COVARIANCE_KEYWORD "PS_COVAR" // FITS keyword to indicate presence of a covariance matrix
+
+/// 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 variance 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;                    ///< Pixel data
+    psArray *variances;                 ///< Variance in the pixel data, or NULL
+    psArray *masks;                     ///< Mask for the pixel data, or NULL
+} pmHDU;
+
+
+/// Allocator for pmHDU
+pmHDU *pmHDUAlloc(const char *extname);   ///< Extension name, or NULL for PHU
+bool psMemCheckHDU(psPtr ptr);
+
+/// 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 variance map
+///
+/// Moves to the appropriate extension
+bool pmHDUReadVariance(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
+                const pmConfig *config  ///< Configuration
+    );
+
+/// Write the HDU header and mask
+bool pmHDUWriteMask(pmHDU *hdu,         ///< HDU to write
+                    psFits *fits,       ///< FITS file to write to
+                    const pmConfig *config  ///< Configuration
+    );
+
+/// Write the HDU header and variance map
+bool pmHDUWriteVariance(pmHDU *hdu,       ///< HDU to write
+                        psFits *fits,     ///< FITS file to write to
+                        const pmConfig *config  ///< Configuration
+    );
+
+
+/// Read identifiers from FITS header
+bool pmHDUReadIdentifiers(psS64 *imageId, ///< Image identifer, returned
+                          psS64 *sourceId, ///< Source identifier, returned
+                          const pmHDU *hdu ///< HDU from which to read
+    );
+
+/// Write identifiers to FITS header
+bool pmHDUWriteIdentifiers(pmHDU *hdu, ///< HDU to which to write
+                           psS64 imageId, ///< Image identifer
+                           psS64 sourceId ///< Source identifier
+                           );
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmHDUGenerate.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmHDUGenerate.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmHDUGenerate.c	(revision 42651)
@@ -0,0 +1,722 @@
+#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) {
+        psWarning("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....
+        psWarning("CELL.READDIR is not set in cell --- ignored.\n");
+        return false;
+    }
+    if (*readdir == 0) {
+        *readdir = cellreaddir;
+    } else if (*readdir != cellreaddir) {
+        psWarning("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 (biassecSource && 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) {
+            psWarning("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....
+            psWarning("CELL.READDIR is not set in cell --- ignored.\n");
+            continue;
+        }
+        if (readdir == 0 && mdok && cellreaddir != 0) {
+            readdir = cellreaddir;
+        } else if (readdir != cellreaddir) {
+            psWarning("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->variance);
+        if (!image) {
+            continue;
+        }
+        if (readout->mask &&
+                (readout->mask->numCols != image->numCols || readout->mask->numRows != image->numRows)) {
+            psWarning("Image and mask have different sizes (%dx%d vs %dx%d)!\n",
+                     image->numCols, image->numRows, readout->mask->numCols, readout->mask->numRows);
+        }
+        if (readout->variance &&
+                (readout->variance->numCols != image->numCols || readout->variance->numRows != image->numRows)) {
+            psWarning("Image and variance have different sizes (%dx%d vs %dx%d)!\n",
+                     image->numCols, image->numRows, readout->variance->numCols, readout->variance->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) {
+        psWarning("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);
+        psWarning("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 varianceType = 0;        // Type of readout variances
+    {
+        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->variances && readout->variance) {
+                    varianceType = checkTypes(varianceType, readout->variance->type.type);
+                }
+            }
+        }
+        psFree(iter);
+    }
+    if (numReadouts == 0 || (imageType == 0 && maskType == 0 && varianceType == 0)) {
+        // Nothing from which to create an HDU
+        // psError(PS_ERR_IO, true, "Nothing from which to create an HDU\n");
+        psWarning("Nothing from which to create an HDU, must be empty\n");
+        return true;
+    }
+
+    // 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 (varianceType) {
+        hdu->variances = psArrayAlloc(numReadouts);
+        for (int i = 0; i < numReadouts; i++) {
+            psImage *variance = psImageAlloc(xSize, ySize, varianceType);
+            psImageInit(variance, 0.0);
+            hdu->variances->data[i] = variance;
+        }
+    }
+
+    // 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 *hduVariances = hdu->variances; // Array of variances 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->variance) {
+                    psImage *new = pasteImage(hduVariances->data[i], readout->variance, trimsec);
+                    psFree(readout->variance);
+                    readout->variance = new;
+                }
+
+                if (!biassecs || !readout->bias) {
+                    // No need to worry about bias section
+                    continue;
+                }
+
+                if (biassecs->n != readout->bias->n) {
+                    psWarning("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);
+    }
+    return true;
+}
+
+// Return the level that an extension applies to
+static pmFPALevel extensionLevel(const 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 info 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->variances) {
+        // 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
+		psFree (cells);
+                return true;
+            }
+	    bool status = generateHDU(hdu, cells);
+	    psFree (cells);
+            return status;
+        }
+    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->variances) {
+        // 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
+		psFree (cells);
+                return true;
+            }
+
+	    bool status = generateHDU(hdu, cells);
+	    psFree (cells);
+            return status;
+        }
+    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->variances) {
+        // 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
+		psFree(cells);
+                return true;
+            }
+            bool status = generateHDU(hdu, cells);
+	    psFree(cells);
+	    return status;
+        }
+    default:
+        psAbort("Shouldn't ever get here: check your camera format configuration.\n");
+    }
+    return false;
+}
Index: /branches/eam_branches/psModules.20240412/src/camera/pmHDUGenerate.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmHDUGenerate.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmHDUGenerate.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/camera/pmHDUUtils.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmHDUUtils.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmHDUUtils.c	(revision 42651)
@@ -0,0 +1,172 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmHDUUtils.h"
+
+pmHDU *pmHDUGetFirst (const pmFPA *fpa) {
+
+    // XXX we probably should have an indicator in pmFPA about the depths.
+
+    if (!fpa) return NULL;
+    if (fpa->hdu) return fpa->hdu;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!chip) continue;
+        if (chip->hdu) return chip->hdu;
+        if (!chip->cells) continue;
+        for (int j = 0; j < chip->cells->n; j++) {
+            pmCell *cell = chip->cells->data[j];
+            if (!cell) continue;
+            if (cell->hdu) return cell->hdu;
+        }
+    }
+    return NULL;
+}
+
+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->variances) {
+        fprintf(fd, "Variances:\n");
+        for (long i = 0; i < hdu->variances->n; i++) {
+            psImage *variance = hdu->variances->data[i]; // Variance image of interest
+            INDENT(fd, level + 2);
+            fprintf(fd, "%ld: %dx%d\n", i, variance->numCols, variance->numRows);
+        }
+    } else {
+        fprintf(fd, "NO variances.\n");
+    }
+
+    return;
+}
Index: /branches/eam_branches/psModules.20240412/src/camera/pmHDUUtils.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmHDUUtils.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmHDUUtils.h	(revision 42651)
@@ -0,0 +1,64 @@
+/* @file pmHDUUtils.h
+ * @brief Utility functions for working with an HDU
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.10 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-07-15 20:25:00 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_HDU_UTILS_H
+#define PM_HDU_UTILS_H
+
+#include <pmHDU.h>
+#include <pmFPA.h>
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// Get the first HDU encountered in the hierarchy
+pmHDU *pmHDUGetFirst (const pmFPA *fpa);
+
+/// 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_branches/psModules.20240412/src/camera/pmReadoutFake.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmReadoutFake.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmReadoutFake.c	(revision 42651)
@@ -0,0 +1,405 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+
+#include "pmSourceGroups.h"
+#include "pmReadoutFake.h"
+
+// XXX this is now hard-wired in pmModelParamsToAxes
+// #define MAX_AXIS_RATIO 20.0             // Maximum axis ratio for PSF model
+
+#define MODEL_MASK (PM_MODEL_STATUS_NONCONVERGE | PM_MODEL_STATUS_OFFIMAGE | \
+                    PM_MODEL_STATUS_BADARGS | PM_MODEL_STATUS_LIMITS) // Mask to apply to models
+
+
+static bool threaded = false;           // Running threaded?
+
+
+
+
+// Given an object model, circularise it by setting the axes to be identical
+static bool circulariseModel(pmModel *model // Model to circularise
+    )
+{
+    assert(model);
+
+    psF32 *params = model->params->data.F32; // Model parameters
+    psEllipseAxes axes = pmPSF_ModelToAxes(params, model->class->useReff); // Ellipse axes
+    // Curiously, the minor axis can be larger than the major axis, so need to check.
+    if (axes.major >= axes.minor) {
+        axes.minor = axes.major;
+    } else {
+        axes.major = axes.minor;
+    }
+    return pmPSF_AxesToModel(params, axes, model->class->useReff);
+}
+
+/// Generate fake sources on a readout
+static bool readoutFake(pmReadout *readout, // Readout of interest
+                        const pmSourceGroups *groups, // Source groups
+                        const psVector *x,        // x coordinates
+                        const psVector *y,        // y coordinates
+                        const psVector *mag,      // Magnitudes
+                        const psVector *xOffset,  // Offsets in x
+                        const psVector *yOffset,  // Offsets in y
+                        const pmPSF *psf,         // PSF
+                        float minFlux,            // Minimum flux
+                        float radius,             // Minimum radius
+                        bool circularise,         // Circularise PSF?
+                        bool normalisePeak,       // Normalise sources for peak?
+                        int groupIndex,           // Group index
+                        int cellIndex             // Cell index
+                        )
+{
+    psArray *cells = groups->groups->data[groupIndex]; // Cells in group
+    psVector *cellSources = cells->data[cellIndex];    // Sources in cell
+
+    for (int i = 0; i < cellSources->n; i++) {
+        int index = cellSources->data.S32[i];                       // Index for source of interest
+        float flux = powf(10.0, -0.4 * mag->data.F32[index]);       // Flux of source
+        float xSrc = x->data.F32[index], ySrc = y->data.F32[index]; // Coordinates of source
+
+        if (normalisePeak) {
+            // Normalise flux
+            pmModel *normModel = pmModelFromPSFforXY(psf, xSrc, ySrc, 1.0); // Model for normalisation
+            if (!normModel || (normModel->flags & MODEL_MASK)) {
+                psFree(normModel);
+                continue;
+            }
+            // check that all params are valid:
+            bool validParams = true;
+            for (int j = 0; validParams && (j < normModel->params->n); j++) {
+                switch (j) {
+                  case PM_PAR_SKY:
+                  case PM_PAR_I0:
+                  case PM_PAR_XPOS:
+                  case PM_PAR_YPOS:
+                    continue;
+                  default:
+                    if (!isfinite(normModel->params->data.F32[j])) {
+                        validParams = false;
+                    }
+                }
+            }
+            if (!validParams) {
+                psFree(normModel);
+                continue;
+            }
+            if (circularise && !circulariseModel(normModel)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to circularise PSF model.");
+                psFree(normModel);
+                return false;
+            }
+
+            flux /= normModel->class->modelFlux(normModel->params);
+            psFree(normModel);
+        }
+
+        pmModel *fakeModel = pmModelFromPSFforXY(psf, xSrc, ySrc, flux);
+        if (!fakeModel || (fakeModel->flags & MODEL_MASK)) {
+            psFree(fakeModel);
+            continue;
+        }
+        // check that all params are valid:
+        bool validParams = true;
+        for (int j = 0; validParams && (j < fakeModel->params->n); j++) {
+            switch (j) {
+              case PM_PAR_SKY:
+              case PM_PAR_I0:
+              case PM_PAR_XPOS:
+              case PM_PAR_YPOS:
+                continue;
+              default:
+                if (!isfinite(fakeModel->params->data.F32[j])) {
+                    validParams = false;
+                }
+            }
+        }
+        if (!validParams) {
+            psFree(fakeModel);
+            continue;
+        }
+        if (circularise && !circulariseModel(fakeModel)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to circularise PSF model.");
+            psFree(fakeModel);
+            return false;
+        }
+	
+        psTrace("psModules.camera", 10, "Adding source at %f,%f with flux %f\n",
+                fakeModel->params->data.F32[PM_PAR_XPOS], fakeModel->params->data.F32[PM_PAR_YPOS],
+                fakeModel->params->data.F32[PM_PAR_I0]);
+
+        pmSource *fakeSource = pmSourceAlloc(); // Fake source to generate
+        fakeSource->peak = pmPeakAlloc(xSrc, ySrc, fakeModel->params->data.F32[PM_PAR_I0], PM_PEAK_LONE);
+        float fakeRadius = 1.0;         // Radius of fake source
+        if (isfinite(minFlux)) {
+            fakeRadius = PS_MAX(fakeRadius, fakeModel->class->modelRadius(fakeModel->params, minFlux));
+        }
+        if (radius > 0) {
+            fakeRadius = PS_MAX(fakeRadius, radius);
+        }
+
+        if (xOffset) {
+            if (!pmSourceDefinePixels(fakeSource, readout, xSrc + xOffset->data.S32[index],
+                                      ySrc + yOffset->data.S32[index], fakeRadius)) {
+                psErrorClear();
+                continue;
+            }
+            if (!pmModelAddWithOffset(fakeSource->pixels, NULL, fakeModel, PM_MODEL_OP_FULL, 0,
+                                      xOffset->data.S32[index], yOffset->data.S32[index])) {
+                psErrorClear();
+                continue;
+            }
+        } else {
+            if (!pmSourceDefinePixels(fakeSource, readout, xSrc, ySrc, fakeRadius)) {
+                psErrorClear();
+                continue;
+            }
+            if (!pmModelAdd(fakeSource->pixels, NULL, fakeModel, PM_MODEL_OP_FULL, 0)) {
+                psErrorClear();
+                continue;
+            }
+        }
+        psFree(fakeSource);
+        psFree(fakeModel);
+    }
+
+    return true;
+}
+
+/// Thread job for readoutFake()
+static bool readoutFakeThread(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    psArray *args = job->args;          // Arguments
+
+    pmReadout *readout = args->data[0];     // Readout of interest
+    const pmSourceGroups *groups = args->data[1]; // Source groups
+    const psVector *x = args->data[2];        // x coordinates
+    const psVector *y = args->data[3];        // y coordinates
+    const psVector *mag = args->data[4];      // Magnitudes
+    const psVector *xOffset = args->data[5];  // Offsets in x
+    const psVector *yOffset = args->data[6];  // Offsets in y
+    const pmPSF *psf = args->data[7];         // PSF
+    float minFlux = PS_SCALAR_VALUE(args->data[8], F32); // Minimum flux
+    float radius = PS_SCALAR_VALUE(args->data[9], S32);  // Minimum radius - typecast to float from S32 outside of PS_SCALAR_VALUE otherwise sets 0.0 
+    bool circularise = PS_SCALAR_VALUE(args->data[10], U8); // Circularise PSF?
+    bool normalisePeak = PS_SCALAR_VALUE(args->data[11], U8); // Normalise for peak?
+    int groupIndex = PS_SCALAR_VALUE(args->data[12], S32); // Group index
+    int cellIndex = PS_SCALAR_VALUE(args->data[13], S32);  // Cell index
+
+    return readoutFake(readout, groups, x, y, mag, xOffset, yOffset, psf, minFlux, radius, circularise,
+                       normalisePeak, groupIndex, cellIndex);
+}
+
+
+bool pmReadoutFakeThreads(bool new)
+{
+    bool old = threaded;                // Old status, to return
+
+    if (!old && new) {
+        threaded = true;
+
+        {
+            psThreadTask *task = psThreadTaskAlloc("PSMODULES_READOUT_FAKE", 14);
+            task->function = &readoutFakeThread;
+            psThreadTaskAdd(task);
+            psFree(task);
+        }
+
+    } else if (old && !new) {
+        threaded = false;
+        psThreadTaskRemove("PSMODULES_READOUT_FAKE");
+    }
+
+    return old;
+}
+
+
+bool pmReadoutFakeFromVectors(pmReadout *readout, int numCols, int numRows,
+                              const psVector *x, const psVector *y, const psVector *mag,
+                              const psVector *xOffset, const psVector *yOffset,
+                              const pmPSF *psf, float minFlux, int radius,
+                              bool circularise, bool normalisePeak)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_INT_LARGER_THAN(numCols, 0, false);
+    PS_ASSERT_INT_LARGER_THAN(numRows, 0, false);
+    PS_ASSERT_VECTOR_NON_NULL(x, false);
+    PS_ASSERT_VECTOR_TYPE(x, PS_TYPE_F32, false);
+    PS_ASSERT_VECTOR_NON_NULL(y, false);
+    PS_ASSERT_VECTOR_TYPE(y, PS_TYPE_F32, false);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(y, x, false);
+    PS_ASSERT_VECTOR_NON_NULL(mag, false);
+    PS_ASSERT_VECTOR_TYPE(mag, PS_TYPE_F32, false);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(mag, x, false);
+    long numSources = x->n;              // Number of sources
+    if (xOffset || yOffset) {
+        PS_ASSERT_VECTOR_NON_NULL(xOffset, false);
+        PS_ASSERT_VECTOR_NON_NULL(yOffset, false);
+        PS_ASSERT_VECTORS_SIZE_EQUAL(xOffset, yOffset, false);
+        PS_ASSERT_VECTOR_TYPE(xOffset, PS_TYPE_S32, false);
+        PS_ASSERT_VECTOR_TYPE(yOffset, PS_TYPE_S32, false);
+        if (xOffset->n != numSources) {
+            psError(PS_ERR_BAD_PARAMETER_SIZE, true,
+                    "Number of offset vectors (%ld) and sources (%ld) doesn't match",
+                    xOffset->n, numSources);
+            return false;
+        }
+    }
+    PS_ASSERT_PTR_NON_NULL(psf, false);
+
+    readout->image = psImageRecycle(readout->image, numCols, numRows, PS_TYPE_F32);
+    psImageInit(readout->image, 0);
+
+    int numThreads = threaded ? psThreadPoolSize() : 0; // Number of threads
+    pmSourceGroups *groups = pmSourceGroupsFromVectors(readout, x, y, numThreads); // Groups of sources
+    if (!groups) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to generate source groups");
+        return false;
+    }
+
+    if (threaded) {
+        for (int i = 0; i < groups->groups->n; i++) {
+            psArray *cells = groups->groups->data[i]; // Cell with sources
+            for (int j = 0; j < cells->n; j++) {
+                psThreadJob *job = psThreadJobAlloc("PSMODULES_READOUT_FAKE");
+                psArray *args = job->args;
+                psArrayAdd(args, 1, readout);
+                psArrayAdd(args, 1, groups);
+                // Casting away const to add to array
+                psArrayAdd(args, 1, (psVector*)x);
+                psArrayAdd(args, 1, (psVector*)y);
+                psArrayAdd(args, 1, (psVector*)mag);
+                psArrayAdd(args, 1, (psVector*)xOffset);
+                psArrayAdd(args, 1, (psVector*)yOffset);
+                psArrayAdd(args, 1, (pmPSF*)psf);
+                PS_ARRAY_ADD_SCALAR(args, minFlux, PS_TYPE_F32);
+                PS_ARRAY_ADD_SCALAR(args, radius, PS_TYPE_S32);
+                PS_ARRAY_ADD_SCALAR(args, circularise, PS_TYPE_U8);
+                PS_ARRAY_ADD_SCALAR(args, normalisePeak, PS_TYPE_U8);
+                PS_ARRAY_ADD_SCALAR(args, i, PS_TYPE_S32);
+                PS_ARRAY_ADD_SCALAR(args, j, PS_TYPE_S32);
+
+                if (!psThreadJobAddPending(job)) {
+                    psFree(groups);
+                    return false;
+                }
+            }
+            if (!psThreadPoolWait(true, true)) {
+                psError(PS_ERR_UNKNOWN, false, "Error waiting for threads.");
+                psFree(groups);
+                return false;
+            }
+        }
+    } else if (!readoutFake(readout, groups, x, y, mag, xOffset, yOffset, psf, minFlux, radius, circularise,
+                            normalisePeak, 0, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to generate fake sources on readout");
+        psFree(groups);
+        return false;
+    }
+
+    psFree(groups);
+
+// Set a concept value
+#define CONCEPT_SET_S32(CONCEPTS, NAME, OLD, NEW) { \
+        psMetadataItem *item = psMetadataLookup(CONCEPTS, NAME); \
+        psAssert(item->type == PS_DATA_S32, "Incorrect type: %x", item->type); \
+        if (item->data.S32 == OLD) { \
+            item->data.S32 = NEW; \
+        } \
+    }
+
+    if (readout->parent) {
+        CONCEPT_SET_S32(readout->parent->concepts, "CELL.XPARITY", 0, 1);
+        CONCEPT_SET_S32(readout->parent->concepts, "CELL.YPARITY", 0, 1);
+        CONCEPT_SET_S32(readout->parent->concepts, "CELL.XBIN", 0, 1);
+        CONCEPT_SET_S32(readout->parent->concepts, "CELL.YBIN", 0, 1);
+    }
+
+    return true;
+
+}
+
+
+bool pmReadoutFakeFromSources(pmReadout *readout, int numCols, int numRows, const psArray *sources,
+                              pmSourceMode sourceMask, const psVector *xOffset, const psVector *yOffset,
+                              const pmPSF *psf, float minFlux, int radius,
+                              bool circularise, bool normalisePeak)
+{
+    PS_ASSERT_ARRAY_NON_NULL(sources, false);
+
+    int numSources = sources->n;          // Number of stars
+    psVector *x = psVectorAllocEmpty(numSources, PS_TYPE_F32);
+    psVector *y = psVectorAllocEmpty(numSources, PS_TYPE_F32);
+    psVector *mag = psVectorAllocEmpty(numSources, PS_TYPE_F32);
+
+    int numGood = 0;                    // Number of good sources
+    for (int i = 0; i < numSources; i++) {
+        pmSource *source = sources->data[i]; // Source of interest
+        if (!source) {
+            continue;
+        }
+        if (source->mode & sourceMask) {
+            continue;
+        }
+        if (!isfinite(source->psfMag)) {
+            continue;
+        }
+        float xSrc, ySrc;                     // Coordinates of source
+        if (source->modelPSF) {
+            xSrc = source->modelPSF->params->data.F32[PM_PAR_XPOS];
+            ySrc = source->modelPSF->params->data.F32[PM_PAR_YPOS];
+        } else {
+            xSrc = source->peak->xf;
+            ySrc = source->peak->yf;
+        }
+
+        x->data.F32[numGood] = xSrc;
+        y->data.F32[numGood] = ySrc;
+        mag->data.F32[numGood] = source->psfMag;
+        numGood++;
+    }
+    x->n = numGood;
+    y->n = numGood;
+    mag->n = numGood;
+
+    bool status = pmReadoutFakeFromVectors(readout, numCols, numRows, x, y, mag, xOffset, yOffset, psf,
+                                           minFlux, radius, circularise, normalisePeak);
+    psFree(x);
+    psFree(y);
+    psFree(mag);
+
+    return status;
+}
Index: /branches/eam_branches/psModules.20240412/src/camera/pmReadoutFake.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmReadoutFake.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmReadoutFake.h	(revision 42651)
@@ -0,0 +1,50 @@
+#ifndef PM_READOUT_FAKE_H
+#define PM_READOUT_FAKE_H
+
+// #include <pslib.h>
+// #include <pmHDU.h>
+// #include <pmFPA.h>
+// 
+// #include <pmMoments.h>
+// #include <pmResiduals.h>
+// #include <pmGrowthCurve.h>
+// #include <pmTrend2D.h>
+// #include <pmPSF.h>
+// #include <pmSourceMasks.h>
+
+/// Set threading
+///
+/// Returns old threading state
+bool pmReadoutFakeThreads(
+    bool new                            // New threading state
+    );
+
+/// Generate a fake readout from vectors
+bool pmReadoutFakeFromVectors(pmReadout *readout, ///< Output readout
+                              int numCols, int numRows, ///< Dimension of image
+                              const psVector *x, const psVector *y, ///< Source coordinates
+                              const psVector *mag, ///< Source magnitudes
+                              const psVector *xOffset, ///< x offsets for sources (source -> img), or NULL
+                              const psVector *yOffset, ///< y offsets for sources (source -> img), or NULL
+                              const pmPSF *psf, ///< PSF for sources
+                              float minFlux, ///< Minimum flux to bother about; for setting source radius
+                              int radius, ///< Fixed radius for sources
+                              bool circularise, ///< Circularise PSF model?
+                              bool normalisePeak ///< Normalise the peak value?
+    );
+
+/// Generate a fake readout from an array of sources
+bool pmReadoutFakeFromSources(pmReadout *readout, ///< Output readout
+                              int numCols, int numRows, ///< Dimension of image
+                              const psArray *sources, ///< Array of pmSource
+                              pmSourceMode sourceMask, ///< Mask for sources
+                              const psVector *xOffset, ///< x offsets for sources (source -> img), or NULL
+                              const psVector *yOffset, ///< y offsets for sources (source -> img), or NULL
+                              const pmPSF *psf, ///< PSF for sources
+                              float minFlux, ///< Minimum flux to bother about; for setting source radius
+                              int radius, ///< Fixed radius for sources
+                              bool circularise, ///< Circularise PSF model?
+                              bool normalise ///< Normalise the peak value?
+    );
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/camera/pmReadoutStack.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmReadoutStack.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmReadoutStack.c	(revision 42651)
@@ -0,0 +1,314 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmReadoutStack.h"
+
+// generate the specified image
+// XXX should it be an error for the image to exist?
+psImage *pmReadoutSetAnalysisImage(pmReadout *readout, // Readout containing image
+                                   const char *name, // Name of image in analysis metadata
+                                   int numCols, int numRows, // Expected size of image
+                                   psElemType type, // Expected type of image
+                                   double init // Initial value
+    )
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+
+    psImage *image = psImageAlloc(numCols, numRows, type);
+
+    if (!psMetadataAddImage(readout->analysis, PS_LIST_TAIL, name, 0, "Analysis image from " __FILE__, image)) {
+        psAbort ("analysis image already exists");
+    }
+    psImageInit(image, init);
+
+    psFree (image); // we still have a view on readout->analysis
+    return image;
+}
+
+// retrieve the specified image
+// XXX not sure why this should call psMemIncrRefCounter
+psImage *pmReadoutGetAnalysisImage(pmReadout *readout, // Readout containing image
+                                   const char *name       // Name of image in analysis metadata
+    )
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+
+    bool mdok;                          // Status of MD lookup
+    psImage *image = psMetadataLookupPtr(&mdok, readout->analysis, name);
+    return image;
+}
+
+psImage *pmReadoutAnalysisImage(pmReadout *readout, // Readout containing image
+                                const char *name, // Name of image in analysis metadata
+                                int numCols, int numRows, // Expected size of image
+                                psElemType type, // Expected type of image
+                                double init // Initial value
+    )
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+
+    bool mdok;                          // Status of MD lookup
+    psImage *image = psMetadataLookupPtr(&mdok, readout->analysis, name);
+    if (!image) {
+        image = psImageAlloc(numCols, numRows, type);
+        psMetadataAddImage(readout->analysis, PS_LIST_TAIL, name, 0, "Analysis image from " __FILE__, image);
+        psImageInit(image, init);
+        return image;
+    }
+    if (image->numCols != numCols || image->numRows != numRows) {
+        psError(PS_ERR_BAD_PARAMETER_SIZE, true, "Analysis image %s has incorrect size (%dx%d vs %dx%d)",
+                name, image->numCols, image->numRows, numCols, numRows);
+        return NULL;
+    }
+    if (image->type.type != type) {
+        psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Analysis image %s has incorrect type (%x vs %x)",
+                name, image->type.type, type);
+        return NULL;
+    }
+    return psMemIncrRefCounter(image);
+}
+
+// XXX for the moment, use col0, row0, numCols, numRows supplied from the outside
+bool pmReadoutStackDefineOutput(pmReadout *readout, int col0, int row0, int numCols, int numRows, bool mask, bool variance, psImageMaskType blank)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+
+    // XXX is this an error?
+    if (readout->image) return false;
+    readout->col0 = col0;
+    readout->row0 = row0;
+
+    // allocate the images
+    readout->image = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psImageInit(readout->image, NAN);
+
+    if (mask) {
+        // XXX is this an error?
+        if (readout->mask) return false;
+        readout->mask = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK);
+        psImageInit(readout->mask, blank);
+    }
+
+    if (variance) {
+        // XXX is this an error?
+        if (readout->variance) return false;
+        readout->variance = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+        psImageInit(readout->variance, NAN);
+    }
+
+    return true;
+}
+
+bool pmReadoutStackSetOutputSize(int *col0, int *row0, int *numCols, int *numRows, const psArray *inputs)
+{
+    PS_ASSERT_ARRAY_NON_NULL(inputs, false);
+    PS_ASSERT_PTR_NON_NULL(col0, false);
+    PS_ASSERT_PTR_NON_NULL(row0, false);
+    PS_ASSERT_PTR_NON_NULL(numCols, false);
+    PS_ASSERT_PTR_NON_NULL(numRows, 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 xMin = INT_MAX;
+    int yMin = INT_MAX;
+    int xMax = 0;
+    int yMax = 0;
+    int xSize = 0;
+    int ySize = 0;           // The size of the output image
+
+    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) continue;
+
+        // 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;
+        psTrace("psModules.camera", 7, "Readout %ld: trimsec: %f,%f - %f,%f\n", i, trimsec->x0, trimsec->y0, trimsec->x1, trimsec->y1);
+    }
+
+    *col0 = xMin;
+    *row0 = yMin;
+    *numCols = xSize;
+    *numRows = ySize;
+
+    if (!valid) {
+        psError(PS_ERR_UNKNOWN, false, "No valid input readouts.");
+    }
+    return valid;
+}
+
+bool pmReadoutUpdateSize(pmReadout *readout, int minCols, int minRows,
+                         int numCols, int numRows, bool mask, bool variance,
+                         psImageMaskType blank)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+
+    if (readout->image) {
+        readout->col0 = PS_MIN(minCols, readout->col0);
+        readout->row0 = PS_MIN(minRows, readout->row0);
+    } else {
+        readout->col0 = minCols;
+        readout->row0 = minRows;
+    }
+
+    // (reAllocate the images
+    if (!readout->image) {
+        readout->image = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+        psImageInit(readout->image, NAN);
+    }
+    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, NAN);
+        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_IMAGE_MASK);
+            psImageInit(readout->mask, blank);
+        }
+        if (readout->mask->numCols < numCols || readout->mask->numRows < numRows) {
+            psImage *newMask = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK);
+            psImageInit(newMask, blank);
+            psImageOverlaySection(newMask, readout->mask, readout->col0, readout->row0, "=");
+            psFree(readout->mask);
+            readout->mask = newMask;
+        }
+    }
+
+    if (variance) {
+        if (!readout->variance) {
+            readout->variance = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+            psImageInit(readout->variance, NAN);
+        }
+        if (readout->variance->numCols < numCols || readout->variance->numRows < numRows) {
+            psImage *newVariance = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+            psImageInit(newVariance, NAN);
+            psImageOverlaySection(newVariance, readout->variance, readout->col0, readout->row0, "=");
+            psFree(readout->variance);
+            readout->variance = newVariance;
+        }
+    }
+
+    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);
+    PS_ASSERT_PTR_NON_NULL(minInputColsPtr, false);
+    PS_ASSERT_PTR_NON_NULL(maxInputColsPtr, false);
+    PS_ASSERT_PTR_NON_NULL(minInputRowsPtr, false);
+    PS_ASSERT_PTR_NON_NULL(maxInputRowsPtr, false);
+    PS_ASSERT_PTR_NON_NULL(numColsPtr, false);
+    PS_ASSERT_PTR_NON_NULL(numRowsPtr, 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) {
+            continue;
+        }
+        if (!readout->process) {
+            continue;
+        }
+        if (!readout->image) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "Input readout %ld 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 = cell ? psMetadataLookupPtr(&mdok, cell->concepts, "CELL.TRIMSEC") : NULL; // Trim section
+        if (!mdok || !trimsec || psRegionIsNaN(*trimsec)) {
+            psWarning("CELL.TRIMSEC is not set for readout %ld --- attempting to use image size.\n", i);
+            xSize = PS_MAX(xSize, readout->image->numCols);
+            ySize = PS_MAX(ySize, readout->image->numRows);
+            xMin = PS_MIN(xMin, 0);
+            xMax = PS_MAX(xMax, readout->image->numCols - 1);
+            yMin = PS_MIN(yMin, 0);
+            yMax = PS_MAX(yMax, readout->image->numRows - 1);
+        } 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_branches/psModules.20240412/src/camera/pmReadoutStack.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/camera/pmReadoutStack.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/camera/pmReadoutStack.h	(revision 42651)
@@ -0,0 +1,53 @@
+#ifndef PM_READOUT_STACK_H
+#define PM_READOUT_STACK_H
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#define PM_READOUT_STACK_ANALYSIS_COUNT "STACK.COUNT" // Name for count image in analysis metadata
+#define PM_READOUT_STACK_ANALYSIS_SIGMA "STACK.SIGMA" // Name for sigma image in analysis metadata
+
+/// 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?
+                         bool weight,   ///< Worry about the weight?
+                         psImageMaskType blank ///< Mask value to give to blank pixels
+    );
+
+/// 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
+    );
+
+psImage *pmReadoutSetAnalysisImage(pmReadout *readout, // Readout containing image
+				   const char *name, // Name of image in analysis metadata
+				   int numCols, int numRows, // Expected size of image
+				   psElemType type, // Expected type of image
+				   double init // Initial value
+    );
+
+// retrieve the specified image
+// XXX not sure why this should call psMemIncrRefCounter
+psImage *pmReadoutGetAnalysisImage(pmReadout *readout, // Readout containing image
+				   const char *name       // Name of image in analysis metadata
+    );
+
+
+/// Return an image from analysis metadata, produced while stacking
+psImage *pmReadoutAnalysisImage(pmReadout *readout, // Readout containing image
+                                const char *name, // Name of image in analysis metadata
+                                int numCols, int numRows, // Expected size of image
+                                psElemType type, // Expected type of image
+                                double init // Initial value
+    );
+
+// XXX for the moment, use col0, row0, numCols, numRows supplied from the outside
+bool pmReadoutStackDefineOutput(pmReadout *readout, int col0, int row0, int numCols, int numRows, bool mask, bool weight, psImageMaskType blank);
+
+bool pmReadoutStackSetOutputSize(int *col0, int *row0, int *numCols, int *numRows, const psArray *inputs);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/concepts/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/Makefile.am	(revision 42651)
@@ -0,0 +1,25 @@
+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 \
+	pmConceptsCopy.c \
+	pmConceptsStandard.c \
+	pmConceptsPhotcode.c \
+	pmConceptsUpdate.c
+
+pkginclude_HEADERS = \
+	pmConcepts.h \
+	pmConceptsAverage.h \
+	pmConceptsRead.h \
+	pmConceptsWrite.h \
+	pmConceptsCopy.h \
+	pmConceptsStandard.h \
+	pmConceptsPhotcode.h \
+	pmConceptsUpdate.h
+
+CLEANFILES = *~
Index: /branches/eam_branches/psModules.20240412/src/concepts/pmConcepts.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConcepts.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConcepts.c	(revision 42651)
@@ -0,0 +1,609 @@
+// 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 "pmConfig.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"
+
+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, pmConceptCopyFunc copy,
+                                  bool required)
+{
+    pmConceptSpec *spec = psAlloc(sizeof(pmConceptSpec));
+    psMemSetDeallocator(spec, (psFreeFunc)conceptSpecFree);
+
+    spec->blank = psMemIncrRefCounter(blank);
+    spec->parse = parse;
+    spec->format = format;
+    spec->copy = copy;
+    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;
+}
+
+psMetadata *pmConceptsSpecs(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;
+    }
+
+    return concepts;
+}
+
+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, pmConceptCopyFunc copy,
+                       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, copy, required); // 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;
+}
+
+
+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 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 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);
+}
+
+// Register a concept
+#define CONCEPT_REGISTER_FUNCTION(TYPENAME, SUFFIX, DEFAULT) \
+static void conceptRegister##SUFFIX(const char *name, /* Name of concept */ \
+                                    const char *comment, /* Comment for concept */ \
+                                    pmConceptParseFunc parse, /* Parsing function, or NULL */ \
+                                    pmConceptFormatFunc format, /* Formatting function, or NULL */ \
+                                    pmConceptCopyFunc copy, /* Copying function, or NULL */ \
+                                    bool required, /* Required concept? */ \
+                                    pmFPALevel level /* Level at which concept applies */ \
+    ) \
+{ \
+    psMetadataItem *item = psMetadataItemAlloc##TYPENAME(name, comment, DEFAULT); /* Item to add */ \
+    pmConceptRegister(item, parse, format, copy, required, level); \
+    psFree(item); \
+    return; \
+}
+
+CONCEPT_REGISTER_FUNCTION(Str, Str, "");
+CONCEPT_REGISTER_FUNCTION(F32, F32, NAN);
+CONCEPT_REGISTER_FUNCTION(F64, F64, NAN);
+CONCEPT_REGISTER_FUNCTION(S32, Enum, -1); // For enums: set default to -1
+CONCEPT_REGISTER_FUNCTION(S32, S32, 0); // For values: set default to 0
+CONCEPT_REGISTER_FUNCTION(Bool, Bool, NULL); // For values: set default to 0
+
+static void conceptRegisterTime(const char *name, /* Name of concept */ \
+                                const char *comment, /* Comment for concept */ \
+                                bool required, /* Required concept? */ \
+                                pmFPALevel level /* Level at which concept applies */ \
+    )
+{
+    psTime *time = psTimeAlloc(PS_TIME_TAI); // Blank time
+    // Not particularly distinguishing, but should be good enough
+    time->sec = 0;
+    time->nsec = 0;
+    psMetadataItem *item = psMetadataItemAlloc(name, PS_DATA_TIME, comment, time);
+    psFree(time);
+    pmConceptRegister(item, p_pmConceptParse_TIME, p_pmConceptFormat_TIME, NULL, required, level);
+    psFree(item);
+}
+
+bool pmConceptsInit(void)
+{
+    if (conceptsInitialised) {
+        return true;
+    }
+
+    conceptsInitialised = true;
+
+    p_psMemAllocatePersistent(true);
+
+    bool init = false;                  // Did we initialise anything?
+
+    if (!conceptsFPA) {
+        conceptsFPA = psMetadataAlloc();
+        init = true;
+
+        // Install the standard concepts
+        conceptRegisterStr("FPA.TELESCOPE", "Telescope of origin", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterStr("FPA.INSTRUMENT", "Instrument name (according to the instrument)", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterStr("FPA.DETECTOR", "Detector name", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterStr("FPA.COMMENT", "Observation comment", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterStr("FPA.OBS.MODE", "Observation mode (eg, survey id)", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterStr("FPA.OBS.GROUP", "Observation group (eg, associated images)", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.FOCUS", "Telescope focus", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.AIRMASS", "Airmass at boresight", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        // XXX p_pmConceptParse_FPA_FILTER -> p_pmConceptParse_FPA_FILTERID (and Format as well)?
+        conceptRegisterStr("FPA.FILTERID", "Filter used (parsed, abstract name)", p_pmConceptParse_FPA_FILTER, p_pmConceptFormat_FPA_FILTER, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterStr("FPA.FILTER", "Filter used (instrument name)", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.POSANGLE", "Position angle of instrument", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.ROTANGLE", "Rotator angle of instrument", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterStr("FPA.RADECSYS", "Celestial coordinate system", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF64("FPA.RA", "Right Ascension of boresight", p_pmConceptParse_FPA_Coords, p_pmConceptFormat_FPA_Coords, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF64("FPA.DEC", "Declination of boresight", p_pmConceptParse_FPA_Coords, p_pmConceptFormat_FPA_Coords, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF64("FPA.LONGITUDE", "West longitude of observatory", p_pmConceptParse_FPA_Coords, p_pmConceptFormat_FPA_Coords, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF64("FPA.LATITUDE", "Latitude of observatory", p_pmConceptParse_FPA_Coords, p_pmConceptFormat_FPA_Coords, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.ELEVATION", "Elevation of observatory (meters)", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+
+        // conceptRegisterStr("FPA.OBSTYPE", "Type of observation", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterStr("FPA.OBSTYPE", "Type of observation", p_pmConceptParse_FPA_OBSTYPE, p_pmConceptFormat_FPA_OBSTYPE, NULL, false, PM_FPA_LEVEL_FPA);
+
+        conceptRegisterStr("FPA.OBJECT", "Object of observation", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF64("FPA.ALT", "Altitude of boresight", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF64("FPA.AZ", "Azimuth of boresight", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterEnum("FPA.TIMESYS", "Time system", p_pmConceptParse_TIMESYS, p_pmConceptFormat_TIMESYS, p_pmConceptCopy_TIMESYS, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterTime("FPA.TIME", "Time of exposure", false, PM_FPA_LEVEL_FPA);
+
+        conceptRegisterStr("FPA.SHUTOUTC", "Time of exposure open", NULL,NULL,NULL,false, PM_FPA_LEVEL_FPA);
+        conceptRegisterStr("FPA.SHUTCUTC", "Time of exposure close", NULL,NULL,NULL,false, PM_FPA_LEVEL_FPA);
+        conceptRegisterStr("FPA.SHMDOUTC", "Time of exposure open mid-focalplane", NULL,NULL,NULL,false, PM_FPA_LEVEL_FPA);
+        conceptRegisterStr("FPA.SHMDCUTC", "Time of exposure close mid-focalplane", NULL,NULL,NULL,false, PM_FPA_LEVEL_FPA);
+
+        conceptRegisterF32("FPA.TEMP", "Temperature of focal plane", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.M1X", "Primary Mirror X Position", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.M1Y", "Primary Mirror Y Position", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.M1Z", "Primary Mirror Z Position", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.M1TIP", "Primary Mirror TIP", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.M1TILT", "Primary Mirror TILT", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.M2X", "Primary Mirror X Position", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.M2Y", "Primary Mirror Y Position", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.M2Z", "Primary Mirror Z Position", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.M2TIP", "Primary Mirror TIP", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.M2TILT", "Primary Mirror TILT", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.ENV.TEMP", "Environment: Temperature", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.ENV.HUMID", "Environment: Humidity", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.ENV.WIND", "Environment: Wind speed", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.ENV.DIR", "Environment: Wind direction", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.TELTEMP.M1", "Telescope Temperatures: M1", p_pmConceptParse_TELTEMPS, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.TELTEMP.M1CELL", "Telescope Temperatures: M1 cell", p_pmConceptParse_TELTEMPS, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.TELTEMP.M2", "Telescope Temperatures: M2", p_pmConceptParse_TELTEMPS, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.TELTEMP.SPIDER", "Telescope Temperatures: spider", p_pmConceptParse_TELTEMPS, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.TELTEMP.TRUSS", "Telescope Temperatures: truss", p_pmConceptParse_TELTEMPS, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.TELTEMP.EXTRA", "Telescope Temperatures: extra", p_pmConceptParse_TELTEMPS, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.PON.TIME", "Power On Time", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterS32("FPA.BURNTOOL.APPLIED", "[T=applied] Burn streaks applied to image data", p_pmConceptParse_BTOOLAPP,p_pmConceptFormat_BTOOLAPP,NULL,false,PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.EXPOSURE", "Exposure time (sec)", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+        conceptRegisterF32("FPA.ZP", "Magnitude zero point", NULL, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+    }
+    if (!conceptsChip) {
+        conceptsChip = psMetadataAlloc();
+        init = true;
+
+        // Install the standard concepts
+        conceptRegisterS32("CHIP.XPARITY", "Orientation in x compared to the rest of the FPA", NULL, NULL, NULL, true, PM_FPA_LEVEL_CHIP);
+        conceptRegisterS32("CHIP.YPARITY", "Orientation in y compared to the rest of the FPA", NULL, NULL, NULL, true, PM_FPA_LEVEL_CHIP);
+        conceptRegisterS32("CHIP.X0", "Position of (0,0) on the FPA",p_pmConceptParse_Positions,p_pmConceptFormat_Positions, NULL, true, PM_FPA_LEVEL_CHIP);
+        conceptRegisterS32("CHIP.Y0", "Position of (0,0) on the FPA",p_pmConceptParse_Positions,p_pmConceptFormat_Positions, NULL, true, PM_FPA_LEVEL_CHIP);
+        conceptRegisterS32("CHIP.XSIZE", "Size of chip (pixels)", NULL, NULL, NULL, true, PM_FPA_LEVEL_CHIP);
+        conceptRegisterS32("CHIP.YSIZE", "Size of chip (pixels)", NULL, NULL, NULL, true, PM_FPA_LEVEL_CHIP);
+        conceptRegisterF32("CHIP.TEMP", "Temperature of chip", NULL, NULL, NULL, false, PM_FPA_LEVEL_CHIP);
+        conceptRegisterF32("CHIP.TEMPERATURE", "Temperature of chip", NULL, NULL, NULL, false, PM_FPA_LEVEL_CHIP);
+        conceptRegisterStr("CHIP.ID", "Chip identifier", NULL, NULL, NULL, false, PM_FPA_LEVEL_CHIP);
+        conceptRegisterF32("CHIP.SEEING", "Seeing FWHM (pixels)", NULL, NULL, NULL, false, PM_FPA_LEVEL_CHIP);
+	conceptRegisterBool("CHIP.VIDEOCELL", "Does this OTA have any video cells", p_pmConceptParse_VideoCell,NULL,NULL,false,PM_FPA_LEVEL_CHIP);
+    }
+
+    if (!conceptsCell) {
+        conceptsCell = psMetadataAlloc();
+        init = true;
+
+        // Install the standard concepts
+        conceptRegisterF32("CELL.GAIN", "CCD gain (e/count)", NULL, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+        conceptRegisterF32("CELL.READNOISE", "CCD read noise (e)", p_pmConceptParse_CELL_READNOISE, p_pmConceptFormat_CELL_READNOISE, NULL, true, PM_FPA_LEVEL_CELL);
+        conceptRegisterF32("CELL.SATURATION", "Saturation level (counts)", NULL, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+        conceptRegisterF32("CELL.BAD", "Bad level (counts)", NULL, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+        conceptRegisterS32("CELL.XPARITY", "Orientation in x compared to the rest of the chip", NULL, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+        conceptRegisterS32("CELL.YPARITY", "Orientation in y compared to the rest of the chip", NULL, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+        conceptRegisterS32("CELL.READDIR", "Read direction, rows=1, cols=2", NULL, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+
+        // 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....
+        conceptRegisterF32("CELL.EXPOSURE", "Exposure time (sec)", NULL, NULL, NULL, false, PM_FPA_LEVEL_CELL);
+        conceptRegisterF32("CELL.DARKTIME", "Time since flush (sec)", NULL, NULL, NULL, false, PM_FPA_LEVEL_CELL);
+
+        conceptRegisterS32("CELL.XBIN", "Binning in x", p_pmConceptParse_CELL_Binning,p_pmConceptFormat_CELL_XBIN, NULL, true, PM_FPA_LEVEL_CELL);
+        conceptRegisterS32("CELL.YBIN", "Binning in y",p_pmConceptParse_CELL_Binning,p_pmConceptFormat_CELL_YBIN, NULL, true, PM_FPA_LEVEL_CELL);
+        conceptRegisterEnum("CELL.TIMESYS", "Time system", p_pmConceptParse_TIMESYS,p_pmConceptFormat_TIMESYS, p_pmConceptCopy_TIMESYS, false, PM_FPA_LEVEL_CELL);
+        conceptRegisterTime("CELL.TIME", "Time of exposure", false, PM_FPA_LEVEL_CELL);
+        conceptRegisterS32("CELL.X0", "Position of (0,0) on the chip",p_pmConceptParse_Positions,p_pmConceptFormat_Positions, NULL, true, PM_FPA_LEVEL_CELL);
+        conceptRegisterS32("CELL.Y0", "Position of (0,0) on the chip",p_pmConceptParse_Positions,p_pmConceptFormat_Positions, NULL, true, PM_FPA_LEVEL_CELL);
+        conceptRegisterS32("CELL.XSIZE", "Size of cell (pixels)", NULL, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+        conceptRegisterS32("CELL.YSIZE", "Size of cell (pixels)", NULL, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+        conceptRegisterS32("CELL.XWINDOW", "Start of cell window (pixels)",p_pmConceptParse_Positions,p_pmConceptFormat_Positions, NULL, true, PM_FPA_LEVEL_CELL);
+        conceptRegisterS32("CELL.YWINDOW", "Start of cell window (pixels)",p_pmConceptParse_Positions,p_pmConceptFormat_Positions, NULL, true, PM_FPA_LEVEL_CELL);
+
+        // 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, p_pmConceptParse_CELL_TRIMSEC,p_pmConceptFormat_CELL_TRIMSEC, NULL, 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, p_pmConceptParse_CELL_BIASSEC, p_pmConceptFormat_CELL_BIASSEC, NULL, true, PM_FPA_LEVEL_CELL);
+            psFree(cellBiassec);
+        }
+
+    }
+
+    p_psMemAllocatePersistent(false);
+
+    return init;
+}
+
+void pmConceptsDone(void)
+{
+    psFree(conceptsFPA);
+    conceptsFPA = NULL;
+    psFree(conceptsChip);
+    conceptsChip = NULL;
+    psFree(conceptsCell);
+    conceptsCell = NULL;
+
+    conceptsInitialised = false;
+}
+
+
+// Interpolate the concept.  Generalises the FPA/Chip/Cell
+#define CONCEPT_INTERPOLATE(SOURCE, NAME, DEFAULT) \
+    if (strncmp(concept, NAME, strlen(NAME)) == 0) { \
+        psString value = NULL; /* Value of concept */ \
+        if (SOURCE) { \
+            psMetadataItem *item = psMetadataLookup((SOURCE)->concepts, concept); /* Item with concept */ \
+            if (!item) { \
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Can't find concept %s in %s", concept, NAME); \
+                psFree(string); \
+                return NULL; \
+            } \
+            \
+            value = psMetadataItemParseString(item); /* Value of concept */ \
+            if (!value) { \
+                psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to parse concept %s", concept); \
+                psFree(string); \
+                return NULL; \
+            } \
+        } else { \
+            if (!(DEFAULT)) { \
+                psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to determine concept %s", concept); \
+                psFree(string); \
+                return NULL; \
+            } \
+            value = psStringCopy(DEFAULT); \
+        } \
+        \
+        char replace[length + 2];       /* String to replace with value */ \
+        replace[0] = '{'; \
+        strcpy(replace + 1, concept); \
+        strcpy(replace + length, "}"); \
+        \
+        psTrace("psModules.concepts", 10, "Interpolating concept %s for %s", replace, value); \
+        \
+        if (!psStringSubstitute(&string, value, replace)) { \
+            psError(PS_ERR_UNKNOWN, false, "Unable to replace concept %s", concept); \
+            psFree(string); \
+            psFree(value); \
+            return NULL; \
+        } \
+        psFree(value); \
+        \
+        continue; \
+    }
+
+
+// XXX Could make the concept delimiters, currently '{' and '}', configurable
+psString pmConceptsInterpolate(const char *input,
+                               const pmFPA *fpa,
+                               const pmChip *chip,
+                               const pmCell *cell
+    )
+{
+    PS_ASSERT_STRING_NON_EMPTY(input, NULL);
+
+    psString string = psStringCopy(input); // Interpolated string, to return
+
+    char *start;                        // Start of a concept
+    while ((start = strchr(string, '{'))) {
+        char *stop = strchr(start, '}'); // End of a concept
+        int length = stop - start;      // Length of the concept name, including terminating \0
+        char concept[length];  // Name of concept
+        strncpy(concept, start + 1, length - 1);
+        concept[length - 1] = '\0';
+
+        // special variants:
+        if (!strcmp(concept, "FPA.DATE")) {
+          psTime *fpaTime = psMetadataLookupPtr(NULL, fpa->concepts, "FPA.TIME");
+          if (!fpaTime) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Missing concept FPA.TIME needed for FPA.DATE");
+            psFree(string);
+            return NULL;
+          }
+          psString dateTimeString = psTimeToISO(fpaTime); // String representation
+          psList *dateTime = psStringSplit(dateTimeString, "T", true);
+          psFree(dateTimeString);
+          psString dateString = psMemIncrRefCounter(psListGet(dateTime, PS_LIST_HEAD)); // The date string
+          psFree (dateTime);
+
+          if (!psStringSubstitute(&string, dateString, "{FPA.DATE}")) {
+              psError(PS_ERR_UNKNOWN, false, "Unable to replace concept %s", concept);
+              psFree(string);
+              psFree(dateString);
+              return NULL;
+          }
+          psFree (dateString);
+          continue;
+        }
+
+        psTrace("psModules.concepts", 7, "Interpolating concept %s", concept);
+
+        CONCEPT_INTERPOLATE(fpa,  "FPA", NULL);
+        CONCEPT_INTERPOLATE(chip, "CHIP", "fpa");
+        CONCEPT_INTERPOLATE(cell, "CELL", "chip");
+
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unrecognised concept: %s", concept);
+        psFree(string);
+        return NULL;
+    }
+
+    return string;
+}
+
+
+psMetadataItem *p_pmConceptsDepend(const char *name, const psMetadata *menu, const psMetadata *source,
+                                   const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+{
+    psAssert(name && strlen(name) > 0, "Concept name is empty");
+    psAssert(menu, "Must have menu");
+    psAssert(source, "Must have source");
+
+    // Check for DEPEND
+    psString depend = NULL; // The CONCEPT.DEPEND
+    psStringAppend(&depend, "%s.DEPEND", name);
+    bool mdok;                          // Status of MD lookup
+    const char *dependConcept = psMetadataLookupStr(&mdok, source, 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);
+        return NULL;
+    }
+    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) {
+        // Not an error --- it may be specified some other way
+        psTrace("psModules.concepts", 7, "Couldn't find DEPEND for %s", name);
+        return NULL;
+    }
+    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);
+        return NULL;
+    }
+    const char *key = dependValue->data.V; // The key to the DEPEND menu
+    psTrace("psModules.concepts", 7, "%s.DEPEND resolves to %s....\n", name, key);
+
+    return psMetadataLookup(menu, key);
+}
+
+int pmConceptsChipNumberFromName (pmFPA *fpa, char *name) {
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!chip) continue;
+        char *thisone = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+        if (!thisone) continue;
+        if (!strcmp (name, thisone)) return (i);
+    }
+    return -1;
+}
+
+pmChip *pmConceptsChipFromName (pmFPA *fpa, char *name) {
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!chip) continue;
+        char *thisone = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+        if (!thisone) continue;
+        if (!strcmp (name, thisone)) return (chip);
+    }
+    return NULL;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/concepts/pmConcepts.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConcepts.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConcepts.h	(revision 42651)
@@ -0,0 +1,168 @@
+/* @file pmConcepts.h
+ * @brief Top-level functions for defining, registering, reading and writing concepts
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.19 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-06-30 00:53:45 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONCEPTS_H
+#define PM_CONCEPTS_H
+
+#include <pslib.h>
+#include <pmFPALevel.h>
+#include <pmFPA.h>
+#include <pmConfig.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_BLANK    = 0x01,  ///< Blank concepts defined, but not read
+    PM_CONCEPT_SOURCE_CELLS    = 0x02,  ///< Concept comes from the camera information
+    PM_CONCEPT_SOURCE_DEFAULTS = 0x04,  ///< Concept comes from defaults
+    PM_CONCEPT_SOURCE_PHU      = 0x08,  ///< Concept comes from PHU
+    PM_CONCEPT_SOURCE_HEADER   = 0x10,  ///< Concept comes from FITS header
+    PM_CONCEPT_SOURCE_DATABASE = 0x20,  ///< Concept comes from database
+    PM_CONCEPT_SOURCE_ALL      = 0xfe   ///< All concepts (exclude BLANK)
+} 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
+    );
+
+/// Function to call to copy a concept
+typedef psMetadataItem* (*pmConceptCopyFunc)(
+    const psMetadataItem *target,       ///< Target concept
+    const psMetadataItem *source,       ///< Source concept
+    const psMetadata *cameraFormat,     ///< Camera format definition
+    const pmFPA *fpa,                   ///< Source FPA for concept, or NULL
+    const pmChip *chip,                 ///< Source chip for concept, or NULL
+    const pmCell *cell                  ///< Source 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, or NULL
+    pmConceptFormatFunc format;         ///< Function to call to write the concept, or NULL
+    pmConceptCopyFunc copy;             ///< Function to call to copy the concept, or NULL
+    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/NULL
+                                  pmConceptFormatFunc format, ///< Function to call to format the concept/NULL
+                                  pmConceptCopyFunc copy, ///< Function to call to copy the concept, or NULL
+                                  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
+                       pmConceptCopyFunc copy, ///< Function to call to copy the concept, or NULL
+                       bool required,   ///< Is concept required?
+                       pmFPALevel level ///< Level at which to store concept in the FPA hierarchy
+                      );
+
+/// Get the specifications for defined concepts of a particular level
+psMetadata *pmConceptsSpecs(pmFPALevel level);
+
+
+/// Set the concepts within the FPA to the blank value
+bool pmConceptsBlankFPA(pmFPA *fpa      ///< FPA for which to set blank concepts
+                       );
+
+/// Set the concepts within the chip to the blank value
+bool pmConceptsBlankChip(pmChip *chip   ///< FPA for which to set blank concepts
+                        );
+
+/// Set the concepts within the cell to the blank value
+bool pmConceptsBlankCell(pmCell *cell   ///< Cell for which to set blank concepts
+                        );
+
+/// 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);
+
+/// Interpolate a concept name to the actual value
+///
+/// Concepts enclosed within braces {}, are replaced with the value of the concept
+psString pmConceptsInterpolate(const char *input, ///< Input string
+                               const pmFPA *fpa, ///< FPA with concept values, or NULL
+                               const pmChip *chip, ///< Chip with concept values, or NULL
+                               const pmCell *cell ///< Cell with concept values, or NULL
+    );
+
+/// Look up a dependency menu to get a concept's value
+///
+/// Returns a psMetadataItem with the concept value
+psMetadataItem *p_pmConceptsDepend(const char *name, ///< Name of concept for which to get dependent value
+                                   const psMetadata *menu, ///< Menu in which to look up key
+                                   const psMetadata *source, ///< Source metadata with CONCEPT.DEPEND
+                                   const pmFPA *fpa, ///< FPA for dependency
+                                   const pmChip *chip, ///< Chip for dependency
+                                   const pmCell *cell ///< Cell for dependency
+    );
+
+// some utility functions:
+int pmConceptsChipNumberFromName (pmFPA *fpa, char *name);
+pmChip *pmConceptsChipFromName (pmFPA *fpa, char *name);
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsAverage.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsAverage.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsAverage.c	(revision 42651)
@@ -0,0 +1,404 @@
+#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; \
+}
+
+// Update a metadata string entry directly
+#define MD_UPDATE_STR(MD, NAME, VALUE) \
+{ \
+    psMetadataItem *item = psMetadataLookup(MD, NAME); \
+    psFree(item->data.str); \
+    item->data.str = psStringCopy(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);
+
+    double time      = 0.0;             // Time of observation
+    double zp        = 0.0;             // Zero point
+    psTimeType timeSys = 0;             // Time system
+    char *filter     = NULL;            // Filter
+    char *filterId   = NULL;            // Filter (parsed, abstract name)
+    char *telescope  = NULL;            // Telescope of origin
+    char *instrument = NULL;            // Instrument name
+    char *detector   = NULL;            // Detector name
+
+    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++;
+
+#define COMPARE_STR(NAME, VALUE) \
+    if (strcmp(VALUE, psMetadataLookupStr(NULL, fpa->concepts, NAME)) != 0) { \
+        psWarning("Differing %s in use: %s vs %s\n", \
+                  NAME, VALUE, psMetadataLookupStr(NULL, fpa->concepts, NAME)); \
+        VALUE = "VARIOUS"; \
+    }
+
+        psTime *fpaTime = psMetadataLookupPtr(NULL, fpa->concepts, "FPA.TIME");
+        psTimeConvert(fpaTime, PS_TIME_TAI);
+        time       += psTimeToMJD(fpaTime);
+
+        zp += psMetadataLookupF32(NULL, fpa->concepts, "FPA.ZP");
+
+        if (num == 1) {
+            timeSys = psMetadataLookupS32(NULL, fpa->concepts, "FPA.TIMESYS");
+            filter = psMetadataLookupStr(NULL, fpa->concepts, "FPA.FILTER");
+            filterId = psMetadataLookupStr(NULL, fpa->concepts, "FPA.FILTERID");
+            telescope = psMetadataLookupStr(NULL, fpa->concepts, "FPA.TELESCOPE");
+            instrument = psMetadataLookupStr(NULL, fpa->concepts, "FPA.INSTRUMENT");
+            detector = psMetadataLookupStr(NULL, fpa->concepts, "FPA.DETECTOR");
+        } else {
+            if (timeSys != psMetadataLookupS32(NULL, fpa->concepts, "FPA.TIMESYS")) {
+                psWarning("Differing FPA.TIMESYS in use: %d vs %d\n",
+                          timeSys, psMetadataLookupS32(NULL, fpa->concepts, "FPA.TIMESYS"));
+            }
+            COMPARE_STR("FPA.FILTER", filter);
+            COMPARE_STR("FPA.FILTERID", filterId);
+            COMPARE_STR("FPA.TELESCOPE", telescope);
+            COMPARE_STR("FPA.INSTRUMENT", instrument);
+            COMPARE_STR("FPA.DETECTOR", detector);
+        }
+    }
+    psFree(sourcesIter);
+
+    time /= (double)num;
+    zp /= (double)num;
+
+    MD_UPDATE(target->concepts, "FPA.TIMESYS", S32, timeSys);
+    MD_UPDATE_STR(target->concepts, "FPA.FILTER", filter);
+    MD_UPDATE_STR(target->concepts, "FPA.FILTERID", filterId);
+    MD_UPDATE_STR(target->concepts, "FPA.TELESCOPE", telescope);
+    MD_UPDATE_STR(target->concepts, "FPA.INSTRUMENT", instrument);
+    MD_UPDATE_STR(target->concepts, "FPA.DETECTOR", detector);
+    MD_UPDATE(target->concepts, "FPA.ZP", F32, zp);
+
+    // FPA.TIME needs special care
+    {
+        psMetadataItem *timeItem = psMetadataLookup(target->concepts, "FPA.TIME");
+        psFree(timeItem->data.V);
+        psTime *new = psTimeFromMJD(time);
+        psTimeConvert(new, timeSys);
+        timeItem->data.V = new;
+    }
+
+    return true;
+}
+
+float averageWithDropouts (psList *sources, char *name) {
+
+    bool status;
+
+    float sum = 0;
+    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;
+        }
+
+        float value = psMetadataLookupF32(&status, cell->concepts, name);
+        if (!status) continue;
+        if (!isfinite(value)) continue;
+
+        sum += value;
+        nCells++;
+    }
+    psFree (sourcesIter);
+
+    float average = sum / nCells;
+    return average;
+}
+float medianWithDropouts (psList *sources, char *name) {
+
+    bool status;
+
+    psListIterator *sourcesIter = psListIteratorAlloc(sources, PS_LIST_HEAD, false); // Iterator for sources
+    pmCell *cell = NULL;                // Source cell from iteration
+
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN);
+    psVector *values = psVectorAlloc(sources->n, PS_TYPE_F32);
+    int nvalues = 0;
+    while ((cell = psListGetAndIncrement(sourcesIter))) {
+        if (!cell) {
+            continue;
+        }
+
+        float value = psMetadataLookupF32(&status, cell->concepts, name);
+        if (!status) continue;
+        if (!isfinite(value)) continue;
+
+        values->data.F32[nvalues++] = value;
+    }
+    psFree (sourcesIter);
+    if (!nvalues) {
+        psWarning("no valid values found for %s\n", name);
+        psFree(values);
+        psFree(stats);
+        return INFINITY;
+    }
+    if (!(values = psVectorRealloc(values, nvalues))) {
+        psWarning("failed to reallocate values vector for %s\n", name);
+        psFree(stats);
+        return INFINITY;
+    }
+    if (!psVectorStats(stats, values, NULL, NULL, 0)) {
+        psWarning("psVectorStats failed for %s\n", name);
+        psFree(values);
+        psFree(stats);
+        return INFINITY;
+    }
+
+    psF32 median = psStatsGetValue(stats, PS_STAT_SAMPLE_MEDIAN);
+
+    psFree(values);
+    psFree(stats);
+
+    return median;
+}
+
+// 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);
+
+    float bad        = -INFINITY;       // Bad level
+    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 xParity = 0, yParity = 0;       // Parity
+
+    float gain      = averageWithDropouts (sources, "CELL.GAIN");
+    float readnoise = averageWithDropouts (sources, "CELL.READNOISE");
+    float exposure  = averageWithDropouts (sources, "CELL.EXPOSURE");
+    float darktime  = averageWithDropouts (sources, "CELL.DARKTIME");
+    float saturation = medianWithDropouts(sources, "CELL.SATURATION");
+
+    // other concepts are a bit more "special"
+    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++;
+        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");
+                xParity = psMetadataLookupS32(NULL, cell->concepts, "CELL.XPARITY");
+                yParity = psMetadataLookupS32(NULL, cell->concepts, "CELL.YPARITY");
+            }
+        } else {
+            if (timeSys != psMetadataLookupS32(NULL, cell->concepts, "CELL.TIMESYS")) {
+                psWarning("Differing CELL.TIMESYS in use: %d vs %d\n",
+                          timeSys, psMetadataLookupS32(NULL, cell->concepts, "CELL.TIMESYS"));
+            }
+            if (readdir != psMetadataLookupS32(NULL, cell->concepts, "CELL.READDIR")) {
+                psWarning("Differing CELL.READDIR in use: %d vs %d\n",
+                          readdir, psMetadataLookupS32(NULL, cell->concepts, "CELL.READDIR"));
+            }
+            if (xBin != psMetadataLookupS32(NULL, cell->concepts, "CELL.XBIN")) {
+                psWarning("Differing CELL.XBIN in use: %d vs %d\n",
+                          xBin, psMetadataLookupS32(NULL, cell->concepts, "CELL.XBIN"));
+            }
+            if (yBin != psMetadataLookupS32(NULL, cell->concepts, "CELL.YBIN")) {
+                psWarning("Differing CELL.YBIN in use: %d vs %d\n",
+                          yBin, psMetadataLookupS32(NULL, cell->concepts, "CELL.YBIN"));
+            }
+            if (same) {
+                if (x0 != psMetadataLookupS32(NULL, cell->concepts, "CELL.X0")) {
+                    psWarning("Differing CELL.X0 in use: %d vs %d\n",
+                              x0, psMetadataLookupS32(NULL, cell->concepts, "CELL.X0"));
+                }
+                if (y0 != psMetadataLookupS32(NULL, cell->concepts, "CELL.Y0")) {
+                    psWarning("Differing CELL.Y0 in use: %d vs %d\n",
+                              y0, psMetadataLookupS32(NULL, cell->concepts, "CELL.Y0"));
+                }
+                if (xParity != psMetadataLookupS32(NULL, cell->concepts, "CELL.XPARITY")) {
+                    psWarning("Differing CELL.XPARITY in use: %d vs %d\n",
+                              xParity, psMetadataLookupS32(NULL, cell->concepts, "CELL.XPARITY"));
+                }
+                if (yParity != psMetadataLookupS32(NULL, cell->concepts, "CELL.YPARITY")) {
+                    psWarning("Differing CELL.YPARITY in use: %d vs %d\n",
+                              yParity, psMetadataLookupS32(NULL, cell->concepts, "CELL.YPARITY"));
+                }
+            }
+        }
+
+        float cellBad = psMetadataLookupF32(NULL, cell->concepts, "CELL.BAD");
+        if (cellBad > bad) {
+            bad = cellBad;
+        }
+    }
+    psFree(sourcesIter);
+
+    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);
+        MD_UPDATE(target->concepts, "CELL.XPARITY", S32, xParity);
+        MD_UPDATE(target->concepts, "CELL.YPARITY", S32, yParity);
+    }
+
+    // CELL.TIME needs special care
+    {
+        psMetadataItem *timeItem = psMetadataLookup(target->concepts, "CELL.TIME");
+        psFree(timeItem->data.V);
+        psTime *new = psTimeFromMJD(time);
+        psTimeConvert(new, timeSys);
+        timeItem->data.V = new;
+    }
+
+    // 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 true;
+}
+
+bool pmConceptsAverageChips(pmChip *target, psList *sources, bool same)
+{
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_INT_POSITIVE(sources->n, false);
+
+    float temp = 0.0;                   // Temperature
+    float seeing = 0.0;                 // Seeing FWHM
+    int x0 = 0, y0 = 0;                 // Offset
+    int xParity = 0, yParity = 0;       // Parity
+    int xSize = 0, ySize = 0;           // Size
+    psString id = NULL;                 // Identifier
+
+    int nChips = 0;                     // Number of chips;
+    psListIterator *sourcesIter = psListIteratorAlloc(sources, PS_LIST_HEAD, false); // Iterator for sources
+    pmChip *chip = NULL;                // Source chip from iteration
+    while ((chip = psListGetAndIncrement(sourcesIter))) {
+        if (!chip) {
+            continue;
+        }
+        temp += psMetadataLookupF32(NULL, chip->concepts, "CHIP.TEMP");
+        seeing += psMetadataLookupF32(NULL, chip->concepts, "CHIP.SEEING");
+        if (nChips == 0) {
+            xSize = psMetadataLookupS32(NULL, chip->concepts, "CHIP.XSIZE");
+            ySize = psMetadataLookupS32(NULL, chip->concepts, "CHIP.YSIZE");
+            xParity = psMetadataLookupS32(NULL, chip->concepts, "CHIP.XPARITY");
+            yParity = psMetadataLookupS32(NULL, chip->concepts, "CHIP.YPARITY");
+            x0 = psMetadataLookupS32(NULL, chip->concepts, "CHIP.X0");
+            y0 = psMetadataLookupS32(NULL, chip->concepts, "CHIP.Y0");
+            id = psMetadataLookupStr(NULL, chip->concepts, "CHIP.ID");
+        } else {
+            if (xSize != psMetadataLookupS32(NULL, chip->concepts, "CHIP.XSIZE")) {
+                psWarning("Differing CHIP.XSIZE in use: %d vs %d\n",
+                    xSize, psMetadataLookupS32(NULL, chip->concepts, "CHIP.XSIZE"));
+            }
+            if (ySize != psMetadataLookupS32(NULL, chip->concepts, "CHIP.YSIZE")) {
+                psWarning("Differing CHIP.YSIZE in use: %d vs %d\n",
+                    ySize, psMetadataLookupS32(NULL, chip->concepts, "CHIP.YSIZE"));
+            }
+            if (xParity != psMetadataLookupS32(NULL, chip->concepts, "CHIP.XPARITY")) {
+                psWarning("Differing CHIP.XPARITY in use: %d vs %d\n",
+                    xParity, psMetadataLookupS32(NULL, chip->concepts, "CHIP.XPARITY"));
+            }
+            if (yParity != psMetadataLookupS32(NULL, chip->concepts, "CHIP.YPARITY")) {
+                psWarning("Differing CHIP.YPARITY in use: %d vs %d\n",
+                    yParity, psMetadataLookupS32(NULL, chip->concepts, "CHIP.YPARITY"));
+            }
+            if (x0 != psMetadataLookupS32(NULL, chip->concepts, "CHIP.X0")) {
+                psWarning("Differing CHIP.X0 in use: %d vs %d\n",
+                    x0, psMetadataLookupS32(NULL, chip->concepts, "CHIP.X0"));
+            }
+            if (y0 != psMetadataLookupS32(NULL, chip->concepts, "CHIP.Y0")) {
+                psWarning("Differing CHIP.Y0 in use: %d vs %d\n",
+                    y0, psMetadataLookupS32(NULL, chip->concepts, "CHIP.Y0"));
+            }
+            psString newID = psMetadataLookupStr(NULL, chip->concepts, "CHIP.ID");
+            if (id && newID && strcmp(id, newID)) {
+                psWarning("Differing CHIP.ID in use: %s vs %s\n", id, newID);
+            }
+        }
+
+        nChips++;
+    }
+    psFree(sourcesIter);
+
+    temp /= (float)nChips;
+    seeing /= (float)nChips;
+
+    MD_UPDATE(target->concepts, "CHIP.TEMP", F32, temp);
+    MD_UPDATE(target->concepts, "CHIP.SEEING", F32, seeing);
+    if (same) {
+        MD_UPDATE(target->concepts, "CHIP.X0", S32, x0);
+        MD_UPDATE(target->concepts, "CHIP.Y0", S32, y0);
+        MD_UPDATE(target->concepts, "CHIP.XSIZE", S32, xSize);
+        MD_UPDATE(target->concepts, "CHIP.YSIZE", S32, ySize);
+        MD_UPDATE(target->concepts, "CHIP.XPARITY", S32, xParity);
+        MD_UPDATE(target->concepts, "CHIP.YPARITY", S32, yParity);
+        MD_UPDATE_STR(target->concepts, "CHIP.ID", id);
+    }
+
+    return true;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsAverage.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsAverage.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsAverage.h	(revision 42651)
@@ -0,0 +1,65 @@
+/* @file pmConceptsAverage.h
+ * @brief Average the values of multiple concepts
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.11 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-04-10 06:31:42 $
+ * 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
+/// - CELL.XPARITY, CELL.YPARITY
+/// 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
+/// - CELL.XPARITY, CELL.YPARITY
+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?
+                           );
+
+/// Set a variety of concepts in a chip by averaging over several 
+bool pmConceptsAverageChips(pmChip *target,///< Target chip
+                            psList *sources, ///< List of source chips
+                            bool same   ///< Are the chips the same chip from different exposures?
+                           );
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsCopy.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsCopy.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsCopy.c	(revision 42651)
@@ -0,0 +1,169 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmHDUUtils.h"
+#include "pmFPA.h"
+#include "pmConcepts.h"
+
+#include "pmConceptsCopy.h"
+
+
+
+// List of concepts not to copy, for each level.
+// Must be NULL-terminated
+static const char *dontCopyConceptsFPA[] = { "FPA.OBS", "FPA.NAME", "FPA.CAMERA", 0 };
+static const char *dontCopyConceptsChip[] = { "CHIP.NAME", 0 };
+static const char *dontCopyConceptsCell[] = { "CELL.NAME", 0 };
+
+// Copy concepts from a source container to a target container, avoiding certain entries
+static bool copyConcepts(psMetadata *target, // Target metadata container
+                         psMetadata *source, // Source metadata container
+                         psMetadata *specs, // Concept specifications
+                         psMetadata *cameraFormat, // Camera format configuration
+                         const pmFPA *fpa,    // FPA of interest
+                         const pmChip *chip,  // Chip of interest, or NULL
+                         const pmCell *cell,  // Cell of interest, or NULL
+                         const char *dontCopyConcepts[] // Don't copy these concepts
+                         )
+{
+    assert(target);
+    assert(source);
+    assert(specs);
+    assert(dontCopyConcepts);
+
+    psMetadataIterator *iter = psMetadataIteratorAlloc(source, PS_LIST_HEAD, NULL);
+    psMetadataItem *sourceItem;         // Item from iteration
+    while ((sourceItem = psMetadataGetAndIncrement(iter))) {
+        const char *name = sourceItem->name;  // Name of concept
+        bool copyOK = true;            // OK to copy
+        for (int i = 0; dontCopyConcepts[i] && copyOK; i++) {
+            if (!strcmp(name, dontCopyConcepts[i])) {
+                copyOK = false;
+            }
+        }
+        if (!copyOK) {
+            continue;
+        }
+
+        bool mdok;                      // Status of MD lookup
+        pmConceptSpec *spec = psMetadataLookupPtr(&mdok, specs, name); // Specification for concept
+        psMetadataItem *copy = NULL;    // Copy of source item
+        if (mdok && spec && spec->copy) {
+            psMetadataItem *targetItem = psMetadataLookup(target, name); // Corresponding item from target
+            copy = spec->copy(targetItem, sourceItem, cameraFormat, fpa, chip, cell);
+            if (!copy) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to copy concept %s", name);
+                return false;
+            }
+        } else {
+            copy = psMetadataItemCopy(sourceItem);
+        }
+        psMetadataAddItem(target, copy, PS_LIST_TAIL, PS_META_REPLACE);
+        psFree(copy);                    // Drop reference
+    }
+    psFree(iter);
+
+    return true;
+}
+
+
+bool pmConceptsCopyFPA(pmFPA *target, const pmFPA *source, bool chips, bool cells)
+{
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+
+    psMetadata *specs = pmConceptsSpecs(PM_FPA_LEVEL_FPA); // Concept specifications
+
+    pmHDU *hdu = target->hdu;           // Header data unit
+    psMetadata *format = hdu ? hdu->format : NULL; // Camera format
+
+    // Copy FPA concepts
+    if (!copyConcepts(target->concepts, source->concepts, specs, format,
+                      source, NULL, NULL, dontCopyConceptsFPA)) {
+        return false;
+    }
+
+    // Copy chip concepts
+    bool status = true;                 // Status of chips
+    if (chips) {
+        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.",
+                    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;
+            }
+
+            status &= pmConceptsCopyChip(targetChip, sourceChip, cells);
+        }
+    }
+
+    return status;
+}
+
+bool pmConceptsCopyChip(pmChip *target, const pmChip *source, bool cells)
+{
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+
+    psMetadata *specs = pmConceptsSpecs(PM_FPA_LEVEL_CHIP); // Concept specifications
+    pmHDU *hdu = pmHDUFromChip(target); // Header data unit
+    psMetadata *format = hdu ? hdu->format : NULL; // Camera format
+
+    // Copy chip concepts
+    if (!copyConcepts(target->concepts, source->concepts, specs, format,
+                      source->parent, source, NULL, dontCopyConceptsChip)) {
+        return false;
+    }
+
+    // Copy cell concepts
+    bool status = true;                 // Status of cells
+    if (cells) {
+        psArray *targetCells = target->cells; // Cells in target
+        psArray *sourceCells = source->cells; // Cells in source
+        if (targetCells->n != sourceCells->n) {
+            psError(PS_ERR_IO, true,
+                    "Number of cells in target (%ld) and source (%ld) differ --- unable to copy concepts.",
+                    targetCells->n, sourceCells->n);
+            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;
+            }
+
+            status &= pmConceptsCopyCell(targetCell, sourceCell);
+        }
+    }
+
+    return status;
+}
+
+
+bool pmConceptsCopyCell(pmCell *target, const pmCell *source)
+{
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+
+    psMetadata *specs = pmConceptsSpecs(PM_FPA_LEVEL_CHIP); // Concept specifications
+    pmHDU *hdu = pmHDUFromCell(target); // Header data unit
+    psMetadata *format = hdu ? hdu->format : NULL; // Camera format
+
+    return copyConcepts(target->concepts, source->concepts, specs, format,
+                        source->parent->parent, source->parent, source, dontCopyConceptsCell);
+}
Index: /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsCopy.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsCopy.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsCopy.h	(revision 42651)
@@ -0,0 +1,31 @@
+#ifndef PM_CONCEPTS_COPY_H
+#define PM_CONCEPTS_COPY_H
+
+#include <pmFPA.h>
+
+/// 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
+                      );
+
+/// Copy the concepts within an FPA to another FPA; optionally recurse to lower levels
+bool pmConceptsCopyFPA(pmFPA *target,   ///< Target FPA
+                       const pmFPA *source, ///< Source FPA
+                       bool chips,      ///< Recurse to chips level?
+                       bool cells       ///< Recurse to cells level?
+    );
+
+/// Copy the concepts within a chip to another chip; optionally recurse to lower level
+bool pmConceptsCopyChip(pmChip *target, ///< Target chip
+                        const pmChip *source, ///< Source chip
+                        bool cells      ///< Recurse to cells level?
+    );
+
+/// Copy the concepts within a cell to another cell
+bool pmConceptsCopyCell(pmCell *target, ///< Target cell
+                        const pmCell *source ///< Source cell
+    );
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsPhotcode.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsPhotcode.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsPhotcode.c	(revision 42651)
@@ -0,0 +1,41 @@
+#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(pmFPAfile *file, const pmFPAview *view)
+{
+    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 photcode rule from camera configuration
+    bool mdok;                          // Status of MD lookup
+    char *rule = psMetadataLookupStr(&mdok, file->camera, "PHOTCODE.RULE");
+    if (!mdok || !rule || strlen(rule) == 0) {
+        psError(PS_ERR_IO, true, "PHOTCODE.RULE not found in camera configuration.");
+        return NULL;
+    }
+
+    // convert rule to real photcode
+    psString photcode = pmFPAfileNameFromRule(rule, file, view);
+
+    return photcode;
+}
Index: /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsPhotcode.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsPhotcode.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsPhotcode.h	(revision 42651)
@@ -0,0 +1,25 @@
+/* @file  pmConceptsPhotcode.h
+ * @brief Generate a photcode from the concepts
+ *
+ * @author Eugene Magnier, IfA
+ *
+ * @version $Revision: 1.10 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-08-12 03:27:14 $
+ * 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 camera configuration
+///
+/// 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 camera configuration.  Interpolation using the usual syntax (e.g., "{CHIP.NAME}") is permitted.
+psString pmConceptsPhotcodeForView(pmFPAfile *file, const pmFPAview *view);
+
+/// @}
+# endif
Index: /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsRead.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsRead.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsRead.c	(revision 42651)
@@ -0,0 +1,678 @@
+#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 "pmConfig.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmHDUUtils.h"
+#include "pmConcepts.h"
+#include "pmConceptsUpdate.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));
+    case PS_DATA_BOOL:
+      return psMetadataItemAllocBool(pattern->name, pattern->comment, psMetadataItemParseBool(concept));
+    default:
+        psWarning("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;
+    }
+    psTrace ("psModules.concepts", 3, "parsing concept: %s\n", spec->blank->name);
+    if (!strcmp (spec->blank->name, "CELL.XPARITY")) {
+        psTrace ("psModules.concepts", 3, "parsing CELL.XPARITY: %s\n", spec->blank->name);
+    }
+
+    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?
+        }
+    }
+
+    // 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;
+}
+
+psMetadataItem *p_pmConceptsReadSingleFromDefaults(const char *name, const psMetadata *defaults,
+                                                  const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+{
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+    PS_ASSERT_METADATA_NON_NULL(defaults, NULL);
+
+    psMetadataItem *item = psMetadataLookup(defaults, name); // The concept, or NULL
+    psTrace("psModules.concepts", 10, "%s: %p\n", name, item);
+    if (item && item->type == PS_DATA_METADATA) {
+        // This is a menu
+        psTrace("psModules.concepts", 5, "%s is of type METADATA.\n", name);
+        item = p_pmConceptsDepend(name, item->data.md, defaults, fpa, chip, cell);
+    }
+    return item;
+}
+
+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 = p_pmConceptsReadSingleFromDefaults(name, defaults, fpa, chip, cell);
+        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"); // 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
+
+
+        psTrace ("psModules.concepts", 3, "reading concept: %s\n", name);
+        if (!strcmp (name, "CELL.XPARITY")) {
+            psTrace ("psModules.concepts", 3, "parsing CELL.XPARITY: %s\n", name);
+        }
+        if (!strcmp (name, "CELL.TRIMSEC")) {
+            psTrace ("psModules.concepts", 3, "parsing CELL.TRIMSEC: %s\n", name);
+        }
+
+        // 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);
+                    }
+                    // if (!headerItem) {
+                    // psWarning("Unable to find concept %s claimed to be in header as %s", name, conceptItem->data.str);
+                    // }
+                    psMemIncrRefCounter(headerItem);
+                }
+                // Leave the error handling to pmConceptsFromCamera, which should already have been called
+            }
+        }
+        if (!headerItem) {
+            psMetadataItem *formatItem = psMetadataLookup(transSpec, name); // Item with keyword
+            if (!formatItem) {
+                continue;
+            }
+            if (formatItem->type == PS_DATA_METADATA) {
+                // This is a menu
+                psTrace("psModules.concepts", 5, "%s is of type METADATA.\n", name);
+                formatItem = p_pmConceptsDepend(name, formatItem->data.md, transSpec, fpa, chip, cell);
+                if (!formatItem) {
+                    continue;
+                }
+            }
+            if (formatItem->type != PS_DATA_STRING) {
+                psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Type for concept %s in TRANSLATION is not STR",
+                        name);
+                psFree(specsIter);
+                return false;
+            }
+            psString keywords = formatItem->data.str; // The FITS keywords
+            // 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;
+}
+
+psMetadataItem *p_pmConceptsReadSingleFromDatabase(const char *name, const psMetadata *database,
+                                                   pmConfig *config, const pmFPA *fpa, const pmChip *chip,
+                                                   const pmCell *cell)
+{
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    psMetadataItem *item = psMetadataLookup(database, name); // Item to return
+    if (item && item->type == PS_DATA_METADATA) {
+        // This is a menu
+        psTrace("psModules.concepts", 5, "%s is of type METADATA.\n", name);
+        item = p_pmConceptsDepend(name, item->data.md, database, fpa, chip, cell);
+    }
+    if (!item) {
+        return NULL;
+    }
+    if (item->type != PS_DATA_STRING) {
+        psWarning("%s in DATABASE in camera format is not of type STR --- ignored.", name);
+        return NULL;
+    }
+
+#ifndef HAVE_PSDB
+    psError(PS_ERR_UNKNOWN, false,
+            "Cannot read concept: psModules was compiled without database support.");
+    return NULL;
+#else
+
+    psDB *db = pmConfigDB(config);      // Database handle
+    if (!db) {
+        psErrorClear();
+        psWarning("Unable to initialise database to write concepts.");
+        return NULL;
+    }
+
+    psString sql = pmConceptsInterpolate(item->data.str, fpa, chip, cell);
+    if (!p_psDBRunQuery(config->database, sql)) {
+        psWarning("Unable to query database for concept %s --- ignored.", name);
+        psFree(sql);
+        return NULL;
+    }
+    psFree(sql);
+
+    psArray *rows = p_psDBFetchResult(config->database); // Rows returned from the query
+    if (rows->n == 0) {
+        psWarning("No rows returned from database query for concept %s --- ignored.", name);
+	psFree(rows);
+        return NULL;
+    }
+    if (rows->n > 1) {
+        psWarning("Multiple rows returned from database query for concept %s --- using the first", name);
+    }
+    psMetadata *row = rows->data[0]; // First (and only) row
+    if (row->list->n > 1) {
+        psWarning("Multiple columns returned from database query for concept %s --- using the first", name);
+    }
+
+    psMetadataItem *concept = psMetadataGet(row, PS_LIST_HEAD); // Item of interest
+
+    psFree(rows);
+
+    return concept;
+#endif // HAVE_PSDB
+}
+
+bool p_pmConceptsReadFromDatabase(psMetadata *target, const psMetadata *specs,
+                                  const pmFPA *fpa, const pmChip *chip, const pmCell *cell, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(specs, false);
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(config, 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
+        psMetadataItem *conceptItem = p_pmConceptsReadSingleFromDatabase(name, dbSpec, config,
+                                                                         fpa, chip, cell);
+        if (conceptItem && !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;
+        }
+    } // Iterating through the concept specifications
+    psFree(specsIter);
+
+    return status;
+#endif
+}
+
+
+
+// Read all registered concepts for the specified level
+static bool conceptsRead(psMetadata **specs, // One of the concepts specifications
+                         pmFPA *fpa,    // The FPA
+                         pmChip *chip,  // The chip
+                         pmCell *cell, // The cell
+                         unsigned int *read,     // What's already been read
+                         pmConceptSource source, // The source of the concepts to read
+                         pmConfig *config, // Configuration
+                         psMetadata *target // Place into which to read the concepts
+                        )
+{
+    assert(specs);
+    assert(read);
+    assert(target);
+
+    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
+
+    if (cell && (cell->conceptsRead == PM_CONCEPT_SOURCE_NONE)) {
+        pmConceptsBlankCell(cell);
+        cell->conceptsRead = PM_CONCEPT_SOURCE_BLANK;
+    }
+    if (chip && (chip->conceptsRead == PM_CONCEPT_SOURCE_NONE)) {
+        pmConceptsBlankChip(chip);
+        chip->conceptsRead = PM_CONCEPT_SOURCE_BLANK;
+    }
+    if (fpa && (fpa->conceptsRead == PM_CONCEPT_SOURCE_NONE)) {
+        pmConceptsBlankFPA(fpa);
+        fpa->conceptsRead = PM_CONCEPT_SOURCE_BLANK;
+    }
+
+    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, config)) {
+            *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;
+}
+
+
+
+
+bool pmConceptsRead(pmFPA *fpa, pmChip *chip, pmCell *cell, pmConceptSource source, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    psMetadata *conceptsFPA = pmConceptsSpecs(PM_FPA_LEVEL_FPA); // Concept specifications
+    bool success = conceptsRead(&conceptsFPA, fpa, chip, cell, &fpa->conceptsRead, source,
+                                config, fpa->concepts);
+    if (chip) {
+        psMetadata *conceptsChip = pmConceptsSpecs(PM_FPA_LEVEL_CHIP); // Concept specifications
+        success &= conceptsRead(&conceptsChip, fpa, chip, cell, &chip->conceptsRead, source,
+                                config, chip->concepts);
+    }
+    if (cell) {
+        psMetadata *conceptsCell = pmConceptsSpecs(PM_FPA_LEVEL_CELL); // Concept specifications
+        success &= conceptsRead(&conceptsCell, fpa, chip, cell, &cell->conceptsRead, source,
+                                config, cell->concepts);
+    }
+
+    return success;
+}
+
+
+
+bool pmConceptsReadFPA(pmFPA *fpa, pmConceptSource source, bool propagateDown, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    psMetadata *conceptsFPA = pmConceptsSpecs(PM_FPA_LEVEL_FPA); // Concept specifications
+    psTrace("psModules.concepts", 5, "Reading FPA concepts: %p %p\n", conceptsFPA, fpa->concepts);
+    bool success = conceptsRead(&conceptsFPA, fpa, NULL, NULL, &fpa->conceptsRead, source,
+                                config, 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, config);
+            }
+        }
+    }
+
+    return success;
+}
+
+
+
+bool pmConceptsReadChip(pmChip *chip, pmConceptSource source, bool propagateUp,
+                        bool propagateDown, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    psMetadata *conceptsChip = pmConceptsSpecs(PM_FPA_LEVEL_CHIP); // Concept specifications
+    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, config,
+                                chip->concepts);
+    if (propagateUp) {
+        psMetadata *conceptsFPA = pmConceptsSpecs(PM_FPA_LEVEL_FPA); // Concept specifications
+        success &= conceptsRead(&conceptsFPA, fpa, chip, NULL, &fpa->conceptsRead, source,
+                                config, 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, config);
+            }
+        }
+    }
+    return success;
+}
+
+
+bool pmConceptsReadCell(pmCell *cell, pmConceptSource source, bool propagateUp, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    psMetadata *conceptsCell = pmConceptsSpecs(PM_FPA_LEVEL_CELL); // Concept specifications
+    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, config,
+                                cell->concepts);
+    if (propagateUp) {
+        psMetadata *conceptsChip = pmConceptsSpecs(PM_FPA_LEVEL_CHIP); // Concept specifications
+        success &= conceptsRead(&conceptsChip, fpa, chip, cell, &chip->conceptsRead, source, config,
+                                chip->concepts);
+        psMetadata *conceptsFPA = pmConceptsSpecs(PM_FPA_LEVEL_FPA); // Concept specifications
+
+        success &= conceptsRead(&conceptsFPA, fpa, chip, cell, &fpa->conceptsRead, source, config,
+                                fpa->concepts);
+    }
+
+    return success;
+}
Index: /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsRead.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsRead.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsRead.h	(revision 42651)
@@ -0,0 +1,134 @@
+/* @file  pmConceptsRead.h
+ * @brief Reading concepts from a variety of sources.
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.8 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-06-17 22:16:38 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONCEPTS_READ_H
+#define PM_CONCEPTS_READ_H
+
+#include <pslib.h>
+#include <pmFPA.h>
+#include <pmConfig.h>
+#include <pmConcepts.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 a single concept from the DEFAULTS in the camera format
+///
+/// The returned item is NOT parsed, but any interpolation for DEPEND is done.
+psMetadataItem *p_pmConceptsReadSingleFromDefaults(
+    const char *name,                   ///< Name of concept
+    const psMetadata *defaults,         ///< DEFAULTS specifications
+    const pmFPA *fpa,                   ///< The FPA
+    const pmChip *chip,                 ///< The chip, or NULL
+    const pmCell *cell                  ///< The cell, or NULL
+    );
+
+/// 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 a single concept from the DATABASE specification in the camera format
+///
+/// The returned item is NOT parsed, but any interpolation for DEPEND is done.
+psMetadataItem *p_pmConceptsReadSingleFromDatabase(
+    const char *name,                   ///< Name of concept
+    const psMetadata *database,         ///< DATABASE specification
+    pmConfig *config,                   ///< Configuration
+    const pmFPA *fpa,                   ///< The FPA
+    const pmChip *chip,                 ///< The chip, or NULL
+    const pmCell *cell                  ///< The cell, or NULL
+    );
+
+/// 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.
+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
+                                  pmConfig *config // Configuration
+                                 );
+
+/// 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
+                    pmConfig *config    ///< Configuration
+                   );
+
+/// 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?
+                       pmConfig *config         ///< Configuration
+                      );
+
+/// 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?
+                        pmConfig *config        ///< Configuration
+                       );
+
+/// 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?
+                        pmConfig *config        ///< Configuration
+                       );
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsStandard.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsStandard.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsStandard.c	(revision 42651)
@@ -0,0 +1,1636 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <ctype.h>			// for tolower()
+#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"
+
+// XXX why are these functions not supporting all types (S64, U64 often missing)?
+
+// 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;
+
+
+// Format type for time
+typedef enum {
+  TIME_FORMAT_YYYYMMDD,                 // Date stored in YYYY-MM-DD order (ISO-standard)
+  TIME_FORMAT_DDMMYYYY,                 // Date stored in DD-MM-YYYY order
+  TIME_FORMAT_MMDDYYYY,                 // Date stored in MM-DD-YYYY order
+  TIME_FORMAT_JD,                       // Date stored as JD
+  TIME_FORMAT_MJD,                      // Date stored as MJD
+} conceptTimeFormatType;
+
+// Format for time
+typedef struct {
+    conceptTimeFormatType format;       // Format type for time
+    bool separate;                      // Date and time stored separately?
+    bool pre2000;                       // Year is pre-2000 (two digits only)?
+} conceptTimeFormat;
+
+
+static double defaultCoordScaling(const psMetadataItem *pattern)
+{
+    if (strcmp(pattern->name, "FPA.RA") == 0 || strcmp(pattern->name, "FPA.LATITUDE") == 0) {
+        psWarning("Assuming format for %s is HOURS.\n", pattern->name);
+        return M_PI / 12.0;
+    }
+    if (strcmp(pattern->name, "FPA.DEC") == 0 || strcmp(pattern->name, "FPA.LONGITUDE") == 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;
+}
+
+
+psMetadataItem *p_pmConceptParse_CELL_READNOISE(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);
+    assert(cell);
+
+    float rn = psMetadataItemParseF32(concept); // Read noise
+    if (isfinite(rn)) {
+        bool mdok;                      // 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, "ADU") == 0) {
+                    float gain = psMetadataLookupF32(NULL, cell->concepts, "CELL.GAIN"); // Gain (e/ADU)
+                    if (!isfinite(gain)) {
+                        // Will need to update the readnoise once the gain is available
+                        psMetadataAddBool(cell->concepts, PS_LIST_TAIL, "CELL.READNOISE.UPDATE",
+                                          PS_META_REPLACE,
+                                          "Need to update CELL.READNOISE when the gain is available.", true);
+                    } else {
+                        rn *= gain;
+                    }
+                } else {
+                    psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unrecognised format for CELL.READNOISE: %s",
+                            format);
+                    return NULL;
+                }
+            }
+        }
+    }
+
+    return psMetadataItemAllocF32(pattern->name, pattern->comment, rn);
+}
+
+psMetadataItem *p_pmConceptFormat_CELL_READNOISE(const psMetadataItem *concept,
+                                                 pmConceptSource source,
+                                                 const psMetadata *cameraFormat,
+                                                 const pmFPA *fpa,
+                                                 const pmChip *chip,
+                                                 const pmCell *cell)
+{
+    assert(concept);
+    assert(cell);
+    assert(concept->type == PS_DATA_F32);
+
+    float rn = concept->data.F32;       // Read noise
+    if (isfinite(rn)) {
+        bool mdok;                      // 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, "ADU") == 0) {
+                    if (!psMetadataLookup(cell->concepts, "CELL.READNOISE.UPDATE")) {
+                        // Gain correction has been applied, so we need to reverse it to format
+                        float gain = psMetadataLookupF32(NULL, cell->concepts, "CELL.GAIN"); // Gain (e/ADU)
+                        if (!isfinite(gain)) {
+                            psWarning("CELL.READNOISE is supposed to be in ADU, but CELL.GAIN isn't set -- forcing to 1.0");
+                            gain = 1.0;
+                        }
+                        rn /= gain;
+                    }
+                } else {
+                    psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unrecognised format for CELL.READNOISE: %s",
+                            format);
+                    return NULL;
+                }
+            }
+        }
+    }
+
+    return psMetadataItemAllocF32(concept->name, concept->comment, rn);
+}
+
+
+// TELTEMPS : parse a list of the form 'X1 X2 X3 X4 X5 ...' : for now use median
+psMetadataItem *p_pmConceptParse_TELTEMPS(const psMetadataItem *concept,
+                                          const psMetadataItem *pattern,
+                                          pmConceptSource source,
+                                          const psMetadata *cameraFormat,
+                                          const pmFPA *fpa,
+                                          const pmChip *chip,
+                                          const pmCell *cell)
+{
+    assert(concept);
+    assert(pattern);
+    double value = NAN;
+    switch (concept->type) {
+      case PS_TYPE_F32:
+        value = concept->data.F32;
+        break;
+      case PS_TYPE_F64:
+        value = concept->data.F64;
+        break;
+      case PS_DATA_STRING: {
+          // parse the list of values into an array of substrings
+          psArray *strValues = psStringSplitArray (concept->data.V, " ,;", false);
+          assert (strValues);
+
+          // convert the substrings into a vector
+          psVector *fltValues = psVectorAlloc (strValues->n, PS_DATA_F32);
+          for (int i = 0; i < strValues->n; i++) {
+              fltValues->data.F32[i] = atof(strValues->data[i]);
+          }
+
+          // take the (for now) MEDIAN of the data
+          psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN);
+
+          if (!psVectorStats (stats, fltValues, NULL, NULL, 0)) {
+              psAbort ("how can this stats function fail?");
+          }
+
+          value = stats->sampleMedian;
+          psFree (stats);
+          psFree (fltValues);
+          psFree (strValues);
+          break;
+      }
+
+      default:
+        psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Invalid type for %s (%x)\n", pattern->name, concept->type);
+        return NULL;
+    }
+
+    psMetadataItem *item = psMetadataItemAllocF32(pattern->name, pattern->comment, value);
+    return (item);
+}
+
+// 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;
+    }
+    if (!concept->data.str || strlen(concept->data.str) == 0) {
+        return psMetadataItemAllocStr(pattern->name, pattern->comment, "");
+    }
+
+    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;
+    }
+    if (!concept->data.str || strlen(concept->data.str) == 0) {
+        return psMetadataItemAllocStr(concept->name, concept->comment, "");
+    }
+
+    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.OBSTYPE
+// convert concept->data.str to new value 
+psMetadataItem *p_pmConceptParse_FPA_OBSTYPE(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;
+    }
+    if (!concept->data.str || strlen(concept->data.str) == 0) {
+        return psMetadataItemAllocStr(pattern->name, pattern->comment, "");
+    }
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *table = psMetadataLookupMetadata(&mdok, fpa->camera, "OBSTYPE.TABLE");
+    if (!mdok || !table) {
+	// if the table is not defined, pass the supplied value unmodified
+        return psMetadataItemAllocStr(pattern->name, pattern->comment, concept->data.str);
+    }
+
+    // the metadata is in the format (external) STR (internal) 
+    // do a lookup to get the internal name
+    char *extname = psStringCopy (concept->data.str);
+    for (int i = 0; i < strlen(extname); i++) {
+	extname[i] = tolower(extname[i]);
+    }
+    char *name = psMetadataLookupStr (&mdok, table, extname);
+    psFree(extname);
+    if (!name) {
+	// if the entry is not defined, pass the supplied value unmodified
+        return psMetadataItemAllocStr(pattern->name, pattern->comment, concept->data.str);
+    }
+
+    return psMetadataItemAllocStr(pattern->name, pattern->comment, name);
+}
+
+// convert concept->data.str to new value 
+psMetadataItem *p_pmConceptFormat_FPA_OBSTYPE(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;
+    }
+    if (!concept->data.str || strlen(concept->data.str) == 0) {
+        return psMetadataItemAllocStr(concept->name, concept->comment, "");
+    }
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *table = psMetadataLookupMetadata(&mdok, fpa->camera, "OBSTYPE.TABLE");
+    if (!mdok || !table) {
+	// if the table is not defined, pass the supplied value unmodified
+        return psMetadataItemAllocStr(concept->name, concept->comment, concept->data.str);
+    }
+
+    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)
+    // do a reverse lookup to get the internal name
+    psMetadataIterator *iter = psMetadataIteratorAlloc(table, 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 OBSTYPE.TABLE in camera configuration is not STR\n", item->name, item->type);
+            continue;
+        }
+        if (strcmp(item->data.str, key) == 0) {
+            name = item->name;
+            break;
+        }
+    }
+    psFree(iter);
+
+    if (!name) {
+	return psMetadataItemAllocStr(concept->name, concept->comment, key);
+    }
+    return psMetadataItemAllocStr(concept->name, concept->comment, name);
+}
+
+// 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;
+            bool negative =  *((char *)concept->data.V) == '-'; // check for sign explicitly since -0 is not less than 0
+
+            // 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 (negative)
+            {
+                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");
+    bool sexagesimal = false;           // Write sexagesimal format?
+    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);
+        }
+
+        if (strcmp(concept->name, "FPA.RA") == 0 || strcmp(concept->name, "FPA.DEC") == 0) {
+            psString ra = psMetadataLookupStr(&mdok, formats, "FPA.RA"); // Format for RA
+            psString dec = psMetadataLookupStr(&mdok, formats, "FPA.DEC"); // Format for Dec
+            if (ra && strcasecmp(ra, "HOURS") == 0 && dec && strcasecmp(dec, "DEGREES") == 0) {
+                sexagesimal = true;
+            }
+        }
+    } else {
+        coords /= defaultCoordScaling(concept);
+    }
+
+    psMetadataItem *coordItem = NULL;   // Item with coordinates, to return
+    if (sexagesimal) {
+        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;
+        small = (float)((int)(small * 1000.0)) / 1000.0;
+        psString coordString = NULL;        // String with the coordinates in sexagesimal format
+        psStringAppend(&coordString, "%s%02d:%02d:%06.3f",
+                       negative ? "-" : (strcmp(concept->name, "FPA.DEC") == 0 ? "+" : ""),
+                       big, medium, small);
+        coordItem = psMetadataItemAllocStr(concept->name, concept->comment, coordString);
+        psFree(coordString);
+    } else {
+        coordItem = psMetadataItemAllocF64(concept->name, concept->comment, coords);
+    }
+
+    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", PS_META_REPLACE,
+                          "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 = NULL; // 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, U64);
+        TYPE_CASE(binning, concept, S8);
+        TYPE_CASE(binning, concept, S16);
+        TYPE_CASE(binning, concept, S32);
+        // TYPE_CASE(binning, concept, S64);
+      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);
+}
+
+// VIDEOCELLS
+psMetadataItem *p_pmConceptParse_VideoCell(const psMetadataItem *concept,
+					  const psMetadataItem *pattern,
+					  pmConceptSource source,
+					  const psMetadata *cameraFormat,
+					  const pmFPA *fpa,
+					  const pmChip *chip,
+					  const pmCell *cell)
+{
+  assert(concept);
+  assert(pattern);
+  bool has_video_cell = false;
+
+  if (concept->type == PS_DATA_BOOL) {
+    has_video_cell = concept->data.B;
+  } else { 
+    if (concept->type != PS_DATA_STRING) {
+        psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Type for %s (%x) is not string\n",
+                concept->name, concept->type);
+        return NULL;
+      }
+
+      char *Vptr = strchr(concept->data.V,'V');
+      if (Vptr) {
+        has_video_cell = true;
+      }
+  }
+
+  return psMetadataItemAllocBool(pattern->name, pattern->comment, has_video_cell);
+}
+  
+   
+
+// BTOOLAPP
+psMetadataItem *p_pmConceptParse_BTOOLAPP(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 bt_status = 0;
+
+  if (concept->type != PS_DATA_BOOL) {
+    psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Type for %s (%x) is not BOOL\n",
+	    concept->name, concept->type);
+    if (concept->type != PS_DATA_S32) {
+      psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Wasn't the type I'd guessed either.\n");
+      return NULL;
+    }
+    psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Looks like an S32 value? (%d)\n",
+	    concept->data.S32);
+
+    if (concept->data.S32 == 0) {
+      bt_status = 1;
+    }
+    else if (concept->data.S32 == 1) {
+      bt_status = -2;
+    }
+  }
+
+  if (concept->data.B == true) {
+    bt_status = -2;
+  }
+  else if (concept->data.B == false) {
+    bt_status = 1 ;
+  }
+  
+  return psMetadataItemAllocS32(concept->name, concept->comment, bt_status);
+}  
+psMetadataItem *p_pmConceptFormat_BTOOLAPP(const psMetadataItem *concept,
+					   pmConceptSource source,
+					   const psMetadata *cameraFormat,
+					   const pmFPA *fpa,
+					   const pmChip *chip,
+					   const pmCell *cell)
+{
+  assert(concept);
+
+  if (concept->type != PS_DATA_S32) {
+    return NULL;
+  }
+
+  if (concept->data.S32 == 0) {
+    return NULL;
+  }
+  else if (concept->data.S32 == -2) {
+    return psMetadataItemAllocBool(concept->name,concept->comment,true);
+  }
+  else if (concept->data.S32 == 1) {
+    return psMetadataItemAllocBool(concept->name,concept->comment,false);
+  }
+  else {
+    return NULL;
+  }
+
+}  
+
+
+// Get the current value of a concept
+static psMetadataItem *conceptGet(const pmFPA *fpa, // FPA of interest
+                                  const pmChip *chip, // Chip of interest, or NULL
+                                  const pmCell *cell, // Cell of interest, or NULL
+                                  const char *name // Concept name
+    )
+{
+    psMetadataItem *item = NULL;        // Item with time system
+    if (cell) {
+        item = psMetadataLookup(cell->concepts, name);
+    }
+    if (!item && chip) {
+        item = psMetadataLookup(chip->concepts, name);
+    }
+    if (!item && fpa) {
+        item = psMetadataLookup(fpa->concepts, name);
+    }
+    if (!item) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find %s in concepts", name);
+        return NULL;
+    }
+    return item;
+}
+
+static conceptTimeFormat conceptGetTimeFormat(const char *name, // Concept name ("CELL.TIME" or "FPA.TIME")
+                                              const psMetadata *cameraFormat // Camera format
+    )
+{
+    conceptTimeFormat time;               // Time format, to return
+    time.format = TIME_FORMAT_YYYYMMDD;
+    time.separate = false;
+    time.pre2000 = false;
+
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *formats = psMetadataLookupMetadata(&mdok, cameraFormat, "FORMATS"); // The formats
+    if (mdok && formats) {
+        psString format = psMetadataLookupStr(&mdok, formats, 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) {
+                    time.separate = true;
+                } else if (strcasecmp(format, "YYYYMMDD") == 0) {
+                    time.format = TIME_FORMAT_YYYYMMDD;
+                } else if (strcasecmp(format, "MMDDYYYY") == 0) {
+                    time.format = TIME_FORMAT_MMDDYYYY;
+                } else if (strcasecmp(format, "DDMMYYYY") == 0) {
+                    time.format = TIME_FORMAT_DDMMYYYY;
+                } else if (strcasecmp(format, "ISO") == 0) {
+                    time.format = TIME_FORMAT_YYYYMMDD;
+                } else if (strcasecmp(format, "YEAR.FIRST") == 0) {
+                    time.format = TIME_FORMAT_YYYYMMDD;
+                } else if (strcasecmp(format, "USA") == 0) {
+                    time.format = TIME_FORMAT_MMDDYYYY;
+                } else if (strcasecmp(format, "BACKWARDS") == 0) {
+                    time.format = TIME_FORMAT_DDMMYYYY;
+                } else if (strcasecmp(format, "PRE2000") == 0) {
+                    time.pre2000 = true;
+                } else if (strcasecmp(format, "MJD") == 0) {
+                    time.format = TIME_FORMAT_MJD;
+                } else if (strcasecmp(format, "JD") == 0) {
+                    time.format = TIME_FORMAT_JD;
+                } else {
+                    psWarning("Unrecognised FORMATS option for %s: %s --- ignored.", name, format);
+                }
+            }
+            psFree(formatListIter);
+            psFree(formatList);
+        }
+    }
+
+    return time;
+}
+
+// Determine the corresponding TIMESYS for one of the TIME concepts
+static psTimeType conceptGetTimesysForTime(const char *name, // Concept name ("CELL.TIME" or "FPA.TIME")
+                                           const pmFPA *fpa, // FPA of interest
+                                           const pmChip *chip, // Chip of interest, or NULL
+                                           const pmCell *cell // Cell of interest, or NULL
+                                           )
+{
+    assert(name);
+
+    psString timesysName = psStringCopy(name); // e.g., "CELL.TIME" --> "CELL.TIMESYS"
+    psStringSubstitute(&timesysName, "TIMESYS", "TIME");
+    psMetadataItem *item = conceptGet(fpa, chip, cell, timesysName); // Time system
+    psFree(timesysName);
+
+    if (!item || item->type != PS_DATA_S32) {
+        psWarning("Unable to find %s in format file --- assuming UTC", timesysName);
+        return PS_TIME_UTC;
+    }
+    return item->data.S32;
+}
+
+// Set the corresponding TIMESYS for one of the TIME concepts
+static bool conceptSetTimesysForTime(const char *name, // Concept name ("CELL.TIME" or "FPA.TIME")
+                                     const pmFPA *fpa, // FPA of interest
+                                     const pmChip *chip, // Chip of interest, or NULL
+                                     const pmCell *cell, // Cell of interest, or NULL
+                                     psTimeType timeSys // The time system value
+                                     )
+{
+    assert(name);
+
+    psString timesysName = psStringCopy(name); // e.g., "CELL.TIME" --> "CELL.TIMESYS"
+    psStringSubstitute(&timesysName, "TIMESYS", "TIME");
+    psMetadataItem *item = conceptGet(fpa, chip, cell, timesysName); // Time system
+    psFree(timesysName);
+
+    if (!item) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find %s in concepts when setting %s\n",
+                timesysName, name);
+        return false;
+    }
+
+    if (item->data.S32 != -1 && item->data.S32 != timeSys) {
+        psWarning("Time system is set to %x; but should be %x", item->data.S32, timeSys);
+    }
+
+    item->data.S32 = timeSys;
+
+    return true;
+}
+
+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) {
+        // XXX is this too low verbosity?
+        psWarning("Can't interpret %s --- assuming UTC (other options: TAI, UT1, TT).", 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 {
+        // XXX is this too low verbosity?
+        psWarning("Can't interpret %s --- assuming UTC (other options: TAI, UT1, TT).", pattern->name);
+    }
+
+    psMetadataItem *old = conceptGet(fpa, chip, cell, pattern->name); // Old value
+    if (old && old->data.S32 != -1 && old->data.S32 != timeSys) {
+        psWarning("%s is already set (%x) and not consistent with new value (%x)",
+                  pattern->name, old->data.S32, timeSys);
+    }
+
+    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);
+
+    psTimeType timeSys = conceptGetTimesysForTime(pattern->name, fpa, chip, cell); // Time system
+
+    conceptTimeFormat timeFormat = conceptGetTimeFormat(pattern->name, cameraFormat); // Format for time
+
+    psTime *time = NULL;                // The time
+    switch (concept->type) {
+      case PS_DATA_LIST: {
+          if (!timeFormat.separate) {
+              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;
+          }
+          switch (timeFormat.format) {
+            case TIME_FORMAT_DDMMYYYY: {
+                // Need to switch days and years
+                int temp = day;
+                day = year;
+                year = temp;
+                break;
+            }
+            case TIME_FORMAT_MMDDYYYY: {
+                // Need to switch everything around.... Yanks!
+                int temp = day;
+                day = month;
+                month = year;
+                year = temp;
+                break;
+            }
+            default:
+              break;
+          }
+          if (year < 100) {
+              if (timeFormat.pre2000) {
+                  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
+          switch (timeFormat.format) {
+            case TIME_FORMAT_JD: {
+                double timeValue = strtod (timeString, NULL);
+                time = psTimeFromJD(timeValue);
+                break;
+            }
+            case TIME_FORMAT_MJD: {
+                double timeValue = strtod (timeString, NULL);
+                time = psTimeFromMJD(timeValue);
+                break;
+            }
+            case TIME_FORMAT_YYYYMMDD: {
+                // this is ISO-standard
+                time = psTimeFromISO(timeString, timeSys);
+                break;
+            }
+            default:
+              psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unable to interpret time string: %s", timeString);
+              return NULL;
+          }
+          break;
+      }
+      case PS_TYPE_F32: {
+          double timeValue = (double)concept->data.F32;
+          switch (timeFormat.format) {
+            case TIME_FORMAT_JD:
+              time = psTimeFromJD(timeValue);
+              break;
+            case TIME_FORMAT_MJD:
+              time = psTimeFromMJD(timeValue);
+              break;
+            default:
+              psError(PS_ERR_UNKNOWN, true, "Unable to interpret time %s (%f)", pattern->name, timeValue);
+              return NULL;
+          }
+          break;
+      }
+      case PS_TYPE_F64: {
+          double timeValue = (double)concept->data.F64;
+          switch (timeFormat.format) {
+            case TIME_FORMAT_JD:
+              time = psTimeFromJD(timeValue);
+              break;
+            case TIME_FORMAT_MJD:
+              time = psTimeFromMJD(timeValue);
+              break;
+            default:
+              psError(PS_ERR_UNKNOWN, true, "Unable to interpret time %s (%f)", pattern->name, timeValue);
+              return NULL;
+          }
+          break;
+      }
+      default:
+        psError(PS_ERR_UNKNOWN, true, "Unable to parse %s.\n", pattern->name);
+        return NULL;
+    }
+
+    if (!time) {
+        psError(PS_ERR_BAD_PARAMETER_SIZE, false, "Unable to parse time for %s", pattern->name);
+        return NULL;
+    }
+
+    // Set the time system appropriately
+    switch (timeFormat.format) {
+      case TIME_FORMAT_JD:
+      case TIME_FORMAT_MJD:
+        conceptSetTimesysForTime(pattern->name, fpa, chip, cell, PS_TIME_TAI);
+        break;
+      default:
+        time->type = timeSys;
+        break;
+    }
+
+    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_DATA_F32 && concept->data.F32 - (int)concept->data.F32 == 0) {
+            offset = concept->data.F32;
+        } else if (concept->type == PS_DATA_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 timeName = psStringCopy(concept->name); // Name of corresponding TIME concept
+    psStringSubstitute(&timeName, "TIME", "TIMESYS");
+
+    conceptTimeFormat timeFormat = conceptGetTimeFormat(timeName, cameraFormat); // Format for time
+    psFree(timeName);
+
+    psTimeType timesys = concept->data.S32; // Time system
+
+    // JD and MJD are converted to TAI before writing
+    switch (timeFormat.format) {
+      case TIME_FORMAT_JD:
+      case TIME_FORMAT_MJD:
+        timesys = PS_TIME_TAI;
+        break;
+      default:
+        break;
+    }
+
+    psString sys = NULL;            // String to store
+    switch (timesys) {
+      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)
+{
+    psAssert (concept->type == PS_DATA_TIME, "programming error: concept not supplied with psTime pointer\n");
+    psTime *time = concept->data.V;     // The time
+
+    psTimeType timeSys = conceptGetTimesysForTime(concept->name, fpa, chip, cell); // Time system
+    psTimeConvert(time, timeSys);
+
+    conceptTimeFormat timeFormat = conceptGetTimeFormat(concept->name, cameraFormat); // Format for time
+
+    if (timeFormat.separate) {
+        // 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 (timeFormat.pre2000) {
+            psError(PS_ERR_UNKNOWN, true, "Don't you realise it's the twenty-first century?\n");
+            return NULL;
+        }
+
+        switch (timeFormat.format) {
+          case TIME_FORMAT_DDMMYYYY: {
+              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
+              break;
+          }
+          case TIME_FORMAT_MMDDYYYY: {
+              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
+              break;
+          }
+          default:
+            break;
+        }
+
+        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;
+    }
+
+    switch (timeFormat.format) {
+      case TIME_FORMAT_JD: {
+          double jd = psTimeToMJD(time);
+          return psMetadataItemAllocF64(concept->name, concept->comment, jd);
+      }
+      case TIME_FORMAT_MJD: {
+          double mjd = psTimeToMJD(time);
+          return psMetadataItemAllocF64(concept->name, concept->comment, mjd);
+      }
+      default: {
+          // If we've gotten this far, it's straight ISO.
+          psString dateTimeString = psTimeToISO(time); // String representation
+          psMetadataItem *item = psMetadataItemAllocStr(concept->name, concept->comment, dateTimeString);
+          psFree(dateTimeString);
+          return item;
+      }
+    }
+
+    return NULL;
+}
+
+
+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_DATA_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);
+}
+
+
+psMetadataItem *p_pmConceptCopy_TIMESYS(const psMetadataItem *target,
+                                        const psMetadataItem *source,
+                                        const psMetadata *cameraFormat,
+                                        const pmFPA *fpa,
+                                        const pmChip *chip,
+                                        const pmCell *cell)
+{
+    if (!target || target->data.S32 == -1) {
+        // Replace
+        return psMetadataItemCopy(source);
+    }
+    // Keep what we've got --- it's been mandated by use of the DEFAULTS
+    return psMemIncrRefCounter((psMetadataItem*)target);
+}
Index: /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsStandard.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsStandard.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsStandard.h	(revision 42651)
@@ -0,0 +1,294 @@
+/* @file  pmConceptsStandard.h
+ * @brief Private functions for parsing and formatting standard concepts.
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-10-13 21:20:36 $
+ * 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 CELL.READNOISE concept to do ADU->e correction if required
+psMetadataItem *p_pmConceptParse_CELL_READNOISE(
+    const psMetadataItem *concept,
+    const psMetadataItem *pattern,
+    pmConceptSource source,
+    const psMetadata *cameraFormat,
+    const pmFPA *fpa,
+    const pmChip *chip,
+    const pmCell *cell
+);
+
+/// Format the CELL.READNOISE concept to do e->ADU correction if required
+psMetadataItem *p_pmConceptFormat_CELL_READNOISE(
+    const psMetadataItem *concept,
+    pmConceptSource source,
+    const psMetadata *cameraFormat,
+    const pmFPA *fpa,
+    const pmChip *chip,
+    const pmCell *cell
+    );
+
+// Parse the TELTEMPS concept : parse a list of the form 'X1 X2 X3 X4 X5 ...' : for now use median
+psMetadataItem *p_pmConceptParse_TELTEMPS(
+    const psMetadataItem *concept,
+    const psMetadataItem *pattern,
+    pmConceptSource source,
+    const psMetadata *cameraFormat,
+    const pmFPA *fpa,
+    const pmChip *chip,
+    const pmCell *cell
+    );
+
+/// 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 FPA.OBSTYPE concept to apply a lookup table
+psMetadataItem *p_pmConceptParse_FPA_OBSTYPE(
+    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.OBSTYPE concept to (reverse-)apply a lookup table
+psMetadataItem *p_pmConceptFormat_FPA_OBSTYPE(
+    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 CHIP.VIDEOCELL concept
+psMetadataItem *p_pmConceptParse_VideoCell(
+   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 for the BTOOLAPP conceptn
+psMetadataItem *p_pmConceptParse_BTOOLAPP(
+    const psMetadataItem *concept, ///< Concept to format
+    const psMetadataItem *pattern,
+    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
+    );
+psMetadataItem *p_pmConceptFormat_BTOOLAPP(
+    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 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
+    );
+
+/// Copy the time system only if not already set
+psMetadataItem *p_pmConceptCopy_TIMESYS(
+    const psMetadataItem *target,
+    const psMetadataItem *source,
+    const psMetadata *cameraFormat,
+    const pmFPA *fpa,
+    const pmChip *chip,
+    const pmCell *cell
+    );
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsUpdate.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsUpdate.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsUpdate.c	(revision 42651)
@@ -0,0 +1,101 @@
+#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
+
+        // CELL.READNOISE needs to be updated if specified in ADU
+        if (psMetadataLookup(cell->concepts, "CELL.READNOISE.UPDATE")) {
+            float gain = psMetadataLookupF32(NULL, cell->concepts, "CELL.GAIN"); // Gain for cell
+            if (isfinite(gain)) {
+                psMetadataItem *rn = psMetadataLookup(cell->concepts, "CELL.READNOISE"); // Read noise
+                psAssert(rn && rn->type == PS_DATA_F32, "Should be of the correct type");
+                rn->data.F32 *= gain;
+                psMetadataRemoveKey(cell->concepts, "CELL.READNOISE.UPDATE");
+            }
+        }
+
+        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_branches/psModules.20240412/src/concepts/pmConceptsUpdate.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsUpdate.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsUpdate.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/concepts/pmConceptsWrite.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsWrite.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsWrite.c	(revision 42651)
@@ -0,0 +1,660 @@
+#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 "pmConfig.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmHDUUtils.h"
+#include "pmConcepts.h"
+#include "pmConceptsRead.h"
+
+#include "pmConceptsWrite.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_BOOL:
+        psTrace("psModules.concepts", 9, "Writing header %s: %d\n", keyword, item->data.B);
+        return psMetadataAddBool(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
+                                 item->data.B);
+      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;
+}
+
+// Return the camera format appropriate for a focal plane hierarchy
+static psMetadata *conceptsCameraFormat(const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+{
+    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
+    if (!hdu) {
+        return NULL;
+    }
+    return hdu->format;
+}
+
+// Return the DATABASE metadata from the format
+static psMetadata *conceptsDatabase(const psMetadata *format)
+{
+    bool mdok;                          // Status of MD lookup
+    return psMetadataLookupMetadata(&mdok, format, "DEFAULTS");
+}
+
+// Return the TRANSLATION metadata from the format
+static psMetadata *conceptsTranslation(const psMetadata *format)
+{
+    bool mdok;                          // Status of MD lookup
+    return psMetadataLookupMetadata(&mdok, format, "TRANSLATION");
+}
+
+// Return the DEFAULTS metadata from the format
+static psMetadata *conceptsDefaults(const psMetadata *format)
+{
+    bool mdok;                          // Status of MD lookup
+    return psMetadataLookupMetadata(&mdok, format, "DEFAULTS");
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool p_pmConceptWriteToCells(const pmCell *cell, const pmConceptSpec *spec,
+                             const psMetadataItem *conceptItem, const psMetadata *format)
+{
+    if (!cell) {
+        return false;
+    }
+    if (!cell->config) {
+        return false;
+    }
+
+    if (!format) {
+        format = conceptsCameraFormat(NULL, NULL, cell);
+        if (!format) {
+            return false;
+        }
+    }
+
+    psMetadataItem *cameraItem = psMetadataLookup(cell->config, conceptItem->name); // Version in the config
+    if (!cameraItem) {
+        return false;
+    }
+
+    psString nameSource = NULL; // String with the concept name and ".SOURCE" added
+    psStringAppend(&nameSource, "%s.SOURCE", conceptItem->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.",
+                          conceptItem->name);
+                psFree(nameSource);
+                return false;
+            }
+
+            // Formatted version
+            psMetadataItem *formatted = conceptFormat(spec, conceptItem, PM_CONCEPT_SOURCE_HEADER,
+                                                      format, NULL, NULL, cell);
+            if (!formatted) {
+                psFree(nameSource);
+                return true;
+            }
+
+            psTrace("psModules.concepts", 8, "Writing %s to header %s\n",
+                    conceptItem->name, cameraItem->data.str);
+            pmHDU *hdu = pmHDUGetLowest(NULL, NULL, cell); // Header data unit
+            if (!hdu) {
+                psError(PS_ERR_UNEXPECTED_NULL, false,
+                        "Unable to find HDU to write concept %s", conceptItem->name);
+                return false;
+            }
+            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,
+                                                      format, NULL, NULL, cell);
+            if (!formatted) {
+                psFree(nameSource);
+                return true;
+            }
+
+            psTrace("psModules.concepts", 8, "Checking %s against camera format.\n", conceptItem->name);
+            if (!compareConcepts(formatted, cameraItem)) {
+                psWarning("Concept %s is specified by value in the camera format, but the values don't match",
+                          conceptItem->name);
+            }
+            psFree(formatted);
+        } else {
+            psWarning("Concept source %s isn't HEADER or VALUE --- can't write", nameSource);
+        }
+    } else {
+        // Assume it's specified by value
+        psMetadataItem *formatted = conceptFormat(spec, conceptItem, PM_CONCEPT_SOURCE_CELLS,
+                                                  format, NULL, NULL, cell);
+        if (!formatted) {
+            psFree(nameSource);
+            return true;
+        }
+
+        if (!compareConcepts(formatted, cameraItem)) {
+            psWarning("Concept %s is specified by value in the camera format, but the values don't match.",
+                      conceptItem->name);
+        }
+        psFree(formatted);
+    }
+    psFree(nameSource);
+
+    return true;
+}
+
+bool p_pmConceptWriteToDefaults(const pmFPA *fpa, const pmChip *chip, const pmCell *cell,
+                                const pmConceptSpec *spec, const psMetadataItem *conceptItem,
+                                const psMetadata *format, const psMetadata *defaults)
+{
+    if (!format) {
+        format = conceptsCameraFormat(fpa, chip, cell);
+        if (!format) {
+            return false;
+        }
+    }
+    if (!defaults) {
+        defaults = conceptsDefaults(format);
+        if (!defaults) {
+            return false;
+        }
+    }
+
+    psMetadataItem *defaultItem = p_pmConceptsReadSingleFromDefaults(conceptItem->name, defaults, fpa, chip, cell);
+    if (!defaultItem) {
+        return false;
+    }
+    psMetadataItem *formatted = conceptFormat(spec, conceptItem, PM_CONCEPT_SOURCE_DEFAULTS, format, fpa, chip, cell);
+    if (!formatted) {
+        return true;
+    }
+
+    if (strcmp(defaultItem->name, conceptItem->name) != 0) {
+        // Correct the name to match the concept name
+        defaultItem = psMetadataItemCopy(defaultItem);
+        psFree(defaultItem->name);
+        defaultItem->name = psStringCopy(conceptItem->name);
+    } else {
+        psMemIncrRefCounter(defaultItem);
+    }
+
+    if (!compareConcepts(formatted, defaultItem)) {
+        psWarning("Concept %s is specified by the DEFAULTS in the camera format, but the values don't match.",
+                  conceptItem->name);
+    }
+    psFree(defaultItem);
+    psFree(formatted);
+
+    return true;
+}
+
+
+bool p_pmConceptWriteToHeader(const pmFPA *fpa, const pmChip *chip, const pmCell *cell,
+                              const pmConceptSpec *spec, const psMetadataItem *conceptItem,
+                              const psMetadata *format, const psMetadata *translation)
+{
+    if (!format) {
+        format = conceptsCameraFormat(fpa, chip, cell);
+        if (!format) {
+            return false;
+        }
+    }
+    if (!translation) {
+        translation = conceptsTranslation(format);
+        if (!translation) {
+            return false;
+        }
+    }
+
+    psMetadataItem *headerItem = psMetadataLookup(translation, conceptItem->name); // How to format for header
+    if (!headerItem) {
+        return false;
+    }
+    if (headerItem->type == PS_DATA_METADATA) {
+        // This is a menu
+        psTrace("psModules.concepts", 5, "%s is of type METADATA.\n", conceptItem->name);
+        headerItem = p_pmConceptsDepend(conceptItem->name, headerItem->data.md, translation, fpa, chip, cell);
+        if (!headerItem) {
+            return false;
+        }
+    }
+    if (headerItem->type != PS_DATA_STRING) {
+        psWarning("TRANSLATION keyword for concept %s isn't of type STR --- ignored.", conceptItem->name);
+        return false;
+    }
+    psTrace("psModules.concepts", 3, "Writing %s to header %s\n", conceptItem->name, headerItem->data.str);
+    psMetadataItem *formatted = conceptFormat(spec, conceptItem, PM_CONCEPT_SOURCE_HEADER,
+                                              format, fpa, chip, cell);
+    if (!formatted) {
+        // Found it, but it doesn't need to be written
+        return true;
+    }
+
+    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // HDU to which to write
+    if (!hdu) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find HDU to write concept %s", conceptItem->name);
+        return false;
+    }
+    writeHeader(hdu, headerItem->data.V, formatted);
+    psFree(formatted);
+
+    return true;
+}
+
+bool p_pmConceptWriteToDatabase(const pmFPA *fpa, const pmChip *chip, const pmCell *cell,
+                                pmConfig *config, const pmConceptSpec *spec,
+                                const psMetadataItem *conceptItem, const psMetadata *format,
+                                const psMetadata *database)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+#ifndef HAVE_PSDB
+    return false;
+#else
+
+    if (!format) {
+        format = conceptsCameraFormat(fpa, chip, cell);
+        if (!format) {
+            return false;
+        }
+    }
+    if (!database) {
+        database = conceptsDatabase(format);
+        if (!database) {
+            return false;
+        }
+    }
+
+    psMetadataItem *dbItem = p_pmConceptsReadSingleFromDatabase(conceptItem->name, database, config,
+                                                                fpa, chip, cell); // Database version
+    if (!dbItem) {
+        return false;
+    }
+
+    psMetadataItem *formatted = conceptFormat(spec, conceptItem, PM_CONCEPT_SOURCE_DATABASE,
+                                              format, fpa, chip, cell);
+    if (!formatted) {
+        return false;
+    }
+
+    if (strcmp(dbItem->name, conceptItem->name) != 0) {
+        // Correct the name to match the concept name
+        dbItem = psMetadataItemCopy(dbItem);
+        psFree(dbItem->name);
+        dbItem->name = psStringCopy(conceptItem->name);
+    } else {
+        psMemIncrRefCounter(dbItem);
+    }
+
+    if (!compareConcepts(formatted, dbItem)) {
+        psWarning("Concept %s is specified by the DATABASE in the camera "
+                  "format, but the values don't match.\n", conceptItem->name);
+    }
+    psFree(dbItem);
+    psFree(formatted);
+
+    return true;
+#endif
+}
+
+
+bool pmConceptWriteSingle(const pmFPA *fpa, const pmChip *chip, const pmCell *cell,
+                          pmConfig *config, const psMetadataItem *conceptItem)
+{
+    pmConceptsInit();
+
+    psMetadata *format = conceptsCameraFormat(fpa, chip, cell); // Camera format
+    if (!format) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to retrieve camera format.");
+        return false;
+    }
+
+    const char *name = conceptItem->name; // Name of concept
+
+    psMetadata *conceptsFPA = pmConceptsSpecs(PM_FPA_LEVEL_FPA); // Concept specifications for FPA
+    bool mdok;                          // Status of MD lookup
+    pmConceptSpec *spec = psMetadataLookupPtr(&mdok, conceptsFPA, name); // Concept specification of interest
+    if (!spec) {
+        psMetadata *conceptsChip = pmConceptsSpecs(PM_FPA_LEVEL_CHIP); // Concept specifications for Chip
+        spec = psMetadataLookupPtr(&mdok, conceptsChip, name);
+        if (!spec) {
+            psMetadata *conceptsCell = pmConceptsSpecs(PM_FPA_LEVEL_CELL); // Concept specifications for Cell
+            spec = psMetadataLookupPtr(&mdok, conceptsCell, name);
+            if (!spec) {
+                psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find specification for concept %s", name);
+                return false;
+            }
+        }
+    }
+
+    if (!p_pmConceptWriteToCells(cell, spec, conceptItem, format) &&
+        !p_pmConceptWriteToDefaults(fpa, chip, cell, spec, conceptItem, format, NULL) &&
+        !p_pmConceptWriteToHeader(fpa, chip, cell, spec, conceptItem, format, NULL) &&
+        !p_pmConceptWriteToDatabase(fpa, chip, cell, config, spec, conceptItem, format, NULL)) {
+        return false;
+    }
+    return true;
+}
+
+
+// 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
+                          pmConfig *config, // Configuration
+                          psMetadata *concepts // The concepts to write out
+                         )
+{
+    assert(specs);
+    assert(concepts);
+
+    pmConceptsInit();
+
+    psTrace("psModules.concepts", 3, "Writing concepts (%p %p %p)\n", fpa, chip, cell);
+
+    psMetadata *format = conceptsCameraFormat(fpa, chip, cell); // Camera format
+    if (!format) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to retrieve camera format.");
+        return false;
+    }
+
+    psMetadata *defaults = conceptsDefaults(format); // DEFAULTS configuration
+    psMetadata *translation = conceptsTranslation(format); // TRANSLATION configuration
+    psMetadata *database = conceptsDatabase(format); // DATABASE configuration
+
+    psMetadataIterator *iter = psMetadataIteratorAlloc(*specs, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item = NULL;    // Item from the specs metadata
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        pmConceptSpec *spec = item->data.V; // The specification
+        psString name = item->name; // The concept name
+
+        psMetadataItem *concept = psMetadataLookup(concepts, name); // Concept to write
+
+        if (!p_pmConceptWriteToCells(cell, spec, concept, format) &&
+            !p_pmConceptWriteToDefaults(fpa, chip, cell, spec, concept, format, defaults) &&
+            !p_pmConceptWriteToHeader(fpa, chip, cell, spec, concept, format, translation) &&
+            !p_pmConceptWriteToDatabase(fpa, chip, cell, config, spec, concept, format, database)) {
+            psTrace("psModules.concepts", 1, "Unable to write concept %s to any output", name);
+        }
+    }
+    psFree(iter);
+
+    return true;
+}
+
+
+bool pmConceptsWriteFPA(const pmFPA *fpa, bool propagateDown, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    psMetadata *conceptsFPA = pmConceptsSpecs(PM_FPA_LEVEL_FPA); // Concept specifications
+    psTrace("psModules.concepts", 5, "Writing FPA concepts: %p %p\n", conceptsFPA, fpa->concepts);
+    bool success = conceptsWrite(&conceptsFPA, fpa, NULL, NULL, config, 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 &= pmConceptsWriteChip(chip, false, true, config);
+            }
+        }
+    }
+    return success;
+}
+
+
+bool pmConceptsWriteChip(const pmChip *chip, bool propagateUp, bool propagateDown, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    psMetadata *conceptsChip = pmConceptsSpecs(PM_FPA_LEVEL_CHIP); // Concept specifications
+    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, config, chip->concepts);
+    if (propagateUp) {
+        psMetadata *conceptsFPA = pmConceptsSpecs(PM_FPA_LEVEL_FPA); // Concept specifications
+        success &= conceptsWrite(&conceptsFPA, fpa, chip, NULL, config, 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 &= pmConceptsWriteCell(cell, false, config);
+            }
+        }
+    }
+    return success;
+}
+
+
+bool pmConceptsWriteCell(const pmCell *cell, bool propagateUp, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    psMetadata *conceptsCell = pmConceptsSpecs(PM_FPA_LEVEL_CELL); // Concept specifications
+    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, config, cell->concepts);
+    if (propagateUp) {
+        psMetadata *conceptsChip = pmConceptsSpecs(PM_FPA_LEVEL_CHIP); // Concept specifications
+        success &= conceptsWrite(&conceptsChip, fpa, chip, cell, config, chip->concepts);
+        psMetadata *conceptsFPA = pmConceptsSpecs(PM_FPA_LEVEL_FPA); // Concept specifications
+        success &= conceptsWrite(&conceptsFPA, fpa, chip, cell, config, fpa->concepts);
+    }
+
+    return success;
+}
Index: /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsWrite.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsWrite.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/concepts/pmConceptsWrite.h	(revision 42651)
@@ -0,0 +1,112 @@
+/* @file  pmConceptsWrite.h
+ * @brief Writing concepts to a variety of sources.
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-06-17 22:16:38 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONCEPTS_WRITE_H
+#define PM_CONCEPTS_WRITE_H
+
+#include <pslib.h>
+#include <pmFPA.h>
+#include <pmConfig.h>
+#include <pmConcepts.h>
+
+/// @addtogroup Concepts Data Abstraction Concepts
+/// @{
+
+/// "Write" concept 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
+/// concept as defined there match the ones defined in the cell.  A warning is produced if the concept does
+/// not match.
+bool p_pmConceptWriteToCells(const pmCell *cell, ///< The cell
+                             const pmConceptSpec *spec, ///< Concept specification
+                             const psMetadataItem *conceptItem, ///< Concept to write
+                             const psMetadata *format ///< Camera format, or NULL
+    );
+
+/// "Write" concept to (actually, check against) the camera format file's DEFAULTS.
+///
+/// Examines the DEFAULTS metadata in the camera format file, and checks that the concept as defined there
+/// match the one defined in the cell.  A warning is produced if the concept does not match.
+bool p_pmConceptWriteToDefaults(const pmFPA *fpa, ///< The FPA
+                                const pmChip *chip, ///< The chip
+                                const pmCell *cell, ///< The cell
+                                const pmConceptSpec *spec, ///< Concept specification
+                                const psMetadataItem *conceptItem,      ///< Concept to write
+                                const psMetadata *format, ///< Camera format, or NULL
+                                const psMetadata *defaults ///< DEFAULTS configuration, or NULL
+    );
+
+/// "Write" concept to (actually, add to, pending a later write) the FITS header.
+///
+/// Examines the FITS header TRANSLATION metadata in the camera format file, and writes concept to the
+/// appropriate FITS header(s) in the HDU, in preparation for a future write of the HDU.
+bool p_pmConceptWriteToHeader(const pmFPA *fpa, ///< The FPA
+                              const pmChip *chip, ///< The chip
+                              const pmCell *cell, ///< The cell
+                              const pmConceptSpec *spec, ///< Concept specification
+                              const psMetadataItem *conceptItem, ///< Concept to write
+                              const psMetadata *format, ///< Camera format, or NULL
+                              const psMetadata *translation ///< TRANSLATION configuration, or NULL
+                              );
+
+/// Write concept to the database.
+///
+/// Examines the DATABASE metadata in the camera format file, and writes (actually, check against)
+/// concept to the database.
+bool p_pmConceptWriteToDatabase(const pmFPA *fpa, ///< The FPA
+                                const pmChip *chip, ///< The chip
+                                const pmCell *cell, ///< The cell
+                                pmConfig *config, ///< Configuration
+                                const pmConceptSpec *spec, ///< Concept specification
+                                const psMetadataItem *conceptItem, ///< Concept to write
+                                const psMetadata *format, ///< Camera format, or NULL
+                                const psMetadata *database ///< DATABASE configuration, or NULL
+                                );
+
+
+bool pmConceptWriteSingle(const pmFPA *fpa, ///< The FPA
+                          const pmChip *chip, ///< The chip
+                          const pmCell *cell, ///< The cell
+                          pmConfig *config, ///< Configuration
+                          const psMetadataItem *concept ///< Concept to write
+    );
+
+
+/// 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
+                        bool propagateDown, ///< Propagate to lower levels?
+                        pmConfig *config        ///< Configuration
+                       );
+
+/// 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
+                         bool propagateUp,///< Propagate to higher levels?
+                         bool propagateDown, ///< Propagate to lower levels?
+                         pmConfig *config       ///< Configuration
+                        );
+
+/// 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
+                         bool propagateUp, ///< Propagate to higher levels?
+                         pmConfig *config ///< Configuration
+                        );
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/config/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/Makefile.am	(revision 42651)
@@ -0,0 +1,45 @@
+noinst_LTLIBRARIES = libpsmodulesconfig.la
+
+# Force recompilation of pmVersion.c, since it gets the version information
+pmVersion.c: pmVersionDefinitions.h
+pmVersionDefinitions.h: pmVersionDefinitions.h.in FORCE
+	pslib-setsvnversion.pl PSMODULES pmVersionDefinitions.h.in pmVersionDefinitions.h
+FORCE: ;
+
+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 \
+	pmConfigDump.c \
+	pmConfigRun.c \
+	pmConfigRecipeValue.c \
+	pmVersion.c \
+	pmErrorCodes.c
+
+pkginclude_HEADERS = \
+	pmConfig.h \
+	pmConfigRecipes.h \
+	pmConfigCamera.h \
+	pmConfigCommand.h \
+	pmConfigMask.h \
+	pmConfigDump.h \
+	pmConfigRun.h \
+	pmConfigRecipeValue.h \
+	pmVersion.h \
+	pmErrorCodes.h
+
+# Error codes.
+BUILT_SOURCES = pmErrorCodes.h pmErrorCodes.c pmVersionDefinitions.h
+CLEANFILES = *~ pmErrorCodes.h pmErrorCodes.c pmVersionDefinitions.h
+
+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_branches/psModules.20240412/src/config/notes.txt
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/notes.txt	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/notes.txt	(revision 42651)
@@ -0,0 +1,56 @@
+
+* current recipe load sequence:
+
+** load the basic config information.
+   (unless camera is explicitly specified, camera-specific recipes are not loaded)
+
+** save the generic command-line options (-D ..) on config->arguments:OPTIONS:(recipe)
+
+** supplement recipe with specific command-line options (-isdark...) to config->arguments:OPTIONS:(recipe)
+
+** SYSTEM
+** load all files from config->complete:RECIPES to config->recipes
+   (loadRecipeSystem)
+
+** CAMERA
+** load the recipes for the specified camera from the camera-specific
+   config file to config->recipes (overlay existing metadata)
+
+** CL:arguments
+** load recipe files defined on command line in the form -recipe-file (recipe) (filename)
+   from config->arguments:RECIPES
+   
+** CL:symbols
+** load symbolic recipes defined on command line in the form -recipe (recipe) (symbolic name)
+   from config->recipeSymbols
+
+** CL:options
+** load values defined on command line in the form -D key value
+   from config->arguments:OPTIONS:(recipe)
+
+---
+
+what we really want:
+
+** SYSTEM
+** load all files from config->complete:RECIPES to config->recipes
+   (loadRecipeSystem)
+
+** CAMERA
+** load the recipes for the specified camera from the camera-specific
+   config file to config->recipesCamera
+
+** CL:arguments
+** load recipe files defined on command line in the form -recipe-file (recipe) (filename)
+   from config->arguments:RECIPES
+   
+** CL:symbols
+** load symbolic recipes defined on command line in the form -recipe (recipe) (symbolic name)
+   from config->recipeSymbols
+
+** CL:options
+** load values defined on command line in the form -D key value
+   from config->arguments:OPTIONS:(recipe)
+
+metadata = psMetadataConfigRead (recipes/NAME.config)
+metadata = 
Index: /branches/eam_branches/psModules.20240412/src/config/pmConfig.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfig.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfig.c	(revision 42651)
@@ -0,0 +1,2010 @@
+/** @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 <stdlib.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 "pmErrorCodes.h"
+#include "pmFPALevel.h"
+#include "pmConfigRecipes.h"
+#include "pmConfigCamera.h"
+#include "pmConfigRun.h"
+
+#include "pmConfig.h"
+#include "pmVisualUtils.h"
+
+#ifdef HAVE_NEBCLIENT
+#include <nebclient.h>
+#endif // ifdef HAVE_NEBCLIENT
+
+#define IPPRC_ENV "IPPRC"        // Name of the environment variable containing the top-level config file
+#define IPPRC_FILE ".ipprc"      // Default top-level config file
+
+#define DEFAULT_LOG STDERR_FILENO       // Default file descriptor for log messages
+#define DEFAULT_TRACE STDERR_FILENO     // Default file descriptor for trace messages
+
+#define CHECK_FILE_RETRY 5              // Number of retries when checking a file
+#define CHECK_FILE_WAIT 250000          // Wait between retries (usec) when checking a file
+
+static bool readCameraConfig = true;    // Read the camera config on startup (with pmConfigRead)?
+static psArray *configPath = NULL;      // Search path for configuration files
+
+static bool checkPath(const char *filename, bool create, bool trunc);
+static psString resolveConfigFile(const char *name);
+
+bool pmConfigReadParamsSet(bool newReadCameraConfig)
+{
+    bool oldReadCameraConfig = readCameraConfig;
+    readCameraConfig = newReadCameraConfig;
+    return oldReadCameraConfig;
+}
+
+static void configFree(pmConfig *config)
+{
+    psFree(config->user);
+    psFree(config->site);
+    psFree(config->system);
+    psFree(config->files);
+    psFree(config->camera);
+    psFree(config->cameraName);
+    psFree(config->format);
+    psFree(config->formatName);
+    psFree(config->recipes);
+    psFree(config->recipesCamera);
+    psFree(config->recipeSymbols);
+    psFree(config->arguments);
+    psFree(config->database);
+    psFree(config->program);
+
+    // Close log and trace files
+    if (config->logFD != STDOUT_FILENO && config->logFD != STDERR_FILENO) {
+        close(config->logFD);
+    }
+    if (config->traceFD != STDOUT_FILENO && config->traceFD != STDERR_FILENO) {
+        close(config->traceFD);
+    }
+
+    return;
+}
+
+// Check the end of a string for a word; return the length of the string without the ending word
+// Used to identify the camera base name (e.g., "MEGACAM" out of "_MEGACAM-CHIP")
+int checkEndForWord(const char *line,   // String to check for ending word
+                    const char *word    // Ending word to check for
+                    )
+{
+    int wlen = strlen(word);            // Length of word
+    int nlen = strlen(line);            // Length of line
+    if (nlen < wlen) {
+        return 0;
+    }
+
+    char *ptr = (char *)line + nlen - wlen; // Expected position of ending word
+    if (strcasecmp(ptr, word)) {
+        return 0;
+    }
+
+    return (nlen - wlen);
+}
+
+// the camera name is of the form: BASE, BASE_CHIP, or BASE_FPA.  pull out BASE:
+char *cameraBaseName(const char *name   // Name of meta-camera
+                     )
+{
+    char *answer;
+
+    int N = checkEndForWord (name, "-CHIP");
+    if (N && name[0] == '_') {
+        psString answer = psStringNCopy(name + 1, N - 1);
+        return answer;
+    }
+
+    N = checkEndForWord(name, "-FPA");
+    if (N && name[0] == '_') {
+        psString answer = psStringNCopy(name + 1, N - 1);
+        return answer;
+    }
+
+    N = checkEndForWord(name, "-SKYCELL");
+    if (N && name[0] == '_') {
+        psString answer = psStringNCopy(name + 1, N - 1);
+        return answer;
+    }
+
+    answer = psStringCopy(name);
+    return answer;
+}
+
+
+pmConfig *pmConfigAlloc()
+{
+    pmConfig *config = psAlloc(sizeof(pmConfig));
+    (void)psMemSetDeallocator(config, (psFreeFunc)configFree);
+
+    // Initialise
+    config->user = NULL;
+    config->site = NULL;
+    config->system = NULL;
+    config->camera = NULL;
+    config->cameraName = NULL;
+    config->format = NULL;
+    config->formatName = NULL;
+    config->recipes = NULL;
+    config->recipesRead = PM_RECIPE_SOURCE_NONE;
+    config->recipesCamera = psMetadataAlloc();
+    config->recipeSymbols = psMetadataAlloc();
+    config->arguments = psMetadataAlloc();
+    config->database = NULL;
+    config->defaultRecipe = NULL;
+    config->program = NULL;
+
+    config->traceFD = DEFAULT_TRACE;
+    config->logFD = DEFAULT_LOG;
+
+    // the file structure is used to carry pmFPAfiles
+    config->files = psMetadataAlloc ();
+
+    config->sourceId = 0;
+    config->imageId = 0;
+    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(PM_ERR_CONFIG, true, "Path %s contains a bad environment variable.\n", dir);
+            return NULL;
+        }
+        if (envName[0] == '{') {
+            envName++;
+            if (envName[0] == '\0') {
+                psError(PM_ERR_CONFIG, 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,);
+
+    assert (configPath == NULL);
+    // XXX why was this being called?  pmConfigSet should only be called once...
+    // 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)
+{
+    if (configPath) {
+        psFree(configPath);
+    }
+    configPath = NULL;
+
+    return;
+}
+
+bool pmConfigFileRead(psMetadata **config, const char *name, const char *description)
+{
+    assert(config);
+    assert(name);
+    assert(description);
+
+    char *realName = NULL;
+    unsigned int numBadLines = 0;
+    struct stat filestat;
+
+    pmErrorRegister();
+
+    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(PM_ERR_CONFIG, 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(PM_ERR_CONFIG, 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(PM_ERR_CONFIG, true, "%d bad lines in %s configuration file (%s)",
+                numBadLines, description, realName);
+        psFree (realName);
+
+        return false;
+    }
+    if (!*config) {
+        psError(PM_ERR_CONFIG, true, "Unable to read %s configuration from %s",
+                description, realName);
+        psFree (realName);
+        return false;
+    }
+
+    psFree (realName);
+    return true;
+}
+
+bool pmConfigFileIngest(psMetadataItem *item, const char *description)
+{
+    PS_ASSERT_METADATA_ITEM_NON_NULL(item, false);
+    PS_ASSERT_STRING_NON_EMPTY(description, false);
+
+    if (item->type == PS_DATA_METADATA) {
+        return true;                    // We've already read it
+    }
+    if (item->type != PS_DATA_STRING) {
+        psError(PM_ERR_CONFIG, true, "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(psErrorCodeLast(), 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 (!pmConfigFileIngest(item, description)) {
+            psError(psErrorCodeLast(), 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(PM_ERR_CONFIG, true, "Unable to find FORMATS in camera configuration %s.\n", name);
+        return false;
+    }
+    if (!metadataReadFiles(formats, "camera format")) {
+        psError(psErrorCodeLast(), 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 (!pmConfigFileIngest(darkNorm, "dark normalisation")) {
+            psWarning("Unable to ingest DARK.NORM in camera %s", cameraName);
+        }
+    } 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->program = psStringCopy(argv[0]);
+    config->defaultRecipe = defaultRecipe;
+
+    // The following section of code attempts to determine which file to use as the
+    // top-level the configuration file.  At the end of this code block, the configFile
+    // variable will contain the name of the configuration file.
+
+    char *configFile = NULL;            // Name of configuration file
+
+    // First, try command line
+    psS32 argNum = psArgumentGet(*argc, argv, "-ipprc");
+    if (argNum != 0) {
+        // remove the "-ipprc" argument from argv, check and remove filename
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum >= *argc) {
+            psWarning("-ipprc command-line switch provided without the required filename --- ignored.\n");
+        } else {
+            configFile = resolveConfigFile(argv[argNum]);
+            psArgumentRemove(argNum, argc, argv);
+        }
+    }
+
+    // Next, try environment variable
+    if (!configFile) {
+        configFile = getenv(IPPRC_ENV);
+        if (configFile) {
+            configFile = psStringCopy(configFile);
+        }
+    }
+
+    // Last chance is ~/.ipprc
+    if (!configFile) {
+        char *home = getenv("HOME");
+        configFile = psStringCopy(home);
+        psStringAppend(&configFile, "/%s", IPPRC_FILE);
+    }
+
+    // Read and parse the config file and store in struct user.
+    // XXX move this section to pmConfigReadUser.c ?
+    if (!pmConfigFileRead(&config->user, configFile, "user")) {
+        psFree(config);
+        psFree(configFile);
+        return NULL;
+    }
+    psFree(configFile);
+
+    pmConfigRunCommand(config, *argc, argv);
+
+    // define the config-file search path (configPath).
+    psAssert(configPath == NULL, "Configuration path is already defined.");
+    psString path = psMetadataLookupStr(NULL, config->user, "PATH");
+    pmConfigSet(path);
+
+    // read the SITE file
+    psMetadataItem *siteItem = psMetadataLookup(config->user, "SITE");
+    if (!siteItem) {
+        psError(PM_ERR_CONFIG, true, "Unable to find SITE in user configuration.");
+        psFree(config);
+        return NULL;
+    }
+    if (!pmConfigFileIngest(siteItem, "site configuration")) {
+        psError(psErrorCodeLast(), false, "Unable to read site configuration");
+        psFree(config);
+        return NULL;
+    }
+    config->site = psMemIncrRefCounter(siteItem->data.md);
+
+    // load the SYSTEM file
+    psMetadataItem *systemItem = psMetadataLookup(config->user, "SYSTEM");
+    if (!systemItem) {
+        psError(PM_ERR_CONFIG, true, "Unable to find SYSTEM in user configuration.");
+        psFree(config);
+        return NULL;
+    }
+    if (!pmConfigFileIngest(systemItem, "system configuration")) {
+        psError(psErrorCodeLast(), false, "Unable to read system configuration");
+        psFree(config);
+        return NULL;
+    }
+    config->system = psMemIncrRefCounter(systemItem->data.md);
+
+    // Set LOG and TRACE options based on the user configuration.  These must be set AFTER
+    // the SITE and SYSTEM config files are read so path:// entries here can be resolved.
+    {
+        bool mdok = true;   // Status of MD lookup result
+
+        // Set logging level
+        int logLevel = psMetadataLookupS32(&mdok, config->user, "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->user, "LOGFORMAT");
+        if (mdok && logFormat)
+        {
+            psTrace("psModules.config", 7, "Setting log format to %s\n", logFormat);
+            psLogSetFormat(logFormat);
+        }
+
+        // Set logging destination first from command line, second from user configuration
+        psString logDest = NULL;        // Logging destination
+        argNum = psArgumentGet(*argc, argv, "-log");
+        if (argNum > 0) {
+            psArgumentRemove(argNum, argc, argv);
+            if (argNum >= *argc) {
+                psWarning("-log command-line switch provided without the required log destination "
+                          "--- ignored.");
+            } else {
+                logDest = psStringCopy(argv[argNum]);
+                psArgumentRemove(argNum, argc, argv);
+            }
+        }
+        if (!logDest) {
+            logDest = psMemIncrRefCounter(psMetadataLookupStr(&mdok, config->user, "LOGDEST"));
+        }
+        if (logDest) {
+            psString resolved = pmConfigConvertFilename(logDest, config, true, false); // Resolved filename
+            if (!resolved || strlen(resolved) == 0) {
+                psError(psErrorCodeLast(), false, "Unable to resolve log destination: %s", logDest);
+                psFree(logDest);
+                return NULL;
+            }
+            pmConfigRunFilenameAddWrite(config, "LOG", logDest);
+            config->logFD = psMessageDestination(resolved);
+            psFree(resolved);
+            psFree(logDest);
+        }
+        if (!psLogSetDestination(config->logFD)) {
+            psError(PS_ERR_IO, false, "Unable to set log destination to file number %d --- ignored",
+                    config->logFD);
+            psFree(config);
+            return NULL;
+        }
+
+        // Set trace levels
+        psMetadata *trace = psMetadataLookupMetadata(&mdok, config->user, "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) {
+                    psWarning("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->user, "TRACEFORMAT");
+        if (mdok && traceFormat) {
+            psTrace("psModules.config", 7, "Setting trace format to %s\n", traceFormat);
+            (void)psTraceSetFormat(traceFormat);
+        }
+
+        // Set trace destinations
+        psString traceDest = NULL;      // Trace destination
+        argNum = psArgumentGet(*argc, argv, "-tracedest");
+        if (argNum > 0) {
+            psArgumentRemove(argNum, argc, argv);
+            if (argNum >= *argc) {
+                psWarning("-tracedest command-line switch provided without the required trace destination "
+                          "--- ignored.\n");
+            } else {
+                traceDest = psStringCopy(argv[argNum]);
+                psArgumentRemove(argNum, argc, argv);
+            }
+        }
+        if (!traceDest) {
+            traceDest = psMemIncrRefCounter(psMetadataLookupStr(&mdok, config->user, "TRACEDEST"));
+        }
+        if (traceDest) {
+            psString resolved = pmConfigConvertFilename(traceDest, config, true, false); // Resolved filename
+            if (!resolved || strlen(resolved) == 0) {
+                psError(psErrorCodeLast(), false, "Unable to resolve trace destination: %s", traceDest);
+                psFree(traceDest);
+                return NULL;
+            }
+            pmConfigRunFilenameAddWrite(config, "TRACE", traceDest);
+            config->traceFD = psMessageDestination(resolved);
+            psFree(resolved);
+            psFree(traceDest);
+        }
+        if (!psTraceSetDestination(config->traceFD)) {
+            psError(PS_ERR_IO, false, "Unable to set trace destination to file number %d --- ignored",
+                    config->traceFD);
+            psFree(config);
+            return NULL;
+        }
+
+        // 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".
+    }
+
+    // Set the visualization levels
+    // argument format is: -visual (facil) (level)
+    while ((argNum = psArgumentGet(*argc, argv, "-visual"))) {
+        if ( (*argc < argNum + 3) ) {
+            psError(PS_ERR_IO, true, "-visual switch specified without facility and level.");
+            return NULL;
+        }
+        psArgumentRemove(argNum, argc, argv);
+        pmVisualSetLevel(argv[argNum], atoi(argv[argNum+1]));
+        psArgumentRemove(argNum, argc, argv);
+        psArgumentRemove(argNum, argc, argv);
+    }
+    if ((argNum = psArgumentGet(*argc, argv, "-visual-all"))) {
+        pmVisualSetLevel(".", 10);
+        psArgumentRemove(argNum, argc, argv);
+    }
+    if ((argNum = psArgumentGet(*argc, argv, "-visual-levels"))) {
+        pmVisualPrintLevels(stdout);
+        psArgumentRemove(argNum, argc, argv);
+    }
+
+    // XXX read TIME from SITE (or system?)
+    {
+        bool mdok = true;
+
+        // Initialise the psLib time handling
+        // XXX is this still needed / desired?
+        psString timeName = psMetadataLookupStr(&mdok, config->system, "TIME");
+        if (mdok && timeName) {
+            psTrace("psModules.config", 7, "Initialising psTime with file %s\n", timeName);
+            psTimeInit(timeName);
+        }
+    }
+
+    // Set the random number generator seed
+    {
+        psU64 seed = 0;                 // RNG seed
+        int argNum = psArgumentGet(*argc, argv, "-seed"); // Argument number
+        if (argNum > 0) {
+            psArgumentRemove(argNum, argc, argv);
+            if (argNum >= *argc) {
+                psWarning("-seed command-line switch provided without the required seed value --- ignored.");
+            } else {
+                char *end = NULL;       // Pointer to end of consumed string
+                seed = strtoull(argv[argNum], &end, 0);
+                if (strlen(end) > 0) {
+                    psError(PM_ERR_CONFIG, true, "Unable to read random number generator seed: %s",
+                            argv[argNum]);
+                    psFree(config);
+                    return NULL;
+                }
+                psArgumentRemove(argNum, argc, argv);
+            }
+        }
+        pmConfigRunSeed(config, seed);
+    }
+
+    // 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->system
+            psMetadata *cameras = psMetadataLookupMetadata(&mdok, config->system, "CAMERAS");
+            if (!cameras) {
+                psError(PM_ERR_CONFIG, 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(PM_ERR_CONFIG, 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(psErrorCodeLast(), 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(psErrorCodeLast(), 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(psErrorCodeLast(), 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->system, cameraName)) {
+                psError(psErrorCodeLast(), false,
+                        "Unable to generate skycell versions of specified camera %s.\n",
+                        cameraName);
+                psFree(config);
+                return NULL;
+            }
+
+            if (!pmConfigCameraMosaickedVersions(config->system, cameraName)) {
+                psError(psErrorCodeLast(), 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->system, "CAMERAS"); // List of cameras
+        if (!mdok || !cameras) {
+            psError(PM_ERR_CONFIG, true, "Unable to find CAMERAS in the system configuration.\n");
+            return false;
+        }
+
+        if (!metadataReadFiles(cameras, "camera configuration")) {
+            psError(psErrorCodeLast(), false, "Unable to read cameras within system 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);
+                psErrorStackPrint(stderr, "errors from read failure\n");
+                psErrorClear();
+                psMetadataRemoveKey(cameras, item->name);
+                continue;
+            }
+            if (!cameraReadCalibrations(item->data.md, item->name)) {
+                psWarning("Unable to read calibrations for camera %s: removed.\n", item->name);
+                psErrorStackPrint(stderr, "errors from read failure\n");
+                psErrorClear();
+                psMetadataRemoveKey(cameras, item->name);
+                continue;
+            }
+        }
+        psFree(iter);
+
+        if (!pmConfigCameraSkycellVersionsAll(config->system)) {
+            psError(psErrorCodeLast(), false, "Unable to generate skycell versions of cameras.\n");
+            psFree(config);
+            return NULL;
+        }
+        if (!pmConfigCameraMosaickedVersionsAll(config->system)) {
+            psError(psErrorCodeLast(), 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_SYSTEM | PM_RECIPE_SOURCE_CAMERA)) {
+        psError(psErrorCodeLast(), 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");
+
+    if (!pmConfigReadRecipes(config, PM_RECIPE_SOURCE_CL)) {
+        psError(psErrorCodeLast(), false, "Failed to read recipes from command-line");
+        psFree(config);
+        return NULL;
+    }
+
+    // Look for command-line options for files to replace
+    while ((argNum = psArgumentGet(*argc, argv, "-F")) > 0) {
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum + 1 >= *argc) {
+            psError(PM_ERR_CONFIG, 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->system, "CAMERAS"); // List of cameras
+        if (!cameras) {
+            psError(PM_ERR_CONFIG, 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);
+    }
+
+    // Look for command-line options for files to replace
+    while ((argNum = psArgumentGet(*argc, argv, "-R")) > 0) {
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum + 2 >= *argc) {
+            psError(PM_ERR_CONFIG, true,
+                    "Filerule element switch (-R) provided without filerule element and value.");
+            psFree(config);
+            return NULL;
+        }
+
+        const char *rulename = argv[argNum]; // The filerule, to be modified
+        psArgumentRemove(argNum, argc, argv);
+        const char *element  = argv[argNum]; // The element, to be modified
+        psArgumentRemove(argNum, argc, argv);
+	const char *value    = argv[argNum]; // The value, to be set
+	psArgumentRemove(argNum, argc, argv);
+
+        psMetadata *cameras = psMetadataLookupMetadata(NULL, config->system, "CAMERAS"); // List of cameras
+        if (!cameras) {
+            psError(PM_ERR_CONFIG, 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, rulename); // The rule of interest
+            if (!newRule) {
+                psTrace("psModules.config", 2,
+                        "Unable to find filerule %s in camera %s --- ignored.", rulename, 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;
+            }
+
+	    // Convert newRule to have the element value requested.
+	    if (!psMetadataLookupStr(NULL,newRule,element)) {
+	      psTrace("psModules.config", 2,
+		      "Unable to find filerule element %s in filerule %s in camera %s --- ignored.",
+		      element,rulename,cameraItem->name);
+	      continue;
+	    }
+	    psMetadataAddStr(newRule, PS_LIST_TAIL, element, PS_META_REPLACE,
+			     "Original replaced by -R option", value);
+	    
+            psMetadataAddMetadata(filerules, PS_LIST_TAIL, rulename, PS_META_REPLACE,
+                                  "Original replaced by -R option", newRule);
+        }
+        psFree(camerasIter);
+    }
+
+    // Look for command-line options for files to replace
+    while ((argNum = psArgumentGet(*argc, argv, "-photcode-rule")) > 0) {
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum >= *argc) {
+            psError(PM_ERR_CONFIG, true,
+                    "-photcode-rule provided without new rule.");
+            psFree(config);
+            return NULL;
+        }
+
+        psString newrule = psStringCopy(argv[argNum]); // The filerule, to be modified
+        psArgumentRemove(argNum, argc, argv);
+
+        psMetadata *cameras = psMetadataLookupMetadata(NULL, config->system, "CAMERAS"); // List of cameras
+        if (!cameras) {
+            psError(PM_ERR_CONFIG, 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
+
+	    psMetadataAddStr (camera, PS_LIST_TAIL, "PHOTCODE.RULE", PS_META_REPLACE, "original replaced by -photcode-rule option", newrule);
+        }
+	psFree(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) {
+            psWarning("-dbserver command-line switch provided without the required server name --- ");
+        } else {
+            char *dbserver = argv[argNum]; // The camera configuration file to read
+            if (!psMetadataAddStr(config->user, PS_LIST_TAIL, "DBSERVER", PS_META_REPLACE,
+                                  NULL, dbserver)) {
+                psWarning("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) {
+            psWarning("-dbname command-line switch provided without the required database name");
+        } else {
+            char *dbname = argv[argNum]; // The camera configuration file to read
+            if (!psMetadataAddStr(config->user, PS_LIST_TAIL, "DBNAME", PS_META_REPLACE, NULL, dbname)) {
+                psWarning("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) {
+            psWarning("-dbuser command-line switch provided without the required database name");
+        } else {
+            char *dbuser = argv[argNum]; // The camera configuration file to read
+            if (!psMetadataAddStr(config->user, PS_LIST_TAIL, "DBUSER", PS_META_REPLACE, NULL, dbuser)) {
+                psWarning("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) {
+            psWarning("-dbpassword command-line switch provided without the required password");
+        } else {
+            char *dbpassword = argv[argNum]; // The camera configuration file to read
+            if (!psMetadataAddStr(config->user, PS_LIST_TAIL, "DBPASSWORD", PS_META_REPLACE,
+                                  NULL, dbpassword)) {
+                psWarning("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) {
+            psWarning("-dbpport command-line switch provided without the required port number");
+        } else {
+            char *dbport = argv[argNum]; // The camera configuration file to read
+            if (!psMetadataAddS32(config->user, PS_LIST_TAIL, "DBPORT", PS_META_REPLACE, NULL,
+                                  (psS32)atoi(dbport))) {
+                psWarning("Failed to overwrite .ipprc DBPORT value");
+            }
+
+            psArgumentRemove(argNum, argc, argv);
+        }
+    }
+
+    argNum = psArgumentGet(*argc, argv, "-image_id");
+    if (argNum > 0) {
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum >= *argc) {
+            psWarning("-image_id command-line switch provided without the required id number");
+        } else {
+            if (!sscanf(argv[argNum], "%" PRId64, &config->imageId)) {
+                psWarning("Failed to parse image_id value %s", argv[argNum]);
+            }
+            psArgumentRemove(argNum, argc, argv);
+        }
+    }
+
+    argNum = psArgumentGet(*argc, argv, "-source_id");
+    if (argNum > 0) {
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum >= *argc) {
+            psWarning("-source_id command-line switch provided without the required id number");
+        } else {
+            if (!sscanf(argv[argNum], "%" PRId64, &config->sourceId)) {
+                psWarning("Failed to parse image_id value %s", argv[argNum]);
+            }
+            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(PM_ERR_CONFIG, 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(PM_ERR_CONFIG, 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.format", 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.format", 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.format", 5, "%s in multi list matches.", ruleItem->name);
+                    break;
+                }
+            }
+            if (!found) {
+                *valid = false;
+                psTrace("psModules.config.format", 5, "%s doesn't match.", ruleItem->name);
+            }
+            continue;
+        }
+
+        psError(PM_ERR_CONFIG, 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
+// if so, return the winning format and the name of the winning format (both allocated here)
+static bool formatFromHeader(bool *status,
+                             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);
+
+    *status = true;                     // error status
+    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(PM_ERR_CONFIG, false, "Unable to find list of FORMATS in camera %s", cameraName);
+        *status = false;
+        return false;
+    }
+
+    if (!metadataReadFiles(formats, "camera format")) {
+        psError(psErrorCodeLast(), false, "Unable to read cameras formats within camera configuration.\n");
+        *status = false;
+        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.format", 5, "trying format %s", formatsItem->name);
+
+        bool valid = false;
+        if (!pmConfigValidateCameraFormat(&valid, testFormat, header)) {
+            psError (psErrorCodeLast(), false, "Error in config scripts for camera %s, format %s\n",
+                     cameraName, formatsItem->name);
+            *status = false;
+            return false;
+        }
+        if (valid) {
+            if (!*format) {
+                psLogMsg("psModules.config.format", PS_LOG_INFO, "Camera %s, format %s matches header.\n",
+                         cameraName, formatsItem->name);
+                *format = psMemIncrRefCounter(testFormat);
+                *name = psStringCopy(formatsItem->name);
+                result = true;
+            } else {
+                psWarning("Camera %s, format %s also matches header --- ignored.\n",
+                         cameraName, formatsItem->name);
+            }
+        }
+    }
+    psFree(formatsIter);
+    *status = true;
+    return result;
+}
+
+// determine the camera format based on the keywords in the header.  If we have already chosen
+// a camera, then we select the formats only from that camera, and the meta-cameras for that
+// camera.  If we are discovering the camera (config->camera == NULL), then we also load the
+// recipe files for the camera.
+psMetadata *pmConfigCameraFormatFromHeader(psMetadata **camera, psString *cameraName, psString *formatName,
+                                           pmConfig *config, const psMetadata *header, bool readRecipes)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_PTR_NON_NULL(header, NULL);
+
+    bool status = false;                // error status
+    psMetadata *format = NULL;          // The winning format
+    psString testFormatName = 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) {
+        psAssert (!config->cameraName, "programming error: cameraName should be NULL if camera is undefined");
+        psAssert (!config->format,     "programming error: format should be NULL if camera is undefined");
+        psAssert (!config->formatName, "programming error: formatName should be NULL if camera is undefined");
+
+        bool mdok;                      // Metadata lookup status
+        psMetadata *cameras = psMetadataLookupMetadata(&mdok, config->system, "CAMERAS");
+        if (! mdok || !cameras) {
+            psError(PM_ERR_CONFIG, true, "Unable to find CAMERAS in the configuration.");
+            return NULL;
+        }
+
+        if (!metadataReadFiles(cameras, "camera configuration")) {
+            psError(psErrorCodeLast(), 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.format", 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(&status, &format, &testFormatName, testCamera, header, camerasItem->name)) {
+                config->camera = psMemIncrRefCounter(testCamera);
+                config->cameraName = psStringCopy(camerasItem->name);
+                config->formatName = testFormatName;
+                config->format = format;
+                if (camera) {
+                    *camera = psMemIncrRefCounter(testCamera);  // view on value saved on config
+                }
+                if (formatName) {
+                    *formatName = psMemIncrRefCounter(testFormatName);    // view on value saved on config
+                }
+                if (cameraName) {
+                    *cameraName = psMemIncrRefCounter(config->cameraName);    // view on value saved on config
+                }
+            } else {
+                if (!status) {
+                    psError(psErrorCodeLast(), false, "Error reading camera config data for %s",
+                            camerasItem->name);
+                    return NULL;
+                }
+            }
+        }
+        psFree(camerasIter);
+
+        // Done looking at all cameras
+        if (!config->camera) {
+            psError(PM_ERR_CONFIG, true, "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(psErrorCodeLast(), false, "Error reading recipes from camera config for %s",
+                    config->cameraName);
+            return NULL;
+        }
+        return psMemIncrRefCounter(format); // a second copy, since the first copy sits on config->format
+    }
+
+    // we have a config with a specified camera.  However, the supplied header may not
+    // correspond to this mosaic level for the camera.  We need to try the CHIP and FPA mosaic
+    // versions as well as the base version
+
+    psAssert (config->cameraName, "programming error: cameraName should not be NULL if camera is defined");
+    psAssert (config->format,     "programming error: format should not be NULL if camera is defined");
+    psAssert (config->formatName, "programming error: formatName should not be NULL if camera is defined");
+
+    // the camera name is of the form: BASE, BASE_CHIP, or BASE_FPA.  pull out BASE:
+    char *baseName = cameraBaseName (config->cameraName);
+
+    bool found = false;
+
+    psMetadata *testCamera = NULL;
+    char *testCameraName = NULL;
+
+    psMetadata *cameras = psMetadataLookupMetadata (NULL, config->system, "CAMERAS");
+    psAssert (cameras, "missing CAMERAS in complete metadata");
+
+    // try the FPA metaCamera
+    if (!found) {
+        testCameraName = NULL;
+        psStringAppend (&testCameraName, "_%s-FPA", baseName);
+
+        testCamera = psMetadataLookupMetadata (NULL, cameras, testCameraName);
+        psAssert (testCamera, "missing %s in CAMERAS in complete metadata", testCameraName);
+
+        bool status;
+        found = formatFromHeader(&status, &format, &testFormatName, testCamera, header, testCameraName);
+        if (!found) psFree (testCameraName);
+    }
+
+    // try the CHIP metaCamera
+    if (!found) {
+        testCameraName = NULL;
+        psStringAppend (&testCameraName, "_%s-CHIP", baseName);
+
+        testCamera = psMetadataLookupMetadata (NULL, cameras, testCameraName);
+        psAssert (testCamera, "missing %s in CAMERAS in complete metadata", testCameraName);
+
+        bool status;
+        found = formatFromHeader(&status, &format, &testFormatName, testCamera, header, testCameraName);
+        if (!found) psFree (testCameraName);
+    }
+
+    // try the SKYCELL metaCamera
+    if (!found) {
+        testCameraName = NULL;
+        psStringAppend (&testCameraName, "_%s-SKYCELL", baseName);
+
+        testCamera = psMetadataLookupMetadata (NULL, cameras, testCameraName);
+        psAssert (testCamera, "missing %s in CAMERAS in complete metadata", testCameraName);
+
+        bool status;
+        found = formatFromHeader(&status, &format, &testFormatName, testCamera, header, testCameraName);
+        if (!found) psFree (testCameraName);
+    }
+
+    // try the base name
+    if (!found) {
+        testCameraName = psMemIncrRefCounter (baseName);
+
+        testCamera = psMetadataLookupMetadata (NULL, cameras, testCameraName);
+        psAssert (testCamera, "missing %s in CAMERAS in complete metadata", testCameraName);
+
+        bool status;
+        found = formatFromHeader(&status, &format, &testFormatName, testCamera, header, testCameraName);
+    }
+
+    if (!found) {
+        psError(PM_ERR_CONFIG, true,
+                "Unable to find a format with the specified camera (%s) that matches the given header.",
+                baseName);
+        psFree (baseName);
+        return NULL;
+    }
+
+    psFree (baseName);
+    if (formatName) {
+        *formatName = testFormatName;
+    } else {
+        psFree (testFormatName);
+    }
+    if (cameraName) {
+        *cameraName = testCameraName;
+    } else {
+        psFree (testCameraName);
+    }
+
+    if (camera) {
+        *camera = psMemIncrRefCounter(testCamera);
+    }
+    return format; // we do NOT need to incr ref counter: this is the only copy
+}
+
+// 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->system, "CAMERAS");
+    if (!cameras) {
+        psError(PM_ERR_CONFIG, true, "Unable to find CAMERAS in the configuration.");
+        return NULL;
+    }
+
+    psMetadataItem *item = psMetadataLookup(cameras, cameraName); // Item with camera of interest
+    if (!pmConfigFileIngest(item, "camera configuration")) {
+        psError(psErrorCodeLast(), false, "Unable to ingest camera configuration.");
+        return NULL;
+    }
+
+    return psMemIncrRefCounter(item->data.md);
+}
+
+psMetadataItem *pmConfigUserSite(const pmConfig *config, const char *name, psDataType type)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+
+    psMetadataItem *item = psMetadataLookup(config->user, name);
+    if (!item) {
+        item = psMetadataLookup(config->site, name);
+        if (!item) {
+            psError(PM_ERR_CONFIG, true,
+                    "Unable to find %s in user or site configuration", name);
+            return NULL;
+        }
+    }
+    if (item->type != type) {
+        psError(PM_ERR_CONFIG, true,
+                "Type of %s (%x) in user/site configuration does not match expected (%x)",
+                name, item->type, type);
+        return NULL;
+    }
+
+    return item;
+}
+
+
+psDB *pmConfigDB(pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_PTR_NON_NULL(config->user, NULL);
+
+#ifndef HAVE_PSDB
+
+    psError(PM_ERR_PROG, false,
+            "Cannot configure database: psModules was compiled without database support.");
+    return NULL;
+
+#else
+
+    if (config->database) {
+        return config->database;
+    }
+
+    // Connection details
+    psMetadataItem *server = pmConfigUserSite(config, "DBSERVER",   PS_DATA_STRING);
+    psMetadataItem *user   = pmConfigUserSite(config, "DBUSER",     PS_DATA_STRING);
+    psMetadataItem *pass   = pmConfigUserSite(config, "DBPASSWORD", PS_DATA_STRING);
+    psMetadataItem *name   = pmConfigUserSite(config, "DBNAME",     PS_DATA_STRING);
+    psMetadataItem *port   = pmConfigUserSite(config, "DBPORT",     PS_TYPE_S32);
+
+    if (!server || !user || !pass || !name) {
+        psWarning("Cannot find DBSERVER/DBUSER/DBPASSWORD/DBNAME in user or site configuration: "
+                  "unable to connect to database.");
+        psErrorClear();
+        return NULL;
+    }
+    if (!port) {
+        psTrace("psModules.config", 1, "Database port defaulting to 0");
+        psErrorClear();
+    }
+
+    if (strcasecmp(name->data.str, "XXX") == 0 || strcasecmp(name->data.str, "NONE") == 0) {
+        psTrace("psModules.config", 1, "Database initialisation skipped: database is %s.", name->data.str);
+        return NULL;
+    }
+
+    config->database = psDBInit(server->data.str, user->data.str, pass->data.str, name->data.str,
+                                port ? port->data.S32 : 0);
+    return config->database;
+
+#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(PM_ERR_CONFIG, 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))) {
+        if (!PS_DATA_IS_PRIMITIVE(rulesItem->type) && rulesItem->type != PS_DATA_STRING) {
+            psError(PM_ERR_CONFIG, false, "Invalid type for RULE %s.", rulesItem->name);
+            return false;
+        }
+
+        psMetadataItem *hdrItem = psMetadataLookup(header, rulesItem->name); // Item from header
+        if (hdrItem && psMetadataItemCompare(hdrItem, rulesItem)) {
+            // It's already there and matches
+            continue;
+        }
+
+        // Look for an operation
+        psMetadataItemCompareOp op = psMetadataItemCompareOperation(rulesItem);
+        switch (op) {
+          case PS_METADATA_ITEM_COMPARE_OP_NONE:
+          case PS_METADATA_ITEM_COMPARE_OP_EQ:
+          case PS_METADATA_ITEM_COMPARE_OP_LE:
+          case PS_METADATA_ITEM_COMPARE_OP_GE: {
+              // A comparison involving equality: add the value
+              psMetadataItem *newItem = psMetadataItemCopy(rulesItem); // Copy of item
+              if (op == PS_METADATA_ITEM_COMPARE_OP_LE || op == PS_METADATA_ITEM_COMPARE_OP_GE) {
+                  // Not clear what the value is supposed to be, so add a warning as well
+                  psFree(newItem->comment);
+                  newItem->comment = psStringCopy("MAY BE WRONG: added to match format rule");
+              }
+              psMetadataAddItem(header, newItem, PS_LIST_TAIL, PS_META_REPLACE);
+              psFree(newItem);                // Drop reference
+              break;
+          }
+          case PS_METADATA_ITEM_COMPARE_OP_LT:
+          case PS_METADATA_ITEM_COMPARE_OP_GT:
+          case PS_METADATA_ITEM_COMPARE_OP_NE:
+            // It's not at all obvious what the value should be, so return an error.
+            psError(PM_ERR_CONFIG, true,
+                    "RULE %s (defined by an OPeration) is not present or not consistent in output header",
+                    rulesItem->name);
+            return false;
+          default:
+            psAbort("Unknown operation: %x", op);
+        }
+    }
+    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;                           // Argument number
+
+    // 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);
+                }
+            }
+            globfree(&globList);
+        }
+        psFree (words);
+    }
+
+    // load the list from the supplied text file
+    if (list && strlen(list) > 0 && (Narg = psArgumentGet(*argc, argv, list))) {
+        psArgumentRemove (Narg, argc, argv);
+        FILE *f = fopen(argv[Narg], "r");
+        if (!f) {
+            psError(PS_ERR_IO, true, "Unable to open specified list file");
+            psFree(input);
+            return NULL;
+        }
+
+        // XXX Reading the list should be reimplemented using psSlurp
+
+        char line[1024]; // XXX limits the list lines to 1024 chars
+        while (fgets(line, 1024, f) != NULL) {
+            char word[1024];
+            int nItems = sscanf(line, "%s", word);
+            switch (nItems) {
+              case 0:
+                break;
+              case 1: {
+                  psString filename = psStringCopy(word);
+                  psArrayAdd(input, 16, filename);
+                  psFree(filename);
+                  break;
+              }
+              default:
+                // rigid format, no comments allowed?
+                psError(PM_ERR_CONFIG, true, "Unable to parse file list: spaces detected.");
+                psFree(input);
+                fclose(f);
+                return NULL;
+            }
+        }
+        psArgumentRemove(Narg, argc, argv);
+        fclose(f);
+    }
+
+    return input;
+}
+
+// XXX this is a prime example of the failing of our error-handling system.  this function has
+// three possible outcomes: the argument was found, it was not found, or we raised an error.
+// returning only the bool does not distinguish failure to find the argument from a deeper
+// error.  requiring the calling function to both test the bool AND trap the error stack is
+// fragile: the error stack may not have been cleared, or they may not do both.  in some
+// places, we solve this by returning two types of boolean status values.  a better option
+// might be to return a psErrorCode value (as RHL proposed), which would be 0 on success and
+// any of several options on failure.
+
+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) {
+        psAbort("error parsing argument list");
+        psError(psErrorCodeLast(), false, "error parsing argument list");
+        psFree (files);
+        return false;
+    }
+
+    // no files found: this is not really an error
+    if (files->n == 0) {
+        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, bool trunc)
+{
+    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 (!checkPath(newName, create, trunc)) {
+            // let checkPath()'s psError() call float up
+            psError(psErrorCodeLast(), false, "error from checkPath for 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(PM_ERR_CONFIG, 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(PM_ERR_CONFIG, 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(PM_ERR_CONFIG, 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 (!checkPath(newName, create, trunc)) {
+            // let checkPath()'s psError() call float up
+            psError(psErrorCodeLast(), false, "error from checkPath for path:// (%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.");
+            return NULL;
+        }
+
+        char *nebfile = NULL;
+        if (!(nebfile = nebFind(server, filename))) {
+            // object does not exist
+            if (create) {
+                nebfile = nebCreate(server, filename, NULL, NULL);
+                if (!nebfile) {
+                    psError(PM_ERR_SYS, true, "failed to create a new nebulous key: %s", nebErr(server));
+                    nebServerFree(server);
+                    return NULL;
+                }
+            } else {
+                // if the object does not exist and create isn't set, then we
+                // should puke
+                psError(PM_ERR_SYS, true, "Unable to access file %s: %s", filename, nebErr(server));
+                nebServerFree(server);
+                return NULL;
+            }
+        }
+
+        // convert nebfile into a psString
+        psString path = psStringCopy(nebfile);
+        nebFree(nebfile);
+        nebServerFree(server);
+
+        // Check to ensure it's there.  Will create the file if Nebulous failed to do so.
+        if (!checkPath(path, create, trunc)) {
+            psError(psErrorCodeLast(), false, "Cannot find file %s", path);
+            psFree(path);
+            return NULL;
+        }
+
+        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(PM_ERR_CONFIG, true, "Unable to find FILERULES in the camera configuration.");
+        return NULL;
+    }
+
+    if (!pmConfigFileIngest(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(&mdok, filerules, realname);
+}
+
+psMetadata *pmConfigFitsType (const pmConfig *config, const psMetadata *camera, const char *fitsType)
+{
+    bool mdok;
+
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_METADATA_NON_NULL(camera, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(fitsType, NULL);
+
+    psMetadataItem *item = psMetadataLookup(camera, "FITSTYPES"); // Item with the file rule of interest
+    if (!item) {
+        psError(PM_ERR_CONFIG, false, "Unable to find FITSTYPES in the camera configuration.");
+        return NULL;
+    }
+
+    if (!pmConfigFileIngest(item, "FITS Types")) {
+        psError(PM_ERR_CONFIG, false, "Unable to read fits types for camera.");
+        return NULL;
+    }
+
+    assert(item->type == PS_DATA_METADATA);
+    psMetadata *fitstypes = item->data.md; // FITS Types from the camera configuration
+
+    // select the name from the FITSTYPES
+    psMetadata *scheme = psMetadataLookupMetadata(&mdok, fitstypes, fitsType);
+    if (!scheme) {
+        psWarning("Unable to find specified FITS Type %s in camera configuration.", fitsType);
+        return NULL;
+    }
+
+    return scheme;
+}
+
+static bool checkPath(const char *filename, bool create, bool trunc)
+{
+    PS_ASSERT_PTR_NON_NULL(filename, false);
+
+    // re-try access up to 5 times (1.25sec) to reduce NFS lurches
+    for (int i = 0; i < CHECK_FILE_RETRY; i++) {
+        if (access(filename, R_OK) == 0) {
+            // file already exists
+            if (trunc) {
+                if(truncate(filename, 0) != 0) {
+                    psError(PS_ERR_IO, true, "Failed to truncate file, %s\n", filename);
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        // file does not exist
+        if (create) {
+            int fd = open(filename, O_WRONLY|O_CREAT, 0666);
+            if (fd == 0) {
+                psError(PS_ERR_IO, true, "Failed to open & create file, %s\n", filename);
+                return false;
+            }
+            if (close(fd) != 0) {
+                psError(PS_ERR_IO, true, "Failed to close file, %s\n", filename);
+                return false;
+            }
+            return true;
+        }
+        usleep(CHECK_FILE_WAIT);
+    }
+
+    // We've tried 5 times to access the file; give up and report a problem.  If the file does
+    // not exist and create isn't set, then we should puke
+    psError(PS_ERR_IO, true, "Unable to access file %s", filename);
+    return false;
+}
+
+static psString resolveConfigFile(const char *nameArg)
+{
+    // if config file name is nebulous path resolve it
+    // otherwise just return a copy of the argument
+    if (strncasecmp(nameArg, "neb://", strlen("neb://"))) {
+        return psStringCopy(nameArg);
+    }
+
+#ifdef HAVE_NEBCLIENT
+    char *neb_server = getenv("NEB_SERVER");
+
+    // if env isn't set, check the config system
+    if (!neb_server) {
+        psError(PM_ERR_CONFIG, true, "NEB_SERVER environment variable must be set in order to resolve config file.");
+            return NULL;
+    }
+
+    nebServer *server = nebServerAlloc(neb_server);
+    if (!server) {
+        psError(PM_ERR_SYS, true, "failed to create a nebServer object.");
+        return NULL;
+    }
+
+    char *nebfile = nebFind(server, nameArg);
+    nebServerFree(server);
+    if (!nebfile) {
+        // object does not exist
+        psError(PM_ERR_SYS, true, "failed to resolve nebulous path: %s.", nameArg);
+        return NULL;
+    }
+    // XXX: do I need to free nebfile?
+
+    return psStringCopy(nebfile);
+#else
+    psError(PM_ERR_PROG, true, "psModules was compiled without nebulous support.");
+    return NULL;
+#endif // ifdef HAVE_NEBCLIENT
+}
Index: /branches/eam_branches/psModules.20240412/src/config/pmConfig.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfig.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfig.h	(revision 42651)
@@ -0,0 +1,207 @@
+/*  @file pmConfig.h
+ *  @brief Configuration functions
+ *
+ *  @author Paul Price, IfA
+ *  @author Eugene Magnier, IfA
+ *
+ *  @version $Revision: 1.44 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-05 19:41:50 $
+ *  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_SYSTEM      = 0x01, ///< System 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: user, site, system, camera and recipe configuration, the
+/// command-line arguments, the pmFPAfiles used, and the database handle.
+typedef struct {
+    psMetadata *user;                   ///< User configuration
+    psMetadata *site;                   ///< Site configuration
+    psMetadata *system;                 ///< System 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 *recipesCamera;          ///< 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
+    psString program;                   ///< Name of program
+    // Private members
+    pmRecipeSource recipesRead;         ///< Which recipe sources have been read
+    psMetadata *recipeSymbols;          ///< Where each recipe came from
+    int traceFD;                        ///< File descriptor for trace messages
+    int logFD;                          ///< File descriptor for log messages
+    psS64 sourceId;                    ///< Database source id for output file
+    psS64 imageId;                     ///< Database image id for output file
+} pmConfig;
+
+/// Allocator for pmConfig
+pmConfig *pmConfigAlloc(void);
+
+/// 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 user configuration (the file name is specified by "-ipprc FILE" on the
+/// command-line, the IPPRC 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 user 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
+    );
+
+/// Ingest a configuration file
+///
+/// Ingest a metadata configuration file into the supplied metadata item, if required.  Produce an error and
+/// return false if there's a problem.
+bool pmConfigFileIngest(psMetadataItem *item, // Item into which to read file
+                        const char *description // Description, for error messages
+    );
+
+/// 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(psMetadata **camera, // selected camera (or meta-camera)
+                                           psString *formatName, // selected format name
+                                           psString *cameraName, // selected camera name
+                                           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
+                                );
+
+/// Derive a value from the user or site configuration
+///
+/// The value in the user configuration takes precedence.  Returns NULL if the value isn't present in either,
+/// or has the wrong type.
+psMetadataItem *pmConfigUserSite(const pmConfig *config, // Configuration
+                                 const char *name, // Name of value
+                                 psDataType type // Expected type
+    );
+
+
+/// 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
+    bool trunc                          ///< truncate the file (if it exists)
+);
+
+/// 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 system 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
+    );
+
+// look up the specified fitstype, interpolating the file if needed
+psMetadata *pmConfigFitsType (const pmConfig *config, const psMetadata *camera, const char *fitsType);
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/config/pmConfigCamera.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfigCamera.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfigCamera.c	(revision 42651)
@@ -0,0 +1,934 @@
+#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);
+
+psString pmConfigCameraRootName(const char *name)
+{
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+
+    if (name[0] != '_') {
+        // It's an original
+        return psStringCopy(name);
+    }
+
+    psString root = psStringCopy(name + 1); // Camera name
+    int length = strlen(name);                     // Length of camera name
+    if (strcmp(root + length - 9, "-SKYCELL") == 0) {
+        length -= 9;
+    } else if (strcmp(root + length - 6, "-CHIP") == 0) {
+        length -= 6;
+    } else if (strcmp(root + length - 5, "-FPA") == 0) {
+        length -= 5;
+    } else {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unrecognised derivative camera: %s", name);
+        psFree(root);
+        return NULL;
+    }
+
+    // Truncate the string
+    root[length] = '\0';
+
+    return root;
+}
+
+psString pmConfigCameraSkycellName(const char *name)
+{
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+
+    psString root = pmConfigCameraRootName(name); // Root name of camera
+    if (!root) {
+        return NULL;
+    }
+
+    psStringAppend(&root, "-SKYCELL");
+    psStringPrepend(&root, "_");
+
+    return root;
+}
+
+psString pmConfigCameraChipName(const char *name)
+{
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+
+    psString root = pmConfigCameraRootName(name); // Root name of camera
+    if (!root) {
+        return NULL;
+    }
+
+    psStringAppend(&root, "-CHIP");
+    psStringPrepend(&root, "_");
+
+    return root;
+}
+
+psString pmConfigCameraFPAName(const char *name)
+{
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+
+    psString root = pmConfigCameraRootName(name); // Root name of camera
+    if (!root) {
+        return NULL;
+    }
+
+    psStringAppend(&root, "-FPA");
+    psStringPrepend(&root, "_");
+
+    return root;
+}
+
+// Generate the skycell version of a named camera configuration
+bool pmConfigCameraSkycellVersion(psMetadata *system, // The system configuration
+                                  const char *name // Name of the un-mosaicked camera
+                                  )
+{
+    PS_ASSERT_METADATA_NON_NULL(system, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *cameras = psMetadataLookupMetadata(&mdok, system, "CAMERAS"); // List of cameras
+    if (!mdok || !cameras) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find CAMERAS in the system configuration.\n");
+        return false;
+    }
+    if (!pmConfigGenerateSkycellVersion(cameras, cameras, name, system)) {
+        psError(PS_ERR_UNKNOWN, true, "Failed to build skycell camera description for %s\n", name);
+        return false;
+    }
+    return true;
+}
+
+
+bool pmConfigCameraSkycellVersionsAll(psMetadata *system)
+{
+    PS_ASSERT_METADATA_NON_NULL(system, false);
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *cameras = psMetadataLookupMetadata(&mdok, system, "CAMERAS"); // List of cameras
+    if (!mdok || !cameras) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find CAMERAS in the system 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, system)) {
+            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, PS_META_REPLACE);
+    }
+    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.X0", "CELL.Y0", "CELL.TIMESYS", 0 };
+const static char *skycellConceptsChip[] = { "CHIP.XPARITY", "CHIP.YPARITY", 0 };
+const static char *skycellConceptsFPA[] = { "FPA.TIMESYS" };
+
+// 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 *system // System configuration
+                                      )
+{
+    for (int i = 0; concepts[i]; i++) {
+        if (strcmp(name, concepts[i]) == 0) {
+            return NULL;
+        }
+    }
+
+    if (!system) {
+        return name;
+    }
+    bool mdok;                          // Status of MD lookup
+    psMetadata *skycells = psMetadataLookupMetadata(&mdok, system, "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 *system // System 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 = pmConfigCameraSkycellName(name); // Name of skycelled camera
+    bool mdok;                       // Status of MD lookup
+    psMetadata *oldCam = psMetadataLookupMetadata(&mdok, oldCameras, newName); // Existing camera configuration
+    if (mdok && oldCam) {
+        // Ensure new camera goes 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!
+        psTrace("psModules.config", 6, "Camera configuration for %s exists, so moving to the front.", newName);
+        psMetadataAddMetadata(newCameras, PS_LIST_HEAD, newName, PS_META_REPLACE, NULL, oldCam);
+        psFree(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");
+        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.OBS", 0, "PHU keyword for unique identifier", "FPA.OBS");
+        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
+
+        psMetadata *concepts;           // List of concepts for each level
+        psMetadataIterator *iter;       // Iterator for concepts
+        psMetadataItem *item;           // Concept specification item, from iteration
+
+        concepts = pmConceptsSpecs(PM_FPA_LEVEL_FPA); // FPA-level concepts
+        iter = psMetadataIteratorAlloc(concepts, PS_LIST_HEAD, NULL);
+        while ((item = psMetadataGetAndIncrement(iter))) {
+            const char *new = skycellConceptName(item->name, skycellConceptsFPA, system); // Name for skycell
+            if (new) {
+                psMetadataAddStr(translation, PS_LIST_TAIL, item->name, 0, NULL, new);
+            }
+        }
+        psFree(iter);
+
+        concepts = pmConceptsSpecs(PM_FPA_LEVEL_CHIP);
+        iter = psMetadataIteratorAlloc(concepts, PS_LIST_HEAD, NULL);
+        while ((item = psMetadataGetAndIncrement(iter))) {
+            const char *new = skycellConceptName(item->name, skycellConceptsChip, system); // Name for skycell
+            if (new) {
+                psMetadataAddStr(translation, PS_LIST_TAIL, item->name, 0, NULL, new);
+            }
+        }
+        psFree(iter);
+
+        concepts = pmConceptsSpecs(PM_FPA_LEVEL_CELL);
+        iter = psMetadataIteratorAlloc(concepts, PS_LIST_HEAD, false);
+        while ((item = psMetadataGetAndIncrement(iter))) {
+            const char *new = skycellConceptName(item->name, skycellConceptsCell, system); // Name for skycell
+            if (new) {
+                psMetadataAddStr(translation, PS_LIST_TAIL, item->name, 0, NULL, new);
+            }
+        }
+        psFree(iter);
+
+        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);
+        psMetadataAddStr(defaults, PS_LIST_TAIL, "CELL.TIMESYS", 0, "Time system", "TAI");
+        psMetadataAddStr(defaults, PS_LIST_TAIL, "FPA.TIMESYS", 0, "Time system", "TAI");
+
+        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");
+        psMetadataAddStr(conceptFormats, PS_LIST_TAIL, "FPA.LONGITUDE", 0, "Units for longitude", "HOURS");
+        psMetadataAddStr(conceptFormats, PS_LIST_TAIL, "FPA.LATITUDE", 0, "Units for latitude", "DEGREES");
+
+        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!
+    psTrace("psModules.config", 6, "Generated new camera configuration for %s.", newName);
+    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 *system, // The system configuration
+                                     const char *name // Name of the un-mosaicked camera
+                                    )
+{
+    PS_ASSERT_METADATA_NON_NULL(system, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *cameras = psMetadataLookupMetadata(&mdok, system, "CAMERAS"); // List of cameras
+    if (!mdok || !cameras) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find CAMERAS in the system 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 *system)
+{
+    PS_ASSERT_METADATA_NON_NULL(system, false);
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *cameras = psMetadataLookupMetadata(&mdok, system, "CAMERAS"); // List of cameras
+    if (!mdok || !cameras) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find CAMERAS in the system 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, PS_META_REPLACE);
+    }
+    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);
+
+    if (name[0] == '_') {
+        // It's already a mosaicked version of some sort
+        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 = mosaicLevel == PM_FPA_LEVEL_CHIP ? pmConfigCameraChipName(name) :
+        pmConfigCameraFPAName(name); // Name of mosaicked camera
+    bool mdok;                       // Status of MD lookup
+    psMetadata *oldCam = psMetadataLookupMetadata(&mdok, oldCameras, newName); // Existing camera configuration
+    if (mdok && oldCam) {
+        // Ensure new camera goes 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!
+        psTrace("psModules.config", 6, "Camera configuration for %s exists, so moving to the front.", newName);
+        psMetadataAddMetadata(newCameras, PS_LIST_HEAD, newName, PS_META_REPLACE, NULL, oldCam);
+        psFree(newName);
+        return true;
+    }
+
+    psMetadata *new = psMetadataCopy(NULL, camera); // Copy of the camera description
+
+    // ** 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.md; // 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");
+        }
+
+        // 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!
+    psTrace("psModules.config", 6, "Generated new camera configuration for %s.", newName);
+    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_branches/psModules.20240412/src/config/pmConfigCamera.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfigCamera.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfigCamera.h	(revision 42651)
@@ -0,0 +1,70 @@
+/*  @file pmConfigCamera.h
+ *  @brief Camera Configuration functions
+ *
+ *  @author Paul Price, IfA
+ *  @author Eugene Magnier, IfA
+ *
+ *  @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-08-06 03:40:45 $
+ *  Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONFIG_CAMERA_H
+#define PM_CONFIG_CAMERA_H
+
+/// @addtogroup Config Configuration System
+/// @{
+
+// Return the name of the original ("root") camera
+//
+// The root name is the name of the camera before it was made into a derivative (e.g., skycell, chip, fpa).
+psString pmConfigCameraRootName(const char *name // Name of camera
+    );
+
+// Return the name of the Skycell derivative camera
+psString pmConfigCameraSkycellName(const char *name // Name of camera
+    );
+
+// Return the name of the Chip derivative camera
+psString pmConfigCameraChipName(const char *name // Name of camera
+    );
+
+// Return the name of the FPA derivative camera
+psString pmConfigCameraFPAName(const char *name // Name of camera
+    );
+
+// 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_branches/psModules.20240412/src/config/pmConfigCommand.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfigCommand.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfigCommand.c	(revision 42651)
@@ -0,0 +1,51 @@
+#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);
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // Connection details
+    psMetadataItem *server = pmConfigUserSite(config, "DBSERVER",   PS_DATA_STRING);
+    psMetadataItem *user   = pmConfigUserSite(config, "DBUSER",     PS_DATA_STRING);
+    psMetadataItem *pass   = pmConfigUserSite(config, "DBPASSWORD", PS_DATA_STRING);
+    psMetadataItem *name   = pmConfigUserSite(config, "DBNAME",     PS_DATA_STRING);
+
+    if (!server || !user || !pass || !name) {
+        psWarning("Cannot find DBSERVER/DBUSER/DBPASSWORD/DBNAME in user or site configuration: "
+                  "unable to connect to database.");
+        psErrorClear();
+        return NULL;
+    }
+
+    psStringAppend(command, " -dbserver %s -dbname %s -dbuser %s -dbpassword %s",
+                   server->data.str, name->data.str, user->data.str, pass->data.str);
+
+    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_DATA_S32);
+        psStringAppend(command, " -trace %s %d", item->name, item->data.S32);
+    }
+    psFree(iter);
+    psFree(levels);
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/config/pmConfigCommand.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfigCommand.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfigCommand.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/config/pmConfigDump.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfigDump.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfigDump.c	(revision 42651)
@@ -0,0 +1,168 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmFPALevel.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmConfigCamera.h"
+#include "pmErrorCodes.h"
+
+#include "pmConfigDump.h"
+
+// Cull entries in the metadata, ignoring the ones listed.
+static bool configCull(psMetadata *md,  // Configuration metadata from which to cull
+                       const psArray *list // List of items NOT to cull
+    )
+{
+    PS_ASSERT_METADATA_NON_NULL(md, false);
+    PS_ASSERT_ARRAY_NON_NULL(list, false);
+
+    psHash *keep = psHashAlloc(list->n); // Hash with strings to keep
+    for (int i = 0; i < keep->n; i++) {
+        psHashAdd(keep, list->data[i], list->data[i]);
+    }
+
+    psMetadataIterator *iter = psMetadataIteratorAlloc(md, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item;               // Item from iteration
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        if (!psHashLookup(keep, item->name)) {
+            psMetadataRemoveKey(md, item->name);
+        }
+    }
+    psFree(iter);
+
+    psFree(keep);
+
+    return true;
+}
+
+bool pmConfigRecipesCull(pmConfig *config, const char *save)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    if (!save || strlen(save) == 0) {
+        return true;
+    }
+
+    psArray *keep = psStringSplitArray(save, " ,;", false); // List of items to keep
+
+    if (!configCull(config->recipes, keep)) {
+        psError(psErrorCodeLast(), false, "Unable to cull system recipes.");
+        psFree(keep);
+        return false;
+    }
+    psFree(keep);
+
+    // Need to cull recipes from all cameras as well
+    psMetadata *cameras = psMetadataLookupMetadata(NULL, config->system, "CAMERAS"); // Known cameras
+    if (!cameras) {
+        psError(PM_ERR_CONFIG, false, "Unable to find CAMERAS in the system configuration.\n");
+        return false;
+    }
+
+    psMetadataIterator *iter = psMetadataIteratorAlloc(cameras, PS_LIST_HEAD, NULL); // Iterator for cameras
+    psMetadataItem *item;               // Item from iteration
+    keep = psArrayAllocEmpty(1);
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        psAssert(item->type == PS_DATA_METADATA, "Should only be metadata types on the camera list.");
+        psMetadata *camera = item->data.md; // Camera configuration
+        bool mdok;                      // Status of MD lookup
+        psMetadata *recipes = psMetadataLookupMetadata(&mdok, camera, "RECIPES"); // Recipe overrides
+        if (!recipes) {
+            continue;
+        }
+        if (!configCull(recipes, keep)) {
+            psError(psErrorCodeLast(), false, "Unable to cull recipes for camera %s", item->name);
+            psFree(iter);
+            psFree(keep);
+            return false;
+        }
+    }
+    psFree(iter);
+    psFree(keep);
+
+    return true;
+}
+
+bool pmConfigCamerasCull(pmConfig *config, const char *additional)
+{
+      PS_ASSERT_PTR_NON_NULL(config, false);
+
+      psMetadata *cameras = psMetadataLookupMetadata(NULL, config->system, "CAMERAS"); // Known cameras
+      if (!cameras) {
+          psError(PM_ERR_CONFIG, false, "Unable to find CAMERAS in the system configuration.\n");
+          return NULL;
+      }
+
+      psArray *keep = NULL;             // List of cameras to keep
+      if (additional) {
+          keep = psStringSplitArray(additional, " ,;", false);
+      } else {
+          keep = psArrayAllocEmpty(1);
+      }
+      psArrayAdd(keep, 1, config->cameraName);
+
+      int numKeep = keep->n;            // Number of cameras to keep
+      for (int i = 0; i < numKeep; i++) {
+          psString orig = keep->data[i];// Original name
+          psString root = pmConfigCameraRootName(orig); // Camera root name
+          psString chip = pmConfigCameraChipName(config->cameraName); // Chip-mosaicked name
+          psString fpa  = pmConfigCameraFPAName(config->cameraName); // FPA-mosaicked name
+          psString sky  = pmConfigCameraSkycellName(config->cameraName); // Skycell name
+
+          // Just in case we weren't given the root name
+          psFree(keep->data[i]);
+          keep->data[i] = root;
+
+          psArrayAdd(keep, 1, chip);
+          psArrayAdd(keep, 1, fpa);
+          psArrayAdd(keep, 1, sky);
+
+          psFree(chip);
+          psFree(fpa);
+          psFree(sky);
+      }
+
+      bool result = configCull(cameras, keep); // Result of culling
+      psFree(keep);
+
+      return result;
+}
+
+
+bool pmConfigDump(const pmConfig *config, const char *filename)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_STRING_NON_EMPTY(filename, false);
+
+    psString resolved = pmConfigConvertFilename(filename, config, true, false); // Resolved filename
+    if (!resolved) {
+        psError(psErrorCodeLast(), false, "Unable to create file for configuration dump: %s", filename);
+        return false;
+    }
+
+    // check for Metadata compression options:
+    char *compressMode = NULL;
+    bool status = false;
+    if (config->camera) {
+	compressMode = psMetadataLookupStr(&status, config->camera, "METADATA.COMPRESSION");
+    }
+
+    if (!psMetadataConfigWrite(config->user, resolved, compressMode)) {
+        psError(psErrorCodeLast(), false, "Unable to dump configuration to %s", filename);
+        psFree(resolved);
+        return false;
+    }
+
+    psFree(resolved);
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/config/pmConfigDump.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfigDump.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfigDump.h	(revision 42651)
@@ -0,0 +1,39 @@
+/*  @file pmConfigDump.h
+ *  @brief Configuration dumping function
+ *
+ *  @author Paul Price, IfA
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-09-05 22:41:58 $
+ *  Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONFIG_DUMP_H
+#define PM_CONFIG_DUMP_H
+
+#include <pmConfig.h>
+#include <pmFPA.h>
+
+/// @addtogroup Config Configuration System
+/// @{
+
+/// Cull recipes from the configuration file, apart from the ones listed
+bool pmConfigRecipesCull(pmConfig *config, ///< Configuration
+                         const char *save ///< List of recipes to save, comma-separated
+    );
+
+/// Cull cameras from the configuration file, apart from the one in use
+bool pmConfigCamerasCull(pmConfig *config, ///< Configuration
+                         const char *additional ///< List of additional cameras to save, comma-separated
+    );
+
+/// Dump the configuration to a file
+///
+bool pmConfigDump(const pmConfig *config, ///< Configuration to dump
+                  const char *filename    ///< Output file name
+    );
+
+
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/config/pmConfigMask.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfigMask.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfigMask.c	(revision 42651)
@@ -0,0 +1,612 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfigMask.h"
+
+// Structure to hold the properties of a mask value
+typedef struct {
+    char *badMaskName;                  // name for "bad" (i.e., mask me please) pixels
+    char *fallbackName;                 // Fallback name in case a bad mask name is not defined
+    psImageMaskType defaultMaskValue;   // Default value in case a bad mask name and its fallback are not defined
+    bool isBad; // include this value as part of the MASK.VALUE entry (generically bad)
+} pmConfigMaskInfo;
+
+static pmConfigMaskInfo masks[] = {
+    // Features of the detector
+    { "DETECTOR",  NULL,       0x01, true  }, // Something is wrong with the detector
+    { "FLAT",      "DETECTOR", 0x01, true  }, // Pixel doesn't flat-field properly
+    { "DARK",      "DETECTOR", 0x01, true  }, // Pixel doesn't dark-subtract properly
+    { "BLANK",     "DETECTOR", 0x01, true  }, // Pixel doesn't contain valid data
+    { "CTE",       "DETECTOR", 0x01, true  }, // Pixel has poor CTE
+    { "BURNTOOL",  NULL,       0x04, false }, // Pixel has been touched by burntool
+    // Invalid signal ranges
+    { "SAT",       NULL,       0x02, true  }, // Pixel is saturated or non-linear
+    { "LOW",       "SAT",      0x02, true  }, // Pixel is low
+    { "SUSPECT",   NULL,       0x04, false }, // Pixel is suspected of being bad
+    // Non-astronomical structures
+    { "CR",        NULL,       0x08, true  }, // Pixel contains a cosmic ray
+    { "SPIKE",     NULL,       0x08, false  }, // Pixel contains a diffraction spike
+    { "GHOST",     NULL,       0x08, false  }, // Pixel contains an optical ghost
+    { "STREAK",    NULL,       0x08, false  }, // Pixel contains a streak
+    { "CROSSTALK", NULL,       0x08, false  }, // Pixel contains crosstalk data
+    { "STARCORE",  NULL,       0x08, false  }, // Pixel contains a bright star core
+    // Effects of convolution and interpolation
+    { "CONV.BAD",  NULL,       0x02, true  }, // Pixel is bad after convolution with a bad pixel
+    { "CONV.POOR", NULL,       0x04, false }, // Pixel is poor after convolution with a bad pixel
+};
+
+// The functions in this file do not force the recipe or header values to be stored as the same
+// type as psImageMaskType : they only check that the given values will fit in the space
+// provided by psImageMaskType.  This should allow some backwards compatibility (old 8-bit
+// masks will work with a 16-bit system), but will catch unhandled conflicts (trying to fit 16
+// bits in 8-bits of space).
+
+// XXX this file does not have psError vs psWarning worked out correctly.  some of the
+// failure modes should result in errors, not just warnings.
+
+// pmConfigMaskSetInMetadata examines named mask values and set the bits for maskValue and
+// markValue.  Ensures that the below-named mask values are set, and calculates the mask value
+// to catch all of the mask values marked as 'bad'.  Supplies the fallback name if the primary
+// name is not found, or the default values if the fallback name is not found.
+
+bool pmConfigMaskSetInMetadata(psImageMaskType *outMaskValue, // Value of MASK.VALUE, returned
+                               psImageMaskType *outMarkValue, // Value of MARK.VALUE, returned
+                               psMetadata *source  // Source of mask bits
+    )
+{
+    PS_ASSERT_METADATA_NON_NULL(source, false);
+
+    // Ensure all the bad mask names exist, and set the value to catch all bad pixels
+    psImageMaskType maskValue = 0;           // Value to mask to catch all the bad pixels
+    psImageMaskType allMasks = 0;            // Value to mask to catch all masked bits (to set MARK)
+
+    int nMasks = sizeof (masks) / sizeof (pmConfigMaskInfo);
+
+    for (int i = 0; i < nMasks; i++) {
+        bool mdok;                      // Status of MD lookup
+        psImageMaskType value = psMetadataLookupImageMaskFromGeneric(&mdok, source, masks[i].badMaskName); // Value of mask
+        if (!mdok) {
+            psWarning ("problem with mask value %s\n", masks[i].badMaskName);
+        }
+
+        if (!value) {
+            if (masks[i].fallbackName) {
+                value = psMetadataLookupImageMaskFromGeneric(&mdok, source, masks[i].fallbackName);
+            }
+            if (!value) {
+                value = masks[i].defaultMaskValue;
+            }
+            psMetadataAddImageMask(source, PS_LIST_TAIL, masks[i].badMaskName, PS_META_REPLACE, NULL, value);
+        }
+        if (masks[i].isBad) {
+            maskValue |= value;
+        }
+        allMasks |= value;
+    }
+
+    // search for an unset bit to use for MARK:
+    psImageMaskType markValue = 0x00;
+    psImageMaskType markTrial = 0x01;
+
+    int nBits = sizeof(psImageMaskType) * 8;
+    for (int i = 0; !markValue && (i < nBits); i++) {
+        if (allMasks & markTrial) {
+            markTrial <<= 1;
+        } else {
+            markValue = markTrial;
+        }
+    }
+    if (!markValue) {
+        psError (PS_ERR_UNKNOWN, true, "Unable to define the MARK bit mask: all bits taken!");
+        return false;
+    }
+
+    // update the list with the results
+    psMetadataAddImageMask(source, PS_LIST_TAIL, "MASK.VALUE", PS_META_REPLACE, NULL, maskValue);
+    psMetadataAddImageMask(source, PS_LIST_TAIL, "MARK.VALUE", PS_META_REPLACE, NULL, markValue);
+
+    if (outMaskValue) {
+        *outMaskValue = maskValue;
+    }
+    if (outMarkValue) {
+        *outMarkValue = markValue;
+    }
+
+    return true;
+}
+
+// Get a mask value by name(s)
+psImageMaskType pmConfigMaskGetFromMetadata(psMetadata *source, // Source of masks
+                                            const char *masks // Mask values to get
+    )
+{
+    psImageMaskType 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
+        psImageMaskType value = psMetadataLookupImageMaskFromGeneric(&mdok, source, name);
+        if (!mdok) {
+            // Try and generate the value if we can
+            if (strcmp(name, "MASK.VALUE") == 0 || strcmp(name, "MARK.VALUE") == 0) {
+                if (!pmConfigMaskSetInMetadata(NULL, NULL, source)) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to set mask bits.");
+                    return 0;
+                }
+                value = psMetadataLookupImageMaskFromGeneric(&mdok, source, name);
+                psAssert(mdok, "Should have generated mask value");
+            } else {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, false, "Unable to find mask value for %s", name);
+                psFree(names);
+                return 0;
+            }
+        }
+        mask |= value;
+    }
+    psFree(names);
+
+    return mask;
+}
+
+// lookup an image mask value by name from a psMetadata, without requiring the entry to
+// be of type psImageMaskType, but verifying that it will fit in psImageMaskType
+psImageMaskType psMetadataLookupImageMaskFromGeneric(bool *status, const psMetadata *md, const char *name)
+{
+    if (!md) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Metadata is NULL.");
+        if (status) {
+            *status = false;
+        }
+        return 0;
+    }
+    if (!name) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Keyword is NULL.");
+        if (status) {
+            *status = false;
+        }
+        return 0;
+    }
+    *status = true;
+
+    // select the mask bit name from the header
+    psMetadataItem *item = psMetadataLookup(md, name);
+    if (!item) {
+        if (status) {
+            *status = false;
+        } else {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, false, "Unable to find keyword %s when parsing mask", name);
+        }
+        return 0;
+    }
+
+    // the value may be any of the U8, U16, U32, U64 types : accept the value regardless of type size
+    psU64 fullValue = 0;
+    switch (item->type) {
+      case PS_DATA_U8:
+        fullValue = item->data.U8;
+        break;
+      case PS_DATA_U16:
+        fullValue = item->data.U16;
+        break;
+      case PS_DATA_U32:
+        fullValue = item->data.U32;
+        break;
+      case PS_DATA_U64:
+        fullValue = item->data.U64;
+        break;
+      case PS_DATA_S8:
+        fullValue = item->data.S8;
+        break;
+      case PS_DATA_S16:
+        fullValue = item->data.S16;
+        break;
+      case PS_DATA_S32:
+        fullValue = item->data.S32;
+        break;
+      case PS_DATA_S64:
+        fullValue = item->data.S64;
+        break;
+      default:
+        if (status) {
+            *status = false;
+        } else {
+            psError(PS_ERR_BAD_PARAMETER_TYPE, true,
+                    "Mask entry %s in metadata is not of a mask type", name);
+        }
+        return 0;
+    }
+
+    // will the incoming value fit within the current image mask type?
+    if (fullValue > PS_MAX_IMAGE_MASK_TYPE) {
+        if (status) {
+            *status = false;
+        } else {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                    "Mask entry %s in metadata is larger than allowed by the psImageMaskType", name);
+        }
+        return 0;
+    }
+    psImageMaskType value = fullValue;
+    // XXX validate that value is a 2^n value?
+
+    return value;
+}
+
+// Remove from the header keywords starting with the provided string
+int pmConfigMaskRemoveHeaderKeywords(psMetadata *header, // Header from which to remove keywords
+                                     const char *start // Remove keywords that start with this string
+    )
+{
+    psString regex = NULL;              // Regular expression for keywords
+    psStringAppend(&regex, "^%s[0-9][0-9]", start);
+    psMetadataIterator *iter = psMetadataIteratorAlloc(header, PS_LIST_HEAD, regex); // Iterator
+    psFree(regex);
+    psMetadataItem *item;               // Item from iteration
+    int num = 0;                        // Number of items removed
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        psMetadataRemoveKey(header, item->name);
+        num++;
+    }
+    psFree(iter);
+    return num;
+}
+
+// look up the named mask value(s) from the MASKS recipe in the config system
+psImageMaskType pmConfigMaskGet(const char *masks, const pmConfig *config)
+{
+    psAssert(config, "Require configuration");
+    PS_ASSERT_STRING_NON_EMPTY(masks, 0);
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *recipe = psMetadataLookupMetadata(&mdok, config->recipes, "MASKS"); // The recipe
+    if (!recipe) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find MASKS recipe.");
+        return 0;
+    }
+
+    psImageMaskType mask = pmConfigMaskGetFromMetadata (recipe, masks);
+    return mask;
+}
+
+bool pmConfigMaskSet(const pmConfig *config, const char *maskName, psImageMaskType maskValue)
+{
+    psAssert(config, "Require configuration");
+    PS_ASSERT_STRING_NON_EMPTY(maskName, false);
+
+    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, "MASKS"); // The recipe
+    if (!recipe) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find MASKS recipe.");
+        return false;
+    }
+
+    bool status = psMetadataAddImageMask(recipe, PS_LIST_TAIL, maskName, PS_META_REPLACE, NULL, maskValue);
+    return status;
+}
+
+
+// replace the named masks in the recipe with values in the header:
+// replace only the names in the header in the recipe
+bool pmConfigMaskReadHeader(pmConfig *config, const psMetadata *header)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_METADATA_NON_NULL(header, false);
+
+    bool status = false;
+
+    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, "MASKS"); // The recipe
+    if (!recipe) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find MASKS recipe.");
+        return false;
+    }
+
+    // MASK.VALUE and MARK.VALUE aren't usually set in the recipe, but may be set in the header: create fake
+    // versions so that it won't complain later
+    if (!psMetadataLookup(recipe, "MASK.VALUE")) {
+        psMetadataAddImageMask(recipe, PS_LIST_TAIL, "MASK.VALUE", 0, "Bits to mask", 0);
+    }
+    if (!psMetadataLookup(recipe, "MARK.VALUE")) {
+        psMetadataAddImageMask(recipe, PS_LIST_TAIL, "MARK.VALUE", 0, "Bits for marking", 0);
+    }
+
+    // How many mask values do we need to read?  We raise an error if this is not found,
+    // unless the MASK.FORCE is set to true in the camera config
+    int nMask = psMetadataLookupS32(&status, header, "MSKNUM");
+    if (!status) {
+        if (psMetadataLookupBool(&status, config->camera, "MASK.FORCE")) {
+            psWarning("No mask values in header.  Assuming MASKS recipe is accurate because of MASK.FORCE");
+            return true;
+        }
+        psError(PS_ERR_UNKNOWN, true, "Unable to find MSKNUM in header.");
+        return false;
+    }
+
+    // Loop over the expected number of header mask names.  For each named mask value, there
+    // should be a pair of header keywords, one for the name and one for the value
+    char namekey[80];                   // Keyword name for symbolic name of mask entry
+    char valuekey[80];                  // Keyword name for value of mask entry
+    for (int i = 0; i < nMask; i++) {
+        snprintf(namekey,  64, "MSKNAM%02d", i);
+        snprintf(valuekey, 64, "MSKVAL%02d", i);
+
+        char *name = psMetadataLookupStr(&status, header, namekey);
+        if (!status || !name) {
+            psWarning("Unable to find header keyword %s when parsing mask", namekey);
+            continue;
+        }
+
+        psImageMaskType headerValue = psMetadataLookupImageMaskFromGeneric (&status, header, valuekey);
+        if (!status) {
+            psWarning("Failed to get mask value %s from header, skipping", valuekey);
+            continue;
+        }
+
+        // since we may read multiple mask files, we need to warn (or error?) if any of the
+        // header mask values conflict with other header mask values; However, the original
+        // mask values from the recipe do not need to match the header values.
+
+        // when we add a header mask value, we will also add the NAME.ALREADY entry; check for
+        // the NAME.ALREADY entry to see if we have previously added this mask value from a
+        // header.
+
+        psString nameAlready = NULL;    // Name of key with ".ALREADY" added
+        psStringAppend(&nameAlready, "%s.ALREADY", name);
+        bool already = psMetadataLookupBool(&status, recipe, nameAlready); // Already read this one?
+
+        bool inRecipe = false;
+        psImageMaskType recipeValue = psMetadataLookupImageMaskFromGeneric (&inRecipe, recipe, name);
+        if (!inRecipe) {
+            psWarning("Mask value %s is not defined in the recipe", name);
+        }
+
+        if (already) {
+            assert (inRecipe); // XXX makes no sense for NAME.ALREADY to be in without NAME
+            if (recipeValue != headerValue) {
+                psWarning("New mask header value does not match previously loaded entry: %x vs %x", headerValue, recipeValue);
+                psMetadataAddImageMask(recipe, PS_LIST_TAIL, name, PS_META_REPLACE, "Bitmask bit value", headerValue);
+                // XXX alternatively, error here
+            }
+        } else {
+            psMetadataAddBool(recipe, PS_LIST_TAIL, nameAlready, 0, "Already read this mask value", true);
+            psMetadataAddImageMask(recipe, PS_LIST_TAIL, name, PS_META_REPLACE, "Bitmask bit value", headerValue);
+        }
+
+        psFree(nameAlready);
+    }
+
+    return true;
+}
+
+// write the named mask bits to the header
+bool pmConfigMaskWriteHeader(const pmConfig *config, psMetadata *header)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_METADATA_NON_NULL(header, false);
+
+    pmConfigMaskRemoveHeaderKeywords(header, "MSKNAM");
+    pmConfigMaskRemoveHeaderKeywords(header, "MSKVAL");
+    if (psMetadataLookup(header, "MSKNUM")) {
+        psMetadataRemoveKey(header, "MSKNUM");
+    }
+
+    char namekey[80];
+    char valuekey[80];
+
+    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, "MASKS"); // The recipe
+    if (!recipe) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find MASKS recipe.");
+        return false;
+    }
+
+    int nMask = 0;
+
+    psMetadataIterator *iter = psMetadataIteratorAlloc(recipe, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item;               // Item from iteration
+    while ((item = psMetadataGetAndIncrement(iter))) {
+
+        // XXX this would give a false positive for mask which include '.ALREADY' in their names
+        char *ptr = strstr (item->name, ".ALREADY");
+        if (ptr) continue;
+
+        psU64 fullValue = 0;
+        switch (item->type) {
+          case PS_DATA_U8:
+            fullValue = item->data.U8;
+            break;
+          case PS_DATA_U16:
+            fullValue = item->data.U16;
+            break;
+          case PS_DATA_U32:
+            fullValue = item->data.U32;
+            break;
+          case PS_DATA_U64:
+            fullValue = item->data.U64;
+            break;
+          default:
+            psWarning("mask recipe entry %s is not a bit value\n", item->name);
+            continue;
+        }
+        assert (fullValue <= PS_MAX_IMAGE_MASK_TYPE); // this should have been asserted on read...
+
+        snprintf(namekey,  64, "MSKNAM%02d", nMask);
+        snprintf(valuekey, 64, "MSKVAL%02d", nMask);
+
+        psMetadataAddStr(header, PS_LIST_TAIL, namekey, 0, "Bitmask bit name", item->name);
+        psMetadataAddImageMask(header, PS_LIST_TAIL, valuekey, 0, "Bitmask bit value", fullValue);
+        nMask++;
+    }
+    psFree(iter);
+
+    psMetadataAddS32(header, PS_LIST_TAIL, "MSKNUM", 0, "Bitmask bit count", nMask);
+    return true;
+}
+
+
+bool pmConfigMaskSetBits(psImageMaskType *outMaskValue, psImageMaskType *outMarkValue, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, "MASKS"); // The recipe
+    if (!recipe) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find MASKS recipe.");
+        return false;
+    }
+
+    bool status = pmConfigMaskSetInMetadata(outMaskValue, outMarkValue, recipe);
+    return status;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// FPA version of mask functions.  These are not ready to go yet.
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#if 0
+
+bool pmFPAMaskWriteHeader(psMetadata *header, const pmFPA *fpa)
+{
+    PS_ASSERT_METADATA_NON_NULL(header, false);
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+
+    // clear out the header of the associated keywords:
+    pmConfigMaskRemoveHeaderKeywords(header, "MSKNAM");
+    pmConfigMaskRemoveHeaderKeywords(header, "MSKVAL");
+    if (psMetadataLookup(header, "MSKNUM")) {
+        psMetadataRemoveKey(header, "MSKNUM");
+    }
+
+    char namekey[80], valuekey[80];     // Mask name and mask value header keywords
+    int numMask = 0;                    // Number of mask entries
+
+    psMetadataIterator *iter = psMetadataIteratorAlloc(fpa->masks, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item;               // Item from iteration
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        if (item->type != PS_TYPE_IMAGE_MASK) {
+            psWarning("mask recipe entry %s is not of a mask type (%x)", item->name, item->type);
+            continue;
+        }
+
+        snprintf(namekey,  64, "MSKNAM%02d", numMask);
+        snprintf(valuekey, 64, "MSKVAL%02d", numMask);
+
+        psMetadataAddStr(header, PS_LIST_TAIL, namekey, 0, "Bitmask bit name", item->name);
+        psMetadataAddImageMask(header, PS_LIST_TAIL, valuekey, 0, "Bitmask bit value", item->data.PS_TYPE_IMAGE_MASK_DATA);
+        numMask++;
+    }
+    psFree(iter);
+
+    return psMetadataAddS32(header, PS_LIST_TAIL, "MSKNUM", 0, "Number of named mask entries", numMask);
+}
+
+bool pmFPAMaskSetValues(psImageMaskType *outMaskValue, psImageMaskType *outMarkValue, pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+
+    return maskSetValues(outMaskValue, outMarkValue, fpa->masks);
+}
+
+psImageMaskType pmFPAMaskGet(const pmFPA *fpa, const char *masks, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, 0);
+    PS_ASSERT_STRING_NON_EMPTY(masks, 0);
+    PS_ASSERT_PTR_NON_NULL(config, 0);
+
+    if (fpa->masks) {
+        return pmConfigMaskGetFromMetadata(fpa->masks, masks);
+    }
+    return pmConfigMaskGet(masks, config);
+}
+
+bool pmFPAMaskSet(pmFPA *fpa, const char *maskName, psImageMaskType maskValue)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, 0);
+    PS_ASSERT_STRING_NON_EMPTY(maskName, false);
+
+    if (!fpa->masks) {
+        fpa->masks = psMetadataAlloc();
+    }
+    return psMetadataAddImageMask(fpa->masks, PS_LIST_TAIL, maskName, PS_META_REPLACE, NULL, maskValue);
+}
+
+bool pmFPAMaskReadHeader(pmFPA *fpa, const psMetadata *header, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_METADATA_NON_NULL(header, false);
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    if (!fpa->masks) {
+        fpa->masks = psMetadataAlloc();
+    }
+
+    bool mdok;                          // Status of MD lookup
+    int numMask = psMetadataLookupS32(&mdok, header, "MSKNUM"); // Number of mask values in header
+    if (!mdok) {
+        if (psMetadataLookupBool(&mdok, config->camera, "MASK.FORCE")) {
+            psWarning("No mask values in header.  Assuming MASKS recipe is accurate because of MASK.FORCE");
+            numMask = 0;
+        } else {
+            psError(PS_ERR_UNKNOWN, true, "Unable to find MSKNUM in header.");
+            return false;
+        }
+    }
+
+    char namekey[80];                   // Keyword name for symbolic name of mask entry
+    char valuekey[80];                  // Keyword name for value of mask entry
+    for (int i = 0; i < numMask; i++) {
+        snprintf(namekey,  64, "MSKNAM%02d", i);
+        snprintf(valuekey, 64, "MSKVAL%02d", i);
+
+        char *name = psMetadataLookupStr(&mdok, header, namekey);
+        if (!mdok || !name) {
+            psWarning("Unable to find header keyword %s when parsing mask", namekey);
+            continue;
+        }
+        psImageMaskType bit = psMetadataLookupImageMask(&mdok, header, valuekey);
+        if (!mdok) {
+            psWarning("Unable to find header keyword %s when parsing mask", namekey);
+            continue;
+        }
+
+        // XXX validate that bit is a 2^n value?
+
+        psMetadataItem *item = psMetadataLookup(fpa->masks, name); // Item in recipe with current value
+        if (item) {
+            psAssert(item->type == PS_TYPE_IMAGE_MASK, "Mask entry %s is not of a mask type (%x)",
+                     name, item->type);
+            if (item->data.PS_TYPE_IMAGE_MASK_DATA != bit) {
+                psWarning("New mask entry %s doesn't match previously loaded entry: %x vs %x",
+                          name, bit, item->data.PS_TYPE_IMAGE_MASK_DATA);
+            }
+        } else {
+            psMetadataAddImageMask(fpa->masks, PS_LIST_TAIL, name, 0, NULL, bit);
+        }
+    }
+
+    // Now copy everything else from the recipe
+    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, "MASKS"); // The recipe
+    if (!recipe) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find MASKS recipe.");
+        return false;
+    }
+
+    psMetadataIterator *iter = psMetadataIteratorAlloc(recipe, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item;               // Item from iteration
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        if (item->type != PS_TYPE_IMAGE_MASK) {
+            psWarning("Recipe mask entry %s is not of a mask type (%x)", item->name, item->type);
+            continue;
+        }
+        if (!psMetadataLookup(fpa->masks, item->name)) {
+            psMetadataAddImageMask(fpa->masks, PS_LIST_TAIL, item->name, 0, item->comment,
+                            item->data.PS_TYPE_IMAGE_MASK_DATA);
+        }
+    }
+    psFree(iter);
+
+    return true;
+}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/config/pmConfigMask.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfigMask.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfigMask.h	(revision 42651)
@@ -0,0 +1,65 @@
+/*  @file pmConfigMask.h
+ *  @brief Mask configuration functions
+ *
+ *  @author Paul Price, IfA
+ *
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-01-27 06:39:38 $
+ *  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
+/// @{
+
+// pmConfigMaskSetInMetadata examines named mask values and set the bits for maskValue and
+// markValue.  Ensures that the below-named mask values are set, and calculates the mask value
+// to catch all of the mask values marked as 'bad'.  Supplies the fallback name if the primary
+// name is not found, or the default values if the fallback name is not found.
+bool pmConfigMaskSetInMetadata(psImageMaskType *outMaskValue, // Value of MASK.VALUE, returned
+                               psImageMaskType *outMarkValue, // Value of MARK.VALUE, returned
+                               psMetadata *source  // Source of mask bits
+  );
+
+
+// Get a mask value by name(s)
+psImageMaskType pmConfigMaskGetFromMetadata(psMetadata *source, // Source of masks
+                                            const char *masks // Mask values to get
+  );
+
+
+// lookup an image mask value by name from a psMetadata, without requiring the entry to
+// be of type psImageMaskType, but verifying that it will fit in psImageMaskType
+psImageMaskType psMetadataLookupImageMaskFromGeneric (bool *status, const psMetadata *md, const char *name);
+
+// Remove from the header keywords starting with the provided string
+int pmConfigMaskRemoveHeaderKeywords(psMetadata *header, // Header from which to remove keywords
+                                     const char *start // Remove keywords that start with this string
+  );
+
+/// Return a mask value given a list of symbolic names
+///
+/// The mask values are derived from the MASKS recipe
+psImageMaskType pmConfigMaskGet(const char *masks, ///< List of symbolic names, space/comma delimited
+                           const pmConfig *config ///< Configuration
+    );
+
+bool pmConfigMaskSet(const pmConfig *config, const char *maskName, psImageMaskType maskValue);
+
+// replace the named masks in the recipe with values in the header:
+// replace only the names in the header in the recipe
+bool pmConfigMaskReadHeader(pmConfig *config, const psMetadata *header);
+
+// write the named mask bits to the header
+bool pmConfigMaskWriteHeader(const pmConfig *config, psMetadata *header);
+
+bool pmConfigMaskSetBits(psImageMaskType *outMaskValue, psImageMaskType *outMarkValue, const pmConfig *config);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/config/pmConfigRecipeValue.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfigRecipeValue.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfigRecipeValue.c	(revision 42651)
@@ -0,0 +1,90 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+
+
+psMetadataItem *pmConfigRecipeValueByView(const pmConfig *config, // Configuration
+                                          const char *recipeName, // Name of recipe
+                                          const char *valueName,  // Name of value in recipe
+                                          const pmFPA *fpa,       // FPA of interest
+                                          const pmFPAview *view   // View to component of interest
+    )
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_METADATA_NON_NULL(config->recipes, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(recipeName, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(valueName, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+    PS_ASSERT_PTR_NON_NULL(view, NULL);
+
+    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, recipeName); // Recipe of interest
+    if (!recipe) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find recipe %s", recipeName);
+        return NULL;
+    }
+    psMetadata *format = config->format;
+    
+    psMetadataItem *fpaItem;
+    fpaItem = psMetadataLookup(format, valueName);
+    if (!fpaItem) {
+      fpaItem = psMetadataLookup(recipe, valueName); // Value for FPA or menu of chips
+    }
+    
+    if (!fpaItem) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find value %s in recipe %s", valueName, recipeName);
+        return NULL;
+    }
+    if (view->chip == -1 && view->cell == -1) {
+        // Default value only
+        return fpaItem;
+    }
+    if (fpaItem->type != PS_DATA_METADATA) {
+        // Single value appropriate for all
+        return fpaItem;
+    }
+
+    psMetadata *menu = fpaItem->data.md;   // Menu with values by chip
+
+    pmChip *chip = pmFPAviewThisChip(view, fpa);                                   // Chip of interest
+    const char *chipName = psMetadataLookupStr(NULL, chip->concepts, "CHIP.NAME"); // Name of chip
+
+    psMetadataItem *chipItem = psMetadataLookup(menu, chipName); // Value for chip of menu of cells
+    if (!chipItem) {
+        psError(PS_ERR_UNEXPECTED_NULL, true,
+                "Chip %s does not have a menu entry in %s within recipe %s.",
+                  chipName, valueName, recipeName);
+        return NULL;
+    }
+    if (view->cell == -1) {
+        // Default value for chip
+        return chipItem;
+    }
+    if (chipItem->type != PS_DATA_METADATA) {
+        // Single value appropriate for all
+        return chipItem;
+    }
+
+    menu = chipItem->data.md;   // Menu with values by chip
+
+    pmCell *cell = pmFPAviewThisCell(view, fpa);                                   // Cell of interest
+    const char *cellName = psMetadataLookupStr(NULL, cell->concepts, "CELL.NAME"); // Name of cell
+
+    psMetadataItem *cellItem = psMetadataLookup(menu, cellName); // Value for cell of menu of cells
+    if (!cellItem) {
+        psError(PS_ERR_UNEXPECTED_NULL, true,
+                "Chip %s, cell %s does not have a menu entry in %s within recipe %s --- using default.",
+                chipName, cellName, valueName, recipeName);
+        return NULL;
+    }
+
+    return cellItem;
+}
Index: /branches/eam_branches/psModules.20240412/src/config/pmConfigRecipeValue.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfigRecipeValue.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfigRecipeValue.h	(revision 42651)
@@ -0,0 +1,18 @@
+#ifndef PM_CONFIG_RECIPE_VALUE_H
+#define PM_CONFIG_RECIPE_VALUE_H
+
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+
+/// Return a recipe value according to the provided view (i.e., chip- and/or cell-dependent)
+psMetadataItem *pmConfigRecipeValueByView(const pmConfig *config, // Configuration
+                                          const char *recipeName, // Name of recipe
+                                          const char *valueName,  // Name of value in recipe
+                                          const pmFPA *fpa,       // FPA of interest
+                                          const pmFPAview *view   // View to component of interest
+    );
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/config/pmConfigRecipes.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfigRecipes.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfigRecipes.c	(revision 42651)
@@ -0,0 +1,614 @@
+#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 "pmConfig.h"
+#include "pmConfigRecipes.h"
+
+static bool loadRecipeSystem(bool *status, pmConfig *config);
+static bool loadRecipeCamera(bool *status, pmConfig *config, psMetadata *source);
+static bool loadRecipeSymbols(bool *status, pmConfig *config, pmRecipeSource source);
+static bool loadRecipeFromArguments(bool *status, pmConfig *config);
+static bool loadRecipeOptions(bool *status, pmConfig *config);
+static bool mergeRecipeCamera(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->system, 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
+// system recipes.  This command strips the argv elements it uses from the argv list.
+bool pmConfigReadRecipes(pmConfig *config, pmRecipeSource source)
+{
+    bool status;
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // Read the recipe file names from the system configuration and camera configuration
+    // It is an error for config->system:recipes not to exist.  all programs install their
+    // master recipe files in the system:recipe location when they are built.
+    psAssert(config->system, "base config data defined");
+    if (!config->recipes || (source & PM_RECIPE_SOURCE_SYSTEM)) {
+        if (!loadRecipeSystem(&status, config)) {
+            psError(PS_ERR_IO, false, "Failed to read recipes from system config");
+            return false;
+        }
+        psTrace ("psModules.config", 3, "read recipes from system 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 {
+            psTrace ("psModules.config", PS_LOG_DETAIL, "no recipe supplied by camera config");
+        }
+    }
+
+    // merge camera and sytem recipes, apply recipes loaded into config->arguments based on command-line
+    // arguments
+    if (config->arguments && (source & PM_RECIPE_SOURCE_CL)) {
+
+        // update the system-level recipes with the symbolically-defined recipes
+        if (!loadRecipeSymbols(&status, config, PM_RECIPE_SOURCE_SYSTEM)) {
+            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 {
+            psTrace ("psModules.config", PS_LOG_DETAIL, "no recipe supplied by symbolic reference");
+        }
+
+        // merge the SYSTEM and CAMERA recipes
+        if (!mergeRecipeCamera(&status, config)) {
+            psError(PS_ERR_IO, false, "Failed to read recipes from symbolic references");
+            return false;
+        }
+        psTrace ("psModules.config", PS_LOG_DETAIL, "merged camera recipes with system recipes");
+
+        // load recipe-files specified on the command line
+        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 {
+            psTrace ("psModules.config", PS_LOG_DETAIL, "no recipe supplied on command-line arguments");
+        }
+
+        // update the system-level recipes with the symbolically-defined recipes
+        if (!loadRecipeSymbols(&status, config, PM_RECIPE_SOURCE_CAMERA)) {
+            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 {
+            psTrace ("psModules.config", PS_LOG_DETAIL, "no recipe supplied by symbolic reference");
+        }
+
+        // override any specific values with values from the command line
+        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 {
+            psTrace ("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 recipe options onto config->arguments:OPTIONS
+    // 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") || !strcasecmp(argv[argNum+1], "t") ||
+                !strcasecmp(argv[argNum+1], "1")) {
+                psMetadataAddBool (recipe, PS_LIST_TAIL, key, PS_META_REPLACE, "", true);
+            } else if (!strcasecmp(argv[argNum+1], "false") || !strcasecmp(argv[argNum+1], "f") ||
+                       !strcasecmp(argv[argNum+1], "0")) {
+                psMetadataAddBool (recipe, PS_LIST_TAIL, key, PS_META_REPLACE, "", false);
+            } else {
+                psWarning("Couldn't interpret option for %s (%s) --- ignored", argv[argNum], argv[argNum+1]);
+            }
+            valid = true;
+        }
+        psFree (words);
+        psAssert (valid, "invalid flag: may be: -D, -Df, -Di, -Db\n");
+
+        psArgumentRemove (argNum, argc, argv);
+        psArgumentRemove (argNum, argc, argv);
+    }
+    return true;
+}
+
+// examine command-line arguments for -recipe RECIPE SYMBOLIC-NAME or -recipe-file RECIPE FILENAME
+// in the first case, the symbolic lookup is saved on config->recipeSymbols
+//   for later interpolation (pmConfigReadRecipes with option CL)
+// in the second case, read as metadata and save on config->arguments with name = KEY
+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
+
+    // -recipe-file: read recipe from file
+    while ((argNum = psArgumentGet(*argc, argv, "-recipe-file")) > 0) {
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum + 1 >= *argc) {
+            psError(PS_ERR_IO, false,
+                    "-recipe command-line switch provided without the required recipe and filename\n");
+            return false;
+        }
+
+        psString recipeName = psStringCopy(argv[argNum]); // Name of the recipe
+        psArgumentRemove(argNum, argc, argv);
+        psString filename = psStringCopy(argv[argNum]); // Filename for the recipe
+        psArgumentRemove(argNum, argc, argv);
+
+        psMetadata *recipe = NULL;      // Recipe from file
+        if (!pmConfigFileRead(&recipe, filename, "recipe")) {
+            psError(PS_ERR_IO, false, "Error reading config file %s\n", filename);
+            psFree(recipeName);
+            psFree(filename);
+            return false;
+        }
+
+        psString comment = NULL;
+        psStringAppend(&comment, "Recipe added at command line from file %s", filename);
+        psMetadataAdd(recipes, PS_LIST_TAIL, recipeName, PS_DATA_METADATA | PS_META_REPLACE,
+                      comment, recipe);
+        psFree(comment);
+        psFree(recipe);                 // Drop reference
+        psFree(recipeName);
+        psFree(filename);
+    }
+
+    // -recipe: read recipe from symbolic link
+    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);
+
+        // 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, NULL,
+                         recipeSource);
+
+        psTrace ("psModules.config", 3, "read recipe %s from %s", recipeName, recipeSource);
+        psFree(recipeName);
+        psFree(recipeSource);
+    }
+
+    return true;
+}
+
+// Load the recipe files for SYSTEM : REQUIRED
+static bool loadRecipeSystem(bool *status,
+                           pmConfig *config // The configuration into which to read the recipes
+    )
+{
+    assert(status);
+    assert(config);
+    *status = false;
+
+    if (!config->system) {
+        psError(PS_ERR_IO, true,
+                "The system configuration has not been read --- cannot read recipes from this location.\n");
+        config->recipesRead &= ~PM_RECIPE_SOURCE_SYSTEM;
+        return false;
+    }
+
+    if (!config->recipes) {
+        config->recipes = psMemIncrRefCounter(psMetadataLookupMetadata(NULL, config->system, "RECIPES")); // The list of recipes
+        if (!config->recipes) {
+            psError(PS_ERR_IO, false, "RECIPES not found in the system configuration\n");
+            return false;
+        }
+    }
+
+    // Read in the component recipes
+    psMetadataIterator *recipesIter = psMetadataIteratorAlloc(config->recipes, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item = NULL;        // MD item containing the filename, from recipe iteration
+    while ((item = psMetadataGetAndIncrement(recipesIter))) {
+        if (!pmConfigFileIngest(item, "recipe")) {
+            psError(PS_ERR_IO, false, "Failed to read recipe %s listed in system configuration",
+                    item->name);
+            psFree(recipesIter);
+            return false;
+        }
+    }
+    psFree(recipesIter);
+    config->recipesRead |= PM_RECIPE_SOURCE_SYSTEM;
+
+    *status = true;
+    return true;
+}
+
+// Load the recipe files for a specific CAMERA.  these are saved on recipesCamera
+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; it this entry is missing,
+    // just return true
+    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 *item = NULL;    // MD item containing the filename, from recipe iteration
+    while ((item = psMetadataGetAndIncrement(recipesIter))) {
+        if (!pmConfigFileIngest(item, "recipe")) {
+            psError(PS_ERR_IO, false, "Failed to read recipe %s listed in camera configuration",
+                    item->name);
+            return false;
+        }
+        const char *recipeName = item->name; // Name of the recipe
+        psMetadata *recipe = item->data.md; // The recipe
+
+        // the named recipe must exist at the system level
+        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);
+            return false;
+        }
+
+        // add the contents of this recipe file to config->recipesCamera
+        psMetadataAdd(config->recipesCamera, PS_LIST_TAIL, recipeName, PS_DATA_METADATA | PS_META_REPLACE,
+                      item->comment, recipe);
+    }
+    psFree(recipesIter);
+    config->recipesRead |= PM_RECIPE_SOURCE_CAMERA;
+    *status = true;
+    return true;
+}
+
+// Merge the CAMERA recipes into the SYSTEM recipes
+static bool mergeRecipeCamera(bool *status, // status variable
+                             pmConfig *config // The configuration into which to read the recipes
+    )
+{
+    assert(status);
+    assert(config);
+    *status = false;
+
+    // Copy contents of config->recipesCamera to config->recipes
+    // 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(config->recipesCamera, PS_LIST_HEAD, NULL);
+    psMetadataItem *folderItem = NULL;    // MD item containing the filename, from recipe iteration
+    while ((folderItem = psMetadataGetAndIncrement(recipesIter))) {
+        char *recipeName = folderItem->name;
+        psMetadata *recipe = folderItem->data.md;
+
+        psTrace("psModules.config", 3, "merging %s from camera with system recipes.\n", recipeName);
+
+        // type mismatch is a serious error
+        if (folderItem->type != PS_DATA_METADATA) {
+            psAbort("%s not of type METADATA", recipeName);
+        }
+
+        // the select the named recipe from the system level
+        psMetadata *current = psMetadataLookupMetadata(NULL, config->recipes, recipeName);
+        psAssert (current, "Failed to find recipe for %s in system recipe list", recipeName);
+
+        // update the contents of this recipe from the one on config->recipesCamera
+        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;
+}
+
+// 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.md; // 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
+// The configuration into which to read the recipes
+static bool loadRecipeSymbols(bool *status, pmConfig *config, pmRecipeSource source) {
+
+    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.str; // 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 in config->recipes (folder name is targetName)
+        psMetadata *folder = psMetadataLookupMetadata(&found, config->recipes, targetName);
+        psMetadata *sourceMD = psMetadataLookupMetadata(&found, folder, sourceName);
+
+        // if we find the desired symbolic name at this level, set the item comment to say "FOUND"
+        if (sourceMD) {
+          if (!psMetadataUpdate(targetMD, sourceMD)) {
+            psError(PS_ERR_IO, false, "Failed to update recipe for %s from camera recipe", targetName);
+            return false;
+          }
+          psStringAppend (&item->comment, "(FOUND)");
+        }
+
+        // if we have not found it by the camera level, we have a problem
+        if (source == PM_RECIPE_SOURCE_CAMERA) {
+          if (strstr (item->comment, "(FOUND)") == NULL) {
+            psError(PS_ERR_IO, false, "Selected symbolic name %s does not exist in recipes. Use syntax '-recipe NAME VALUE'", sourceName);
+            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"); // 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.md;
+
+        // 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_branches/psModules.20240412/src/config/pmConfigRecipes.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfigRecipes.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfigRecipes.h	(revision 42651)
@@ -0,0 +1,35 @@
+/*  @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
+
+#include <pmConfig.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_branches/psModules.20240412/src/config/pmConfigRun.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfigRun.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfigRun.c	(revision 42651)
@@ -0,0 +1,208 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmFPAfile.h"
+
+#include "pmConfigRun.h"
+
+// Get the nominated element of the configuration
+static psMetadata *configElement(psMetadata *md, // Metadata with element
+                                 const char *name, // Name of element
+                                 const char *comment // Comment for element
+                             )
+{
+    psAssert(md, "Require metadata");
+    psAssert(name, "Require name of element");
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *elem = psMetadataLookupMetadata(&mdok, md, name); // Element of interest
+    if (!elem) {
+        elem = psMetadataAlloc();
+        psMetadataAddMetadata(md, PS_LIST_HEAD, name, 0, comment, elem);
+        psFree(elem);                   // Drop reference
+    }
+    return elem;
+}
+
+// Get the RUN information from the configuration
+static psMetadata *configRun(pmConfig *config // Configuration
+                             )
+{
+    psAssert(config, "Require configuration");
+    return configElement(config->user, "RUN", "Run-time information");
+}
+
+// Add a file to a nominated metadata in the RUN information
+static bool configRunFileAdd(pmConfig *config, // Configuration
+                             const char *description, // Description of file
+                             const char *name, // Name of file
+                             const char *target // Name of metadata to which to add
+                             )
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_STRING_NON_EMPTY(description, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+    PS_ASSERT_STRING_NON_EMPTY(target, false);
+
+    psMetadata *run = configRun(config);// RUN information
+    psAssert(run, "Require run-time information");
+    psMetadata *files = configElement(run, target, "Filerules used during execution");
+    psAssert(files, "Require list of files");
+
+    psString regex = NULL;              // Regular expression for iteration
+    psStringAppend(&regex, "^%s$", description);
+    psMetadataIterator *iter = psMetadataIteratorAlloc(files, PS_LIST_HEAD, regex);
+    psFree(regex);
+    psMetadataItem *item;               // Item from iteration
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        psAssert(item->type == PS_DATA_STRING, "We only put STRING types here.");
+        if (strcmp(item->data.str, name) == 0) {
+            // It's already present
+            psFree(iter);
+            return true;
+        }
+    }
+    psFree(iter);
+
+    return psMetadataAddStr(files, PS_LIST_TAIL, description, PS_META_DUPLICATE_OK, NULL, name);
+}
+
+bool pmConfigRunFileAddRead(pmConfig *config, const pmFPAfile *file)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    return configRunFileAdd(config, file->name, file->origname ? file->origname : file->filename,
+                            "FILES.INPUT");
+}
+
+bool pmConfigRunFilenameAddRead(pmConfig *config, const char *description, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_STRING_NON_EMPTY(description, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+
+    return configRunFileAdd(config, description, name, "FILES.INPUT");
+}
+
+bool pmConfigRunFileAddWrite(pmConfig *config, const pmFPAfile *file)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    return configRunFileAdd(config, file->name, file->origname ? file->origname : file->filename,
+                            "FILES.OUTPUT");
+}
+
+bool pmConfigRunFilenameAddWrite(pmConfig *config, const char *description, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_STRING_NON_EMPTY(description, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+
+    return configRunFileAdd(config, description, name, "FILES.OUTPUT");
+}
+
+// Get an array of filenames from the nominated RUN information
+static psArray *configRunFileGet(pmConfig *config, // Configuration
+                                 const char *name, // Name of file
+                                 const char *source // Source metadata for file
+                                 )
+{
+    psMetadata *run = configRun(config);// RUN information
+    psAssert(run, "Require run-time information");
+    psMetadata *files = configElement(run, source, "Filerules used during execution");
+    psAssert(files, "Require list of files");
+
+    if (psListLength(files->list) == 0) {
+        // Can't find anything
+        return NULL;
+    }
+
+    psList *list = psListAlloc(NULL);   // List of file names
+
+    psString regex = NULL;              // Regular expression for iteration
+    psStringAppend(&regex, "^%s$", name);
+    psMetadataIterator *iter = psMetadataIteratorAlloc(files, PS_LIST_HEAD, regex);
+    psFree(regex);
+    psMetadataItem *item;               // Item from iteration
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        psAssert(item->type == PS_DATA_STRING, "We only put STRING types here.");
+        psListAdd(list, PS_LIST_TAIL, item->data.str);
+    }
+    psFree(iter);
+
+    if (psListLength(list) == 0) {
+        // Didn't find anything
+        psFree(list);
+        return NULL;
+    }
+
+    psArray *array = psListToArray(list); // Array of file names, to return
+    psFree(list);
+
+    return array;
+}
+
+
+psArray *pmConfigRunFileGet(pmConfig *config, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+
+    // Try the input and output, in turn
+    psArray *files = configRunFileGet(config, name, "FILES.INPUT"); // Files from RUN metadata
+    if (!files) {
+        files = configRunFileGet(config, name, "FILES.OUTPUT");
+    }
+
+    return files;
+}
+
+
+bool pmConfigRunCommand(pmConfig *config, int argc, char **argv)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *run = configRun(config);// Run-time information
+    psAssert(run, "Require run-time information");
+
+    psString command = NULL;
+    for (int i = 0; i < argc; i++) {
+        psStringAppend(&command, "%s ", argv[i]);
+    }
+
+    psMetadataAddStr(run, PS_LIST_TAIL, "COMMAND", PS_META_REPLACE, "Command line", command);
+    psFree(command);
+
+    return true;
+}
+
+
+bool pmConfigRunSeed(pmConfig *config, psU64 seed)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    psMetadata *run = configRun(config);// Run-time information
+    psAssert(run, "Require run-time information");
+
+    if (!seed) {
+        bool mdok;                          // Status of MD lookup
+        seed = psMetadataLookupU64(&mdok, run, "SEED");
+    }
+    if (!seed) {
+        bool mdok;                          // Status of MD lookup
+        seed = psMetadataLookupU64(&mdok, config->user, "SEED");
+    }
+
+    // seed may still be zero by this point
+    seed = psRandomSeed(seed);
+    return psMetadataAddU64(run, PS_LIST_TAIL, "SEED", PS_META_REPLACE, "Random number generator seed", seed);
+}
+
Index: /branches/eam_branches/psModules.20240412/src/config/pmConfigRun.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmConfigRun.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmConfigRun.h	(revision 42651)
@@ -0,0 +1,53 @@
+#ifndef PM_CONFIG_RUN_H
+#define PM_CONFIG_RUN_H
+
+#include <pslib.h>
+#include <pmConfig.h>
+#include <pmFPAfile.h>
+
+/// Add a file to the list of files read in the run-time information
+bool pmConfigRunFileAddRead(
+    pmConfig *config,                   ///< Configuration
+    const pmFPAfile *file               ///< File to add
+    );
+
+/// Add a filename to the list of files read in the run-time information
+bool pmConfigRunFilenameAddRead(
+    pmConfig *config,                   ///< Configuration
+    const char *description,            ///< Description of file
+    const char *name                    ///< Name of file
+    );
+
+/// Add a file to the list of files written in the run-time information
+bool pmConfigRunFileAddWrite(
+    pmConfig *config,                   ///< Configuration
+    const pmFPAfile *file               ///< File to add
+    );
+
+/// Add a filename to the list of files written in the run-time information
+bool pmConfigRunFilenameAddWrite(
+    pmConfig *config,                   ///< Configuration
+    const char *description,            ///< Description of file
+    const char *name                    ///< Name of file
+    );
+
+/// Retrieve file names for a symbolic file from the run-time information
+psArray *pmConfigRunFileGet(
+    pmConfig *config,                   ///< Configuration
+    const char *name                    ///< Name of symbolic file (pmFPAfile)
+    );
+
+/// Add the command line to the run-time information
+bool pmConfigRunCommand(
+    pmConfig *config,                   ///< Configuration
+    int argc, char **argv               ///< Command line arguments
+    );
+
+/// Record the random number generator seed in the run-time information
+bool pmConfigRunSeed(
+    pmConfig *config,                   ///< Configuration
+    psU64 seed                          ///< RNG seed
+    );
+
+#endif
+
Index: /branches/eam_branches/psModules.20240412/src/config/pmErrorCodes.c.in
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmErrorCodes.c.in	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmErrorCodes.c.in	(revision 42651)
@@ -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_branches/psModules.20240412/src/config/pmErrorCodes.dat
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmErrorCodes.dat	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmErrorCodes.dat	(revision 42651)
@@ -0,0 +1,20 @@
+#
+# 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
+STAMPS			Unable to select stamps for PSF-matching
+SMALL_AREA              Small area for PSF-matching
+# 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_branches/psModules.20240412/src/config/pmErrorCodes.h.in
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmErrorCodes.h.in	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmErrorCodes.h.in	(revision 42651)
@@ -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 = 1000,
+    PM_ERR_${ErrorCode},
+    PM_ERR_NERROR
+} pmErrorCode;
+
+void pmErrorRegister(void);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/config/pmVersion.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmVersion.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmVersion.c	(revision 42651)
@@ -0,0 +1,78 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmVersion.h"
+#include "pmVersionDefinitions.h"
+
+#ifndef PSMODULES_VERSION
+#error "PSMODULES_VERSION is not set"
+#endif
+#ifndef PSMODULES_BRANCH
+#error "PSMODULES_BRANCH is not set"
+#endif
+#ifndef PSMODULES_SOURCE
+#error "PSMODULES_SOURCE is not set"
+#endif
+
+psString psModulesVersion(void)
+{
+    char *value = NULL;
+    psStringAppend(&value, "%s@%s", PSMODULES_BRANCH, PSMODULES_VERSION);
+    return value;
+}
+
+psString psModulesSource(void)
+{
+    return psStringCopy(PSMODULES_SOURCE);
+}
+
+
+psString psModulesVersionLong(void)
+{
+    psString version = psModulesVersion();  // Version, to return
+    psString source = psModulesSource();    // Source
+
+    psStringPrepend(&version, "psModules ");
+    psStringAppend(&version, " from %s, built %s, %s", source, __DATE__, __TIME__);
+    psFree(source);
+
+#ifdef __OPTIMIZE__
+    psStringAppend(&version, " optimised");
+#else
+    psStringAppend(&version, " unoptimised");
+#endif
+
+#ifdef HAVE_NEBCLIENT
+    psStringAppend(&version, " with nebclient");
+#else
+    psStringAppend(&version, " without nebclient");
+#endif
+
+    return version;
+};
+
+
+bool psModulesVersionHeader(psMetadata *header)
+{
+    PS_ASSERT_METADATA_NON_NULL(header, false);
+
+    psString version = psModulesVersion(); // Software version
+    psString source = psModulesSource();   // Software source
+
+    psMetadataAddStr(header, PS_LIST_TAIL, "MODULE_V", PS_META_REPLACE, NULL, PSMODULES_VERSION);
+    
+    psStringPrepend(&version, "psModules version: ");
+    psStringPrepend(&source, "psModules source: ");
+
+    psMetadataAddStr(header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, NULL, version);
+    psMetadataAddStr(header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, NULL, source);
+
+    psFree(version);
+    psFree(source);
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/config/pmVersion.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmVersion.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmVersion.h	(revision 42651)
@@ -0,0 +1,49 @@
+/*  @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 source
+ *
+ *  Returns the current psModules source as a string.
+ *
+ *  @return psString: String with source.
+ */
+psString psModulesSource(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);
+
+/// Populate a header with version information
+bool psModulesVersionHeader(
+    psMetadata *header                  ///< Header to populate
+    );
+
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/config/pmVersionDefinitions.h.in
===================================================================
--- /branches/eam_branches/psModules.20240412/src/config/pmVersionDefinitions.h.in	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/config/pmVersionDefinitions.h.in	(revision 42651)
@@ -0,0 +1,8 @@
+#ifndef PM_VERSION_DEFINITIONS_H
+#define PM_VERSION_DEFINITIONS_H
+
+#define PSMODULES_VERSION @PSMODULES_VERSION@ // SVN version
+#define PSMODULES_BRANCH  @PSMODULES_BRANCH@  // SVN branch
+#define PSMODULES_SOURCE  @PSMODULES_SOURCE@  // SVN source
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/detrend/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/Makefile.am	(revision 42651)
@@ -0,0 +1,47 @@
+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 \
+	pmMaskStats.c \
+	pmNonLinear.c \
+	pmNewNonLinear.c \
+	pmBias.c \
+	pmOverscan.c \
+	pmDetrendDB.c \
+	pmShutterCorrection.c \
+	pmDetrendThreads.c \
+	pmShifts.c \
+	pmDark.c \
+	pmRemnance.c \
+	pmPattern.c \
+	pmPatternIO.c
+
+#	pmSkySubtract.c
+
+pkginclude_HEADERS = \
+	pmFlatField.h \
+	pmFlatNormalize.h \
+	pmFringeStats.h \
+	pmMaskBadPixels.h \
+	pmMaskStats.h \
+	pmNonLinear.h \
+	pmNewNonLinear.h \
+	pmBias.h \
+	pmOverscan.h \
+	pmDetrendDB.h \
+	pmShutterCorrection.h \
+	pmDetrendThreads.h \
+	pmShifts.h \
+	pmDark.h \
+	pmRemnance.h \
+	pmPattern.h \
+	pmPatternIO.h
+
+#	pmSkySubtract.h
+
+CLEANFILES = *~
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmBias.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmBias.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmBias.c	(revision 42651)
@@ -0,0 +1,254 @@
+#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 "pmDetrendThreads.h"
+
+#include "pmOverscan.h"
+#include "pmBias.h"
+
+bool pmBiasSubtractScan_Threaded(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+    pmReadout *in = job->args->data[0];
+    const pmReadout *sub = job->args->data[1];
+    float scale  = PS_SCALAR_VALUE(job->args->data[2],F32);
+    int xOffset  = PS_SCALAR_VALUE(job->args->data[3],S32);
+    int yOffset  = PS_SCALAR_VALUE(job->args->data[4],S32);
+    int rowStart = PS_SCALAR_VALUE(job->args->data[5],S32);
+    int rowStop  = PS_SCALAR_VALUE(job->args->data[6],S32);
+    return pmBiasSubtractScan(in, sub, scale, xOffset, yOffset, rowStart, rowStop);
+}
+
+bool pmBiasSubtractScan(pmReadout *in, const pmReadout *sub, float scale,
+                        int xOffset, int yOffset, int rowStart, int rowStop)
+{
+    psImage *inImage  = in->image;      // The input image
+    psImage *inMask   = in->mask;       // The input mask
+    const psImage *subImage = sub->image; // The image to be subtracted
+    const psImage *subMask  = sub->mask; // The mask for the subtraction image
+
+    if (scale == 1.0) {
+        for (int i = rowStart; i < rowStop; i++) {
+            for (int j = 0; j < inImage->numCols; j++) {
+                inImage->data.F32[i][j] -= subImage->data.F32[i+yOffset][j+xOffset];
+                if (inMask && subMask) {
+                    inMask->data.PS_TYPE_IMAGE_MASK_DATA[i][j] |= subMask->data.PS_TYPE_IMAGE_MASK_DATA[i+yOffset][j+xOffset];
+                }
+            }
+        }
+    } else {
+        for (int i = rowStart; i < rowStop; i++) {
+            for (int j = 0; j < inImage->numCols; j++) {
+                inImage->data.F32[i][j] -= subImage->data.F32[i+yOffset][j+xOffset] * scale;
+                if (inMask && subMask) {
+                    inMask->data.PS_TYPE_IMAGE_MASK_DATA[i][j] |= subMask->data.PS_TYPE_IMAGE_MASK_DATA[i+yOffset][j+xOffset];
+                }
+            }
+        }
+    }
+    return true;
+}
+
+// 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
+                         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 *subImage = sub->image;     // The image to be subtracted
+
+    // 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;
+    }
+
+    int xOffset = x0in - x0sub;
+    int yOffset = y0in - y0sub;
+
+    bool threaded = true;
+    int scanRows = pmDetrendGetScanRows();
+    if (scanRows == 0) {
+        threaded = false;
+        scanRows = inImage->numRows;
+    }
+
+    for (int rowStart = 0; rowStart < inImage->numRows; rowStart += scanRows) {
+        int rowStop = PS_MIN(rowStart + scanRows, inImage->numRows);
+
+        if (threaded) {
+            // allocate a job, construct the arguments for this job
+            psThreadJob *job = psThreadJobAlloc("PSMODULES_DETREND_BIAS");
+            psArrayAdd(job->args, 1, in);
+            psArrayAdd(job->args, 1, sub);
+            PS_ARRAY_ADD_SCALAR(job->args, scale, PS_TYPE_F32);
+            PS_ARRAY_ADD_SCALAR(job->args, xOffset, PS_TYPE_S32);
+            PS_ARRAY_ADD_SCALAR(job->args, yOffset, PS_TYPE_S32);
+            PS_ARRAY_ADD_SCALAR(job->args, rowStart, PS_TYPE_S32);
+            PS_ARRAY_ADD_SCALAR(job->args, rowStop, PS_TYPE_S32);
+
+            if (!psThreadJobAddPending(job)) {
+                return false;
+            }
+        } else if (!pmBiasSubtractScan(in, sub, scale, xOffset, yOffset, rowStart, rowStop)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to apply bias correction.");
+            return false;
+        }
+    }
+
+    if (threaded) {
+        // wait here for the threaded jobs to finish
+        if (!psThreadPoolWait(true, true)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to apply bias correction.");
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool pmBiasSubtract(pmReadout *in, 
+                    pmReadout *bias, 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) {
+        psWarning("Dark processing is now available using pmDark --- perhaps you should use that instead?");
+        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
+
+    // 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, "Bias/dark processing completed at ");
+    psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK,
+                     timeString, "");
+    psFree(timeString);
+
+
+    return true;
+}
+
+
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmBias.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmBias.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmBias.h	(revision 42651)
@@ -0,0 +1,50 @@
+/* @file pmBias.h
+ * @brief Subtract the overscan, bias and dark
+ *
+ * @author George Gusciora, MHPCC
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.11 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-09-09 04:10:14 $
+ * 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
+                    pmReadout *bias, ///< Bias to subtract, or NULL
+                    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
+                         pmReadout *sub, // Readout to be subtracted from input
+                         float scale   // Scale to apply before subtracting
+    );
+
+/// Thread entry point for bias subtraction
+bool pmBiasSubtractScan_Threaded(psThreadJob *job ///< Job to execute
+    );
+
+/// Do bias subtraction for a scan
+bool pmBiasSubtractScan(
+    pmReadout *in,                      ///< Input image to correct
+    const pmReadout *sub,               ///< Bias+dark image to subtract
+    float scale,                        ///< Scale to apply
+    int xOffset, int yOffset,           ///< Offset between input and bias images
+    int rowStart, int rowStop           ///< Scan range
+    );
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmDark.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmDark.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmDark.c	(revision 42651)
@@ -0,0 +1,1219 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+#include <string.h>
+#include <strings.h>
+
+#include "psPolynomialMD.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmHDUUtils.h"
+#include "pmFPARead.h"
+#include "pmFPAWrite.h"
+#include "pmReadoutStack.h"
+#include "pmDetrendThreads.h"
+#include "pmErrorCodes.h"
+#include "pmDark.h"
+
+#define PM_DARK_FITS_EXTNAME "PS_DARK"  // FITS extension name for ordinates table
+#define PM_DARK_FITS_NAME    "NAME"     // Column name for concept name in ordinates table
+#define PM_DARK_FITS_RULE    "RULE"     // Column name for concept rule in ordinates table
+#define PM_DARK_FITS_ORDER   "ORDER"    // Column name for polynomial order in ordinates table
+#define PM_DARK_FITS_SCALE   "SCALE"    // Column name for scaling option in ordinates table
+#define PM_DARK_FITS_MIN     "MIN"      // Column name for minimum value in ordinates table
+#define PM_DARK_FITS_MAX     "MAX"      // Column name for maximum value in ordinates table
+
+static bool ordinateParseConcept(double *value, const pmReadout *readout, const char *name) {
+
+    *value = NAN;
+
+    pmCell *cell = readout->parent; // Parent cell
+    psAssert(cell, "readout is missing cell \n");
+
+    psMetadataItem *item = psMetadataLookup(cell->concepts, name);
+    if (!item) {
+        pmChip *chip = cell->parent; // Parent chip
+        psAssert(chip, "cell is missing chip \n");
+
+        item = psMetadataLookup(chip->concepts, name);
+        if (!item) {
+            pmFPA *fpa = chip->parent; // Parent FPA
+            psAssert(fpa, "chip is missing fpa \n");
+
+            item = psMetadataLookup(fpa->concepts, name);
+            if (!item) {
+                psError(PM_ERR_CONFIG, true, "Unable to find concept %s in readout", name);
+                return false;
+            }
+        }
+    }
+
+    *value = psMetadataItemParseF64(item); // Value of interest
+    if (!isfinite(*value)) {
+        psWarning("Non-finite value (%f) of concept %s in readout", *value, name);
+    }
+    return true;
+}
+
+static bool ordinateParseRule(double *value, const pmReadout *readout, const char *name, const char *rule) {
+
+    psAssert(name, "ordinate name is not defined");
+    psAssert(rule, "ordinate rule is not defined");
+
+    *value = NAN;
+
+    psArray *words = psStringSplitArray(rule, " ", false);
+
+    // we should have a rule of the form (concept) OP (concept) OP (concept) ...
+    // for now, the only allowed OP is * (eventually, we can steal code from opihi for a better
+    // RPN parser).
+
+    if (words->n % 2 == 0) {
+        psError(PM_ERR_CONFIG, true, "syntax error in DARK.ORDINATE %s rule %s\n", name, rule);
+        psFree(words);
+        return false;
+    }
+
+    for (int i = 1; i < words->n; i+=2) {
+        if (strcmp((char *)words->data[i], "*")) {
+            psError(PM_ERR_CONFIG, true, "syntax error in DARK.ORDINATE %s rule %s\n", name, rule);
+            psFree(words);
+            return false;
+        }
+    }
+
+    if (!ordinateParseConcept(value, readout, words->data[0])) {
+        psError(PM_ERR_CONFIG, false, "syntax error in DARK.ORDINATE %s rule %s\n", name, rule);
+        psFree(words);
+        return false;
+    }
+
+    double value2 = 0.0;
+    for (int i = 2; i < words->n; i+=2) {
+        if (!ordinateParseConcept(&value2, readout, words->data[i])) {
+            psError(PM_ERR_CONFIG, false, "syntax error in DARK.ORDINATE %s rule %s\n", name, rule);
+            psFree(words);
+            return false;
+        }
+        *value *= value2;
+    }
+    psFree(words);
+    return true;
+}
+
+// Look up the value of an ordinate in a readout
+static bool ordinateLookup(double *value, // Value of ordinate, to return
+                           bool *inRange, // is value within min : max range?
+                           const char *name, // Name of ordinate (concept or abstract name)
+                           const char *rule, // Rule for generating the value (if NULL use name as concept)
+                           bool scale,  // Scale the value?
+                           float min, float max, // Minimum and maximum values for scaling
+                           const pmReadout *readout // Readout of interest
+                           )
+{
+    assert(value);
+    assert(name);
+    assert(readout);
+
+    *inRange = true;
+
+    if (rule) {
+        if (!ordinateParseRule(value, readout, name, rule)) {
+            psError(PM_ERR_CONFIG, false, "trouble parsing rule %s for DARK.ORDINATE %s", rule, name);
+            return false;
+        }
+    } else {
+        if (!ordinateParseConcept(value, readout, name)) {
+            psError(PM_ERR_CONFIG, false, "trouble parsing rule %s for DARK.ORDINATE %s", "NULL", name);
+            return false;
+        }
+    }
+
+    if (scale) {
+        if (*value < min || *value > max) {
+            psWarning("Value of concept %s (%f) outside range (%f:%f)", name, *value, min, max);
+            *inRange = false;
+        }
+        *value = 2.0 * (*value - min) / (max - min) - 1.0;
+    }
+
+    return true;
+}
+
+static void darkOrdinateFree(pmDarkOrdinate *ord)
+{
+    psFree(ord->name);
+    psFree(ord->rule);
+    return;
+}
+
+pmDarkOrdinate *pmDarkOrdinateAlloc(const char *name, int order)
+{
+    pmDarkOrdinate *ord = psAlloc(sizeof(pmDarkOrdinate)); // Ordinate data, to return
+    psMemSetDeallocator(ord, (psFreeFunc)darkOrdinateFree);
+
+    ord->name = psStringCopy(name);
+    ord->rule = NULL;
+    ord->order = order;
+    ord->scale = false;
+    ord->min = NAN;
+    ord->max = NAN;
+
+    return ord;
+}
+
+// this creates and saves: values, roMask, norm, orders, counts, sigma, and saves the on output->analysis
+bool pmDarkCombinePrepare(pmCell *output, const psArray *inputs, psArray *ordinates, const char *normConcept)
+{
+    psArray *values = psArrayAlloc(inputs->n);
+    psVector *roMask = psVectorAlloc(inputs->n, PS_TYPE_VECTOR_MASK); // Mask for bad readouts
+    psVector *norm = normConcept ? psVectorAlloc(inputs->n, PS_TYPE_F32) : NULL; // Normalizations for each
+    psVector *orders = psVectorAlloc(ordinates->n, PS_TYPE_U8); // Orders for each concept
+
+    psVectorInit(roMask, 0);
+
+    bool inRange = false;
+    int numBadInputs = 0;               // Number of bad inputs
+
+    // build the 'norm' vector and the 'values' vectors, count the number of bad inputs
+    for (int i = 0; i < inputs->n; i++) {
+        values->data[i] = psVectorAlloc(ordinates->n, PS_TYPE_F32);
+        if (!norm) continue;
+
+        pmReadout *readout = inputs->data[i]; // Readout of interest
+        double normValue;            // Normalisation value
+        if (!ordinateLookup(&normValue, &inRange, normConcept, NULL, false, NAN, NAN, readout)) {
+            psError(PM_ERR_CONFIG, false, "problem finding concept %s for DARK.NORM", normConcept);
+            return false;
+        }
+        if (!isfinite(normValue)) {
+            psWarning("Unable to find acceptable value of %s for readout %d", normConcept, i);
+            roMask->data.PS_TYPE_VECTOR_MASK_DATA[i] = 0xff;
+            norm->data.F32[i] = NAN;
+            numBadInputs++;
+            continue;
+        }
+        if (normValue == 0.0) {
+            psWarning("Normalisation value (%s) for readout %d is zero", normConcept, i);
+            roMask->data.PS_TYPE_VECTOR_MASK_DATA[i] = 0xff;
+            norm->data.F32[i] = NAN;
+            numBadInputs++;
+            continue;
+        }
+        norm->data.F32[i] = 1.0 / normValue;
+    }
+
+    // build the 'orders' vector and set the array of 'values'
+    for (int i = 0; i < ordinates->n; i++) {
+        pmDarkOrdinate *ord = ordinates->data[i]; // Ordinate information
+        if (ord->order <= 0) {
+            psError(PS_ERR_UNKNOWN, true, "Bad order for DARK.ORDINATE %s (%d) --- ignored", ord->name, ord->order);
+            psFree(values);
+            psFree(roMask);
+            psFree(orders);
+            psFree(norm);
+            return false;
+        }
+        orders->data.U8[i] = ord->order;
+
+        for (int j = 0; j < inputs->n; j++) {
+            psVector *val = values->data[j]; // Value vector for readout
+            if (roMask->data.PS_TYPE_VECTOR_MASK_DATA[j]) {
+                val->data.F32[i] = NAN;
+                continue;
+            }
+
+            pmReadout *readout = inputs->data[j]; // Readout of interest
+            double value = NAN;          // Value of ordinate
+            if (!ordinateLookup(&value, &inRange, ord->name, ord->rule, ord->scale, ord->min, ord->max, readout)) {
+                psError(PM_ERR_CONFIG, false, "problem finding rule for DARK.ORDINATE %s", ord->name);
+                return false;
+            }
+            if (!isfinite(value)) {
+                psWarning("Unable to find acceptable value of DARK.ORDINATE %s for readout %d", ord->name, i);
+                roMask->data.PS_TYPE_VECTOR_MASK_DATA[j] = 0xff;
+                val->data.F32[i] = NAN;
+                numBadInputs++;
+                continue;
+            }
+            if (!inRange) {
+                psWarning("Value of DARK.ORDINATE %s for readout %d is out of range", ord->name, i);
+                roMask->data.PS_TYPE_VECTOR_MASK_DATA[j] = 0xff;
+                val->data.F32[i] = NAN;
+                numBadInputs++;
+                continue;
+            }
+            val->data.F32[i] = value;
+        }
+    }
+
+    if (psTraceGetLevel("psModules.detrend") > 9) {
+        for (int i = 0; i < inputs->n; i++) {
+            psVector *val = values->data[i];
+            (void) val; // avoid unused variable message when tracing is compiled out
+            for (int j = 0; j < ordinates->n; j++) {
+                psTrace("psModules.detrend", 9, "Image %d, ordinate %d: %f\n", i, j, val->data.F32[j]);
+            }
+        }
+    }
+
+    int numTerms = 1;                   // Number of terms in polynomial
+    for (int i = 0; i < orders->n; i++) {
+        numTerms += orders->data.U8[i];
+    }
+
+    if (numTerms > inputs->n - numBadInputs) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Insufficient inputs (%ld) to fit polynomial terms (%d).",
+                inputs->n - numBadInputs, numTerms);
+        psFree(values);
+        psFree(roMask);
+        psFree(norm);
+        return false;
+    }
+
+    // determine the output image size based on the input images
+    int row0, col0, numCols, numRows;
+    if (!pmReadoutStackSetOutputSize(&col0, &row0, &numCols, &numRows, inputs)) {
+        psError(PS_ERR_UNKNOWN, false, "problem setting output readout size.");
+        return false;
+    }
+
+    // the output is potentially a cube (depending on the dimensionality of the fit)
+    if (output->readouts->n != numTerms) {
+        output->readouts = psArrayRealloc(output->readouts, numTerms);
+    }
+
+    // generate the required output images based on the specified sizes
+    for (int i = 0; i < numTerms; i++) {
+        pmReadout *readout = output->readouts->data[i]; // Readout to update
+        if (!readout) {
+            readout = output->readouts->data[i] = pmReadoutAlloc(output);
+        }
+
+        pmReadoutStackDefineOutput(readout, col0, row0, numCols, numRows, false, false, 0);
+        psTrace("psModules.imcombine", 7, "Output minimum: %d,%d\n", col0, row0);
+    }
+
+    // these calls allocate and save the requested images on the output analysis metadata
+    psImage *counts = pmReadoutSetAnalysisImage(output->readouts->data[0], PM_READOUT_STACK_ANALYSIS_COUNT,
+                                                numCols, numRows, PS_TYPE_U16, 0);
+    if (!counts) {
+        return false;
+    }
+    psImage *sigma = pmReadoutSetAnalysisImage(output->readouts->data[0], PM_READOUT_STACK_ANALYSIS_SIGMA,
+                                               numCols, numRows, PS_TYPE_F32, NAN);
+    if (!sigma) {
+        return false;
+    }
+
+    psMetadataAddPtr(output->analysis, PS_LIST_TAIL, PM_DARK_ANALYSIS_ORDINATES,
+                     PS_DATA_ARRAY | PS_META_REPLACE, "Dark ordinates", ordinates);
+    psMetadataAddStr(output->analysis, PS_LIST_TAIL, PM_DARK_ANALYSIS_NORM, PS_META_REPLACE,
+                     "Dark normalisation", normConcept);
+
+    psMetadataAddPtr(output->analysis, PS_LIST_TAIL, "DARK.VALUES",  PS_DATA_ARRAY  | PS_META_REPLACE,
+                     "Dark values", values);
+    psMetadataAddPtr(output->analysis, PS_LIST_TAIL, "DARK.RO.MASK", PS_DATA_VECTOR | PS_META_REPLACE,
+                     "Dark Readout Mask", roMask);
+    psMetadataAddPtr(output->analysis, PS_LIST_TAIL, "DARK.NORM",    PS_DATA_VECTOR | PS_META_REPLACE,
+                     "Dark norm", norm);
+    psMetadataAddPtr(output->analysis, PS_LIST_TAIL, "DARK.ORDERS",  PS_DATA_VECTOR | PS_META_REPLACE,
+                     "Dark orders", orders);
+
+    for (int i = 0; i < numTerms; i++) {
+        pmReadout *readout = output->readouts->data[i]; // Readout to update
+        readout->data_exists = true;
+    }
+    output->data_exists = true;
+    output->parent->data_exists = true;
+
+    psFree(norm);
+    psFree(roMask);
+    psFree(orders);
+    psFree(values);
+
+    return true;
+}
+
+// do the combine work for this portion of the output (range is set by input data)
+bool pmDarkCombine(pmCell *output, const psArray *inputs, int iter, float rej, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(output, false);
+    PS_ASSERT_PTR_NON_NULL(output->readouts, false);
+    PS_ASSERT_INT_NONNEGATIVE(output->readouts->n, false);
+    PS_ASSERT_ARRAY_NON_NULL(inputs, false);
+    PS_ASSERT_INT_NONNEGATIVE(iter, false);
+    PS_ASSERT_FLOAT_LARGER_THAN(rej, 0.0, false);
+
+    bool mdok = false;
+
+    // retrieve the required parameter vectors
+    psArray *in_values  = psMetadataLookupPtr(&mdok, output->analysis, "DARK.VALUES");
+    psAssert(in_values, "values not supplied");
+    psVector *roMask = psMetadataLookupPtr(&mdok, output->analysis, "DARK.RO.MASK");
+    psAssert(roMask, "roMask not supplied");
+    psVector *max_orders = psMetadataLookupPtr(&mdok, output->analysis, "DARK.ORDERS");
+    psAssert(max_orders, "orders not supplied");
+
+    psArray *values_set = psArrayAlloc(max_orders->n);
+    psArray *poly_set = psArrayAlloc(max_orders->n);
+    psVector *logL = psVectorAlloc(max_orders->n,PS_TYPE_F64);
+
+    for (int i = 0; i < max_orders->n; i++) {
+      psVector *orders = psVectorAlloc(i+1,PS_TYPE_U8);
+      for (int j = 0; j < orders->n; j++) {
+	orders->data.U8[j] = max_orders->data.U8[j];
+      }
+      poly_set->data[i] =  psPolynomialMDAlloc(orders); // Polynomial for fitting
+      
+      psArray *values = psArrayAlloc(in_values->n);
+      
+      for (int j = 0; j < values->n; j++) {
+	psVector *these_values = psVectorAlloc(i+1,PS_TYPE_F32);
+	psVector *input_values = in_values->data[j];
+
+	for (int k = 0; k < orders->n; k++) {
+	  these_values->data.F32[k] = input_values->data.F32[k];
+	}
+	values->data[j] = these_values;
+      }
+      values_set->data[i] = values;
+      psFree(orders);
+    }
+      
+    // retrieve the norm vector, if supplied
+    psVector *norm       = psMetadataLookupPtr(&mdok, output->analysis, "DARK.NORM");
+
+    // retrieve the 'counts' and 'sigma' images
+    psImage *counts = pmReadoutGetAnalysisImage(output->readouts->data[0], PM_READOUT_STACK_ANALYSIS_COUNT);
+    if (!counts) {
+        return false;
+    }
+    psImage *sigma = pmReadoutGetAnalysisImage(output->readouts->data[0], PM_READOUT_STACK_ANALYSIS_SIGMA);
+    if (!sigma) {
+        return 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;
+    }
+
+    pmDarkVisualInit(values_set->data[max_orders->n - 1]);
+
+    pmReadout *outReadout = output->readouts->data[0];
+
+    // Iterate over pixels, fitting polynomial
+    psVector *pixels = psVectorAlloc(inputs->n, PS_TYPE_F32); // Stack of pixels
+    psVector *mask   = psVectorAlloc(inputs->n, PS_TYPE_VECTOR_MASK); // Mask for stack
+    for (int i = minInputRows; i < maxInputRows; i++) {
+        int yOut = i - outReadout->row0; // y position on output readout
+
+#ifdef SHOW_BUSY
+        if (psTraceGetLevel("psModules.detrend") > 9) {
+            printf("Processing row %d\r", i);
+            fflush(stdout);
+        }
+#endif
+
+        for (int j = minInputCols; j < maxInputCols; j++) {
+            int xOut = j - outReadout->col0; // x position on output readout
+
+            psVectorInit(mask, 0);
+            for (int r = 0; r < inputs->n; r++) {
+                if (roMask->data.PS_TYPE_VECTOR_MASK_DATA[r]) {
+                    mask->data.PS_TYPE_VECTOR_MASK_DATA[r] = 0xff;
+                    continue;
+                }
+                pmReadout *readout = inputs->data[r]; // Input readout
+		if ((!readout)||(!readout->image)) {
+		  mask->data.PS_TYPE_VECTOR_MASK_DATA[r] = 0xff;
+		  continue;
+		}		  
+                int yIn = i - readout->row0; // y position on input readout
+                int xIn = j - readout->col0; // x position on input readout
+
+                pixels->data.F32[r] = readout->image->data.F32[yIn][xIn];
+                if (norm) {
+                    pixels->data.F32[r] *= norm->data.F32[r];
+                }
+                if (readout->mask) {
+                    mask->data.PS_TYPE_VECTOR_MASK_DATA[r] = (readout->mask->data.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn] & maskVal);
+                }
+
+            }
+
+	    int k_best = 0;
+	    for (int k = 0; k < max_orders->n; k++) {
+	      psPolynomialMD *poly = poly_set->data[k];
+	      psArray *values = values_set->data[k];
+	      
+	      if (!psPolynomialMDClipFit(poly, pixels, NULL, mask, 0xff, values, iter, rej)) {
+                psErrorClear();         // Nothing we can do about it
+                psVectorInit(poly->coeff, NAN);
+	      }
+
+	      pmDarkVisualPixelFit(pixels, mask);
+	      pmDarkVisualPixelModel(poly, values);
+
+	      // Insert math here to choose optimum model.
+	      logL->data.F64[k] = 0.0;
+	      psPolynomialMD *polySig = poly_set->data[0];
+	      for (int m = 0; m < poly->deviations->n; m++) {
+		logL->data.F64[k] += pow(poly->deviations->data.F32[m] / polySig->stdevFit,2);
+/* 		if ((xOut == 20) && (yOut == 256)) { */
+/* 		  psTrace("psModules.detrend",3,"pmDarkCombine DEV: %d %d: input %d models: Norders: %d logL: %g value: %g\n", */
+/* 			  xOut,yOut,m,k,logL->data.F64[k],poly->deviations->data.F32[m]); */
+/* 		} */
+	      }
+	      if (k > 0) {
+		if ( ( logL->data.F64[k - 1] - logL->data.F64[k] ) > 1) { // Hard coded criterion for a ~5% limit with one degree of freedom
+		  k_best = k;
+		}
+	      }
+	      if ((xOut <= 600) && (yOut <= 600)) {
+		psTrace("psModules.detrend",3,"pmDarkCombine: %d %d: models: Norders: %d logL: %g BestOrders: %d\n",
+			xOut,yOut,k,logL->data.F64[k],k_best);
+	      }
+	    }
+	    if (k_best > 1) {
+	      k_best = 1;
+	    }
+/* 	    k_best = 1; */
+	    // Select the polynomial that seems best.
+	    psPolynomialMD *poly = poly_set->data[k_best];
+	      
+	    //            for (int k = 0; k < poly->coeff->n; k++) {
+	    for (int k = 0; k < max_orders->n + 1; k++) { // There is one more coefficient than is stored here.
+                pmReadout *ro = output->readouts->data[k]; // Readout of interest
+		if (k < poly->coeff->n) {
+		  ro->image->data.F32[yOut][xOut] = poly->coeff->data.F64[k];
+		}
+		else {
+		  ro->image->data.F32[yOut][xOut] = 0.0;
+		}
+            }
+            counts->data.U16[yOut][xOut] = poly->numFit;
+            sigma->data.F32[yOut][xOut] = poly->stdevFit;
+        }
+    }
+
+    psFree(pixels);
+    psFree(mask);
+
+    for (int i = 0; i < max_orders->n; i++) {
+      psFree(poly_set->data[i]);
+      psArray *values = values_set->data[i];
+      for (int j = 0; j < values->n; j++) {
+	psFree(values->data[j]);
+      }
+      psFree(values_set->data[i]);
+    }
+    psFree(values_set);
+    psFree(poly_set);
+    psFree(logL);
+
+    return true;
+}
+
+bool pmDarkApplyScan_Threaded(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    pmReadout *readout     = job->args->data[0]; // Readout to correct
+    pmCell *dark           = job->args->data[1]; // Dark to apply
+    const psVector *orders = job->args->data[2]; // Polynomial orders for each ordinate
+    const psVector *values = job->args->data[3]; // Values for each ordinate
+
+    psImageMaskType bad = PS_SCALAR_VALUE(job->args->data[4], PS_TYPE_IMAGE_MASK_DATA); // Mask value to give bad pixels
+    bool doNorm    = PS_SCALAR_VALUE(job->args->data[5], U8); // Normalise values?
+    float norm     = PS_SCALAR_VALUE(job->args->data[6], F32); // Value by which to normalise
+    int rowStart   = PS_SCALAR_VALUE(job->args->data[7], S32); // Starting row for scan
+    int rowStop    = PS_SCALAR_VALUE(job->args->data[8], S32); // Stopping row for scan
+
+    return pmDarkApplyScan(readout, dark, orders, values, bad, doNorm, norm, rowStart, rowStop);
+}
+
+bool pmDarkApplyScan(pmReadout *readout, const pmCell *dark, const psVector *orders, const psVector *values,
+                     psImageMaskType bad, bool doNorm, float norm, int rowStart, int rowStop)
+{
+    int numCols = readout->image->numCols;
+    int numTerms = dark->readouts->n;   // Number of polynomial terms
+
+    psPolynomialMD *poly = psPolynomialMDAlloc(orders); // Polynomial to apply
+
+    for (int y = rowStart; y < rowStop; y++) {
+        for (int x = 0; x < numCols; x++) {
+            for (int i = 0; i < numTerms; i++) {
+                pmReadout *ro = dark->readouts->data[i]; // Dark readout
+                poly->coeff->data.F64[i] = ro->image->data.F32[y][x];
+            }
+            float value = psPolynomialMDEval(poly, values); // Value of dark current
+            if (doNorm) {
+                value *= norm;
+            }
+            readout->image->data.F32[y][x] -= value;
+            if (readout->mask && !isfinite(value)) {
+                readout->mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] = bad;
+            }
+        }
+    }
+    psFree(poly);
+
+    return true;
+}
+
+bool pmDarkApply(pmReadout *readout, pmCell *dark, psImageMaskType bad)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(dark, false);
+    PS_ASSERT_IMAGE_NON_NULL(readout->image, false);
+    PS_ASSERT_IMAGE_TYPE(readout->image, PS_TYPE_F32, false);
+    int numCols = readout->image->numCols, numRows = readout->image->numRows; // Size of image
+    if (readout->mask) {
+        PS_ASSERT_IMAGE_NON_NULL(readout->mask, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(readout->mask, readout->image, false);
+        PS_ASSERT_IMAGE_TYPE(readout->mask, PS_TYPE_IMAGE_MASK, false);
+    }
+    int numTerms = dark->readouts->n;   // Number of polynomial terms
+    for (int i = 0; i < numTerms; i++) {
+        pmReadout *ro = dark->readouts->data[i]; // Dark readout
+        PS_ASSERT_PTR_NON_NULL(ro, false);
+        PS_ASSERT_IMAGE_NON_NULL(ro->image, false);
+        PS_ASSERT_IMAGE_SIZE(ro->image, numCols, numRows, false);
+        PS_ASSERT_IMAGE_TYPE(ro->image, PS_TYPE_F32, false);
+    }
+    psArray *ordinates = psMetadataLookupPtr(NULL, dark->analysis, PM_DARK_ANALYSIS_ORDINATES); // Ordinates
+    if (!ordinates) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find dark ordinates.");
+        return false;
+    }
+    bool mdok;                          // Status of MD lookup
+    psString normConcept = psMetadataLookupStr(&mdok, dark->analysis, PM_DARK_ANALYSIS_NORM); // Normalisation
+    bool inRange = false;
+
+    int numOrdinates = ordinates->n;    // Number of ordinates
+    psVector *values = psVectorAlloc(numOrdinates, PS_TYPE_F32); // Values of ordinates
+    for (int i = 0; i < numOrdinates; i++) {
+        pmDarkOrdinate *ord = ordinates->data[i]; // Ordinate of interest
+        double value = NAN;              // Value for ordinate
+        if (!ordinateLookup(&value, &inRange, ord->name, ord->rule, ord->scale, ord->min, ord->max, readout)) {
+            psError(PS_ERR_UNKNOWN, true, "Unable to find value for DARK.ORDINATE %s", ord->name);
+            psFree(values);
+            return false;
+        }
+        if (!isfinite(value)) {
+            psError(PS_ERR_UNKNOWN, true, "Value for DARK.ORDINATE %s is NAN", ord->name);
+            psFree(values);
+            return false;
+        }
+        values->data.F32[i] = value;
+    }
+    double norm = NAN;                   // Normalisation value
+    bool doNorm = false;                // Do normalisation?
+    if (normConcept && strlen(normConcept) > 0) {
+        if (!ordinateLookup(&norm, &inRange, normConcept, NULL, false, NAN, NAN, readout)) {
+            psError(PS_ERR_UNKNOWN, true, "Unable to find value for %s", normConcept);
+            psFree(values);
+            return false;
+        }
+        doNorm = true;
+    }
+
+    psVector *orders = psVectorAlloc(numOrdinates, PS_TYPE_U8); // Order for each polynomial
+    for (int i = 0; i < numOrdinates; i++) {
+        pmDarkOrdinate *ord = ordinates->data[i]; // Ordinate information
+        if (ord->order <= 0) {
+            psError(PS_ERR_UNKNOWN, true, "Bad order for DARK.ORDINATE %s (%d) --- ignored", ord->name, ord->order);
+            psFree(values);
+            psFree(orders);
+            return false;
+        }
+        orders->data.U8[i] = ord->order;
+    }
+
+    // thread here by scan
+
+    bool threaded = true;
+    int scanRows = pmDetrendGetScanRows();
+    if (scanRows == 0) {
+        threaded = false;
+        scanRows = readout->image->numRows;
+    }
+
+    for (int rowStart = 0; rowStart < readout->image->numRows; rowStart += scanRows) {
+        int rowStop = PS_MIN(rowStart + scanRows, readout->image->numRows);
+
+        if (threaded) {
+            psThreadJob *job = psThreadJobAlloc("PSMODULES_DETREND_DARK");
+            psArrayAdd(job->args, 1, readout);
+            psArrayAdd(job->args, 1, dark);
+            psArrayAdd(job->args, 1, orders);
+            psArrayAdd(job->args, 1, values);
+            PS_ARRAY_ADD_SCALAR(job->args, bad, PS_TYPE_IMAGE_MASK);
+            PS_ARRAY_ADD_SCALAR(job->args, doNorm, PS_TYPE_U8);
+            PS_ARRAY_ADD_SCALAR(job->args, norm, PS_TYPE_F32);
+            PS_ARRAY_ADD_SCALAR(job->args, rowStart, PS_TYPE_S32);
+            PS_ARRAY_ADD_SCALAR(job->args, rowStop, PS_TYPE_S32);
+
+            if (!psThreadJobAddPending (job)) {
+                psFree(orders);
+                psFree(values);
+                return false;
+            }
+        } else if (!pmDarkApplyScan(readout, dark, orders, values, bad, doNorm, norm, rowStart, rowStop)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to apply dark.");
+            psFree(orders);
+            psFree(values);
+            return false;
+        }
+    }
+
+    if (threaded) {
+        // wait here for the threaded jobs to finish
+        if (!psThreadPoolWait(true, true)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to apply dark.");
+            psFree(orders);
+            psFree(values);
+            return false;
+        }
+    }
+
+    psFree(orders);
+    psFree(values);
+
+    return true;
+}
+
+bool pmFPAWriteDark(pmFPA *fpa, psFits *fits, pmConfig *config, bool blank, bool recurse)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    if (!pmFPAWrite(fpa, fits, config, blank, recurse)) {
+        psError(PS_ERR_IO, false, "Unable to write FPA dark images");
+        return false;
+    }
+
+    pmHDU *hdu = fpa->hdu;
+    if (blank || !hdu) {
+        // No more to do
+        return true;
+    }
+
+    psArray *ordinates = NULL;      // Dark ordinates, to write
+    const char *normConcept = NULL;     // Normalisation concept
+    psArray *chips = fpa->chips;    // Component chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i]; // Chip of interest
+        psArray *cells = chip->cells; // Component cells
+        for (int j = 0; j < cells->n; j++) {
+            pmCell *cell = cells->data[j]; // Cell of interest
+            bool mdok;              // Status of MD lookup
+            psArray *newOrd = psMetadataLookupPtr(&mdok, cell->analysis, PM_DARK_ANALYSIS_ORDINATES); // Ords
+            if (!mdok) {
+                continue;
+            }
+            psString newNorm = psMetadataLookupPtr(&mdok, cell->analysis, PM_DARK_ANALYSIS_NORM); // Norm
+            if (ordinates) {
+                if (newOrd != ordinates) {
+                    psError(PS_ERR_UNKNOWN, true, "Dark ordinates differ across cells.");
+                    return false;
+                }
+                if ((normConcept && (!newNorm || strcmp(normConcept, newNorm) != 0)) ||
+                    (!normConcept && newNorm)) {
+                    psError(PS_ERR_UNKNOWN, true, "Dark normalisations differ across cells.");
+                    return false;
+                }
+            } else {
+                ordinates = newOrd;
+                normConcept = newNorm;
+            }
+        }
+    }
+
+    if (!ordinates) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find dark ordinates.");
+        return false;
+    }
+
+    return pmDarkWrite(fits, hdu->header, ordinates, normConcept);
+}
+
+bool pmChipWriteDark(pmChip *chip, psFits *fits, pmConfig *config, bool blank, bool recurse)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    if (!pmChipWrite(chip, fits, config, blank, recurse)) {
+        psError(PS_ERR_IO, false, "Unable to write chip dark images");
+        return false;
+    }
+
+    pmHDU *hdu = pmHDUFromChip(chip);
+    if (blank || !hdu) {
+        // No more to do
+        return true;
+    }
+
+    psArray *ordinates = NULL;          // Dark ordinates, to write
+    const char *normConcept = NULL;     // Normalisation concept
+    psArray *cells = chip->cells;       // Component cells
+    for (int j = 0; j < cells->n; j++) {
+        pmCell *cell = cells->data[j]; // Cell of interest
+        bool mdok;                      // Status of MD lookup
+        psArray *newOrd = psMetadataLookupPtr(&mdok, cell->analysis, PM_DARK_ANALYSIS_ORDINATES); // Ordinates
+        if (!mdok) {
+            continue;
+        }
+        psString newNorm = psMetadataLookupPtr(&mdok, cell->analysis, PM_DARK_ANALYSIS_NORM); // Normalisation
+        if (ordinates) {
+            if (newOrd != ordinates) {
+                psError(PS_ERR_UNKNOWN, true, "Dark ordinates differ across cells.");
+                return false;
+            }
+            if ((normConcept && (!newNorm || strcmp(normConcept, newNorm) != 0)) ||
+                (!normConcept && newNorm)) {
+                psError(PS_ERR_UNKNOWN, true, "Dark normalisations differ across cells.");
+                return false;
+            }
+        } else {
+            ordinates = newOrd;
+            normConcept = newNorm;
+        }
+    }
+
+    if (!ordinates) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find dark ordinates.");
+        return false;
+    }
+
+    return pmDarkWrite(fits, hdu->header, ordinates, normConcept);
+}
+
+bool pmCellWriteDark(pmCell *cell, psFits *fits, pmConfig *config, bool blank)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    // Allow the usual pmFPAWrite functions to handle the heavy lifting for the images
+    if (!pmCellWrite(cell, fits, config, blank)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to write dark cell.");
+        return false;
+    }
+
+    pmHDU *hdu = pmHDUFromCell(cell);
+    if (blank || !hdu) {
+        // No more to do
+        return true;
+    }
+
+    psArray *ordinates = psMetadataLookupPtr(NULL, cell->analysis, PM_DARK_ANALYSIS_ORDINATES);
+    if (!ordinates) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find dark ordinates.");
+        return false;
+    }
+    bool mdok;                          // Status of MD lookup
+    psString normConcept = psMetadataLookupPtr(&mdok, cell->analysis, PM_DARK_ANALYSIS_NORM); // Normalisation
+
+    return pmDarkWrite(fits, hdu->header, ordinates, normConcept);
+}
+
+bool pmDarkWrite(psFits *fits, psMetadata *header, const psArray *ordinates, const char *normConcept)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    PS_ASSERT_FITS_WRITABLE(fits, false);
+    PS_ASSERT_ARRAY_NON_NULL(ordinates, false);
+
+    // Only write table once per FITS file
+    bool write = false;             // Write table?
+    if (psFitsGetSize(fits) <= 1) {
+        write = true;
+    } else {
+        psMetadata *headers = psFitsReadHeaderSet(NULL, fits); // FITS headers
+        if (!psMetadataLookup(headers, PM_DARK_FITS_EXTNAME)) {
+            write = true;
+        }
+        psFree(headers);
+    }
+    if (!write) {
+        return true;
+    }
+
+    if (!psMemIncrRefCounter((psMetadata*)header)) {
+        header = psMetadataAlloc();
+    }
+    psMetadataAddStr(header, PS_LIST_TAIL, PM_DARK_HEADER_NORM, PS_META_REPLACE,
+                     "Dark normalisation concept", normConcept);
+
+    if (ordinates->n > 0) {
+        // Format ordinates into FITS table
+        int numOrdinates = ordinates->n;// Number of ordinates
+        psArray *table = psArrayAlloc(numOrdinates); // FITS table, constructed from ordinates
+        for (int i = 0; i < ordinates->n; i++) {
+            pmDarkOrdinate *ord = ordinates->data[i]; // Ordinate of interest
+            psMetadata *row = psMetadataAlloc(); // FITS table row
+            psMetadataAddStr(row, PS_LIST_TAIL, PM_DARK_FITS_NAME, 0, "DARK.ORDINATE name", ord->name);
+
+            // XXX write a dummy value if ord->rule == NULL? (eg, NONE)
+            if (ord->rule) {
+                psMetadataAddStr(row, PS_LIST_TAIL, PM_DARK_FITS_RULE, 0, "DARK.ORDINATE rule", ord->rule);
+            } else {
+                psMetadataAddStr(row, PS_LIST_TAIL, PM_DARK_FITS_RULE, 0, "DARK.ORDINATE rule", "NONE");
+            }
+
+            psMetadataAddS32(row, PS_LIST_TAIL, PM_DARK_FITS_ORDER, 0, "Polynomial order", ord->order);
+            psMetadataAddBool(row, PS_LIST_TAIL, PM_DARK_FITS_SCALE, 0, "Scale values?", ord->scale);
+            psMetadataAddF32(row, PS_LIST_TAIL, PM_DARK_FITS_MIN, 0, "Minimum value", ord->min);
+            psMetadataAddF32(row, PS_LIST_TAIL, PM_DARK_FITS_MAX, 0, "Maximum value", ord->max);
+            table->data[i] = row;
+        }
+
+        if (!psFitsWriteTable(fits, header, table, PM_DARK_FITS_EXTNAME)) {
+            psError(PS_ERR_IO, false, "Unable to write dark ordinates.");
+            psFree(table);
+            psFree(header);
+            return false;
+        }
+        psFree(table);
+    } else {
+        // No ordinates to write to a table, so write to a blank header.
+        if (!psFitsWriteBlank(fits, header, PM_DARK_FITS_EXTNAME)) {
+            psError(PS_ERR_IO, false, "Unable to write dark header.");
+            psFree(header);
+            return false;
+        }
+    }
+    psFree(header);
+
+    return true;
+}
+
+
+bool pmFPAReadDark(pmFPA *fpa, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    if (!pmFPARead(fpa, fits, config)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read dark FPA.");
+        return false;
+    }
+
+    psString normConcept = NULL;        // Normalisation concept
+    psArray *ordinates = pmDarkRead(&normConcept, fits); // Dark ordinates
+    if (!ordinates) {
+        psError(PS_ERR_IO, false, "Unable to read dark ordinates.");
+        return false;
+    }
+
+    psArray *chips = fpa->chips;        // Component chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // Chip of interest
+        psArray *cells = chip->cells;   // Component cells
+        for (int j = 0; j < cells->n; j++) {
+            pmCell *cell = cells->data[j]; // Cell of interest
+            psMetadataAddPtr(cell->analysis, PS_LIST_TAIL, PM_DARK_ANALYSIS_ORDINATES,
+                             PS_DATA_ARRAY | PS_META_REPLACE, "Dark ordinates", ordinates);
+            psMetadataAddStr(cell->analysis, PS_LIST_TAIL, PM_DARK_ANALYSIS_NORM, PS_META_REPLACE,
+                             "Dark normalisation", normConcept);
+        }
+    }
+    psFree(ordinates);
+    psFree(normConcept);
+
+    return true;
+}
+
+bool pmChipReadDark(pmChip *chip, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    if (!pmChipRead(chip, fits, config)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read dark chip.");
+        return false;
+    }
+
+    psString normConcept = NULL;        // Normalisation concept
+    psArray *ordinates = pmDarkRead(&normConcept, fits); // Dark ordinates
+    if (!ordinates) {
+        psError(PS_ERR_IO, false, "Unable to read dark ordinates.");
+        return false;
+    }
+
+    psArray *cells = chip->cells;       // Component cells
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // Cell of interest
+        psMetadataAddPtr(cell->analysis, PS_LIST_TAIL, PM_DARK_ANALYSIS_ORDINATES,
+                         PS_DATA_ARRAY | PS_META_REPLACE, "Dark ordinates", ordinates);
+        psMetadataAddStr(cell->analysis, PS_LIST_TAIL, PM_DARK_ANALYSIS_NORM, PS_META_REPLACE,
+                         "Dark normalisation", normConcept);
+    }
+    psFree(ordinates);
+    psFree(normConcept);
+
+    return true;
+}
+
+
+bool pmCellReadDark(pmCell *cell, psFits *fits, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    // Allow the usual pmFPARead functions to handle the heavy lifting for the images
+    if (!pmCellRead(cell, fits, config)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read dark cell.");
+        return false;
+    }
+
+    psString normConcept = NULL;        // Normalisation concept
+    psArray *ordinates = pmDarkRead(&normConcept, fits); // Dark ordinates
+    if (!ordinates) {
+        psError(PS_ERR_IO, false, "Unable to read dark ordinates.");
+        return false;
+    }
+    psMetadataAddPtr(cell->analysis, PS_LIST_TAIL, PM_DARK_ANALYSIS_ORDINATES,
+                     PS_DATA_ARRAY | PS_META_REPLACE, "Dark ordinates", ordinates);
+    psMetadataAddStr(cell->analysis, PS_LIST_TAIL, PM_DARK_ANALYSIS_NORM, PS_META_REPLACE,
+                     "Dark normalisation", normConcept);
+    psFree(ordinates);
+    psFree(normConcept);
+
+    return true;
+}
+
+psArray *pmDarkRead(psString *normConcept, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(normConcept, NULL);
+    PS_ASSERT_PTR_NULL(*normConcept, NULL);
+    PS_ASSERT_FITS_NON_NULL(fits, NULL);
+
+    if (!psFitsMoveExtName(fits, PM_DARK_FITS_EXTNAME)) {
+        psError(PS_ERR_IO, false, "Unable to find extension with dark ordinates table (%s).",
+                PM_DARK_FITS_EXTNAME);
+        return false;
+    }
+
+    psMetadata *header = psFitsReadHeader(NULL, fits); // Header
+    bool mdok;                          // Status of MD lookup
+    *normConcept = psMemIncrRefCounter(psMetadataLookupStr(&mdok, header, PM_DARK_HEADER_NORM));
+
+    psArray *ordinates = NULL;          // Dark ordinates to return
+
+    psFitsType type = psFitsGetExtType(fits); // Type of FITS extension
+    switch (type) {
+      case PS_FITS_TYPE_IMAGE: {
+          // Check that it's of zero size; otherwise we might have some conflict
+          int numCols = psMetadataLookupS32(&mdok, header, "NAXIS1");
+          int numRows = psMetadataLookupS32(&mdok, header, "NAXIS2");
+          if (numCols != 0 || numRows != 0) {
+              psError(PS_ERR_UNKNOWN, true, "Extension %s is not a DARK table.", PM_DARK_FITS_EXTNAME);
+              psFree(header);
+              return NULL;
+          }
+          // No ordinates fit --- only a constant term
+          ordinates = psArrayAlloc(0);
+          break;
+      }
+      case PS_FITS_TYPE_BINARY_TABLE:
+      case PS_FITS_TYPE_ASCII_TABLE: {
+          psArray *table = psFitsReadTable(fits); // FITS Table with ordinates
+          int numOrdinates = table->n;        // Number of ordinates
+          ordinates = psArrayAlloc(numOrdinates);
+
+          for (int i = 0; i < numOrdinates; i++) {
+              psMetadata *row = table->data[i]; // Row of interest
+              const char *name = psMetadataLookupStr(NULL, row, PM_DARK_FITS_NAME); // Concept name
+              int order = psMetadataLookupS32(NULL, row, PM_DARK_FITS_ORDER); // Polynomial order
+              if (!name || order <= 0) {
+                  psError(PS_ERR_UNKNOWN, false, "Bad value reading dark ordinates table.");
+                  psFree(table);
+                  psFree(ordinates);
+                  return false;
+              }
+              pmDarkOrdinate *ord = pmDarkOrdinateAlloc(name, order); // Ordinate data
+              ord->scale = psMetadataLookupBool(NULL, row, PM_DARK_FITS_SCALE);
+              ord->min = psMetadataLookupF32(NULL, row, PM_DARK_FITS_MIN);
+              ord->max = psMetadataLookupF32(NULL, row, PM_DARK_FITS_MAX);
+
+              // load the ordinate rule; it is not an error if this field is missing or NULL
+              // a NULL rule means 'use the name as the single concept'
+              const char *rule = psMetadataLookupStr(&mdok, row, PM_DARK_FITS_RULE);
+              if (!rule || !strcasecmp(rule, "NONE")) {
+                  ord->rule = NULL;
+              } else {
+                  ord->rule = psStringCopy(rule);
+              }
+              ordinates->data[i] = ord;
+          }
+          psFree(table);
+          break;
+      }
+      default:
+        psError(PS_ERR_UNKNOWN, true, "Unrecognised FITS extension type.");
+        return NULL;
+    }
+    psFree(header);
+
+    return ordinates;
+}
+
+#if (HAVE_KAPA)
+#include <kapa.h>
+#include "pmKapaPlots.h"
+#include "pmVisual.h"
+
+static int nKapa = 0;
+static int *kapa = NULL;
+static bool plotFlag = true;
+static psArray *xVectors = NULL;
+
+// this init function only gets the ordinates for the first readout...
+bool pmDarkVisualInit(psArray *values) {
+
+    if (!pmVisualIsVisual()) return true;
+
+    // skip if we have already opened the windows (or if none are requested...)
+    if (nKapa) return true;
+
+    // values has Ninput vectors with Norder elements; we need Norder vectors with Ninput elements...
+    int nOrders = 0;
+    for (int i = 0; i < values->n; i++) {
+        psVector *vect = values->data[i];
+        if (!nOrders) {
+            nOrders = vect->n;
+        } else {
+            psAssert (nOrders == vect->n, "mismatch in order vector lengths");
+        }
+    }
+    xVectors = psArrayAlloc(nOrders);
+    for (int i = 0; i < nOrders; i++) {
+        xVectors->data[i] = psVectorAlloc(values->n, PS_TYPE_F32);
+    }
+
+    for (int i = 0; i < values->n; i++) {
+        psVector *vect = values->data[i];
+        for (int j = 0; j < vect->n; j++) {
+            psVector *xVec = xVectors->data[j];
+            xVec->data.F32[i] = vect->data.F32[j];
+        }
+    }
+
+    nKapa = nOrders;
+
+    kapa = psAlloc(nKapa*sizeof(int));
+
+    for (int i = 0; i < nKapa; i++) {
+        kapa[i] = -1;
+        pmVisualInitWindow(&kapa[i], "ppmerge");
+    }
+    return true;
+}
+
+bool pmDarkVisualPixelFit(psVector *pixels, psVector *mask) {
+
+    Graphdata graphdata;
+
+    if (!pmVisualIsVisual()) return true;
+
+    KapaInitGraph(&graphdata);
+
+    psAssert(nKapa == xVectors->n, "inconsistent number of orders %d vs %ld\n", nKapa, xVectors->n);
+
+    psVector *xSub = psVectorAlloc(pixels->n, PS_TYPE_F32);
+    psVector *ySub = psVectorAlloc(pixels->n, PS_TYPE_F32);
+
+    for (int i = 0; i < xVectors->n; i++) {
+        psVector *x = xVectors->data[i];
+
+        // generate vectors of the unmasked values
+        int nSub = 0;
+        for (int j = 0; j < pixels->n; j++) {
+            if (mask && mask->data.PS_TYPE_VECTOR_MASK_DATA[j]) continue;
+            xSub->data.F32[nSub] = x->data.F32[j];
+            ySub->data.F32[nSub] = pixels->data.F32[j];
+            nSub ++;
+        }
+        xSub->n = ySub->n = nSub;
+
+        // plot the unmasked values
+        pmVisualScaleGraphdata (&graphdata, xSub, ySub, false);
+        KapaSetGraphData(kapa[i], &graphdata);
+        KapaSetLimits(kapa[i], &graphdata);
+        KapaClearPlots (kapa[i]);
+
+        KapaSetFont (kapa[i], "courier", 14);
+        KapaBox (kapa[i], &graphdata);
+        KapaSendLabel (kapa[i], "ordinate", KAPA_LABEL_XM);
+        KapaSendLabel (kapa[i], "pixel values", KAPA_LABEL_YM);
+
+        graphdata.color = KapaColorByName("black");
+        graphdata.style = KAPA_PLOT_POINTS;
+        graphdata.ptype = KAPA_POINT_CROSS;
+        KapaPrepPlot  (kapa[i], xSub->n, &graphdata);
+        KapaPlotVector(kapa[i], xSub->n, xSub->data.F32, "x");
+        KapaPlotVector(kapa[i], xSub->n, ySub->data.F32, "y");
+    }
+    pmVisualAskUser (&plotFlag);
+    return true;
+}
+
+bool pmDarkVisualPixelModel(psPolynomialMD *poly, psArray *values) {
+
+    Graphdata graphdata;
+
+    if (!pmVisualIsVisual()) return true;
+
+    KapaInitGraph(&graphdata);
+
+    psAssert(nKapa == xVectors->n, "inconsistent number of orders %d vs %ld\n", nKapa, xVectors->n);
+
+    psVector *yFit = psVectorAlloc(values->n, PS_TYPE_F32);
+
+    for (int i = 0; i < values->n; i++) {
+        psVector *coord = values->data[i];
+        yFit->data.F32[i] = psPolynomialMDEval (poly, coord);
+    }
+
+    for (int i = 0; i < xVectors->n; i++) {
+        psVector *xFit = xVectors->data[i];
+
+        KapaGetGraphData(kapa[i], &graphdata);
+        graphdata.color = KapaColorByName("red");
+        graphdata.style = KAPA_PLOT_POINTS;
+        graphdata.ptype = KAPA_POINT_CIRCLE_OPEN;
+        KapaPrepPlot  (kapa[i], xFit->n, &graphdata);
+        KapaPlotVector(kapa[i], xFit->n, xFit->data.F32, "x");
+        KapaPlotVector(kapa[i], xFit->n, yFit->data.F32, "y");
+    }
+    pmVisualAskUser (&plotFlag);
+    return true;
+}
+
+bool pmDarkVisualCleanup() {
+
+    for (int i = 0; i < nKapa; i++) {
+        KapaClose(kapa[i]);
+    }
+    psFree (kapa);
+    psFree (xVectors);
+    return true;
+}
+
+# else
+
+bool pmDarkVisualInit(psArray *values) { return true; }
+bool pmDarkVisualPixelFit(psVector *pixels, psVector *mask) { return true; }
+bool pmDarkVisualPixelModel(psPolynomialMD *poly, psArray *values) { return true; }
+bool pmDarkVisualCleanup() { return true; }
+
+# endif
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmDark.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmDark.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmDark.h	(revision 42651)
@@ -0,0 +1,124 @@
+#ifndef PM_DARK_H
+#define PM_DARK_H
+
+#include <pslib.h>
+#include <pmHDU.h>
+#include <pmFPA.h>
+#include <pmConfig.h>
+
+#define PM_DARK_ANALYSIS_ORDINATES "DARK.ORDINATES" // Name for dark ordinates in the cell analysis metadata
+#define PM_DARK_ANALYSIS_NORM "DARK.NORM" // Name for dark normalisation concept in cell analysis metadata
+#define PM_DARK_HEADER_NORM "PSDRKNRM"  // Header keyword for dark normalisation concept
+
+// An ordinate for fitting darks
+typedef struct {
+    psString name;                      // Name of ordinate
+    psString rule;                      // Rule for generating ordinate (math on concepts)
+    int order;                          // Polynomial order to fit
+    bool scale;                         // Rescale values?
+    float min, max;                     // Minimum and maximum values for rescaling
+} pmDarkOrdinate;
+
+// Allocator
+pmDarkOrdinate *pmDarkOrdinateAlloc(const char *name, // Name for ordinate
+                                    int order // Order for ordinate
+    );
+
+
+// Combine darks -- preparation step
+bool pmDarkCombinePrepare(pmCell *output,      // Output cell; readouts will be attached
+                          const psArray *inputs, // Input readouts for combination
+                          psArray *ordinates,  // Ordinates for fitting
+                          const char *normConcept // Concept name to use to divide input pixel values
+    );
+
+// Combine darks -- do the actual work
+bool pmDarkCombine(pmCell *output,      // Output cell; readouts will be attached
+                   const psArray *inputs, // Input readouts for combination
+                   int iter,            // Number of rejection iterations
+                   float rej,           // Rejection threshold (standard deviations)
+                   psImageMaskType maskVal   // Value to mask
+    );
+
+// Thread entry point for pmDarkApplyScan
+bool pmDarkApplyScan_Threaded(psThreadJob *job // Job to execute
+    );
+
+// Apply the dark correction to a scan
+bool pmDarkApplyScan(pmReadout *readout, // Readout to correct
+                     const pmCell *dark, // Dark to apply
+                     const psVector *orders, // Polynomial orders for each ordinate
+                     const psVector *values, // Values for each ordinate
+                     psImageMaskType bad,    // Value to give bad pixels
+                     bool doNorm,       // Normalise values?
+                     float norm,        // Value by which to normalise
+                     int rowStart, int rowStop // Scan range to work on
+    );
+
+// Apply dark
+bool pmDarkApply(pmReadout *readout,    // Readout to which to apply dark
+                 pmCell *dark,    // Dark to apply
+                 psImageMaskType bad         // Mask value to give bad pixels
+    );
+
+// I/O functions for darks
+
+// Write all darks within an FPA
+bool pmFPAWriteDark(pmFPA *fpa,         // FPA to write
+                    psFits *fits,       // FITS file to which to write
+                    pmConfig *config,   // Configuration
+                    bool blank,         // Write a blank only?
+                    bool recurse        // Recurse to lower levels?
+    );
+
+// Write all darks within a chip
+bool pmChipWriteDark(pmChip *chip,      // Chip to write
+                     psFits *fits,      // FITS file to which to write
+                     pmConfig *config,  // Configuration
+                     bool blank,        // Write a blank only?
+                     bool recurse       // Recurse to lower levels?
+    );
+
+// Write a dark to a FITS file
+bool pmCellWriteDark(pmCell *cell,      // Cell containing dark information
+                     psFits *fits,      // FITS file to which to write
+                     pmConfig *config,  // Configuration
+                     bool blank         // Write a blank only?
+    );
+
+// Read dark for all FPA from a FITS file
+bool pmFPAReadDark(pmFPA *fpa,          // FPA for which to read
+                   psFits *fits,        // FITS file to read
+                   pmConfig *config     // Configuration
+    );
+
+// Read dark for all chip from a FITS file
+bool pmChipReadDark(pmChip *chip,       // Chip for which to read
+                    psFits *fits,       // FITS file to read
+                    pmConfig *config    // Configuration
+    );
+
+// Read dark for a cell from a FITS file
+bool pmCellReadDark(pmCell *cell,       // Cell for which to read
+                    psFits *fits,       // FITS file to read
+                    pmConfig *config    // Configuration
+    );
+
+// Write dark table to FITS file
+bool pmDarkWrite(psFits *fits,          // FITS file to which to write
+                 psMetadata *header,    // Header to write
+                 const psArray *ordinates, // Dark ordinates to write
+                 const char *normConcept // Normalisation concept name
+    );
+
+// Read dark table from FITS file
+psArray *pmDarkRead(psString *normConcept, // Normalisation concept name
+                    psFits *fits        // FITS file to read
+    );
+
+bool pmDarkVisualInit(psArray *values);
+bool pmDarkVisualPixelFit(psVector *pixels, psVector *mask);
+bool pmDarkVisualCleanup();
+bool pmDarkVisualPixelModel(psPolynomialMD *poly, psArray *values);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmDetrendDB.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmDetrendDB.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmDetrendDB.c	(revision 42651)
@@ -0,0 +1,410 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmErrorCodes.h"
+#include "pmConfig.h"
+#include "pmConfigCommand.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmConfigCamera.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(void)
+{
+
+    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(TYPE) \
+  case PM_DETREND_TYPE_##TYPE: \
+    return psStringCopy(#TYPE);
+
+    switch (type) {
+        DETREND_STRING_CASE(NONE);
+        DETREND_STRING_CASE(MASK);
+        DETREND_STRING_CASE(BIAS);
+        DETREND_STRING_CASE(DARK);
+        DETREND_STRING_CASE(FLAT);
+        DETREND_STRING_CASE(FLATCORR);
+        DETREND_STRING_CASE(SHUTTER);
+        DETREND_STRING_CASE(FRINGE);
+        DETREND_STRING_CASE(ASTROM);
+        DETREND_STRING_CASE(NOISEMAP);
+	DETREND_STRING_CASE(VIDEOMASK);
+	DETREND_STRING_CASE(VIDEODARK);
+	DETREND_STRING_CASE(LINEARITY);
+	DETREND_STRING_CASE(NEWNONLIN);
+	DETREND_STRING_CASE(AUXMASK);
+	DETREND_STRING_CASE(KH_CORRECT);
+	DETREND_STRING_CASE(PATTERN_ROW_AMP);
+	DETREND_STRING_CASE(PATTERN_DEAD_CELLS);
+    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);
+
+    psIOBuffer *buffer = NULL;
+    psPipe *pipe = NULL;
+    psMetadata *answer = NULL;
+
+    int status, exit_status;
+    psString line = NULL;
+    psString time = psTimeToISO (&options->time);
+
+    char *type = NULL;
+    if (options->dettype) {
+        type = psMemIncrRefCounter (options->dettype);
+    } else {
+        type = pmDetrendTypeToString (options->type);
+	if (!type) {
+	  psError (PM_ERR_CONFIG, false, "unknown detrend type %d", options->type);
+	  goto failure_v1; // this goto must NOT free 'results'
+	}
+    }
+
+    pmDetrendSelectResults *results = pmDetrendSelectResultsAlloc();
+    psString realCamera = pmConfigCameraRootName (options->camera);
+    psStringAppend(&line, "detselect -search -inst %s -det_type %s -time %s", realCamera, type, time);
+    psFree (realCamera);
+
+    // require a filter for certain types of detrends:
+    if ((options->type == PM_DETREND_TYPE_FLAT) && !options->filter) {
+        psError (PM_ERR_CONFIG, false, "requesting a FLAT-class of detrend without a filter");
+        goto failure;
+    }
+    if ((options->type == PM_DETREND_TYPE_FLATCORR) && !options->filter) {
+        psError (PM_ERR_CONFIG, false, "requesting a FLATCORR-class of detrend without a filter");
+        goto failure;
+    }
+    if ((options->type == PM_DETREND_TYPE_FRINGE) && !options->filter) {
+        psError (PM_ERR_CONFIG, false, "requesting a FRINGE-class of detrend without a filter");
+        goto failure;
+    }
+
+    // add the restrictions
+    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);
+    }
+
+    if (!pmConfigDatabaseCommand(&line, config)) {
+        psError (PS_ERR_IO, false, "error building detrend command %s", line);
+        goto failure;
+    }
+
+    if (!pmConfigTraceCommand(&line)) {
+        psError (PS_ERR_IO, false, "error building detrend command %s", line);
+        goto failure;
+    }
+
+    psTrace("psModules.detrend", 5, "running %s", line);
+
+    // use psPipe to exec the command, wait for response
+    buffer = psIOBufferAlloc (512);
+    pipe = psPipeOpen (line);
+    if (!pipe) {
+        psError (PS_ERR_IO, false, "error calling command %s", line);
+        psLogMsg ("psModules.detrend", PS_LOG_ERROR, "detselect command:\n %s\n", line);
+        goto failure;
+    }
+
+    status = psIOBufferReadEmpty (buffer, 2000, pipe->fd_stdout);
+    if (!status) {
+        psError (PS_ERR_IO, false, "detselect is not responding");
+        psLogMsg ("psModules.detrend", PS_LOG_ERROR, "detselect command:\n %s\n", line);
+        goto failure;
+    }
+    exit_status = psPipeClose (pipe);
+    if (exit_status) {
+        psError (PS_ERR_IO, false, "error running detselect");
+        psLogMsg ("psModules.detrend", PS_LOG_ERROR, "detselect command:\n %s\n", line);
+        goto failure;
+    }
+
+    if (!buffer->data || strlen(buffer->data) == 0) {
+        psError(PS_ERR_IO, true, "Unable to find suitable detrend");
+        psLogMsg("psModules.detrend", PS_LOG_ERROR, "detselect command:\n %s\n", line);
+        goto failure;
+    }
+
+    psTrace("psModules.detrend", 5, "got answer: %s\n", buffer->data);
+
+    unsigned int nFail = 0;
+    answer = psMetadataConfigParse (NULL, &nFail, buffer->data, false);
+    if (!answer) {
+        psError(PS_ERR_IO, false, "failed to parse response from detselect\n");
+        psLogMsg ("psModules.detrend", PS_LOG_ERROR, "detselect response (%d bytes):\n %s\n", buffer->n, buffer->data);
+        psLogMsg ("psModules.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 ("psModules.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 ("psModules.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 ("psModules.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 ("psModules.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);
+ failure_v1:
+    psFree (answer);
+    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;
+    }
+
+    // timeout somewhat longer than 2sec.  this could still be too short....
+    status = psIOBufferReadEmpty (buffer, 2000, 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);
+
+    if (!buffer->n) {
+        psLogMsg ("psModules.detrend", PS_LOG_ERROR, "detselect response (%d bytes):\n %s\n", buffer->n, buffer->data);
+        psError(PS_ERR_IO, true, "no matching detrend data in database\n");
+        goto failure;
+    }
+
+    psMetadata *answer = psMetadataConfigParse (NULL, &nFail, buffer->data, false);
+    if (!answer) {
+        psError(PS_ERR_IO, false, "failed to parse response from detselect\n");
+        psLogMsg ("psModules.detrend", PS_LOG_ERROR, "detselect response (%d bytes):\n %s\n", buffer->n, buffer->data);
+        goto failure;
+    }
+    psMetadataItem *item = psMetadataLookup (answer, "detNormalizedImfile");
+    if ((item->type == PS_DATA_METADATA_MULTI) && (item->data.list->n > 1)) {
+        psError(PS_ERR_IO, false, "detselect returned too many files\n");
+        goto failure;
+    }
+
+    psMetadata *md = psMetadataLookupPtr (NULL, answer, "detNormalizedImfile");
+    if (!md) {
+        psError(PS_ERR_IO, false, "detselect response is missing 'detNormalizedImfile' Metadata\n");
+        psLogMsg ("psModules.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);
+
+    // XXX: A somewhat hacked bit of code to force the analysis to use a specific version of the detrend file
+    char *is_nebulous = strstr(result,"neb://");
+    if (is_nebulous) { // This file matches the nebulous string
+      psString truncated = psStringCopy(is_nebulous + 6);
+      //      printf("A: %s %s\n",result,truncated);
+
+      psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes,"PPIMAGE");
+      psMetadata *detloc = psMetadataLookupMetadata(&status, recipe, "DETREND.LOCATIONS");
+
+      if (detloc) { // This exists, so we have the information.
+	psString location = psMetadataLookupStr(&status, detloc, classID);
+	//	printf("B: %s %s\n", classID,location);
+	if (location) { // Found a location
+	  psString ext_key = strstr(truncated,"/"); // Find the first '/'
+	  //	  printf("C: %s\n", ext_key);
+	  if (ext_key) {
+	    psString copyBecauseReasons = psStringCopy(ext_key);
+	    psStringPrepend(&copyBecauseReasons,"neb://%s",location);
+	    //	    printf("D: %s\n",copyBecauseReasons);
+	    psFree(result); // Yes, because we are going to reallocate a clean copy of cBR to this object.
+	    result = psStringCopy(copyBecauseReasons);
+	    //	    psFree(ext_key); // No, because this is just a pointer into some other memory space.
+	    psTrace("psModules.detrend", 5, "altered detrend file: %s\n", result);
+	    psFree(copyBecauseReasons); // Yes, because we copied this to the output object, and so we don't need this any more.
+	  }
+	  //	  psFree(location);  // Don't free this because the string lookup isn't a copy, so it deletes it from the config structure entirely.
+	}
+      }
+      //      psFree(is_nebulous);  // No, because this is just a pointer into some other memory space.
+      psFree(truncated);
+    }
+	  
+    
+    psFree (answer);
+    psFree (pipe);
+    psFree (buffer);
+    psFree (line);
+    return result;
+
+failure:
+    psFree (array);
+    psFree (pipe);
+    psFree (buffer);
+    psFree (line);
+    return NULL;
+}
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmDetrendDB.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmDetrendDB.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmDetrendDB.h	(revision 42651)
@@ -0,0 +1,79 @@
+/* @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.16 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-10-27 00:23:15 $
+ * 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
+/// @{
+
+#include <pslib.h>
+
+#include "pmConfig.h"
+
+typedef enum {
+    PM_DETREND_TYPE_NONE,
+    PM_DETREND_TYPE_MASK,
+    PM_DETREND_TYPE_BIAS,
+    PM_DETREND_TYPE_DARK,
+    PM_DETREND_TYPE_FLAT,
+    PM_DETREND_TYPE_FLATCORR,
+    PM_DETREND_TYPE_SHUTTER,
+    PM_DETREND_TYPE_FRINGE,
+    PM_DETREND_TYPE_BACKGROUND,
+    PM_DETREND_TYPE_ASTROM,
+    PM_DETREND_TYPE_NOISEMAP,
+    PM_DETREND_TYPE_VIDEOMASK,
+    PM_DETREND_TYPE_VIDEODARK,
+    PM_DETREND_TYPE_LINEARITY,
+    PM_DETREND_TYPE_NEWNONLIN,
+    PM_DETREND_TYPE_AUXMASK,
+    PM_DETREND_TYPE_KH_CORRECT,
+    PM_DETREND_TYPE_PATTERN_ROW_AMP,
+    PM_DETREND_TYPE_PATTERN_DEAD_CELLS,
+} 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(void);
+pmDetrendSelectResults *pmDetrendSelect (const pmDetrendSelectOptions *options, const pmConfig *config);
+char *pmDetrendFile (const char *detID, const char *classID, const pmConfig *config);
+
+/// @}
+# endif
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmDetrendThreads.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmDetrendThreads.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmDetrendThreads.c	(revision 42651)
@@ -0,0 +1,65 @@
+#include <stdio.h>
+#include <pslib.h>
+#include <string.h>
+
+#include "psPolynomialMD.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmHDUUtils.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+
+#include "pmOverscan.h"
+#include "pmBias.h"
+#include "pmDark.h"
+#include "pmShutterCorrection.h"
+#include "pmFlatField.h"
+#include "pmDetrendThreads.h"
+
+static int scanRows = 0;                // Number of rows to work on at once
+
+int pmDetrendGetScanRows(void)
+{
+    return scanRows;
+}
+
+bool pmDetrendSetThreadTasks (int newScanRows)
+{
+    psAssert(scanRows == 0, "programming error: program called pmDetrendSetThreadTasks twice");
+
+    PS_ASSERT_INT_POSITIVE(newScanRows, false);
+    scanRows = newScanRows;
+
+    {
+        psThreadTask *task = psThreadTaskAlloc("PSMODULES_DETREND_BIAS", 7);
+        task->function = &pmBiasSubtractScan_Threaded;
+        psThreadTaskAdd(task);
+        psFree(task);
+    }
+
+    {
+        psThreadTask *task = psThreadTaskAlloc("PSMODULES_DETREND_DARK", 9);
+        task->function = &pmDarkApplyScan_Threaded;
+        psThreadTaskAdd(task);
+        psFree(task);
+    }
+
+    {
+        psThreadTask *task = psThreadTaskAlloc("PSMODULES_DETREND_SHUTTER", 8);
+        task->function = &pmShutterCorrectionApplyScan_Threaded;
+        psThreadTaskAdd(task);
+        psFree(task);
+    }
+
+    {
+        psThreadTask *task = psThreadTaskAlloc("PSMODULES_DETREND_FLAT", 10);
+        task->function = &pmFlatFieldScan_Threaded;
+        psThreadTaskAdd(task);
+        psFree(task);
+    }
+
+    // NOISEMAP : for now, not applied in the threaded loop 
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmDetrendThreads.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmDetrendThreads.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmDetrendThreads.h	(revision 42651)
@@ -0,0 +1,23 @@
+/* @file pmDetrendThreads.h
+ * @brief theading functions related to detrends
+ * @author Eugene Magnier, IfA
+ *
+ * @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-08-05 01:24:47 $
+ * Copyright 2004-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_DETREND_THREADS_H
+#define PM_DETREND_THREADS_H
+
+/// @addtogroup detrend Detrend Creation and Application
+/// @{
+
+/// init the thread handler tasks for detrending
+bool pmDetrendSetThreadTasks (int newScanRows);
+
+/// get the requested number of scan rows per thread
+int pmDetrendGetScanRows(void);
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmFlatField.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmFlatField.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmFlatField.c	(revision 42651)
@@ -0,0 +1,178 @@
+#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"
+#include "pmDetrendThreads.h"
+
+bool pmFlatFieldScan_Threaded(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    psImage *inImage = job->args->data[0]; // Input image
+    psImage *inMask  = job->args->data[1]; // Input mask
+    psImage *inVar   = job->args->data[2]; // Input variance
+    const psImage *flatImage = job->args->data[3]; // Flat-field image
+    const psImage *flatMask  = job->args->data[4]; // Flat-field mask
+
+    psImageMaskType badFlat = PS_SCALAR_VALUE(job->args->data[5], PS_TYPE_IMAGE_MASK_DATA);
+    int xOffset        = PS_SCALAR_VALUE(job->args->data[6], S32);
+    int yOffset        = PS_SCALAR_VALUE(job->args->data[7], S32);
+    int rowStart       = PS_SCALAR_VALUE(job->args->data[8], S32);
+    int rowStop        = PS_SCALAR_VALUE(job->args->data[9], S32);
+
+    return pmFlatFieldScan(inImage, inMask, inVar, flatImage, flatMask, badFlat,
+                           xOffset, yOffset, rowStart, rowStop);
+}
+
+
+bool pmFlatFieldScan(psImage *inImage, psImage *inMask, psImage *inVar, const psImage *flatImage,
+                     const psImage *flatMask, psImageMaskType badFlat,
+                     int xOffset, int yOffset, int rowStart, int rowStop)
+{
+    // Neglecting asserts, because inputs should have been checked already
+
+    int numCols = inImage->numCols;     // Number of columns
+
+    // Using i,j for image; x,y for flat.
+    for (int j = rowStart, y = rowStart + yOffset; j < rowStop; j++, y++) {
+        for (int i = 0, x = xOffset; i < numCols; i++, x++) {
+            float flatValue = 1.0 / flatImage->data.F32[y][x];
+            if (!isfinite(flatValue) || flatValue <= 0.0 ||
+                (flatMask && flatMask->data.PS_TYPE_IMAGE_MASK_DATA[y][x])) {
+                if (inMask) {
+                    inMask->data.PS_TYPE_IMAGE_MASK_DATA[j][i] |= badFlat;
+                }
+                inImage->data.F32[j][i] = NAN;
+                if (inVar) {
+                    inVar->data.F32[j][i] = NAN;
+                }
+            } else {
+                inImage->data.F32[j][i] *= flatValue;
+                if (inVar) {
+                    inVar->data.F32[j][i] *= PS_SQR(flatValue);
+                }
+            }
+        }
+    }
+
+    return true;
+}
+
+bool pmFlatField(pmReadout *in, const pmReadout *flat, psImageMaskType badFlat)
+{
+    PM_ASSERT_READOUT_NON_NULL(in, false);
+    PM_ASSERT_READOUT_IMAGE(in, false);
+    PM_ASSERT_READOUT_NON_NULL(flat, false);
+    PM_ASSERT_READOUT_IMAGE(flat, false);
+    if (in->mask) {
+        PM_ASSERT_READOUT_MASK(in, false);
+    }
+    if (in->variance) {
+        PM_ASSERT_READOUT_VARIANCE(in, false);
+    }
+    if (flat->mask) {
+        PM_ASSERT_READOUT_MASK(flat, false);
+    }
+
+    psImage *inImage   = in->image;     // Input image
+    psImage *inMask    = in->mask;      // Mask for input image
+    psImage *inVar     = in->variance;  // Variance 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;
+    }
+
+    bool threaded = true;
+    int scanRows = pmDetrendGetScanRows();
+    if (scanRows == 0) {
+        threaded = false;
+        scanRows = inImage->numRows;
+    }
+
+    for (int rowStart = 0; rowStart < inImage->numRows; rowStart += scanRows) {
+      int rowStop = PS_MIN(rowStart + scanRows, inImage->numRows);
+
+      if (threaded) {
+          // allocate a job, construct the arguments for this job
+          psThreadJob *job = psThreadJobAlloc("PSMODULES_DETREND_FLAT");
+          psArrayAdd(job->args, 1, inImage);
+          psArrayAdd(job->args, 1, inMask);
+          psArrayAdd(job->args, 1, inVar);
+          psArrayAdd(job->args, 1, flatImage);
+          psArrayAdd(job->args, 1, flatMask);
+          PS_ARRAY_ADD_SCALAR(job->args, badFlat, PS_TYPE_IMAGE_MASK);
+          PS_ARRAY_ADD_SCALAR(job->args, xOffset, PS_TYPE_S32);
+          PS_ARRAY_ADD_SCALAR(job->args, yOffset, PS_TYPE_S32);
+          PS_ARRAY_ADD_SCALAR(job->args, rowStart, PS_TYPE_S32);
+          PS_ARRAY_ADD_SCALAR(job->args, rowStop, PS_TYPE_S32);
+
+          if (!psThreadJobAddPending(job)) {
+              return false;
+          }
+      } else if (!pmFlatFieldScan(inImage, inMask, inVar, flatImage, flatMask, badFlat,
+                                  xOffset, yOffset, rowStart, rowStop)) {
+          psError(PS_ERR_UNKNOWN, false, "Unable to flat-field image.");
+          return false;
+      }
+    }
+
+    if (threaded) {
+        // wait here for the threaded jobs to finish
+        if (!psThreadPoolWait(true, true)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to flat-field image.");
+            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_branches/psModules.20240412/src/detrend/pmFlatField.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmFlatField.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmFlatField.h	(revision 42651)
@@ -0,0 +1,48 @@
+/* @file pmFlatField.h
+ * @brief Apply flat field calibration
+ *
+ * @author Ross Harman, MHPCC
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.16 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-12 19:25:52 $
+ * 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
+                 psImageMaskType badFlat     ///< Mask value to give bad flat pixels
+                );
+
+/// Thread entry point for flat-fielding
+bool pmFlatFieldScan_Threaded(psThreadJob *job ///< Job to exectute
+    );
+
+/// Flat-field a scan
+bool pmFlatFieldScan(
+    psImage *inImage,                   ///< Input image to correct
+    psImage *inMask,                    ///< Input mask image
+    psImage *inVar,                     ///< Input variance image
+    const psImage *flatImage,           ///< Flat-field image
+    const psImage *flatMask,            ///< Flat-field mask
+    psImageMaskType badFlag,            ///< Mask value to give bad pixels
+    int xOffset, int yOffset,           ///< Offset between input and flat-field
+    int rowStart, int rowStop           ///< Scan range
+    );
+
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmFlatNormalize.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmFlatNormalize.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmFlatNormalize.c	(revision 42651)
@@ -0,0 +1,191 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <math.h>
+#include <pslib.h>
+
+#include "pmFlatNormalize.h"
+
+// XXX this function should take the abstract mask names and set bad values in a more precise way
+
+// 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_IMAGE_MASK); // Mask for bad measurements
+    psImageInit(fluxMask, 0);
+    psVector *gainMask = psVectorAlloc(numChips, PS_TYPE_VECTOR_MASK); // Mask for bad gains
+    psVectorInit(gainMask, 0);
+    psVector *expMask = psVectorAlloc(numExps, PS_TYPE_VECTOR_MASK); // 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.PS_TYPE_IMAGE_MASK_DATA[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.PS_TYPE_VECTOR_MASK_DATA[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.PS_TYPE_VECTOR_MASK_DATA[j] && !fluxMask->data.PS_TYPE_IMAGE_MASK_DATA[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.PS_TYPE_VECTOR_MASK_DATA[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.PS_TYPE_VECTOR_MASK_DATA[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.PS_TYPE_IMAGE_MASK_DATA[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.PS_TYPE_VECTOR_MASK_DATA[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.PS_TYPE_VECTOR_MASK_DATA[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.PS_TYPE_VECTOR_MASK_DATA[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.PS_TYPE_VECTOR_MASK_DATA[i]) {
+            chipGains->data.F32[i] = expf(chipGains->data.F32[i]);
+        }
+    }
+    for (int i = 0; i < numExps; i++) {
+        if (!expMask->data.PS_TYPE_VECTOR_MASK_DATA[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_branches/psModules.20240412/src/detrend/pmFlatNormalize.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmFlatNormalize.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmFlatNormalize.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/detrend/pmFringeStats.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmFringeStats.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmFringeStats.c	(revision 42651)
@@ -0,0 +1,1229 @@
+#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"
+
+#include "psPolynomialMD.h"
+#include "psMinimizePolyFit.h"
+#include "psVector.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);
+    }
+
+    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_VECTOR_MASK);
+    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_VECTOR_MASK, 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]);
+        psVectorMaskType maskValue = 0;
+        if (mask && mask->data.PS_TYPE_VECTOR_MASK_DATA[i]) {
+            maskValue = 0xff;
+        }
+        psMetadataAddVectorMask(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_VECTOR_MASK); // mask
+    regions->x = x;
+    regions->y = y;
+    regions->mask = mask;
+
+    #define READ_REGIONS_ROW(VECTOR, TYPE, DATATYPE, NAME, DESCRIPTION) \
+    VECTOR->data.DATATYPE[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, F32, "x", "x position");
+        READ_REGIONS_ROW(y, F32, F32, "y", "y position");
+        READ_REGIONS_ROW(mask, VectorMask, PS_TYPE_VECTOR_MASK_DATA, "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, psImageMaskType 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;
+
+	if (readout->parent->hdu) {
+	  psTrace("psModules.detrend", 7, "[%d:%d,%d:%d]: %f %f : %s\n", (int)region.x0, (int)region.x1,
+		  (int)region.y0, (int)region.y1, fPt[i], dfPt[i], readout->parent->hdu->extname);
+	}
+	else {
+	  psTrace("psModules.detrend", 7, "[%d:%d,%d:%d]: %f %f : THIS_IS_A_SPOOKY_GHOST_CELL\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
+	if (!fringe) {
+	    psWarning ("skipping empty fringe stats for entry %ld -- video cell?\n", i);
+	    continue;
+	}
+        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_VECTOR_MASK);
+    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
+	if (!fringe) {
+	    psWarning ("skipping empty fringe stats for entry %ld -- video cell?\n", i);
+	    continue;
+	}
+        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.PS_TYPE_VECTOR_MASK_DATA[offset], regions->mask->data.PS_TYPE_VECTOR_MASK_DATA, regions->mask->n * sizeof(psVectorMaskType));
+        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
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+psArray *pmFringesFormatTable(psMetadata *header, const psArray *fringes)
+{
+    PS_ASSERT_PTR_NON_NULL(header, 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 NULL;
+        }
+    }
+
+    // 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_VECTOR_MASK, false);
+        PS_ASSERT_VECTOR_SIZE(mask, (long)numRows, false);
+    }
+
+    // We need to write:
+    // Scalars: dX, dY, nX, nY
+    // Vectors: x, y, mask, f, df
+
+    psMetadataAddS32(header, PS_LIST_TAIL, "PSFRNGDX", PS_META_REPLACE, "Median box half-width", regions->dX);
+    psMetadataAddS32(header, PS_LIST_TAIL, "PSFRNGDY", PS_META_REPLACE, "Median box half-height", regions->dY);
+    psMetadataAddS32(header, PS_LIST_TAIL, "PSFRNGNX", PS_META_REPLACE, "Large-scale smoothing in x", regions->nX);
+    psMetadataAddS32(header, 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]);
+        psVectorMaskType maskValue = 0;             // Mask value
+        if (mask && mask->data.PS_TYPE_VECTOR_MASK_DATA[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);
+
+        psMetadataAddVectorMask(row, PS_LIST_TAIL, "mask", PS_META_REPLACE, "Mask", maskValue);
+        table->data[i] = row;
+    }
+
+    return table;
+}
+
+psArray *pmFringesParseTable(psArray *table, psMetadata *header)
+{
+    PS_ASSERT_PTR_NON_NULL(table, NULL);
+    PS_ASSERT_PTR_NON_NULL(header, NULL);
+
+    bool mdok;                          // Status of MD lookup
+
+    // 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_VECTOR_MASK); // 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, DATATYPE, NAME, DESCRIPTION) \
+    { \
+        VECTOR->data.DATATYPE[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_DATA_##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; \
+        } \
+    }
+
+    // XXX : need to extend this to support arbitrary types for the vectors on disk
+    // 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, F32, "x", "x position");
+        READ_FRINGES_VECTOR_ROW(y, F32, F32, "y", "y position");
+        READ_FRINGES_VECTOR_ROW(mask, VectorMask, PS_TYPE_VECTOR_MASK_DATA, "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;
+}
+
+bool pmFringesFormat(pmCell *cell, psMetadata *inHeader, const psArray *fringes)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_ARRAY_NON_NULL(fringes, false);
+
+    psMetadata *header = psMemIncrRefCounter(inHeader); // Metadata to hold the scalars; will be the header
+    if (!inHeader) {
+	inHeader = psMetadataAlloc();
+    }
+
+    psArray *table = pmFringesFormatTable(header, fringes);
+    if (!table) {
+	psError (PS_ERR_UNKNOWN, false, "unable to generate table from fringes");
+	psFree(header);
+	return NULL;
+    }
+
+    psMetadataAdd(cell->analysis, PS_LIST_TAIL, "FRINGE.HEADER", PS_DATA_METADATA, "Header for fringe data", header);
+    psFree(header);
+
+    psMetadataAdd(cell->analysis, PS_LIST_TAIL, "FRINGE.TABLE", PS_DATA_ARRAY, "Fringe data", table);
+    psFree(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.TABLE"); // FITS table
+    if (!table) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to find table for fringe data.\n");
+        return NULL;
+    }
+
+    psArray *fringes = pmFringesParseTable(table, header);
+    if (!fringes) {
+	psError(PS_ERR_UNKNOWN, false, "Unable to add parse the fringe table data");
+	return NULL;
+    }
+
+    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.PS_TYPE_VECTOR_MASK_DATA[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.PS_TYPE_VECTOR_MASK_DATA[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)) {
+        psLogMsg("psModules.detrend", PS_LOG_INFO, "Could not solve linear equations.  Returning NULL.\n");
+	psFree(A);
+	psFree(B);
+        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.PS_TYPE_VECTOR_MASK_DATA[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_VECTOR_MASK);
+    assert(diffs->n == mask->n);
+
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_QUARTILE); // Statistics
+    if (!psVectorStats(stats, diffs, NULL, mask, 1)) {
+	psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	return 0;
+    }
+    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.PS_TYPE_VECTOR_MASK_DATA[i], diffs->data.F32[i]);
+        if (!mask->data.PS_TYPE_VECTOR_MASK_DATA[i] && fabs(diffs->data.F32[i]) > middle + thresh) {
+            psTrace("psModules.detrend", 5, "Masking %d: %f\n", i, diffs->data.F32[i]);
+            mask->data.PS_TYPE_VECTOR_MASK_DATA[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_VECTOR_MASK);
+        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.PS_TYPE_VECTOR_MASK_DATA[j] = 1;
+                psTrace("psModules.detrend", 9, "Masking region %d because not finite in fringe %d.\n", j, i);
+            }
+	    else if (fabs(fringe->f->data.F32[j]) > 0.1) {
+	      mask->data.PS_TYPE_VECTOR_MASK_DATA[j] = 1;
+	      psTrace("psModules.detrend", 9, "Masking region %d because too large fringe %d.\n", j, i);
+	    }
+	    // Mask bad points in the science data as well.
+	    if ((i == 0) && (!isfinite(science->f->data.F32[j]))) {
+                mask->data.PS_TYPE_VECTOR_MASK_DATA[j] = 1;
+                psTrace("psModules.detrend", 9, "Masking region %d because not finite in science fringe %d.\n", j, i);
+	    }	      
+	    psTrace("psModules.detrend", 7, "F %f %f %f %d\n",
+		    fringe->f->data.F32[j], science->f->data.F32[j],
+		    1 / science->df->data.F32[j],(int) mask->data.PS_TYPE_VECTOR_MASK_DATA[j]);
+        }
+    }
+    // Allocate array of vectors to hold data.
+    psArray *bins = psArrayAlloc(4000);
+    for (int j = 0; j < bins->n; j++) {
+      bins->data[j] = psVectorAllocEmpty(1,PS_TYPE_F32);
+      //      psVector *v = psVectorAllocEmpty(1,PS_TYPE_F32);
+      //      bins = psArrayAdd(bins,1,v);
+    }
+
+    // Fill vectors
+    pmFringeStats *fringe = fringes->data[0];
+    for (int j = 0; j < numRegions; j++) {
+      if (mask->data.PS_TYPE_VECTOR_MASK_DATA[j] == 0) {
+	int array_bin = (int) ((fringe->f->data.F32[j] - -0.1) / 5e-5);
+	psVector *bin = bins->data[array_bin];
+	psVectorAppend(bin,science->f->data.F32[j]);
+      }
+    }
+    
+    psVector *fringe_positions = psVectorAllocEmpty(4000,PS_TYPE_F32);
+    psVector *science_values   = psVectorAllocEmpty(4000,PS_TYPE_F32);
+    psVector *science_errors   = psVectorAllocEmpty(4000,PS_TYPE_F32);
+    psVector *science_counts   = psVectorAllocEmpty(4000,PS_TYPE_S32);
+
+    psStats *binStats = psStatsAlloc(PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV);
+    for (int i = 0; i < 4000; i++) {
+      psVector *bin = bins->data[i];
+      if (bin->n > 10) {
+	psStatsInit(binStats);
+
+	psVectorStats(binStats,bin,NULL,NULL,1);
+	
+	if (isfinite(binStats->clippedStdev) &&
+	    isfinite(binStats->clippedMean) &&
+	    (binStats->clippedStdev > 0) &&
+	    (binStats->clippedNvalues > 10) &&
+	    (binStats->clippedNvalues > 0.5 * bin->n)
+	    ) {
+	  psVectorAppend(fringe_positions,-0.1 + i * 5e-5);
+	  psVectorAppend(science_values, binStats->clippedMean);
+	  psVectorAppend(science_errors, binStats->clippedStdev);
+	  psVectorAppend(science_counts, bin->n);
+	}
+      }
+      psFree(bins->data[i]);
+    }
+    psFree(bins);
+    psFree(binStats);
+
+    for (int i = 0; i < fringe_positions->n; i++) {
+      psTrace("psModules.detrend",7,"FITDATA: %f %f %f %d\n",
+	      fringe_positions->data.F32[i],
+	      science_values->data.F32[i],
+	      science_errors->data.F32[i],
+	      science_counts->data.S32[i]);
+    }
+/*     // Begin switch from old outlier removal and fitting code. */
+
+    psPolynomial1D *poly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, 1);
+
+    //    pmFringeStats *fringe = fringes->data[0];
+/*     psVector *errors = psVectorAlloc(science->df->n,PS_TYPE_F32); */
+/*     for (int j = 0; j < errors->n; j++) { */
+/*       errors->data.F32[j] = 1 / science->df->data.F32[j]; */
+/*     } */
+/*     psVectorFitPolynomial1D(poly,mask,0xff,science->f,errors,fringe->f); */
+    psVectorFitPolynomial1D(poly,NULL,0xff,science_values,science_errors,fringe_positions);
+    psFree(fringe_positions);
+    psFree(science_values);
+    psFree(science_errors);
+    psFree(science_counts);
+    
+    for (int i = 0; i <= poly->nX; i++) {
+      scale->coeff->data.F32[i] = poly->coeff[i];
+      psTrace("psModules.detrend",7,"COEFFS: %d %g %g %g\n",i,scale->coeff->data.F32[i],poly->coeff[i],poly->coeffErr[i]);
+    }
+
+    psFree(poly);
+    //    psFree(fringe);
+    //    psFree(errors);
+
+    psFree(median);
+    psFree(diff);
+    return scale;
+    // End switch from old code.
+    
+
+    
+# 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.PS_TYPE_VECTOR_MASK_DATA[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
+    if (!psVectorStats(median, science->f, NULL, NULL, 0)) {
+	psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	return NULL;
+    }
+    scale->coeff->data.F32[0] = median->sampleMedian;
+    for (int i = 0; i < fringes->n; i++) {
+        pmFringeStats *fringe = fringes->data[i]; // The fringe of interest
+        if (!psVectorStats(median, fringe->f, NULL, NULL, 0)) {
+	    psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	    return NULL;
+	}
+        scale->coeff->data.F32[0] -= median->sampleMedian;
+	if (i != 0) {
+	  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;
+    //# endif
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Fringe correction
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// XXX note that this modifies the input fringe images
+psImage *pmFringeCorrect(pmReadout *readout, pmFringeRegions *fringes, psArray *fringeImages,
+                         psArray *fringeStats, psImageMaskType 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_branches/psModules.20240412/src/detrend/pmFringeStats.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmFringeStats.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmFringeStats.h	(revision 42651)
@@ -0,0 +1,226 @@
+/* @file pmFringeStats.h
+ * @brief Measure fringe statistics, and apply correction
+ *
+ * @author Eugene Magnier, IfA
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-01-27 06:39:38 $
+ * 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
+                                    psImageMaskType 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
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/// Convert an array of fringes measurements to a psArray suitable for writing as a FITS table.
+///
+/// Converts an array of fringe measurements for a cell into the corresponding rows of a FITS
+/// table (array of psMetadata).  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 rows (as
+/// x, y, mask, f, df; f and df are vectors).  Use psFitsTableWrite to save the resulting rows
+/// to disk.
+psArray *pmFringesFormatTable(psMetadata *header, const psArray *fringes);
+
+
+/// Parses an array of fringes measurements from a FITS table.
+///
+/// The fringes for the cell are read from the FITS table (array of psMetadata rows).  The
+/// table provides the region and the (possibly multiple) fringe statistics for that region.
+/// The supplied header defines the scalar values dX, dY, nX and nY (as PSFRNGDX, PSFRNGDY,
+/// PSFRNGNX, PSFRNGNY)
+psArray *pmFringesParseTable(psArray *table, psMetadata *header);
+
+/// Deprecated Function: converts the fringes to a FITS table (array of psMetadata) and saves
+/// them on the cell->analysis; scalar values are dX, dY, nX and nY are written to the header
+/// (as PSFRNGDX, PSFRNGDY, PSFRNGNX, PSFRNGNY)
+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
+                    );
+
+/// Deprecated Function: pulls a the header and FITS table (array of psMetadata) representing
+/// the fringes from the cell->analysis and converts to fringe measurements
+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
+                         psImageMaskType 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_branches/psModules.20240412/src/detrend/pmGainTweak.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmGainTweak.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmGainTweak.c	(revision 42651)
@@ -0,0 +1,59 @@
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmFPA.h"
+
+#include "pmGainTweak.h"
+
+bool pmGainTweak(const pmCell *cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    pmChip *chip = cell->parent;
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+
+    int numCells = chip->cells;         // Number of cells
+    psVector *gains = psVectorAlloc(numCells, PS_TYPE_F32); // Gain for each cell
+    psVector *mask = psVectorAlloc(numCells, PS_TYPE_VECTOR_MASK); // Mask for gains
+    psVectorInit(mask, 0);
+    int numGood = 0;                    // Number of good gains
+
+   for (int i = 0; i < numCells; i++) {
+        pmCell *otherCell = chip->cells->data[i]; // A different cell
+        if (otherCell == cell) {
+            mask->data.PS_TYPE_VECTOR_MASK_DATA[i] = 0xFF;
+            continue;
+        }
+        float gain = psMetadataLookupF32(NULL, otherCell->concepts, "CELL.GAIN"); // Gain for cell
+        gains->data.F32[i] = gain;
+        if (!isfinite(gains->data.F32[i])) {
+            mask->data.PS_TYPE_VECTOR_MASK_DATA[i] = 0xFF;
+        } else {
+            numGood++;
+        }
+   }
+
+   if (numGood == 0) {
+       psError(PM_ERR_DETREND, true, "No other cell gains available with which to tweak gain.");
+       psFree(gains);
+       psFree(mask);
+       return false;
+   }
+
+   psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN); // Statistics
+   if (!psVectorStats(stats, gains, NULL, mask, 0xFF)) {
+       psError(psErrorCodeLast(), false, "Unable to determine target gain");
+       psFree(stats);
+       psFree(gains);
+       psFree(mask);
+       return false;
+   }
+   float target = stats->sampleMedian;  // Target gain
+   psFree(stats);
+   psFree(gains);
+   psFree(mask);
+
+   psMetadataItem *item = psMetadataLookup(cell->concepts, "CELL.GAIN");
+   item->data.F32 = target;
+
+   return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmMaskBadPixels.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmMaskBadPixels.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmMaskBadPixels.c	(revision 42651)
@@ -0,0 +1,332 @@
+#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 "pmFPAMaskWeight.h"
+#include "pmMaskBadPixels.h"
+
+bool pmMaskBadPixels(pmReadout *input, const pmReadout *mask, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(input, false);
+    PS_ASSERT_PTR_NON_NULL(input->mask, false);
+    PS_ASSERT_IMAGE_TYPE(input->mask, PS_TYPE_IMAGE_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_IMAGE_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_IMAGE_MASK
+    psImageMaskType **exVal = exMask->data.PS_TYPE_IMAGE_MASK_DATA;
+    psImageMaskType **inVal = inMask->data.PS_TYPE_IMAGE_MASK_DATA;
+
+    // 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] |= (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;
+}
+
+
+bool pmMaskFlagSuspectPixelsBySigma(pmReadout *output, const pmReadout *readout, float median, float stdev,
+				    float rej, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_FLOAT_LARGER_THAN(rej, 0.0, false);
+    PS_ASSERT_IMAGE_NON_NULL(readout->image, false);
+    PS_ASSERT_IMAGE_NON_EMPTY(readout->image, false);
+    PS_ASSERT_IMAGE_TYPE(readout->image, PS_TYPE_F32, false);
+    if (readout->mask) {
+        PS_ASSERT_IMAGE_NON_EMPTY(readout->mask, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(readout->image, readout->mask, false);
+        PS_ASSERT_IMAGE_TYPE(readout->mask, PS_TYPE_IMAGE_MASK, false);
+    }
+    PS_ASSERT_PTR_NON_NULL(output, false);
+
+    bool mdok;                          // Status of MD lookup
+    psImage *suspect = psMetadataLookupPtr(&mdok, output->analysis, PM_MASK_ANALYSIS_SUSPECT); // Suspect img
+    if (suspect) {
+        PS_ASSERT_IMAGE_NON_EMPTY(suspect, false);
+        PS_ASSERT_IMAGE_TYPE(suspect, PS_TYPE_F32, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(readout->image, suspect, false);
+        psMemIncrRefCounter(suspect);
+    } else {
+        suspect = psImageAlloc(readout->image->numCols, readout->image->numRows, PS_TYPE_F32);
+        psImageInit(suspect, 0);
+        psMetadataAddImage(output->analysis, PS_LIST_TAIL, PM_MASK_ANALYSIS_SUSPECT, PS_META_REPLACE,
+                           "Suspect pixels", suspect);
+        psMetadataAddS32(output->analysis, PS_LIST_TAIL, PM_MASK_ANALYSIS_NUM, PS_META_REPLACE,
+                         "Number of input images", 0);
+    }
+
+    if (!isfinite(median) || !isfinite(stdev)) {
+        // If we get down here and the statistics are missing, then we should go and mask the entire image
+        psWarning("Missing statistics --- flagging entire image as suspect.");
+        psBinaryOp (suspect, suspect, "+", psScalarAlloc(1.0, PS_TYPE_F32));
+        return true;
+    }
+
+    psImage *image = readout->image;    // Image of interest
+    psImage *mask = readout->mask;      // Corresponding mask
+
+    psTrace ("psModules.detrend", 3, "suspect: %f +/- %f\n", median, stdev);
+
+    // XXX this loop could result in pixels with suspect = 0.0 but no valid input pixels (all
+    // masked).  need to track the number of good as well as suspect pixels?
+    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) continue;
+	    if (mask && (mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskVal)) continue;
+	    suspect->data.F32[y][x] += 1.0;
+        }
+    }
+    psFree(suspect);                    // Drop reference
+
+    psMetadataItem *numItem = psMetadataLookup(output->analysis, PM_MASK_ANALYSIS_NUM); // Item with number
+    assert(numItem);
+    numItem->data.S32++;
+
+    return true;
+}
+
+bool pmMaskFlagSuspectPixelsByValue(pmReadout *output, const pmReadout *readout, 
+				    float min, float max, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_IMAGE_NON_NULL(readout->image, false);
+    PS_ASSERT_IMAGE_NON_EMPTY(readout->image, false);
+    PS_ASSERT_IMAGE_TYPE(readout->image, PS_TYPE_F32, false);
+    if (readout->mask) {
+        PS_ASSERT_IMAGE_NON_EMPTY(readout->mask, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(readout->image, readout->mask, false);
+        PS_ASSERT_IMAGE_TYPE(readout->mask, PS_TYPE_IMAGE_MASK, false);
+    }
+    PS_ASSERT_PTR_NON_NULL(output, false);
+
+    bool mdok;                          // Status of MD lookup
+    psImage *suspect = psMetadataLookupPtr(&mdok, output->analysis, PM_MASK_ANALYSIS_SUSPECT); // Suspect img
+    if (suspect) {
+        PS_ASSERT_IMAGE_NON_EMPTY(suspect, false);
+        PS_ASSERT_IMAGE_TYPE(suspect, PS_TYPE_F32, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(readout->image, suspect, false);
+        psMemIncrRefCounter(suspect);
+    } else {
+        suspect = psImageAlloc(readout->image->numCols, readout->image->numRows, PS_TYPE_F32);
+        psImageInit(suspect, 0);
+        psMetadataAddImage(output->analysis, PS_LIST_TAIL, PM_MASK_ANALYSIS_SUSPECT, PS_META_REPLACE,
+                           "Suspect pixels", suspect);
+        psMetadataAddS32(output->analysis, PS_LIST_TAIL, PM_MASK_ANALYSIS_NUM, PS_META_REPLACE,
+                         "Number of input images", 0);
+    }
+
+    psImage *image = readout->image;    // Image of interest
+    psImage *mask = readout->mask;      // Corresponding mask
+
+    psTrace ("psModules.detrend", 3, "suspect: < %f or > %f\n", min, max);
+    int nFlagged = 0;
+
+    // XXX this loop could result in pixels with suspect = 0.0 but no valid input pixels (all
+    // masked).  need to track the number of good as well as suspect pixels?
+    for (int y = 0; y < image->numRows; y++) {
+        for (int x = 0; x < image->numCols; x++) {
+	    bool above = image->data.F32[y][x] > max;
+	    bool below = image->data.F32[y][x] < min;
+	    if (!above && !below) continue;
+	    if (mask && (mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskVal)) continue;
+	    suspect->data.F32[y][x] += 1.0;
+	    nFlagged ++;
+        }
+    }
+    psFree(suspect);                    // Drop reference
+
+    psMetadataItem *numItem = psMetadataLookup(output->analysis, PM_MASK_ANALYSIS_NUM); // Item with number
+    assert(numItem);
+    numItem->data.S32++;
+
+    psTrace ("psModules.detrend", 3, "Number of flagged pixels: %d\n", nFlagged);
+
+    return true;
+}
+
+// the maskVal supplied here is the value SET for this mask (ie, it is not used to avoid pixels)
+bool pmMaskIdentifyBadPixels(pmReadout *output, psImageMaskType maskVal, float thresh, pmMaskIdentifyMode mode)
+{
+    PS_ASSERT_PTR_NON_NULL(output, false);
+    psImage *suspects = psMetadataLookupPtr(NULL, output->analysis, PM_MASK_ANALYSIS_SUSPECT); // Suspect img
+    if (!suspects) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to find image with suspected bad pixels.");
+        return false;
+    }
+    PS_ASSERT_IMAGE_NON_EMPTY(suspects, false);
+    PS_ASSERT_IMAGE_TYPE(suspects, PS_TYPE_F32, false);
+    if (output->mask) {
+        PS_ASSERT_IMAGE_NON_EMPTY(output->mask, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(output->mask, suspects, false);
+        PS_ASSERT_IMAGE_TYPE(output->mask, PS_TYPE_IMAGE_MASK, false);
+    } else {
+        output->mask = psImageAlloc(suspects->numCols, suspects->numRows, PS_TYPE_IMAGE_MASK);
+    }
+    int num = psMetadataLookupS32(NULL, output->analysis, PM_MASK_ANALYSIS_NUM); // Number of inputs
+    PS_ASSERT_INT_POSITIVE(num, false);
+
+    float limit = NAN;                  // Limit for masking
+    switch (mode) {
+      case PM_MASK_ID_VALUE:
+        limit = thresh;
+        break;
+
+      case PM_MASK_ID_FRACTION:
+        limit = thresh * num;
+        break;
+
+      case PM_MASK_ID_SIGMA: {
+        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;
+        psTrace ("psModules.detrend", 3, "bad: %f -> %f\n", stats->clippedStdev, limit);
+        psFree(stats);
+        break;
+      }
+
+      case PM_MASK_ID_POISSON: {
+        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;
+            }
+        }
+	psFree(histo);
+
+        // 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);
+
+        psTrace ("psModules.detrend", 3, "bad: mode: %d, stdev: %f, limit: %f\n",
+                 max, sqrtf((float)max + 1.0), limit);
+        break;
+      }
+      default:
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Invalid mask identify mode");
+        return NULL;
+    }
+
+    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);
+    }
+
+    psTrace ("psModules.detrend", 3, "bad pixel threshold: %f", limit);
+
+    psImage *badpix = output->mask;     // Bad pixel mask
+    psImageInit(badpix, 0);
+
+    for (int y = 0; y < suspects->numRows; y++) {
+        for (int x = 0; x < suspects->numCols; x++) {
+            if (suspects->data.F32[y][x] >= limit) {
+                badpix->data.PS_TYPE_IMAGE_MASK_DATA[y][x] = maskVal;
+            }
+        }
+    }
+
+    return true;
+}
+
+pmMaskIdentifyMode pmMaskIdentifyModeFromString (const char *string) {
+
+    if (!strcasecmp(string, "VALUE")) {
+      return PM_MASK_ID_VALUE;
+    }
+    if (!strcasecmp(string, "FRACTION")) {
+      return PM_MASK_ID_FRACTION;
+    }
+    if (!strcasecmp(string, "SIGMA")) {
+      return PM_MASK_ID_SIGMA;
+    }
+    if (!strcasecmp(string, "POISSON")) {
+      return PM_MASK_ID_POISSON;
+    }
+    return PM_MASK_ID_NONE;
+}
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmMaskBadPixels.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmMaskBadPixels.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmMaskBadPixels.h	(revision 42651)
@@ -0,0 +1,85 @@
+/* @file pmMaskBadPixels.h
+ * @brief Mask bad pixels
+ *
+ * @author Ross Harman, MHPCC
+ * @author Eugene Magnier, IfA
+ *
+ * @version $Revision: 1.17 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-01-27 06:39:38 $
+ * 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
+/// @{
+
+#define PM_MASK_ANALYSIS_SUSPECT "MASK.SUSPECT" // Readout analysis metadata keyword for suspect image
+#define PM_MASK_ANALYSIS_NUM "MASK.NUM" // Readout analysis metadata keyword for number of inputs
+
+
+typedef enum {
+  PM_MASK_ID_NONE,
+  PM_MASK_ID_VALUE,
+  PM_MASK_ID_FRACTION,
+  PM_MASK_ID_SIGMA,
+  PM_MASK_ID_POISSON,
+} pmMaskIdentifyMode;
+
+pmMaskIdentifyMode pmMaskIdentifyModeFromString (const char *string);
+
+/// 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
+                     psImageMaskType 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.  The relevant median and standard deviation must be supplied in the
+/// readout->analysis metadata as READOUT.MEDIAN, READOUT.STDEV
+bool pmMaskFlagSuspectPixelsBySigma(pmReadout *output, ///< Output readout, optionally with suspect pixels image
+                             const pmReadout *readout, ///< Readout to inspect
+                             float median, ///< Image median
+                             float stdev, ///< Image standard deviation
+                             float rej, ///< Rejection threshold (standard deviations)
+                             psImageMaskType maskVal ///< Mask value for statistics
+    );
+
+/// Find out-of-range pixels and flag them
+///
+/// Pixels great > max or < min 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.  The relevant median and standard deviation must be supplied in the
+/// readout->analysis metadata as READOUT.MEDIAN, READOUT.STDEV
+bool pmMaskFlagSuspectPixelsByValue(pmReadout *output, ///< Output readout, optionally with suspect pixels image
+                             const pmReadout *readout, ///< Readout to inspect
+                             float min, ///< Image min acceptable value
+                             float max, ///< Image max acceptable value
+                             psImageMaskType maskVal ///< Mask value for statistics
+    );
+
+/// Identify bad pixels from the suspect pixels image
+///
+/// Bad pixels are identified from the suspect pixels image (accumulated over a large number of images),
+/// according to the chosen mode.
+bool pmMaskIdentifyBadPixels(pmReadout *output, ///< Output readout, with suspect pixels imageOut
+                             psImageMaskType maskVal, ///< Value to set for bad pixels
+                             float thresh, ///< Threshold for bad pixel
+                             pmMaskIdentifyMode mode ///< Mode for identifying bad pixels
+    );
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmMaskStats.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmMaskStats.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmMaskStats.c	(revision 42651)
@@ -0,0 +1,121 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPAfileIO.h"
+#include "pmFPAAstrometry.h"
+
+#include "pmMaskStats.h"
+
+#define ESCAPE { \
+        psError(psErrorCodeLast(), false, "I/O failure in pmMaskStats"); \
+        psFree (view);                                                  \
+        return false;                                                   \
+    }
+
+
+
+
+bool pmFPAMaskStats(pmFPA *fpa, pmConfig *config) {
+  PS_ASSERT_PTR_NON_NULL(fpa, false);
+  PS_ASSERT_PTR_NON_NULL(config, false);
+
+  bool status;
+
+  psImageMaskType staticMaskVal = psMetadataLookupImageMask(&status, config->recipes, "MASKSTAT.STATIC");
+  psImageMaskType magicMaskVal = psMetadataLookupImageMask(&status, config->recipes, "MASKSTAT.MAGIC");
+  psImageMaskType dynamicMaskVal = psMetadataLookupImageMask(&status, config->recipes, "MASKSTAT.DYNAMIC");
+  psImageMaskType advisoryMaskVal = psMetadataLookupImageMask(&status, config->recipes, "MASKSTAT.ADVISORY");
+
+  psS32 Npix_valid = 0;
+  psS32 Npix_static = 0;
+  psS32 Npix_magic = 0;
+  psS32 Npix_dynamic = 0;
+  psS32 Npix_advisory = 0;
+
+  pmChip *chip;
+  pmCell *cell;
+  pmReadout *readout;
+  pmFPAview *view = pmFPAviewAlloc(0);
+  while ((chip = pmFPAviewNextChip(view, fpa, 1)) != NULL) {
+    if (!chip->process || !chip->file_exists) { continue; }
+    if (!chip->fromFPA) { continue; }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+    while ((cell = pmFPAviewNextCell(view, fpa, 1)) != NULL) {
+      if (!cell->process || !cell->file_exists) {continue; }
+      while ((readout = pmFPAviewNextReadout(view, fpa, 1)) != NULL) {
+        if (!readout->data_exists) {continue; }
+
+        psImage *mask = readout->mask;
+        if (!pmSingleImageMaskStats(mask,&Npix_valid,&Npix_static,&Npix_magic,
+                                    &Npix_dynamic,&Npix_advisory,
+                                    staticMaskVal,magicMaskVal,
+                                    dynamicMaskVal,advisoryMaskVal)) {
+          psError(PS_ERR_UNKNOWN, false, "Unable to calculate masks for readout.");
+          return(false);
+        }
+        psMetadataAddS32(readout->analysis, PS_LIST_TAIL,"MASKFRAC_NPIX", 0,
+                         "Number of valid pixels", Npix_valid);
+        psMetadataAddF32(readout->analysis,PS_LIST_TAIL, "MASKFRAC_STATIC", 0,
+                         "Fraction of pixels statically masked", (float) Npix_static / Npix_valid);
+        psMetadataAddF32(readout->analysis,PS_LIST_TAIL, "MASKFRAC_DYNAMIC", 0,
+                         "Fraction of pixels dynamically masked", (float) Npix_dynamic / Npix_valid);
+        psMetadataAddF32(readout->analysis,PS_LIST_TAIL, "MASKFRAC_MAGIC", 0,
+                         "Fraction of pixels magically masked", (float) Npix_magic / Npix_valid);
+        psMetadataAddF32(readout->analysis,PS_LIST_TAIL, "MASKFRAC_ADVISORY", 0,
+                         "Fraction of pixels masked as an advisory", (float) Npix_advisory / Npix_valid);
+      }
+    }
+  }
+  return(true);
+}
+
+
+
+bool pmSingleImageMaskStats(psImage *mask,
+                            psS32 *Npix_valid, psS32 *Npix_static, psS32 *Npix_magic,
+                            psS32 *Npix_dynamic, psS32 *Npix_advisory,
+                            psImageMaskType staticMaskVal, psImageMaskType magicMaskVal,
+                            psImageMaskType dynamicMaskVal, psImageMaskType advisoryMaskVal) {
+  PS_ASSERT_IMAGE_NON_NULL(mask, false);
+  *Npix_valid = 0;
+  *Npix_static = 0;
+  *Npix_magic = 0;
+  *Npix_dynamic = 0;
+  *Npix_advisory = 0;
+
+  psImageMaskType **maskData = mask->data.PS_TYPE_IMAGE_MASK_DATA;
+  for (int i = 0; i < mask->numRows; i++) {
+    for (int j = 0; j < mask->numCols; j++) {
+      *Npix_valid += 1;
+      if (maskData[i][j] & staticMaskVal) {
+        *Npix_static += 1;
+        continue;
+      }
+      if (maskData[i][j] & dynamicMaskVal) {
+        *Npix_dynamic += 1;
+        continue;
+      }
+      if (maskData[i][j] & magicMaskVal) {
+        *Npix_magic += 1;
+        continue;
+      }
+      if (maskData[i][j] & advisoryMaskVal) {
+        *Npix_advisory += 1;
+        continue;
+      }
+    }
+  }
+  return(true);
+}
+
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmMaskStats.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmMaskStats.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmMaskStats.h	(revision 42651)
@@ -0,0 +1,14 @@
+#ifndef PM_MASK_STATS_H
+#define PM_MASK_STATS_H
+
+bool pmFPAMaskStats(pmFPA *fpa, pmConfig *config);
+bool pmSingleImageMaskStats(psImage *mask,
+                            psS32 *Npix_valid, psS32 *Npix_static, psS32 *Npix_magic,
+                            psS32 *Npix_dynamic, psS32 *Npix_advisory,
+                            psImageMaskType staticMaskVal, psImageMaskType magicMaskVal,
+                            psImageMaskType dynamicMaskVal, psImageMaskType advisoryMaskVal);
+
+
+//bool pmMaskStats(pmFPA *fpa, pmConfig *config, psMetadata *results);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmNewNonLinear.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmNewNonLinear.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmNewNonLinear.c	(revision 42651)
@@ -0,0 +1,56 @@
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmNewNonLinear.h"
+
+// model is stored as a FITS table, read into an array
+// each row is one knot, with (X, Y, dY2)
+bool pmNewNonLinearityApply(pmReadout *inputReadout, psArray *table)
+{
+    PS_ASSERT_PTR_NON_NULL(table, false);
+    PS_ASSERT_PTR_NON_NULL(inputReadout, false);
+    PS_ASSERT_PTR_NON_NULL(inputReadout->image, false);
+    PS_ASSERT_IMAGE_TYPE(inputReadout->image, PS_TYPE_F32, false);
+
+    psTimerStart ("nonlinear");
+
+    psImage *image = inputReadout->image;
+
+    // create an empty 1D spline with table->n knots
+    psSpline1D *model = psSpline1DCreate(table->n);
+    
+    // parse the table entries
+    for (int i = 0; i < table->n; i++) {
+	psMetadata *row = table->data[i];
+	
+	bool status;
+	model->xKnots[i]   = psMetadataLookupF32(&status, row, "X_KNOT");
+	model->yKnots[i]   = psMetadataLookupF32(&status, row, "Y_KNOT");
+	model->d2yKnots[i] = psMetadataLookupF32(&status, row, "DY2_DX");
+    }
+
+    // set equal spacing info?
+    // psSpline1DisEqualSpacing (model);
+
+    for (int i = 0; i < image->numRows; i++) { // Loop over rows : note: problem with discontinuity here
+	for (int j = 0; j < image->numCols; j++) { // Loop over columns
+	    // Calculate correction factor contribution for this pixel.
+	    psF32 flux = image->data.F32[i][j];
+	    psF32 lFlux = (flux > 1.0) ? log10(flux) : 0.0; // do not introduce NANs for negative flux
+	    
+	    psF32 factor = psSpline1DEval (model, lFlux);
+	    
+	    // Apply correction to image data
+	    image->data.F32[i][j] = flux * factor;
+
+	}
+    }
+    psLogMsg ("psModules", PS_LOG_MINUTIA, "apply correction: %f sec\n", psTimerMark ("nonlinear"));
+
+    psFree (model);
+  
+    return(true);
+}
+
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmNewNonLinear.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmNewNonLinear.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmNewNonLinear.h	(revision 42651)
@@ -0,0 +1,18 @@
+/* @file pmNewNonLinear.h
+ * @brief Perform new (2023) non-linear correction using spline fits
+ *
+ * @author Eugene Magnier, IfA
+ * Copyright 2023 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_NEW_NON_LINEAR_H
+#define PM_NEW_NON_LINEAR_H
+
+/// @addtogroup detrend Detrend Creation and Application
+/// @{
+
+/// Correct non-linearity using spline
+bool pmNewNonLinearityApply(pmReadout *inputReadout, psArray *table);
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmNonLinear.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmNonLinear.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmNonLinear.c	(revision 42651)
@@ -0,0 +1,383 @@
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmNonLinear.h"
+
+psF32 pmNonLinearityMeasureNoisy(psF32 flux, psVector *correction_fluxes, psVector *correction_factors);
+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;							\
+        } }
+
+
+# define PS_BIN_INTERPOLATE(RESULT, VECTOR, BOUNDS, BIN, VALUE) {	\
+	float dX, dY, Xo, Yo, Xt;					\
+	if (BIN == BOUNDS->n - 1) {					\
+	    dX = 0.5*(BOUNDS->data.F32[BIN+1] - BOUNDS->data.F32[BIN-1]); \
+	    dY = VECTOR->data.F32[BIN] - VECTOR->data.F32[BIN-1];	\
+	    Xo = 0.5*(BOUNDS->data.F32[BIN+1] + BOUNDS->data.F32[BIN]);	\
+	    Yo = VECTOR->data.F32[BIN];					\
+	} else {							\
+	    dX = 0.5*(BOUNDS->data.F32[BIN+2] - BOUNDS->data.F32[BIN]);	\
+	    dY = VECTOR->data.F32[BIN+1] - VECTOR->data.F32[BIN];	\
+	    Xo = 0.5*(BOUNDS->data.F32[BIN+1] + BOUNDS->data.F32[BIN]);	\
+	    Yo = VECTOR->data.F32[BIN];					\
+	}								\
+	if (dY != 0) {							\
+	    Xt = (VALUE - Yo)*dX/dY + Xo;				\
+	} else {							\
+	    Xt = Xo;							\
+	}								\
+	Xt = PS_MIN (BOUNDS->data.F32[BIN+1], PS_MAX(BOUNDS->data.F32[BIN], Xt)); \
+	psTrace("pmNonLinear", 6, "(Xo, Yo, dX, dY, Xt, Yt) is (%.2f %.2f %.2f %.2f %.2f %.2f)\n", \
+		Xo, Yo, dX, dY, Xt, VALUE);				\
+	RESULT = Xt; }
+
+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);
+    // XXX unused psS32 tableSize = inFlux->n;
+    if (inFlux->n != outFlux->n) {
+        // XXX unused 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;
+}
+
+bool pmNonLinearityApply(pmReadout *inputReadout, psArray *Ltab)
+{
+    PS_ASSERT_PTR_NON_NULL(inputReadout, false);
+    PS_ASSERT_PTR_NON_NULL(inputReadout->image, false);
+    PS_ASSERT_IMAGE_TYPE(inputReadout->image, PS_TYPE_F32, false);
+    PS_ASSERT_PTR_NON_NULL(Ltab, false);
+
+    psTimerStart ("nonlinear");
+
+    psS32 numSamples = 39;
+    psS32 numBorder  = 10;
+
+    //  psS32 tableSize = Ltab->n;
+
+    psImage *image = inputReadout->image;
+
+    // Load default data.
+    psVector *default_row_correction_fluxes = psVectorAlloc(numSamples,PS_TYPE_F32);
+    psVector *default_col_correction_fluxes = psVectorAlloc(numSamples,PS_TYPE_F32);
+
+    psVector *default_row_correction_factors = psVectorAlloc(numSamples,PS_TYPE_F32);
+    psVector *default_col_correction_factors = psVectorAlloc(numSamples,PS_TYPE_F32);
+
+    // pre-allocate the correction vectors
+    psVector *row_correction_fluxes  = NULL;
+    psVector *col_correction_fluxes  = NULL;
+    psVector *row_correction_factors = NULL;
+    psVector *col_correction_factors = NULL;
+
+    int n = 0;
+    int m = 0;
+    for (int k = 0; k < Ltab->n; k++) { // Begin load default tables
+	psMetadata *row = Ltab->data[k];	
+	if (psMetadataLookupS32(NULL,row,"POSITION") != -1) {
+	    continue;
+	}
+	if (psMetadataLookupS32(NULL,row,"DIRECTION") == 0) {
+	    psVectorSet(default_row_correction_fluxes,n,psMetadataLookupF32(NULL,row,"FLUX"));
+	    psVectorSet(default_row_correction_factors,n,psMetadataLookupF32(NULL,row,"FACTOR"));
+	    n++;
+	}
+	else {
+	    psVectorSet(default_col_correction_fluxes,m,psMetadataLookupF32(NULL,row,"FLUX"));
+	    psVectorSet(default_col_correction_factors,m,psMetadataLookupF32(NULL,row,"FACTOR"));
+	    m++;
+	}
+    } // End load default tables
+  
+    psLogMsg ("psModules", PS_LOG_MINUTIA, "load default data from table: %f sec\n", psTimerMark ("nonlinear"));
+
+    // pre-allocate arrays with the correction vectors for the borders
+    psArray *x_lo_flux = psArrayAlloc(numBorder);
+    psArray *x_hi_flux = psArrayAlloc(numBorder);
+    psArray *y_lo_flux = psArrayAlloc(numBorder);
+    psArray *y_hi_flux = psArrayAlloc(numBorder);
+
+    psArray *x_lo_fact = psArrayAlloc(numBorder);
+    psArray *x_hi_fact = psArrayAlloc(numBorder);
+    psArray *y_lo_fact = psArrayAlloc(numBorder);
+    psArray *y_hi_fact = psArrayAlloc(numBorder);
+    for (int i = 0; i < numBorder; i++) {
+	// pre-allocate the correction vectors
+	x_lo_flux->data[i] = psVectorAllocEmpty(numSamples,PS_TYPE_F32);
+	x_hi_flux->data[i] = psVectorAllocEmpty(numSamples,PS_TYPE_F32);
+	y_lo_flux->data[i] = psVectorAllocEmpty(numSamples,PS_TYPE_F32);
+	y_hi_flux->data[i] = psVectorAllocEmpty(numSamples,PS_TYPE_F32);
+
+	x_lo_fact->data[i] = psVectorAllocEmpty(numSamples,PS_TYPE_F32);
+	x_hi_fact->data[i] = psVectorAllocEmpty(numSamples,PS_TYPE_F32);
+	y_lo_fact->data[i] = psVectorAllocEmpty(numSamples,PS_TYPE_F32);
+	y_hi_fact->data[i] = psVectorAllocEmpty(numSamples,PS_TYPE_F32);
+    }	
+
+    // parse out the full table:
+    for (int k = 0; k < Ltab->n; k++) {
+	psMetadata *row = Ltab->data[k];	
+	int dir = psMetadataLookupS32(NULL,row,"DIRECTION");
+	int pos = psMetadataLookupS32(NULL,row,"POSITION");
+
+	psVector *fluxVector = NULL;
+	psVector *factVector = NULL;
+
+	int seq = -1;
+	if ((dir == 0) && (pos < image->numCols/2)) {
+	  seq = pos ;
+	    if (seq < 0) continue;
+	    fluxVector = x_lo_flux->data[seq];
+	    factVector = x_lo_fact->data[seq];
+	} 
+	if ((dir == 0) && (pos > image->numCols/2)) {
+	    seq = pos + numBorder - image->numCols;
+	    if (seq >= image->numCols) continue;
+	    fluxVector = x_hi_flux->data[seq];
+	    factVector = x_hi_fact->data[seq];
+	}
+	if ((dir == 1) && (pos < image->numRows/2)) {
+	    seq = pos;
+	    if (seq < 0) continue;
+	    fluxVector = y_lo_flux->data[seq];
+	    factVector = y_lo_fact->data[seq];
+	} 
+	if ((dir == 1) && (pos > image->numRows/2)) {
+	    seq = pos + numBorder - image->numRows;
+	    if (seq >= image->numRows) continue;
+	    fluxVector = y_hi_flux->data[seq];
+	    factVector = y_hi_fact->data[seq];
+	}
+
+	float flux = psMetadataLookupF32(NULL,row,"FLUX");
+	float factor = psMetadataLookupF32(NULL,row,"FACTOR");
+
+	psVectorAppend(fluxVector, flux);
+	psVectorAppend(factVector, factor);
+    }
+    psLogMsg ("psModules", PS_LOG_MINUTIA, "load border data from table: %f sec\n", psTimerMark ("nonlinear"));
+
+    for (int i = 0; i < image->numRows; i++) { // Loop over rows : note: problem with discontinuity here
+	row_correction_fluxes = NULL;
+	if (i < numBorder) {
+	    row_correction_fluxes  = y_lo_flux->data[i];
+	    row_correction_factors = y_lo_fact->data[i];
+	}
+	if (i > image->numRows - numBorder) {
+	    row_correction_fluxes  = y_hi_flux->data[i + numBorder - image->numRows];
+	    row_correction_factors = y_hi_fact->data[i + numBorder - image->numRows];
+	}
+	if (row_correction_fluxes == NULL) {
+	  row_correction_factors = default_row_correction_factors;
+	    row_correction_fluxes = default_row_correction_fluxes;
+	}
+
+	for (int j = 0; j < image->numCols; j++) { // Loop over columns
+	    col_correction_fluxes = NULL;
+	    if (j < numBorder) {
+		col_correction_fluxes  = x_lo_flux->data[j];
+		col_correction_factors = x_lo_fact->data[j];
+	    }
+	    if (j > image->numCols - numBorder) {
+		col_correction_fluxes  = x_hi_flux->data[j + numBorder - image->numCols];
+		col_correction_factors = x_hi_fact->data[j + numBorder - image->numCols];
+	    }
+	    if (col_correction_fluxes == NULL) {
+	      col_correction_factors = default_col_correction_factors;
+	      col_correction_fluxes = default_col_correction_fluxes;
+	    }
+
+	    // Calculate correction factor contribution for this pixel.
+	    psF32 factor_row = pmNonLinearityMeasure(image->data.F32[i][j], row_correction_fluxes,row_correction_factors);
+	    psF32 factor_col = pmNonLinearityMeasure(image->data.F32[i][j], col_correction_fluxes,col_correction_factors);
+#if (0)
+	    if (((i == 200)&&(j == 200))||((i == 9)&&(j == 5))) { // Print out if we're looking at a test case.
+	      psF32 factor_row = pmNonLinearityMeasureNoisy(image->data.F32[i][j], row_correction_fluxes,row_correction_factors);
+	      psF32 factor_col = pmNonLinearityMeasureNoisy(image->data.F32[i][j], col_correction_fluxes,col_correction_factors);
+	      
+		psTrace("psModules.nonlin",6,"Linearity: %d %d %s %f %f %f %d %d\n",i,j,
+			psMetadataLookupStr(NULL,inputReadout->parent->concepts,"CELL.NAME"),
+			image->data.F32[i][j],factor_row,factor_col,numBorder,numSamples);
+		psTrace("psModules.nonlin",6,"Linearity: R: %d %d %d C: %d %d %d\n",
+			i,(i < numBorder),(image->numRows - i),
+			j,(j < numBorder),(image->numCols - j));
+
+		psTrace("psModules.nonlin",6,"Linearity: V: ");
+		for (int k = 0; k < numSamples; k++) {
+		    psTrace("psModules.nonlin",6,"(%f %f) (%f %f) DDDD> (%f %f) (%f %f)",
+			    col_correction_fluxes->data.F32[k],col_correction_factors->data.F32[k],
+			    row_correction_fluxes->data.F32[k],row_correction_factors->data.F32[k],
+			    default_col_correction_fluxes->data.F32[k],default_col_correction_factors->data.F32[k],
+			    default_row_correction_fluxes->data.F32[k],default_row_correction_factors->data.F32[k]);
+		}
+		psTrace("psModules.nonlin",6,"\n");
+	    } // End Test case
+#endif
+	    // Apply correction to image data
+#if (0)
+	    if (((i == 200)&&(j == 200))||((i == 9)&&(j == 5))) { // Print out if we're looking at a test case.
+	      psTrace("psModules.nonlin",4,"Applied Linearity Correction: %s %d %d : %f %f -> %f\n",
+		      psMetadataLookupStr(NULL,inputReadout->parent->concepts,"CELL.NAME"),
+		      i,j,image->data.F32[i][j],(factor_row + factor_col) / 2.0,
+		      image->data.F32[i][j] + (factor_row + factor_col) / 2.0);
+	    }
+# endif
+	    image->data.F32[i][j] = image->data.F32[i][j] - ( factor_row + factor_col ) / 2.0;
+
+	} // End loop over columns
+    } // End loop over rows
+    psLogMsg ("psModules", PS_LOG_MINUTIA, "apply correction: %f sec\n", psTimerMark ("nonlinear"));
+
+    psFree(x_lo_flux);
+    psFree(x_hi_flux);
+    psFree(y_lo_flux);
+    psFree(y_hi_flux);
+
+    psFree(x_lo_fact);
+    psFree(x_hi_fact);
+    psFree(y_lo_fact);
+    psFree(y_hi_fact);
+
+    psFree(default_row_correction_fluxes);
+    psFree(default_row_correction_factors);
+    psFree(default_col_correction_fluxes);
+    psFree(default_col_correction_factors);
+  
+    return(true);
+}
+
+psF32 pmNonLinearityMeasure(psF32 flux, psVector *correction_fluxes, psVector *correction_factors) {
+    //  psS32 numPixels = 0;
+    psF32 result = 0;
+    psU32 bin = 0;
+
+    bin = correction_fluxes->n - 1;
+    psTrace("psModules.nonlin",6,"NLMN: %f %d %f %f\n",flux,bin,correction_fluxes->data.F32[0],correction_fluxes->data.F32[bin]);
+    if (bin < 0) { /* warn? */ }
+    if (flux < correction_fluxes->data.F32[0]) {
+	return(0.0);
+    }
+  
+    for (int i = 0; i < correction_fluxes->n - 1; i++) {
+	if ((flux >= correction_fluxes->data.F32[i])&&
+	    (flux <  correction_fluxes->data.F32[i+1])) {
+	    result = correction_factors->data.F32[i] +
+		(flux - correction_fluxes->data.F32[i]) *
+		((correction_factors->data.F32[i+1] - correction_factors->data.F32[i]) /
+		 (correction_fluxes->data.F32[i+1] - correction_fluxes->data.F32[i]));
+	    continue;
+	}
+    }
+
+    if (!isfinite(result)) {
+	result = 0.0;
+    }
+    return(result);
+}
+
+psF32 pmNonLinearityMeasureNoisy(psF32 flux, psVector *correction_fluxes, psVector *correction_factors) {
+    //  psS32 numPixels = 0;
+    psF32 result = 0;
+    psU32 bin = 0;
+
+    bin = correction_fluxes->n - 1;
+    psTrace("psModules.nonlin",6,"NLMN: %f %d %f %f\n",flux,bin,correction_fluxes->data.F32[0],correction_fluxes->data.F32[bin]);
+    if (bin < 0) { /* warn? */ }
+    if (flux < correction_fluxes->data.F32[0]) {
+	return(0.0);
+    }
+
+    for (int i = 0; i < correction_fluxes->n - 1; i++) {
+      psTrace("psModules.nonlin",6,"NLMN: %f %d %f %f %f %f\n",flux,i,correction_fluxes->data.F32[i],correction_fluxes->data.F32[i],
+	      correction_factors->data.F32[i],correction_factors->data.F32[i+1]);
+	if ((flux >= correction_fluxes->data.F32[i])&&
+	    (flux <  correction_fluxes->data.F32[i+1])) {
+	    result = correction_factors->data.F32[i] +
+		(flux - correction_fluxes->data.F32[i]) *
+		((correction_factors->data.F32[i+1] - correction_factors->data.F32[i]) /
+		 (correction_fluxes->data.F32[i+1] - correction_fluxes->data.F32[i]));
+	    continue;
+	}
+    }
+
+    if (!isfinite(result)) {
+	result = 0.0;
+    }
+    return(result);
+}
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmNonLinear.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmNonLinear.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmNonLinear.h	(revision 42651)
@@ -0,0 +1,36 @@
+/* @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
+                               );
+bool pmNonLinearityApply(pmReadout *inputReadout, psArray *Ltab);
+psF32 pmNonLinearityMeasure(psF32 flux, psVector *correction_fluxes, psVector *correction_factors);
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmOverscan.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmOverscan.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmOverscan.c	(revision 42651)
@@ -0,0 +1,553 @@
+#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);
+
+    opts->minValid = 0.0; // default value if not defined
+    opts->maxValid = (float) 0x10000; // default value if not defined
+    opts->maskVal = 0x0001; // default value if not defined
+
+    // 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_VECTOR_MASK); // 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.PS_TYPE_VECTOR_MASK_DATA[i] = 0;
+            ordinate->data.F32[i] = 2.0*(float)i/(float)pixels->n - 1.0; // Scale to [-1,1]
+            if (!psVectorStats(myStats, values, NULL, NULL, 0)) {
+		psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+		return false;
+	    }
+            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.PS_TYPE_VECTOR_MASK_DATA[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 = NULL;                   // 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 it assumes
+	// a knot for every input point.  it needs an argument like 'number of knots' for the
+	// output spline.  EAM: still true 2023.01.22
+
+        // overscanOpts->spline = psVectorFitSpline1D(reduced, ordinate);
+        // fitted = psSpline1DEvalVector(overscanOpts->spline, ordinate);
+        psError(PS_ERR_UNKNOWN, true, "Spline overscan fitting is broken\n");
+        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);
+
+	// NOTE psBinaryOp frees arg2 if it is a scalar
+	(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, "");
+    psTrace ("psModules.detrend", 4, "%s\n", 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);
+
+	if (!psVectorStats(stats, pixels, NULL, NULL, 0)) {
+	    psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	    return false;
+	}
+	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);
+
+	psScalar *reducedScalar = psScalarAlloc(reduced, PS_TYPE_F32);
+	psBinaryOp (image, image, "-", psMemIncrRefCounter(reducedScalar)); // NOTE: psBinaryOp frees arg2 if it a scalar, so we need to bump to re-use
+
+	// subtract the measured value from each overscan region as well
+	iter = psListIteratorAlloc(overscans, PS_LIST_HEAD, false); // Iterator
+	overscan = NULL;   // Overscan image from iterator
+	while ((overscan = psListGetAndIncrement(iter))) {
+	  psBinaryOp(overscan, overscan, "-", psMemIncrRefCounter(reducedScalar)); // NOTE: psBinaryOp frees arg2 if it a scalar, so we need to bump to re-use
+	}
+	psFree(iter);
+	psFree(reducedScalar);
+
+	// EAM 2022.03.29 : if the calculated overscan value is below the threshold,
+	// declare the readout dead and mask
+
+	if ((reduced < overscanOpts->minValid) || (reduced > overscanOpts->maxValid)) {
+	    fprintf (stderr, "bad overscan (1) %f, masking readout\n", reduced);
+	    psImage *mask = input->mask;
+	    for (int y = 0; y < mask->numRows; y++) {
+		for (int x = 0; x < mask->numCols; x++) {
+		    mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= overscanOpts->maskVal;
+		}
+	    }
+	}
+
+	psFree(stats);
+	return true;
+    } 
+
+    bool mdok = false;
+
+    // We are performing a row-by-row overscan subtraction
+    int cellreaddir = psMetadataLookupS32(&mdok, input->parent->concepts, "CELL.READDIR"); // Read direction
+    if ((cellreaddir != 1) && (cellreaddir != 2)) {
+	psError(PS_ERR_UNKNOWN, true, "CELL.READDIR must be 1 (rows) or 2 (cols)\n");
+	return false;
+    }
+
+    float chi2 = NAN;           // chi^2 from fit
+
+    // adjust operation depending on the read direction : need to re-org pixels for columns
+    if (cellreaddir == 1) {
+	// 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;
+	}
+
+	// generate stats of overscan vector for header
+	{ 
+	    psString comment = NULL;    // Comment to add
+	    psStats *vectorStats = psStatsAlloc (PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+	    if (!psVectorStats (vectorStats, reduced, NULL, NULL, 0)) {
+		psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+		return false;
+	    }
+	    psStringAppend(&comment, "Mean Overscan value: %f", vectorStats->sampleMean);
+	    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 mean", vectorStats->sampleMean);
+	    psMetadataAddF32(hdu->header, PS_LIST_TAIL, "OVER_SIG", PS_META_REPLACE, "Overscan stdev", vectorStats->sampleStdev);
+
+	    // EAM 2022.03.29 : if the calculated overscan value is below the threshold,
+	    // declare the readout dead and mask
+	  
+	    if ((vectorStats->sampleMean < overscanOpts->minValid) || (vectorStats->sampleMean > overscanOpts->maxValid)) {
+		fprintf (stderr, "bad overscan (2) %f, masking readout\n", vectorStats->sampleMean);
+		psImage *mask = input->mask;
+		for (int y = 0; y < mask->numRows; y++) {
+		    for (int x = 0; x < mask->numCols; x++) {
+			mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= overscanOpts->maskVal;
+		    }
+		}
+	    }
+
+	    psFree (vectorStats);
+	}
+
+	// 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];
+	    }
+	}
+
+	// subtract from the overscan regions
+	{
+	  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.
+	    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
+	      for (int k = 0; k < overscan->numCols; k++) {
+		overscan->data.F32[j][k] -= reduced->data.F32[j];
+	      }
+	    }
+	  }
+	  psFree(iter);
+	}
+	psFree(reduced);
+    } 
+    if (cellreaddir == 2) {
+	// 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;
+	}
+
+	// generate stats of overscan vector for header
+	{ 
+	  psString comment = NULL;    // Comment to add
+	  psStats *vectorStats = psStatsAlloc (PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+	  if (!psVectorStats (vectorStats, reduced, NULL, NULL, 0)) {
+	      psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	      return false;
+	  }
+	  psStringAppend(&comment, "Mean Overscan value: %f", vectorStats->sampleMean);
+	  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 mean", vectorStats->sampleMean);
+	  psMetadataAddF32(hdu->header, PS_LIST_TAIL, "OVER_SIG", PS_META_REPLACE, "Overscan stdev", vectorStats->sampleStdev);
+
+	  // EAM 2022.03.29 : if the calculated overscan value is below the threshold,
+	  // declare the readout dead and mask
+	  
+	  if ((vectorStats->sampleMean < overscanOpts->minValid) || (vectorStats->sampleMean > overscanOpts->maxValid)) {
+	      fprintf (stderr, "bad overscan (3) %f, masking readout\n", vectorStats->sampleMean);
+	      psImage *mask = input->mask;
+	      for (int y = 0; y < mask->numRows; y++) {
+		  for (int x = 0; x < mask->numCols; x++) {
+		      mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= overscanOpts->maskVal;
+		  }
+	      }
+	  }
+	  psFree (vectorStats);
+	}
+
+	// Subtract column by column 
+	for (int j = 0; j < image->numRows; j++) {
+	  for (int i = 0; i < image->numCols; i++) {
+		image->data.F32[j][i] -= reduced->data.F32[i];
+	    }
+	}
+
+	// subtract from the overscan regions
+	{
+	  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.
+	    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 j = i - diff;
+	      // i is col on image
+	      // j is col on overscan
+	      for (int k = 0; i < overscan->numRows; k++) {
+		overscan->data.F32[k][j] -= reduced->data.F32[j];
+	      }
+	    }
+	  }
+	  psFree(iter);
+	}
+
+	psFree(reduced);
+    }
+
+    pmOverscanUpdateHeader (hdu, overscanOpts, chi2);
+    psFree(stats);
+
+    return true;
+
+} // End of overscan subtraction
+    
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmOverscan.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmOverscan.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmOverscan.h	(revision 42651)
@@ -0,0 +1,76 @@
+/* @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
+    float minValid;			///< if overscan is too low, readout is dead : mask
+    float maxValid;			///< if overscan is too high, readout is dead : mask
+    psImageMaskType maskVal;            ///< Mask value to give dead readouts
+
+    // 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_branches/psModules.20240412/src/detrend/pmPattern.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmPattern.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmPattern.c	(revision 42651)
@@ -0,0 +1,1985 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmFPA.h"
+
+#include "pmPattern.h"
+
+#define PATTERN_ROW_BKG_FIX 1
+
+/* some in-line notes:
+
+   patternMaskRow sets the data value to NAN, inconsistent with new plan
+   
+   here is the outline of pmPatternRow
+
+   * at this point, we have already done overscan subtraction, right?
+
+   * measure stats on the full cell (MEDIAN, STDEV)
+   ** subsample?
+   ** if it fails, it masks the entire cell and set the value to NAN
+
+   * calculate an upper and lower threshold (median +/- T * sigma)
+   * define a normalized x-coordinate ('index') : 
+   ** see note below on chebys
+
+   * each row is treated independently
+   * pixels are masked for the fit if they are out-of-range 
+     or if they are already masked
+
+   ** note that the clipping threshold will be larger if there 
+      are pixels which have astronomical structures
+      
+      a possible better option would be to set the threshold based on the median
+      and a sigma calculated from Poisson stats (do we know the gain?)
+      
+   ** fit is allowed to proceed if even N+1 pixels exist, which is clearly too low
+   
+   ** Remaining pixels are fitted with clip-fit 
+
+   ** solution is subtracted from the data
+   (this is implemented with psPolynomial1DEvalVector)
+   perhaps faster if we fixed the order to 2 and hardwired the result
+   
+   * after each row is fitted, the intercept (A value) is fitted
+   as a function of the y-coordinate and the result is subtracted
+
+   * the slope value is also fitted as a function of the column 
+     and added back in -- I'm not sure I understand this step.
+
+   *****************
+
+   ** what we calculate are related to chebychevs (domain is -1 : +1)
+   *** T0(x) = 1
+   *** T1(x) = x
+   *** T2(x) = 2x^2 - 1
+
+   *** we calculate y = A + Bx + Cx^2
+
+   a_0 + a_1 x + a_2 (2x^2 - 1) = A + B + Cx^2
+
+   a_1       = B
+   a_0 - a_2 = A
+   2 a_2     = C
+
+   a_0       = A + C/2
+   a_1       = B
+   a_2       = C/2
+
+   *****************
+   
+   I have 3 goals in re-working the code:
+   
+   1) improve overall speed
+   2) improve reliability of the fit
+   3) skip fit if we can
+
+   Let's assume the signal in the cell is light + bias drift
+
+   The bias drift has an amplitude of ~5 - 10 DN
+
+   That makes a detectable source with ~N * a few counts (multiple pixels in a row)
+   
+   So, the effective flux is ~10 * 5 = 50 DN
+   for which sky level is this value - 3 sigma?
+   
+   50 / sqrt(sky sigma^2 * effective area)
+
+   area ~ 5pixels, sky sigma^2 = sky
+
+   10 * Npix / sqrt(sky * Npix) = 3
+
+   Npix = sky * (S/N)^2 / (peak^2)
+   
+   sky = Npix * peak^2 / SN^2
+
+   if (sky < Npix * peak^2 / SN^2), we should skip:
+   
+   Npix ~ 5
+   peak ~ 10
+   SN ~ 3 (or even less)
+
+   sky < 5 * 100 / 9 = 55 or so
+
+
+   To address these in order:
+   
+   1) speed: 
+   * the analysis threaded is not threaded: thread across cells
+   
+   2) 
+
+ */
+
+// Mask a row as bad
+static void patternMaskRow(pmReadout *ro, // Readout to mask
+                           int y,       // Row to mask
+                           psImageMaskType bad // Mask value to give
+                           )
+{
+    psImage *image = ro->image;         // Image to mask
+    psAssert(image, "Require image");
+    psAssert(y < image->numRows, "Row not in image");
+
+    int numCols = image->numCols;       // Number of columns
+    for (int x = 0; x < numCols; x++) {
+        image->data.F32[y][x] = NAN;
+    }
+    if (ro->mask) {
+        psImage *mask = ro->mask;       // Mask image to mask
+        for (int x = 0; x < numCols; x++) {
+            mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= bad;
+        }
+    }
+    return;
+}
+
+// Comparison and swap functions for sorting values directly
+#define SORT_COMPARE(A,B) (sampleArray[A] < sampleArray[B])
+#define SORT_SWAP(TYPE,A,B) {			\
+    if (A != B) { \
+        TYPE temp = sampleArray[A];			\
+        sampleArray[A] = sampleArray[B]; \
+        sampleArray[B] = temp; \
+    } \
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Measurement and application
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmPatternRowUnbinned(pmReadout *ro, int order, int iter, float rej, float thresh,
+			  psStatsOptions clipMean, psStatsOptions clipStdev,
+			  psImageMaskType maskVal, psImageMaskType maskBad);
+
+
+bool pmPatternRowBinned(pmReadout *ro, int order, int iter, float rej, float thresh,
+			psStatsOptions clipMean, psStatsOptions clipStdev,
+			psImageMaskType maskVal, psImageMaskType maskBad);
+
+
+// XXX allow user choice of binned vs unbinned analysis?
+bool pmPatternRow(pmReadout *ro, int order, int iter, float rej, float thresh,
+                  psStatsOptions clipMean, psStatsOptions clipStdev,
+                  psImageMaskType maskVal, psImageMaskType maskBad) {
+
+  bool status = false;
+  if (true) {
+    status = pmPatternRowBinned(ro, order, iter, rej, thresh, clipMean, clipStdev, maskVal, maskBad);
+  } else {
+    status = pmPatternRowUnbinned(ro, order, iter, rej, thresh, clipMean, clipStdev, maskVal, maskBad);
+  }
+  return status;
+}
+
+// USE_BACKGROUND_STDEV: if TRUE, the analysis will use the measured robust stdev to clip the out-of-range pixles
+// if FALSE, the stdev will be estimated based on the Poisson statistics of the median (sky level).
+# define USE_BACKGROUND_STDEV 0
+
+bool pmPatternRowUnbinned(pmReadout *ro, int order, int iter, float rej, float thresh,
+                  psStatsOptions clipMean, psStatsOptions clipStdev,
+                  psImageMaskType maskVal, psImageMaskType maskBad)
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PM_ASSERT_READOUT_IMAGE(ro, false);
+    PS_ASSERT_INT_NONNEGATIVE(order, false);
+    PS_ASSERT_INT_NONNEGATIVE(iter, false);
+    PS_ASSERT_FLOAT_LARGER_THAN(rej, 0.0, false);
+    PS_ASSERT_FLOAT_LARGER_THAN(thresh, 0.0, false);
+
+    bool mdok;                          // Status of MD lookup
+
+    pmCell *cell = ro->parent;
+    pmChip *chip = cell->parent;
+    const char *chipName = psMetadataLookupStr(&mdok, chip->concepts, "CHIP.NAME"); // Name of chip
+    const char *cellName = psMetadataLookupStr(&mdok, cell->concepts, "CELL.NAME"); // Name of cell
+
+    psImage *image = ro->image;         // Image to correct
+    psImage *mask = ro->mask;           // Mask for image
+    int numCols = image->numCols, numRows = image->numRows; // Size of image
+
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS); // Random number generator
+    if (!psImageBackground(stats, NULL, ro->image, ro->mask, maskVal, rng)) {
+	psWarning("Unable to calculate statistics on readout; skipping pattern correction for %s, %s.", chipName, cellName);
+        psErrorClear();
+        psFree(stats);
+        psFree(rng);
+        return true;
+    }
+
+# if (USE_BACKGROUND_STDEV) 
+    float lower = stats->robustMedian - thresh * stats->robustStdev; // Lower bound for data
+    float upper = stats->robustMedian + thresh * stats->robustStdev; // Upper bound for data
+    float background = stats->robustMedian;
+# else
+    // the signal we are looking for is a small variation on top of the background.  if
+    // the background is uniform with only read noise + sky noise, then the pixel-to-pixel
+    // stdev should only be due to known noise sources and predictable.  If the
+    // pixel-to-pixel variations are from other features, then those variations will
+    // probably dominate the row-by-row bias variations.
+
+    // instead of using the image pixel statistics to measure the stdev, lets assume only
+    // dark noise plus poisson sky noise.  we are not carrying in the read noise, but it is
+    // fairly modest for GPC1 (~10 DN)
+
+    // if we assume a gain of 1 and the read noise of 10 DN, then a sky of 200 would have
+    // a noise of N = sqrt (1 * 200 + 10^2) = sqrt (300) ~ 17
+
+    // if the gain were as much as 2, then the noise in DN would be N = sqrt(2 * (200 + 100)) / 2 = sqrt(300) / sqrt(2)
+    // so smaller by a factor of 1.4 than what we predict, which is not very large
+
+    // find the nominal signal amplitude (check the ghost and/or crosstalk recipe file)
+    float nominalAmplitude = psMetadataLookupF32 (&mdok, cell->analysis, "PTN.ROW.AMP");
+    if (!mdok) nominalAmplitude = 20; // XXX EAM : somewhat arbitrary number
+    // If we cannot determine the nominal amplitude, we fall-back on the worst case
+
+    // XXX retrieve noise and gain from 'concepts' if possible
+# define READNOISE 10 /* arbitrary number */
+    float sigma = sqrt(stats->robustMedian + PS_SQR(READNOISE));
+    float delta = PS_MIN (thresh * sigma, 2*nominalAmplitude); 
+    float lower = stats->robustMedian - delta; // Lower bound for data
+    float upper = stats->robustMedian + delta; // Upper bound for data
+    float background = stats->robustMedian;
+
+    // if the noise from the background is too large 
+    float significance = nominalAmplitude / sigma;
+   
+    // XXX EAM : arbitrary number
+    if (isfinite(nominalAmplitude) && (significance < 1.0/6.0)) {
+      psLogMsg("ppImage", PS_LOG_INFO, "Skipping row pattern correction for %s, %s, stats: %f - %f - %f : %f %f %f\n", chipName, cellName, lower, background, upper, sigma, nominalAmplitude, significance);
+      return true; 
+    }
+    fprintf (stderr, "correcting pattern row background %s: %f - %f - %f : %f %f %f\n", cellName, lower, background, upper, sigma, nominalAmplitude, significance);
+    psLogMsg("ppImage", PS_LOG_INFO, "Performing row pattern correction for %s, %s, stats: %f - %f - %f : %f %f %f\n", chipName, cellName, lower, background, upper, sigma, nominalAmplitude, significance);
+# endif    
+    
+    psFree(stats);
+    psFree(rng);
+
+    // Indices are distributed [-1:1)
+    psVector *indices = psVectorAlloc(numCols, PS_TYPE_F32); // Indices for fitting
+    float norm = 2.0 / (float)numCols;  // Normalisation for indices
+    for (int x = 0; x < numCols; x++) {
+        indices->data.F32[x] = x * norm - 1.0;
+    }
+
+    psStats *clip = psStatsAlloc(clipMean | clipStdev); // Clipping statistics
+    clip->clipIter = iter;
+    clip->clipSigma = rej;
+    psVector *clipMask = psVectorAlloc(numCols, PS_TYPE_VECTOR_MASK); // Mask for clipping
+    psPolynomial1D *poly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, order); // Polynomial to fit
+    psVector *data = psVectorAlloc(numCols, PS_TYPE_F32); // Data to fit
+
+    psImage *corr = psImageAlloc(order + 1, numRows, PS_TYPE_F64); // Corrections applied
+    psImageInit(corr, NAN);
+
+#ifdef PATTERN_ROW_BKG_FIX
+    // CZW: 2011-11-30
+    // Define the vectors to hold the "x" and "y" slope trends.
+    // Briefly, the slope trend in the y-axis is a due to variations in the 0-th order term
+    // of the PATTERN.ROW fit between individual rows across the cell.  Similarly, the 1-st
+    // order term of the PATTERN.ROW fit defines the trend in the x-axis (as that's what we
+    // are fitting with PATTERN.ROW in the first place).  However, the thing we're trying to
+    // fix with PATTERN.ROW is the detector level bias wiggles.  These should be overlaid on
+    // the true sky level.  Therefore, simply applying the PATTERN.ROW correction will
+    // introduce cell-to-cell sky variations as these two trends are removed.  To avoid this,
+    // We store the 0th and 1st order values used for each row, and then fit a polynomial to
+    // these results.  By re-adding these systematic trends back, we can remove the row-to-row
+    // variations without improperly removing the real sky trend.
+    psVector *yaxisData = psVectorAlloc(numRows, PS_TYPE_F32); // Data to fit to the constant term
+    psVector *yaxisMask = psVectorAlloc(numRows, PS_TYPE_VECTOR_MASK); // Mask for rows with no fit
+    psVector *xaxisData = psVectorAlloc(numRows, PS_TYPE_F32); // Data to fit to the linear term
+    psVectorInit(yaxisMask, 0);
+#endif
+
+    // we really need more than order + 1 points (= 4).
+    // this should be tunable, but let's try 5 - 10%
+    int validNmin = numCols * 0.1;
+
+    for (int y = 0; y < numRows; y++) {
+        psVectorInit(clipMask, 0);
+        data = psImageRow(data, image, y);
+        int num = 0;                    // Number of good pixels
+
+	// if the unmasked pixels only span a small range in x then we cannot fit the
+	// 2nd order polynomial variations very well.  Require a minimum fractional range
+	float validXmin = +1;
+	float validXmax = -1;
+
+	// XXX can we do just as well fitting 1/3 of the pixels? (NOT REALLY)
+	// (x % 3) ||
+        for (int x = 0; x < numCols; x++) {
+	    if ((mask && mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskVal) ||
+		data->data.F32[x] < lower || data->data.F32[x] > upper) {
+		clipMask->data.PS_TYPE_VECTOR_MASK_DATA[x] = 0xFF;
+            } else {
+                clipMask->data.PS_TYPE_VECTOR_MASK_DATA[x] = 0;
+                num++;
+		validXmin = PS_MIN(indices->data.F32[x], validXmin);
+		validXmax = PS_MAX(indices->data.F32[x], validXmax);
+            }
+        }
+
+	// XXX how much time is spent in the fitting
+        if (num < validNmin) {
+            // Not enough points to fit
+            patternMaskRow(ro, y, maskBad);
+#ifdef PATTERN_ROW_BKG_FIX
+	    // Ignore this row in our subsequent fits, because the fit failed.
+	    yaxisMask->data.PS_TYPE_VECTOR_MASK_DATA[y] = 0xFF;
+#endif
+            continue;
+        }
+	// XXX does this need to be a clipped fit if we are clipping based on the median poisson noise?
+        if (!psVectorClipFitPolynomial1D(poly, clip, clipMask, 0xFF, data, NULL, indices)) {
+            psWarning("Unable to fit polynomial to row %d", y);
+            psErrorClear();
+            patternMaskRow(ro, y, maskBad);
+#ifdef PATTERN_ROW_BKG_FIX
+	    // Ignore this row in our subsequent fits, because the fit failed.
+	    yaxisMask->data.PS_TYPE_VECTOR_MASK_DATA[y] = 0xFF;
+#endif
+            continue;
+        }
+#ifndef PATTERN_ROW_BKG_FIX
+ 	poly->coeff[0] -= background;
+#else
+	// Store the results we found for this row.
+	yaxisData->data.F32[y] = poly->coeff[0];
+	xaxisData->data.F32[y] = poly->coeff[1];
+	psTrace("pattern",1,"%d %g %g\n",y,poly->coeff[0],poly->coeff[1]);
+	
+	//	yaxisData->data.F32[y] = 0.0;
+/* 	xaxisData->data.F32[y] = 0.0; */
+	
+#endif
+        memcpy(corr->data.F64[y], poly->coeff, (order + 1) * PSELEMTYPE_SIZEOF(PS_TYPE_F64));
+        psVector *solution = psPolynomial1DEvalVector(poly, indices); // Solution vector
+        if (!solution) {
+            psWarning("Unable to evaluate polynomial for row %d", y);
+            psErrorClear();
+            patternMaskRow(ro, y, maskBad);
+#ifdef PATTERN_ROW_BKG_FIX
+	    yaxisMask->data.PS_TYPE_VECTOR_MASK_DATA[y] = 0xFF;
+#endif
+            continue;
+        }
+
+        for (int x = 0; x < numCols; x++) {
+            image->data.F32[y][x] -= solution->data.F32[x];
+	    psTrace("pattern",5,"A: %d %d %g\n",x,y,solution->data.F32[x]);
+        }
+        psFree(solution);
+    }
+
+#ifdef PATTERN_ROW_BKG_FIX
+    // Put the global trends back that were removed by the PATTERN.ROW correction.
+    // Set up the indices for the polynomial
+    psVector *yaxisIndices = psVectorAlloc(numRows, PS_TYPE_F32);
+    norm = 2.0 / (float)numRows;
+    for (int y = 0; y < numRows; y++) {
+      yaxisIndices->data.F32[y] = y * norm - 1.0;
+      psTrace("psModules.detrend.pattern",10,"%d %f %f\n",y,yaxisIndices->data.F32[y],yaxisData->data.F32[y]);
+    }
+
+    // Fit the trend of the constant term, producing the y-axis global trend
+    psStatsInit(clip);
+    psPolynomial1D *yaxisPoly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, 1); // Polynomial to fit.
+    if (!psVectorClipFitPolynomial1D(yaxisPoly,clip,yaxisMask,0xFF,yaxisData, NULL, yaxisIndices)) {
+      psWarning("Unable to fit polynomial to y-axis trend");
+      psErrorClear();
+      // If we've failed, we need to do something, so add back in the background level, and
+      // expect that the final image will have background mismatches.
+      for (int y = 0; y < numRows; y++) {
+	for (int x = 0; x < numCols; x++) {
+	  image->data.F32[y][x] += background;
+	}
+	corr->data.F64[y][0]  -= background;
+      }
+    }
+    else {
+      psVector *solution = psPolynomial1DEvalVector(yaxisPoly,yaxisIndices);
+      if (!solution) {
+	psWarning("Unable to evaluate polynomial");
+	psErrorClear();
+	// If we've failed, we need to do something, so add back in the background level, and
+	// expect that the final image will have background mismatches.
+	for (int y = 0; y < numRows; y++) {
+	  for (int x = 0; x < numCols; x++) {
+	    image->data.F32[y][x] += background;
+	  }
+	  corr->data.F64[y][0]  -= background;
+	}
+      }
+      else {
+	for (int y = 0; y < numRows; y++) {
+	  for (int x = 0; x < numCols; x++) {
+	    image->data.F32[y][x] += solution->data.F32[y];
+	    psTrace("pattern",5,"B: %d %d %g\n",x,y,solution->data.F32[y]);
+	  }
+	  corr->data.F64[y][0]  -= solution->data.F32[y];
+	}
+      }
+      psFree(solution);
+    }      
+
+    // Fit the trend of the linear term, producing the x-axis global trend
+    // We can use the same mask vector, as the same rows failed the row-fit earlier.
+    psStatsInit(clip);
+    psPolynomial1D *xaxisPoly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, 1); // Polynomial to fit.
+    if (!psVectorClipFitPolynomial1D(xaxisPoly,clip,yaxisMask,0xFF,xaxisData, NULL, yaxisIndices)) {
+      psWarning("Unable to fit polynomial to x-axis trend");
+      psErrorClear();
+    }
+    else {
+      psVector *solution = psPolynomial1DEvalVector(xaxisPoly,yaxisIndices);
+      if (!solution) {
+	psWarning("Unable to evaluate polynomial");
+	psErrorClear();
+      }
+      else {
+	for (int y = 0; y < numRows; y++) {
+	  for (int x = 0; x < numCols; x++) {
+	    image->data.F32[y][x] += solution->data.F32[y] * indices->data.F32[x];
+	    // XXX EAM : this was [x] which is wrong
+	    psTrace("pattern",5,"C: %d %d %g %g\n",x,y,solution->data.F32[y],indices->data.F32[x]);
+	  }
+	  corr->data.F64[y][1]  -= solution->data.F32[y] ;
+	}
+      }
+      psFree(solution);
+    }
+    psFree(yaxisPoly);
+    psFree(xaxisPoly);
+    psFree(yaxisIndices);
+    psFree(yaxisMask);
+    psFree(yaxisData);
+    psFree(xaxisData);
+    // End PATTERN_ROW_BKG_FIX global trend replacement
+#endif 
+    
+    psMetadataAddImage(ro->analysis, PS_LIST_TAIL, PM_PATTERN_ROW_CORRECTION, PS_META_REPLACE,
+                       "Pattern row correction", corr);
+    psFree(corr);
+
+    psFree(indices);
+    psFree(clip);
+    psFree(clipMask);
+    psFree(poly);
+    psFree(data);
+
+    return true;
+}
+
+# define NPIX 15
+
+// bin by NPIX in the x-direction to reduce the number of calculations needed to measure
+// the pattern
+bool pmPatternRowBinned(pmReadout *ro, int order, int iter, float rej, float thresh,
+                  psStatsOptions clipMean, psStatsOptions clipStdev,
+                  psImageMaskType maskVal, psImageMaskType maskBad)
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PM_ASSERT_READOUT_IMAGE(ro, false);
+    PS_ASSERT_INT_NONNEGATIVE(order, false);
+    PS_ASSERT_INT_NONNEGATIVE(iter, false);
+    PS_ASSERT_FLOAT_LARGER_THAN(rej, 0.0, false);
+    PS_ASSERT_FLOAT_LARGER_THAN(thresh, 0.0, false);
+
+    bool mdok;                          // Status of MD lookup
+
+    pmCell *cell = ro->parent;
+    pmChip *chip = cell->parent;
+    const char *chipName = psMetadataLookupStr(&mdok, chip->concepts, "CHIP.NAME"); // Name of chip
+    const char *cellName = psMetadataLookupStr(&mdok, cell->concepts, "CELL.NAME"); // Name of cell
+
+    psImage *image = ro->image;         // Image to correct
+    psImage *mask = ro->mask;           // Mask for image
+    int numCols = image->numCols, numRows = image->numRows; // Size of image
+
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS); // Random number generator
+    if (!psImageBackground(stats, NULL, ro->image, ro->mask, maskVal, rng)) {
+	psWarning("Unable to calculate statistics on readout; skipping pattern correction for %s, %s.", chipName, cellName);
+        psErrorClear();
+        psFree(stats);
+        psFree(rng);
+	
+	// EAM 20211011 : we used to mask cells which fail the above, but this seems excessive
+        // psImageInit(image, NAN);
+        // if (mask) {
+        //     psBinaryOp(mask, mask, "|", psScalarAlloc(maskBad, PS_TYPE_IMAGE_MASK));
+        // }
+        // if (ro->variance) {
+        //     psImageInit(image, NAN);
+        // }
+        return true;
+    }
+
+    // if USE_BACKGROUND_STDEV is TRUE, the observed standard deviation is used to set the
+    // thresholds.  this is going to be an overestimate if there is any structure in the
+    // image.  If FALSE, the thresholds are set based on poisson stats for the background
+    // level.  We assume the gain is 1, so this is an overestimate if the gain is > 1
+
+# if (USE_BACKGROUND_STDEV) 
+    float lower = stats->robustMedian - thresh * stats->robustStdev; // Lower bound for data
+    float upper = stats->robustMedian + thresh * stats->robustStdev; // Upper bound for data
+    float background = stats->robustMedian;
+# else
+    // the signal we are looking for is a small variation on top of the background.  if
+    // the background is uniform with only read noise + sky noise, then the pixel-to-pixel
+    // stdev should only be due to known noise sources and predictable.  If the
+    // pixel-to-pixel variations are from other features, then those variations will
+    // probably dominate the row-by-row bias variations.
+
+    // instead of using the image pixel statistics to measure the stdev, lets assume only
+    // dark noise plus poisson sky noise.  we are not carrying in the read noise, but it is
+    // fairly modest for GPC1 (~10 DN)
+
+    // if we assume a gain of 1 and the read noise of 10 DN, then a sky of 200 would have
+    // a noise of N = sqrt (1 * 200 + 10^2) = sqrt (300) ~ 17
+
+    // if the gain were as much as 2, then the noise in DN would be N = sqrt(2 * (200 + 100)) / 2 = sqrt(300) / sqrt(2)
+    // so smaller by a factor of 1.4 than what we predict, which is not very large
+
+    // find the nominal signal amplitude (check the ghost and/or crosstalk recipe file)
+    float nominalAmplitude = psMetadataLookupF32 (&mdok, cell->analysis, "PTN.ROW.AMP");
+    if (!mdok) nominalAmplitude = 20; // XXX EAM : somewhat arbitrary number
+    if (!isfinite(nominalAmplitude)) nominalAmplitude = 20; // XXX EAM : somewhat arbitrary number
+    // If we cannot determine the nominal amplitude, we fall-back on the worst case
+
+    // retrieve noise and gain from 'concepts' if possible
+# define READNOISE 10 /* arbitrary number */
+    float sigma = sqrt(stats->robustMedian + PS_SQR(READNOISE));
+    float delta = PS_MIN (thresh * sigma, 5*nominalAmplitude); 
+    float lower = stats->robustMedian - delta; // Lower bound for data
+    float upper = stats->robustMedian + delta; // Upper bound for data
+    float background = stats->robustMedian;
+
+    // if the noise from the background is too large 
+    float significance = nominalAmplitude / sigma;
+   
+    // XXX EAM : arbitrary number
+    if (isfinite(nominalAmplitude) && (significance < 1.0/6.0)) {
+      psLogMsg("ppImage", PS_LOG_INFO, "Skipping row pattern correction for %s, %s, stats: %f - %f - %f : %f %f %f\n", chipName, cellName, lower, background, upper, sigma, nominalAmplitude, significance);
+      psFree(stats);
+      psFree(rng);
+      return true; 
+    }
+    psLogMsg("ppImage", PS_LOG_INFO, "Performing row pattern correction for %s, %s, stats: %f - %f - %f : %f %f %f\n", chipName, cellName, lower, background, upper, sigma, nominalAmplitude, significance);
+# endif    
+
+    psFree(stats);
+    psFree(rng);
+
+    // the vector 'indices' maps the x-coordinate to a range [-1:1].  the element number (i) of indices
+    // related to the x-coordinate (column number) by x = (i + 0.5) * NPIX
+
+    int nSamples = numCols / NPIX;
+
+    psVector *indices = psVectorAlloc(numCols, PS_TYPE_F32); // Indices for fit solutions
+    psVector *xFit = psVectorAlloc(nSamples, PS_TYPE_F32); // x-coordinate for fitting
+    psVector *yFit = psVectorAlloc(nSamples, PS_TYPE_F32); // flux values for fitting
+
+    // xFit elements run from 0 - nSamples, element 'sample' corresponds to the middle of the bin sample*NPIX + 0.5*NPIX
+
+    float norm = 2.0 / (float)numCols;  // Normalisation for indices
+    for (int sample = 0; sample < nSamples; sample ++) {
+	int x = (sample + 0.5)*NPIX;
+        xFit->data.F32[sample] = x * norm - 1.0;
+    }
+    for (int x = 0; x < numCols; x ++) {
+        indices->data.F32[x] = x * norm - 1.0;
+    }
+
+    psStats *clip = psStatsAlloc(clipMean | clipStdev); // Clipping statistics
+    clip->clipIter = iter;
+    clip->clipSigma = rej;
+    psVector *clipMask = psVectorAlloc(nSamples, PS_TYPE_VECTOR_MASK); // Mask for clipping
+    psPolynomial1D *poly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, order); // Polynomial to fit
+    psVector *data = psVectorAlloc(numCols, PS_TYPE_F32); // Data to fit
+
+    psImage *corr = psImageAlloc(order + 1, numRows, PS_TYPE_F64); // Corrections applied
+    psImageInit(corr, NAN);
+
+    // CZW: 2011-11-30
+    // Define the vectors to hold the "x" and "y" slope trends.
+    // Briefly, the slope trend in the y-axis is a due to variations in the 0-th order term
+    // of the PATTERN.ROW fit between individual rows across the cell.  Similarly, the 1-st
+    // order term of the PATTERN.ROW fit defines the trend in the x-axis (as that's what we
+    // are fitting with PATTERN.ROW in the first place).  However, the thing we're trying to
+    // fix with PATTERN.ROW is the detector level bias wiggles.  These should be overlaid on
+    // the true sky level.  Therefore, simply applying the PATTERN.ROW correction will
+    // introduce cell-to-cell sky variations as these two trends are removed.  To avoid this,
+    // We store the 0th and 1st order values used for each row, and then fit a polynomial to
+    // these results.  By re-adding these systematic trends back, we can remove the row-to-row
+    // variations without improperly removing the real sky trend.
+    psVector *yaxisData = psVectorAlloc(numRows, PS_TYPE_F32); // Data to fit to the constant term
+    psVector *yaxisMask = psVectorAlloc(numRows, PS_TYPE_VECTOR_MASK); // Mask for rows with no fit
+    psVector *xaxisData = psVectorAlloc(numRows, PS_TYPE_F32); // Data to fit to the linear term
+    psVectorInit(yaxisMask, 0);
+
+    // validNmin is the minimum number of samples needed to measure the trend.
+    // this should be tunable, but let's try 5 - 10%
+    int validNmin = PS_MAX (nSamples * 0.1, order + 2);
+
+    for (int y = 0; y < numRows; y++) {
+        psVectorInit(clipMask, 0);
+        data = psImageRow(data, image, y);
+        int num = 0;                    // Number of good pixels
+
+	// if the unmasked pixels only span a small range in x then we cannot fit the
+	// 2nd order polynomial variations very well.  Require a minimum fractional range
+	float validXmin = +1;
+	float validXmax = -1;
+
+        for (int sample = 0; sample < nSamples; sample ++) {
+
+	    // store valid samples in the array to be sorted
+	    float sampleArray[NPIX];
+	    int seq = 0;
+	    for (int j = 0; j < NPIX; j++) {
+		int pix = sample  * NPIX + j; // real pixel elements in x-dir
+		psAssert (pix >= 0, "invalid pix value");
+		psAssert (pix < numCols, "invalid pix value");
+		if ((mask && mask->data.PS_TYPE_IMAGE_MASK_DATA[y][pix] & maskVal)) continue;
+		if (data->data.F32[pix] < lower || data->data.F32[pix] > upper) continue;
+		sampleArray[seq] = data->data.F32[pix]; // store the value to be sorted
+		seq ++;
+            } 
+	    if (seq < 1) {
+		clipMask->data.PS_TYPE_VECTOR_MASK_DATA[sample] = 0xFF;
+		yFit->data.F32[sample] = NAN;
+		continue;
+	    }
+	    // note that we are treating the x-coordinate as the center
+	    // of the binned pixel group, even if some or most pixels have
+	    // been masked.  compared to the amplitude of the slope, this
+	    // error is small
+	    clipMask->data.PS_TYPE_VECTOR_MASK_DATA[sample] = 0;
+	    validXmin = PS_MIN(xFit->data.F32[sample], validXmin);
+	    validXmax = PS_MAX(xFit->data.F32[sample], validXmax);
+	    num++;
+
+	    // PSSORT operates on sampleArray (see define of macro SORT_SWAP above)
+	    PSSORT (seq, SORT_COMPARE, SORT_SWAP, float);
+
+	    int midPt = 0.5 * seq;
+	    if (seq % 2 == 1) { psAssert (midPt >= 0, "invalid midPt"); }
+	    if (seq % 2 == 0) { psAssert (midPt >= 1, "invalid midPt"); }
+	    psAssert (midPt < NPIX, "invalid midPt");
+
+	    float medValue = (seq % 2) ? sampleArray[midPt] : 0.5*(sampleArray[midPt] + sampleArray[midPt-1]);
+	    yFit->data.F32[sample] = medValue;
+        }
+
+	// If not enough points are valid, skip
+        if (num < validNmin) {
+	    // Ignore this row in our subsequent fits, because the fit failed.
+	    yaxisMask->data.PS_TYPE_VECTOR_MASK_DATA[y] = 0xFF;
+            continue;
+        }
+	// XXX does this need to be a clipped fit if we are clipping based on the median poisson noise?
+        if (!psVectorClipFitPolynomial1D(poly, clip, clipMask, 0xFF, yFit, NULL, xFit)) {
+            psWarning("Unable to fit polynomial to row %d", y);
+            psErrorClear();
+	    // Ignore this row in our subsequent fits, because the fit failed.
+	    yaxisMask->data.PS_TYPE_VECTOR_MASK_DATA[y] = 0xFF;
+            continue;
+        }
+	// Store the results we found for this row.
+	yaxisData->data.F32[y] = poly->coeff[0];
+	xaxisData->data.F32[y] = poly->coeff[1];
+	psTrace("pattern",1,"%d %g %g\n",y,poly->coeff[0],poly->coeff[1]);
+
+        memcpy(corr->data.F64[y], poly->coeff, (order + 1) * PSELEMTYPE_SIZEOF(PS_TYPE_F64));
+        psVector *solution = psPolynomial1DEvalVector(poly, indices); // Solution vector
+        if (!solution) {
+            psWarning("Unable to evaluate polynomial for row %d", y);
+            psErrorClear();
+	    yaxisMask->data.PS_TYPE_VECTOR_MASK_DATA[y] = 0xFF;
+            continue;
+        }
+
+	psAssert (solution->n == numCols, "oops");
+        for (int x = 0; x < numCols; x++) {
+            image->data.F32[y][x] -= solution->data.F32[x];
+	    psTrace("pattern",5,"A: %d %d %g\n",x,y,solution->data.F32[x]);
+        }
+        psFree(solution);
+    }
+
+    // Put the global trends back that were removed by the PATTERN.ROW correction.
+    // Set up the indices for the polynomial
+    psVector *yaxisIndices = psVectorAlloc(numRows, PS_TYPE_F32);
+    norm = 2.0 / (float)numRows;
+    for (int y = 0; y < numRows; y++) {
+      yaxisIndices->data.F32[y] = y * norm - 1.0;
+      psTrace("psModules.detrend.pattern",10,"%d %f %f\n",y,yaxisIndices->data.F32[y],yaxisData->data.F32[y]);
+    }
+
+    // Fit the trend of the constant term, producing the y-axis global trend
+    psStatsInit(clip);
+    psPolynomial1D *yaxisPoly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, 1); // Polynomial to fit.
+    if (!psVectorClipFitPolynomial1D(yaxisPoly,clip,yaxisMask,0xFF,yaxisData, NULL, yaxisIndices)) {
+      psWarning("Unable to fit polynomial to y-axis trend");
+      psErrorClear();
+      // If we've failed, we need to do something, so add back in the background level, and
+      // expect that the final image will have background mismatches.
+      for (int y = 0; y < numRows; y++) {
+	for (int x = 0; x < numCols; x++) {
+	  image->data.F32[y][x] += background;
+	}
+	corr->data.F64[y][0]  -= background;
+      }
+    } else {
+      psVector *solution = psPolynomial1DEvalVector(yaxisPoly,yaxisIndices);
+      if (!solution) {
+	psWarning("Unable to evaluate polynomial");
+	psErrorClear();
+	// If we've failed, we need to do something, so add back in the background level, and
+	// expect that the final image will have background mismatches.
+	for (int y = 0; y < numRows; y++) {
+	  for (int x = 0; x < numCols; x++) {
+	    image->data.F32[y][x] += background;
+	  }
+	  corr->data.F64[y][0]  -= background;
+	}
+      } else {
+	psAssert (solution->n == numRows, "oops");
+	for (int y = 0; y < numRows; y++) {
+	  for (int x = 0; x < numCols; x++) {
+	    image->data.F32[y][x] += solution->data.F32[y];
+	    // XXX EAM : this was [x], which is wrong
+	    psTrace("pattern",5,"B: %d %d %g\n",x,y,solution->data.F32[y]);
+	  }
+	  corr->data.F64[y][0]  -= solution->data.F32[y];
+	}
+      }
+      psFree(solution);
+    }      
+
+    // Fit the trend of the linear term, producing the x-axis global trend
+    // We can use the same mask vector, as the same rows failed the row-fit earlier.
+    psStatsInit(clip);
+    psPolynomial1D *xaxisPoly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, 1); // Polynomial to fit.
+    if (!psVectorClipFitPolynomial1D(xaxisPoly,clip,yaxisMask,0xFF,xaxisData, NULL, yaxisIndices)) {
+      psWarning("Unable to fit polynomial to x-axis trend");
+      psErrorClear();
+    } else {
+      psVector *solution = psPolynomial1DEvalVector(xaxisPoly,yaxisIndices);
+      if (!solution) {
+	psWarning("Unable to evaluate polynomial");
+	psErrorClear();
+      } else {
+	psAssert (solution->n == numRows, "oops");
+	for (int y = 0; y < numRows; y++) {
+	  for (int x = 0; x < numCols; x++) {
+	    image->data.F32[y][x] += solution->data.F32[y] * indices->data.F32[x];
+	    // XXX EAM : this was set to [x] which is wrong (numCols > numRows)
+	    psTrace("pattern",5,"C: %d %d %g %g\n",x,y,solution->data.F32[y],indices->data.F32[x]);
+	  }
+	  corr->data.F64[y][1]  -= solution->data.F32[y] ;
+	}
+      }
+      psFree(solution);
+    }
+    psFree(yaxisPoly);
+    psFree(xaxisPoly);
+    psFree(yaxisIndices);
+    psFree(yaxisMask);
+    psFree(yaxisData);
+    psFree(xaxisData);
+    
+    psMetadataAddImage(ro->analysis, PS_LIST_TAIL, PM_PATTERN_ROW_CORRECTION, PS_META_REPLACE,
+                       "Pattern row correction", corr);
+    psFree(corr);
+
+    psFree(indices);
+    psFree(xFit);
+    psFree(yFit);
+    psFree(clip);
+    psFree(clipMask);
+    psFree(poly);
+    psFree(data);
+
+    return true;
+}
+
+bool pmPatternRowApply(pmReadout *ro, psImageMaskType maskBad)
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PM_ASSERT_READOUT_IMAGE(ro, false);
+
+    bool mdok;                          // Status of MD lookup
+    psImage *corr = psMetadataLookupPtr(&mdok, ro->analysis, PM_PATTERN_ROW_CORRECTION); // Correction
+    if (!mdok) {
+        // No correction to apply
+        return true;
+    }
+
+    psImage *image = ro->image; // Image of interest
+    int numCols = image->numCols, numRows = image->numRows; // Size of image
+
+    if (corr->numRows != numRows) {
+        psError(PS_ERR_BAD_PARAMETER_SIZE, true,
+                "Number of rows of image (%d) does not match number of rows of pattern correction (%d)\n",
+                numRows, corr->numRows);
+        return false;
+    }
+
+    int order = corr->numCols - 1;                                        // Polynomial order
+    psPolynomial1D *poly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, order); // Polynomial to apply
+    psVector *indices = psVectorAlloc(numCols, PS_TYPE_F32); // Indices for polynomial
+    float norm = 2.0 / (float)numCols;  // Normalisation for indices
+    for (int x = 0; x < numCols; x++) {
+        indices->data.F32[x] = x * norm - 1.0;
+    }
+
+    for (int y = 0; y < numRows; y++) {
+        memcpy(poly->coeff, corr->data.F64[y], (order + 1) * PSELEMTYPE_SIZEOF(PS_TYPE_F64));
+        psVector *solution = psPolynomial1DEvalVector(poly, indices); // Solution vector
+        if (!solution) {
+            psWarning("Unable to evaluate polynomial for row %d", y);
+            psErrorClear();
+            patternMaskRow(ro, y, maskBad);
+            continue;
+        }
+
+        for (int x = 0; x < numCols; x++) {
+            image->data.F32[y][x] -= solution->data.F32[x];
+        }
+        psFree(solution);
+    }
+
+    psFree(poly);
+    psFree(indices);
+
+    return true;
+}
+
+
+bool pmPatternCell(pmChip *chip, const psVector *tweak, psStatsOptions bgStat, psStatsOptions cellStat,
+                   psImageMaskType maskVal, psImageMaskType maskBad)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_VECTOR_NON_NULL(tweak, false);
+    PS_ASSERT_VECTOR_SIZE(tweak, chip->cells->n, false);
+    PS_ASSERT_VECTOR_TYPE(tweak, PS_TYPE_U8, false);
+
+    int numCells = tweak->n;            // Number of cells
+
+    psVector *mean = psVectorAlloc(numCells, PS_TYPE_F32); // Mean for each cell
+    psVector *meanMask = psVectorAlloc(numCells, PS_TYPE_VECTOR_MASK); // Mask for means
+    psVectorInit(mean, NAN);
+    psVectorInit(meanMask, 0);
+
+    // Mask bits
+    enum {
+        PM_PATTERN_IGNORE = 0x01,       // Ignore this cell
+        PM_PATTERN_TWEAK  = 0x02,       // Tweak this cell
+        PM_PATTERN_ERROR  = 0x04,       // Error in calculating background
+        PM_PATTERN_ALL    = 0xFF,       // All causes
+    };
+
+    // Count number of cells to tweak
+    int numTweak = 0;                   // Number of cells to tweak
+    int numIgnore = 0;                  // Number of cells to ignore
+    for (int i = 0; i < numCells; i++) {
+        pmCell *cell = chip->cells->data[i]; // Cell of interest
+        if (!cell || !cell->data_exists || !cell->process ||
+            cell->readouts->n == 0 || cell->readouts->n > 1 || !cell->readouts->data[0]) {
+            numIgnore++;
+            meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] = PM_PATTERN_IGNORE;
+            continue;
+        }
+        if (tweak->data.U8[i]) {
+            meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] = PM_PATTERN_TWEAK;
+            numTweak++;
+        }
+    }
+    if (numTweak == 0) {
+        // Nothing to do
+        psFree(mean);
+        psFree(meanMask);
+        return true;
+    }
+    if (numTweak >= numCells - numIgnore) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Cannot pattern-correct all cells within a chip.");
+        psFree(mean);
+        psFree(meanMask);
+        return false;
+    }
+
+    // Measure mean of each cell
+    // This is not really the perfect thing to do, which would be to take a common mean for the set of cells
+    // which aren't being tweaked (because some cells will be heavily masked, so shouldn't be weighted the
+    // same as pure cells), but it's simple and fast.
+    psStats *bgStats = psStatsAlloc(bgStat); // Statistics on background
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS); // Random number generator
+    for (int i = 0; i < numCells; i++) {
+        if (meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] & PM_PATTERN_IGNORE) {
+            continue;
+        }
+        pmCell *cell = chip->cells->data[i]; // Cell of interest
+        pmReadout *ro = cell->readouts->data[0]; // Readout of interest
+
+        psStatsInit(bgStats);
+#if 1
+        if (!psImageBackground(bgStats, NULL, ro->image, ro->mask, maskVal, rng)) {
+            psWarning("Unable to measure background for cell %d\n", i);
+            psErrorClear();
+            meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] |= PM_PATTERN_ERROR;
+            continue;
+        }
+#else
+        if (!psImageStats(bgStats, ro->image, ro->mask, maskVal)) {
+            psWarning("Unable to measure background for cell %d\n", i);
+            psErrorClear();
+            meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] |= PM_PATTERN_ERROR;
+            continue;
+        }
+#endif
+        mean->data.F32[i] = psStatsGetValue(bgStats, bgStat);
+        if (!isfinite(mean->data.F32[i])) {
+            psWarning("Non-finite background for cell %d\n", i);
+            psErrorClear();
+            meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] |= PM_PATTERN_ERROR;
+            continue;
+        }
+    }
+    psFree(bgStats);
+    psFree(rng);
+
+    psStats *cellStats = psStatsAlloc(cellStat); // Statistics on cells
+    if (!psVectorStats(cellStats, mean, NULL, meanMask, PM_PATTERN_ALL)) {
+        // an error in psVectorStats implies a programming error
+        psError(PS_ERR_UNKNOWN, false, "Unable to calculate mean cell background.");
+        psFree(mean);
+        psFree(meanMask);
+        psFree(cellStats);
+        return false;
+    }
+
+    float background = psStatsGetValue(cellStats, cellStat); // Background value for chip
+    psFree(cellStats);
+    if (!isfinite(background)) {
+        // this can happen if all data in the image is bad -- in this case, do not correct, but
+        // do not treat this as an error (other functions are responsible for check data validity)
+        psLogMsg("psModules.detrend", PS_LOG_DETAIL, "Non-finite mean cell background -- skipping correction (data probabaly bad).");
+        psFree(mean);
+        psFree(meanMask);
+        return true;
+    }
+
+    psLogMsg("psModules.detrend", PS_LOG_DETAIL, "Mean chip background is %f", background);
+
+    for (int i = 0; i < numCells; i++) {
+        if (meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] & PM_PATTERN_IGNORE) {
+            continue;
+        }
+        if (!(meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] & PM_PATTERN_TWEAK)) {
+            continue;
+        }
+        pmCell *cell = chip->cells->data[i]; // Cell of interest
+        pmReadout *ro = cell->readouts->data[0]; // Readout of interest
+        if (meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] & PM_PATTERN_ERROR) {
+            psImageInit(ro->image, NAN);
+            psBinaryOp(ro->mask, ro->mask, "|", psScalarAlloc(maskBad, PS_TYPE_IMAGE_MASK));
+            psMetadataAddF32(ro->analysis, PS_LIST_TAIL, PM_PATTERN_CELL_CORRECTION, PS_META_REPLACE,
+                             "Pattern cell correction solution", NAN);
+            continue;
+        }
+        float correction = background - mean->data.F32[i]; // Correction to apply
+        const char *cellName = psMetadataLookupStr(NULL, cell->concepts, "CELL.NAME"); // Name of cell
+        psLogMsg("psModules.detrend", PS_LOG_DETAIL, "Correcting background of cell %s by %f",
+                 cellName, correction);
+        psBinaryOp(ro->image, ro->image, "+", psScalarAlloc(correction, PS_TYPE_F32));
+        psMetadataAddF32(ro->analysis, PS_LIST_TAIL, PM_PATTERN_CELL_CORRECTION, PS_META_REPLACE,
+                         "Pattern cell correction solution", correction);
+    }
+
+    psFree(mean);
+    psFree(meanMask);
+
+    return true;
+}
+
+// Compare the backs (cellBackgrounds) vector to each of the patterns loaded for this chip.
+// Choose the one that matches best & mask as appropriate
+// can we pass the backgrounds in using an image (8 x 8)
+bool pmPatternDeadCells(pmChip *chip, const psVector *backs, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_VECTOR_NON_NULL(backs, false);
+    PS_ASSERT_VECTOR_SIZE(backs, chip->cells->n, false);
+    PS_ASSERT_VECTOR_TYPE(backs, PS_TYPE_F32, false);
+
+    // Look for the dead cell pattern cube in the analysis metadata.
+    // NOTE: not all chips have the pattern, skip if not found.
+    bool mdok;
+    psImage *deadCellPattern = (psImage *) psMetadataLookupPtr (&mdok, chip->analysis, "PTN.DEAD.CELL");
+    if (!mdok) {
+        psLogMsg("psModules.detrend", PS_LOG_DETAIL, "No DEAD CELL pattern for chip, skipping\n");
+	return true;
+    }
+
+    int numCells = backs->n;            // Number of cells
+    int numColsD = deadCellPattern->numCols;
+    int numRowsD = deadCellPattern->numRows;
+
+    assert (backs->n == numRowsD);
+
+    // The background values need to be normalized (divide by the median).
+    // First extract the valid data values
+    psVector *valid = psVectorAllocEmpty(backs->n, PS_TYPE_F32); // Mean for each cell
+    for (int i = 0; i < backs->n; i++) {
+	float value = backs->data.F32[i];
+	if (!isfinite(value)) { backs->data.F32[i] = NAN; continue; }
+	if (value > 50000.0)  { backs->data.F32[i] = NAN; continue; } // XXX warning: hard-wired value
+	psVectorAppend (valid, value);
+    }
+    if (valid->n == 0) {
+	psLogMsg("psModules.detrend", PS_LOG_DETAIL, "No valid backgrounds, skipping\n");
+	psFree (valid);
+	return true;
+    }
+
+    // Second, calculate the median
+    psVectorSortInPlace (valid);
+    int midPt = valid->n / 2.0;
+    float median = valid->n % 2 ? valid->data.F32[midPt] : 0.5*(valid->data.F32[midPt] + valid->data.F32[midPt-1]);
+    psFree (valid);
+
+    // Finally, renormalize:
+    for (int i = 0; i < backs->n; i++) {
+	if (!isfinite(backs->data.F32[i])) { continue; }
+	backs->data.F32[i] /= median;
+    }
+
+    // there are 2 columns (value & mask) for each mode, plus the constant mode
+    int nModes = numColsD / 2 + 1;
+    
+    psVector *stdev = psVectorAlloc(nModes, PS_TYPE_F32); // Mean for each cell
+
+    // measure stdev for each comparison
+    for (int i = 0; i < nModes; i++) {
+	float Sum1 = 0.0;
+	float Sum2 = 0.0;
+	int   Npts = 0;
+
+	// choose the column with the background values for this mode
+	int nModeColumn = 2*(i - 1);
+
+	for (int j = 0; j < backs->n; j++) {
+
+	    float valObs = backs->data.F32[j];
+	    float valRef = (i == 0) ? 0.0 : deadCellPattern->data.F32[j][nModeColumn];
+
+	    if (!isfinite(valObs)) continue;
+	    if (!isfinite(valRef)) continue;
+
+	    float dS = valObs - valRef;
+	    Sum1 += dS;
+	    Sum2 += dS*dS;
+	    Npts ++;
+	}
+	if (Npts == 0) {
+	    // is this is an error?
+	    // no valid data, ignore this test
+	    psLogMsg("psModules.detrend", PS_LOG_DETAIL, "No valid backgrounds, skipping\n");
+	    psFree (stdev);
+	    return true;
+	}
+
+	float mean = Sum1 / Npts;
+	float sigma = sqrt(Sum2 / Npts - mean*mean);
+	stdev->data.F32[i] = sigma;
+	fprintf (stderr, "mode: %d, stdev: %f\n", i, sigma);
+    }
+
+    // loop over stdev and choose the lowest one
+    int   minI = 0;
+    float minV = stdev->data.F32[minI];
+
+    for (int i = 1; i < nModes; i++) {
+	if (stdev->data.F32[i] < minV) {
+	    minI = i;
+	    minV = stdev->data.F32[i];
+	}
+    }
+    psFree (stdev);
+
+    if (minI == 0) return true;
+
+    int nModeColumnMask = 2*(minI - 1) + 1;
+
+    // now mask bad cells
+    for (int i = 0; i < numCells; i++) {
+        if (deadCellPattern->data.F32[i][nModeColumnMask] > 0) continue;
+
+        pmCell *cell = chip->cells->data[i]; // Cell of interest
+        pmReadout *ro = cell->readouts->data[0]; // Readout of interest
+
+	psImage *mask = ro->mask; // mask of interest
+	int numCols = mask->numCols, numRows = mask->numRows; // Size of image
+	
+	for (int y = 0; y < numRows; y++) {
+	    for (int x = 0; x < numCols; x++) {
+		mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= maskVal;
+	    }
+	}
+    }
+    return true;
+}
+
+bool pmPatternCellApply(pmReadout *ro, psImageMaskType maskBad)
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PM_ASSERT_READOUT_IMAGE(ro, false);
+
+    bool mdok;                          // Status of MD lookup
+    float corr = psMetadataLookupF32(&mdok, ro->analysis, PM_PATTERN_CELL_CORRECTION); // Correction to apply
+    if (!mdok) {
+        // No correction to apply
+        return true;
+    }
+
+    psImage *image = ro->image, *mask = ro->mask; // Image and mask of interest
+    int numCols = image->numCols, numRows = image->numRows; // Size of image
+
+    if (!isfinite(corr)) {
+        for (int y = 0; y < numRows; y++) {
+            for (int x = 0; x < numCols; x++) {
+                image->data.F32[y][x] = NAN;
+            }
+        }
+        if (mask) {
+            for (int y = 0; y < numRows; y++) {
+                for (int x = 0; x < numCols; x++) {
+                    mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= maskBad;
+                }
+            }
+        }
+    } else {
+        for (int y = 0; y < numRows; y++) {
+            for (int x = 0; x < numCols; x++) {
+                image->data.F32[y][x] += corr;
+            }
+        }
+    }
+
+    return true;
+}
+
+
+
+bool pmPatternContinuity(pmChip *chip, const psVector *tweak, psStatsOptions bgStat, psStatsOptions cellStat,
+			 psImageMaskType maskVal, psImageMaskType maskBad, int edgeWidth)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_VECTOR_NON_NULL(tweak, false);
+    PS_ASSERT_VECTOR_SIZE(tweak, chip->cells->n, false);
+    PS_ASSERT_VECTOR_TYPE(tweak, PS_TYPE_U8, false);
+
+    int numCells = tweak->n;            // Number of cells
+
+    psVector *meanMask = psVectorAlloc(numCells, PS_TYPE_VECTOR_MASK); // Mask for means
+    psVectorInit(meanMask, 0);
+
+    // Mask bits
+    enum {
+        PM_PATTERN_IGNORE = 0x01,       // Ignore this cell
+        PM_PATTERN_TWEAK  = 0x02,       // Tweak this cell
+        PM_PATTERN_ERROR  = 0x04,       // Error in calculating background
+        PM_PATTERN_ALL    = 0xFF,       // All causes
+    };
+
+    // Count number of cells to tweak
+    int numTweak = 0;                   // Number of cells to tweak
+    int numIgnore = 0;                  // Number of cells to ignore
+    for (int i = 0; i < numCells; i++) {
+        pmCell *cell = chip->cells->data[i]; // Cell of interest
+        if (!cell || !cell->data_exists || !cell->process ||
+            cell->readouts->n == 0 || cell->readouts->n > 1 || !cell->readouts->data[0]) {
+            numIgnore++;
+            meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] = PM_PATTERN_IGNORE;
+            continue;
+        }
+        if (tweak->data.U8[i]) {
+            meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] = PM_PATTERN_TWEAK;
+            numTweak++;
+        }
+    }
+    if (numTweak == 0) {
+        // Nothing to do
+        psFree(meanMask);
+        return true;
+    }
+
+    // Measure mean of each cell edge, and use that to determine the cell offsets.
+
+    psStats *bgStats = psStatsAlloc(bgStat); // Statistics on background
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS); // Random number generator
+
+    psRegion region = {0,0,0,0};
+
+    /* These images hold the edge data for the OTA structure.  */
+    psImage *A = psImageAlloc(8,8,PS_TYPE_F64); // Top edge
+    psImage *B = psImageAlloc(8,8,PS_TYPE_F64); // Bottom edge
+    psImage *C = psImageAlloc(8,8,PS_TYPE_F64); // Right edge
+    psImage *D = psImageAlloc(8,8,PS_TYPE_F64); // Left edge
+    psImageInit(A,0.0);
+    psImageInit(B,0.0);
+    psImageInit(C,0.0);
+    psImageInit(D,0.0);
+    
+    for (int i = 0; i < numCells; i++) {
+        if (meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] & PM_PATTERN_IGNORE) {
+            continue;
+        }
+        pmCell *cell = chip->cells->data[i]; // Cell of interest
+        pmReadout *ro = cell->readouts->data[0]; // Readout of interest
+
+        psStatsInit(bgStats);
+
+	// Convert cell iterator i into an xy coordinate on the grid of cells
+	int y = (i % 8);
+	int x = (i - y) / 8;
+	
+	for (int j = 0; j < 4; j++) {
+	  if (j == 0) {  // Region B
+	    region = psRegionSet(0,ro->image->numCols,
+				 0,edgeWidth);
+	  }
+	  else if (j == 1) { // Region A
+	    region = psRegionSet(0,ro->image->numCols,
+				 ro->image->numRows - edgeWidth,ro->image->numRows);
+	  }
+	  else if (j == 2) { // Region D
+	    region = psRegionSet(0,edgeWidth,
+				 0,ro->image->numRows);
+	  }
+	  else if (j == 3) { // Region C
+	    region = psRegionSet(ro->image->numCols - edgeWidth,ro->image->numCols,
+				 0,ro->image->numRows);
+	  }
+	  psImage *subset  = psImageSubset(ro->image,region);
+	  psImage *submask = psImageSubset(ro->mask,region);
+
+	  if (!psImageBackground(bgStats, NULL, subset, submask, maskVal, rng)) {
+            psWarning("Unable to measure background for cell %d on edge %d\n", i, j);
+            psErrorClear();
+            meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] |= PM_PATTERN_ERROR;
+	    if (j == 0)      { B->data.F64[y][x] = NAN; }
+	    else if (j == 1) { A->data.F64[y][x] = NAN; }
+	    else if (j == 2) { C->data.F64[y][x] = NAN; }
+	    else if (j == 3) { D->data.F64[y][x] = NAN; }
+	    psFree(subset);
+	    psFree(submask);
+            continue; // Move on to next edge, as only part of this cell may be a problem
+	  }
+ 
+	  // If the returned value is zero, assume something is wrong.  Do I still need this?
+	  if (psStatsGetValue(bgStats,bgStat) < 1e-6) {
+	    if (j == 0)      { B->data.F64[y][x] = NAN; }
+	    else if (j == 1) { A->data.F64[y][x] = NAN; }
+	    else if (j == 2) { C->data.F64[y][x] = NAN; }
+	    else if (j == 3) { D->data.F64[y][x] = NAN; }
+	  }
+	  // If we have an error for this cell/edge, make sure we mask the value
+	  if (meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] & PM_PATTERN_ERROR) {
+	    if (j == 0)      { B->data.F64[y][x] = NAN; }
+	    else if (j == 1) { A->data.F64[y][x] = NAN; }
+	    else if (j == 2) { C->data.F64[y][x] = NAN; }
+	    else if (j == 3) { D->data.F64[y][x] = NAN; }
+	  }
+	  else { // Set the value to match what we got from the edge box.
+	    if (j == 0)      { B->data.F64[y][x] = psStatsGetValue(bgStats,bgStat); }
+	    else if (j == 1) { A->data.F64[y][x] = psStatsGetValue(bgStats,bgStat); }
+	    else if (j == 2) { C->data.F64[y][x] = psStatsGetValue(bgStats,bgStat); }
+	    else if (j == 3) { D->data.F64[y][x] = psStatsGetValue(bgStats,bgStat); }
+	  }
+
+	  for (int u = 0; u < subset->numCols; u++) {
+	    for (int v = 0; v < subset->numRows; v++) {
+	      psTrace("psModules.detrend.cont",10,"BOX: %d %d (%d %d) (%d %d) %f %d",
+		      i,j,x,y,u,v,subset->data.F32[v][u],submask->data.PS_TYPE_IMAGE_MASK_DATA[v][u]);
+	    }
+	  }	  
+	  
+	  psFree(subset);
+	  psFree(submask);
+
+	}
+	psTrace("psModules.detrend.cont",5, "CELL: %d (%d %d) A: %f B: %f C: %f D: %f",
+		i,x,y,
+		A->data.F64[y][x],B->data.F64[y][x],C->data.F64[y][x],D->data.F64[y][x]);		
+    }
+    psFree(bgStats);
+    psFree(rng);
+
+    // We've now allocated all the edge values, so we can now minimize the offsets.
+    // This involves solving the equation A x = b, where
+    // A is the (64x64 for GPC1) matrix containing the edges that match for each cell
+    // x is the solution vector
+    // b is the combination of offsets across each cell boundary for each cell.
+    // Below "XX" is used as the matrix A, and "solution" is used as both b and x
+    //   (due to the way psMatrixLUSolve operates).
+    psVector *solution = psVectorAlloc(64,PS_TYPE_F64);
+    psImage  *XX       = psImageAlloc(64,64,PS_TYPE_F64);
+    psVectorInit(solution,0.0);
+    psImageInit(XX,0.0);
+    
+    for (int i = 0; i < numCells; i++) {
+      // Accumulate all the possible edge differences we can for this cell.
+      // As we do so, make a note of the correlations by incrementing the element of the matrix.
+      int y = (i % 8);
+      int x = (i - y) / 8;
+      int j;
+      double critical_value = 0.0;
+      if (meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] & PM_PATTERN_IGNORE) {
+	continue;
+      }
+      if (x + 1 < 8) {  // We have a neighbor adjacent in the +x direction
+	j = 8 * (x + 1) + y; // Determine that neighbor's index
+	if (fabs(C->data.F64[y][x]) > fabs(D->data.F64[y][x+1])) {
+	  critical_value = 2.0 * fabs(D->data.F64[y][x+1]);
+	}
+	else {
+	  critical_value = 2.0 * fabs(C->data.F64[y][x]);
+	}
+	if (critical_value < 25) { critical_value = 25; }
+	psTrace("psModules.detrend.cont",5,"CmD %d %d %d %d %g %g %g", // diagnostic
+		i,x,y,j,
+		C->data.F64[y][x],
+		D->data.F64[y][x+1],
+		critical_value
+		);
+	if (!(meanMask->data.PS_TYPE_VECTOR_MASK_DATA[j] & PM_PATTERN_IGNORE)&&  // If there are no errors with the neighbor,
+	    (isfinite(C->data.F64[y][x]))&&(isfinite(D->data.F64[y][x+1]))&&     // and all edges have valid values,
+	    (fabs(C->data.F64[y][x] - D->data.F64[y][x+1]) < critical_value)     // and there are no large discontinuities,
+	    ) {    
+	  solution->data.F64[i] += C->data.F64[y][x] - D->data.F64[y][x+1];     // Take the difference
+	  XX->data.F64[i][i] += 1;                                              // increment our relation with ourself
+	  XX->data.F64[i][j] += -1;                                             // decrement our relation with the neighbor
+	}
+      }
+      if (x - 1 > -1) { // etc.
+	j = 8 * (x - 1) + y;
+	if (fabs(C->data.F64[y][x-1]) > fabs(D->data.F64[y][x])) {
+	  critical_value = 2.0 * fabs(D->data.F64[y][x]);
+	}
+	else {
+	  critical_value = 2.0 * fabs(C->data.F64[y][x-1]);
+	}
+	if (critical_value < 25) { critical_value = 25; }
+	psTrace("psModules.detrend.cont",5,"DmC %d %d %d %d %g %g %g",
+		i,x,y,j,
+		D->data.F64[y][x],
+		C->data.F64[y][x-1],
+		critical_value
+		);
+
+	if (!(meanMask->data.PS_TYPE_VECTOR_MASK_DATA[j] & PM_PATTERN_IGNORE)&&
+	    (isfinite(D->data.F64[y][x]))&&(isfinite(C->data.F64[y][x-1]))&&
+	    (fabs(D->data.F64[y][x] - C->data.F64[y][x-1]) < critical_value)
+	    ) {
+	  solution->data.F64[i] += D->data.F64[y][x] - C->data.F64[y][x-1];
+	  XX->data.F64[i][i] += 1;
+	  XX->data.F64[i][j] += -1;
+	}
+      }
+      if (y + 1 < 8) {
+	j = 8 * x + (y + 1);
+	psTrace("psModules.detrend.cont",5,"AmB %d %d %d %d %g %g",
+		i,x,y,j,
+		A->data.F64[y][x],
+		B->data.F64[y+1][x]
+		);
+	if (fabs(A->data.F64[y][x]) > fabs(B->data.F64[y+1][x])) {
+	  critical_value = 2.0 * fabs(B->data.F64[y+1][x]);
+	}
+	else {
+	  critical_value = 2.0 * fabs(A->data.F64[y][x]);
+	}
+	if (critical_value < 25) { critical_value = 25; }
+	if (!(meanMask->data.PS_TYPE_VECTOR_MASK_DATA[j] & PM_PATTERN_IGNORE)&&
+	    (isfinite(A->data.F64[y][x]))&&(isfinite(B->data.F64[y+1][x]))&&
+	    (fabs(A->data.F64[y][x] - B->data.F64[y+1][x]) < critical_value)
+	    ) {
+	  solution->data.F64[i] += A->data.F64[y][x] - B->data.F64[y+1][x];
+	  XX->data.F64[i][i] += 1;
+	  XX->data.F64[i][j] += -1;
+	}
+      }
+      if (y - 1 > -1) {
+	j = 8 * x +  (y - 1);
+	psTrace("psModules.detrend.cont",5,"BmA %d %d %d %d %g %g",
+		i,x,y,j,
+		B->data.F64[y][x],
+		A->data.F64[y-1][x]
+		);
+	if (fabs(A->data.F64[y-1][x]) > fabs(B->data.F64[y][x])) {
+	  critical_value = 2.0 * fabs(B->data.F64[y][x]);
+	}
+	else {
+	  critical_value = 2.0 * fabs(A->data.F64[y-1][x]);
+	}
+	if (critical_value < 25) { critical_value = 25; }
+	if (!(meanMask->data.PS_TYPE_VECTOR_MASK_DATA[j] & PM_PATTERN_IGNORE)&&
+	    (isfinite(B->data.F64[y][x]))&&(isfinite(A->data.F64[y-1][x]))&&
+	    (fabs(B->data.F64[y][x] - A->data.F64[y-1][x]) < critical_value)
+	    ) {
+	  solution->data.F64[i] += B->data.F64[y][x] - A->data.F64[y-1][x];
+	  XX->data.F64[i][i] += 1;
+	  XX->data.F64[i][j] += -1;
+	}
+      }
+    }
+    double max_XX = 0;
+    double solution_V = 0;
+    int i_peak = -1;
+    for (int i = 0; i < numCells; i++) { // If any cells have no value of themself, set the matrix to 1.0.
+      if (XX->data.F64[i][i] == 0.0) {
+	XX->data.F64[i][i] = 1.0;
+      }
+      if (XX->data.F64[i][i] > max_XX) {
+	max_XX = XX->data.F64[i][i];
+	solution_V = solution->data.F64[i];
+	i_peak = i;
+      }
+    }
+    psTrace("psModules.detrend.cont",5,"fixed point: %d %g\n",
+	    i_peak,solution_V);
+
+    for (int i = 0; i < numCells; i++) {
+/*        if (!((XX->data.F64[i][i] == 1.0)&& */
+/*  	    (solution->data.F64[i] == 0.0))) { */
+	solution->data.F64[i] -= solution_V;
+	if (i != i_peak) {
+	  for (int j = 0; j < numCells; j++) {
+	    XX->data.F64[i][j] -= XX->data.F64[i_peak][j];
+	  }
+	}
+/*        } */
+    }
+    for (int i = 0; i < numCells; i++) {
+      XX->data.F64[i_peak][i] = 0.0;
+    }
+    XX->data.F64[i_peak][i_peak] = 1.0;
+    
+    
+#if (1)
+    for (int i = 0; i < numCells; i++) { // print matrix A
+      psTrace("psModules.detrend.cont",5,"A: %3d % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f",
+	      i,
+	      XX->data.F64[i][0],	      XX->data.F64[i][1],	      XX->data.F64[i][2],	      XX->data.F64[i][3],
+	      XX->data.F64[i][4],	      XX->data.F64[i][5],	      XX->data.F64[i][6],	      XX->data.F64[i][7],
+	      XX->data.F64[i][8],	      XX->data.F64[i][9],	      XX->data.F64[i][10],	      XX->data.F64[i][11],
+	      XX->data.F64[i][12],	      XX->data.F64[i][13],	      XX->data.F64[i][14],	      XX->data.F64[i][15],
+	      XX->data.F64[i][16],	      XX->data.F64[i][17],	      XX->data.F64[i][18],	      XX->data.F64[i][19],
+	      XX->data.F64[i][20],	      XX->data.F64[i][21],	      XX->data.F64[i][22],	      XX->data.F64[i][23],
+	      XX->data.F64[i][24],	      XX->data.F64[i][25],	      XX->data.F64[i][26],	      XX->data.F64[i][27],
+	      XX->data.F64[i][28],	      XX->data.F64[i][29],	      XX->data.F64[i][30],	      XX->data.F64[i][31],
+	      XX->data.F64[i][32],	      XX->data.F64[i][33],	      XX->data.F64[i][34],	      XX->data.F64[i][35],
+	      XX->data.F64[i][36],	      XX->data.F64[i][37],	      XX->data.F64[i][38],	      XX->data.F64[i][39],
+	      XX->data.F64[i][40],	      XX->data.F64[i][41],	      XX->data.F64[i][42],	      XX->data.F64[i][43],
+	      XX->data.F64[i][44],	      XX->data.F64[i][45],	      XX->data.F64[i][46],	      XX->data.F64[i][47],
+	      XX->data.F64[i][48],	      XX->data.F64[i][49],	      XX->data.F64[i][50],	      XX->data.F64[i][51],
+	      XX->data.F64[i][52],	      XX->data.F64[i][53],	      XX->data.F64[i][54],	      XX->data.F64[i][55],
+	      XX->data.F64[i][56],	      XX->data.F64[i][57],	      XX->data.F64[i][58],	      XX->data.F64[i][59],
+	      XX->data.F64[i][60],	      XX->data.F64[i][61],	      XX->data.F64[i][62],	      XX->data.F64[i][63]
+	      );
+    }
+
+    for (int i = 0; i < numCells; i++) { // print vector b
+      psTrace("psModules.detrend.cont",5,"b: %d %f",
+	      i,
+	      solution->data.F64[i]
+	      );
+    }
+#endif    
+    
+    // Solve the Ax=b equation
+    //    psMatrixLUSolve(XX,solution);
+    psMatrixGJSolve(XX,solution);
+#if (1)
+    for (int i = 0; i < numCells; i++) { // print vector b
+      psTrace("psModules.detrend.cont",5,"x: %d %f",
+	      i,
+	      solution->data.F64[i]
+	      );
+    }
+#endif
+    
+    /* old code to remove the minimum solution value from the set, to give a "minimal set of offsets." Mathematically unnecessary. */
+/*     double min = 99e99; */
+/*     for (int i = 0; i < numCells; i++) { */
+/*       if (solution->data.F64[i] < min) { */
+/* 	min = solution->data.F64[i]; */
+/*       } */
+/*       psTrace("psModules.detrend.cont",5,"x: %d %f %f ", */
+/* 	      i, */
+/* 	      solution->data.F64[i],min */
+/* 	      ); */
+/*     } */
+/*     for (int i = 0; i < numCells; i++) { */
+/* 	if (solution->data.F64[i] != 0.0) { */
+/* 	  solution->data.F64[i] -= min; */
+/* 	} */
+/*     } */
+
+    // Cleanup
+    psFree(XX);
+    psFree(A);
+    psFree(B);
+    psFree(C);
+    psFree(D);
+
+    // Correct cells based on the offsets calculated, and store the result in the analysis metadata.
+    for (int i = 0; i < numCells; i++) {
+        if (meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] & PM_PATTERN_IGNORE) {
+            continue;
+        }
+        if (!(meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] & PM_PATTERN_TWEAK)) {
+            continue;
+        }
+        pmCell *cell = chip->cells->data[i]; // Cell of interest
+        pmReadout *ro = cell->readouts->data[0]; // Readout of interest
+
+        float correction = solution->data.F64[i];
+        const char *cellName = psMetadataLookupStr(NULL, cell->concepts, "CELL.NAME"); // Name of cell
+        psLogMsg("psModules.detrend", PS_LOG_DETAIL, "Correcting background of cell %s by %f",
+                 cellName, correction);
+        psBinaryOp(ro->image, ro->image, "-", psScalarAlloc(correction, PS_TYPE_F32));
+        psMetadataAddF32(ro->analysis, PS_LIST_TAIL, PM_PATTERN_CELL_CORRECTION, PS_META_REPLACE,
+                         "Pattern cell correction solution", correction);
+    }
+
+    psFree(solution);
+    psFree(meanMask);
+
+    return true;
+}
+
+bool pmPatternContinuityApply(pmReadout *ro, psImageMaskType maskBad)
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PM_ASSERT_READOUT_IMAGE(ro, false);
+
+    bool mdok;                          // Status of MD lookup
+    float corr = psMetadataLookupF32(&mdok, ro->analysis, PM_PATTERN_CELL_CORRECTION); // Correction to apply
+    if (!mdok) {
+        // No correction to apply
+        return true;
+    }
+
+    psImage *image = ro->image, *mask = ro->mask; // Image and mask of interest
+    int numCols = image->numCols, numRows = image->numRows; // Size of image
+
+    if (!isfinite(corr)) {
+        for (int y = 0; y < numRows; y++) {
+            for (int x = 0; x < numCols; x++) {
+                image->data.F32[y][x] = NAN;
+            }
+        }
+        if (mask) {
+            for (int y = 0; y < numRows; y++) {
+                for (int x = 0; x < numCols; x++) {
+                    mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= maskBad;
+                }
+            }
+        }
+    } else {
+        for (int y = 0; y < numRows; y++) {
+            for (int x = 0; x < numCols; x++) {
+                image->data.F32[y][x] += corr;
+            }
+        }
+    }
+
+    return true;
+}
+
+// This should reuse code more efficiently
+bool pmPatternContinuityBackground(pmFPAfile *in, pmFPAfile *out,  psStatsOptions bgStat, psStatsOptions cellStat,
+				   psImageMaskType maskVal, psImageMaskType maskBad, int edgeWidth)
+{
+    PS_ASSERT_PTR_NON_NULL(in, false);
+    PS_ASSERT_PTR_NON_NULL(out, false);
+
+    int numChips = out->fpa->chips->n;            // Number of cells
+
+    psVector *meanMask = psVectorAlloc(numChips, PS_TYPE_VECTOR_MASK); // Mask for means
+    psVectorInit(meanMask, 0);
+
+    // Mask bits
+    enum {
+        PM_PATTERN_IGNORE = 0x01,       // Ignore this cell
+        PM_PATTERN_TWEAK  = 0x02,       // Tweak this cell
+        PM_PATTERN_ERROR  = 0x04,       // Error in calculating background
+        PM_PATTERN_ALL    = 0xFF,       // All causes
+    };
+
+    // Measure mean of each cell edge, and use that to determine the cell offsets.
+    psStatsOptions stat = cellStat;     // Define which statistic to use.
+    
+    psStats *bgStats = psStatsAlloc(stat); // Statistics on background
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS); // Random number generator
+
+    psRegion region = {0,0,0,0};
+
+    /* These images hold the edge data for the OTA structure.  */
+    psImage *A = psImageAlloc(8,8,PS_TYPE_F64); // Top edge
+    psImage *B = psImageAlloc(8,8,PS_TYPE_F64); // Bottom edge
+    psImage *C = psImageAlloc(8,8,PS_TYPE_F64); // Right edge
+    psImage *D = psImageAlloc(8,8,PS_TYPE_F64); // Left edge
+    psImageInit(A,0.0);
+    psImageInit(B,0.0);
+    psImageInit(C,0.0);
+    psImageInit(D,0.0);
+
+    // Corners don't exist.
+    A->data.F64[0][0] = NAN;
+    B->data.F64[0][0] = NAN;
+    C->data.F64[0][0] = NAN;
+    D->data.F64[0][0] = NAN;
+    A->data.F64[7][0] = NAN;
+    B->data.F64[7][0] = NAN;
+    C->data.F64[7][0] = NAN;
+    D->data.F64[7][0] = NAN;
+    A->data.F64[0][7] = NAN;
+    B->data.F64[0][7] = NAN;
+    C->data.F64[0][7] = NAN;
+    D->data.F64[0][7] = NAN;
+    A->data.F64[7][7] = NAN;
+    B->data.F64[7][7] = NAN;
+    C->data.F64[7][7] = NAN;
+    D->data.F64[7][7] = NAN;
+    
+    for (int i = 0; i < numChips; i++) {
+      pmChip *chip = out->fpa->chips->data[i];
+      pmCell *cell = chip->cells->data[0]; // Cell of interest
+
+      
+      psStatsInit(bgStats);
+
+      // Convert cell iterator i into an xy coordinate on the grid of cells
+      // This is wrong for chips
+      const char *chipName = psMetadataLookupStr(NULL,chip->concepts, "CHIP.NAME");
+      int xParity          = psMetadataLookupS16(NULL,chip->concepts, "CHIP.XPARITY");
+      int yParity          = psMetadataLookupS16(NULL,chip->concepts, "CHIP.YPARITY");
+      int x = chipName[2] - '0';
+      int y = chipName[3] - '0';
+
+      if ((cell->readouts->n != 1)||(!chip->data_exists))  {
+	A->data.F64[y][x] = NAN;
+	B->data.F64[y][x] = NAN;
+	C->data.F64[y][x] = NAN;
+	D->data.F64[y][x] = NAN;
+	continue;
+      }
+      pmReadout *ro = cell->readouts->data[0]; // Readout of interest      
+      for (int j = 0; j < 4; j++) {
+	if (j == 0) {  // Region B
+	  region = psRegionSet(0,ro->image->numCols,
+			       0,edgeWidth);
+	}
+	else if (j == 1) { // Region A
+	  region = psRegionSet(0,ro->image->numCols,
+			       ro->image->numRows - edgeWidth,ro->image->numRows);
+	}
+	else if (j == 2) { // Region D
+	  region = psRegionSet(0,edgeWidth,
+			       0,ro->image->numRows);
+	}
+	else if (j == 3) { // Region C
+	  region = psRegionSet(ro->image->numCols - edgeWidth,ro->image->numCols,
+			       0,ro->image->numRows);
+	}
+	psImage *subset  = psImageSubset(ro->image,region);
+
+	if (!psImageBackground(bgStats, NULL, subset, NULL, maskVal, rng)) {
+	  psWarning("Unable to measure background for cell %d on edge %d\n", i, j);
+	  psErrorClear();
+	  meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] |= PM_PATTERN_ERROR;
+	  if (j == 0)      { B->data.F64[y][x] = NAN; }
+	  else if (j == 1) { A->data.F64[y][x] = NAN; }
+	  else if (j == 2) { C->data.F64[y][x] = NAN; }
+	  else if (j == 3) { D->data.F64[y][x] = NAN; }
+	  psFree(subset);
+	  continue; // Move on to next edge, as only part of this cell may be a problem
+	}
+	
+	// If the returned value is zero, assume something is wrong.  Do I still need this?
+	if (psStatsGetValue(bgStats,stat) < 1e-6) {
+	  if (j == 0)      { B->data.F64[y][x] = NAN; }
+	  else if (j == 1) { A->data.F64[y][x] = NAN; }
+	  else if (j == 2) { C->data.F64[y][x] = NAN; }
+	  else if (j == 3) { D->data.F64[y][x] = NAN; }
+	}
+	// If we have an error for this cell/edge, make sure we mask the value
+	if (meanMask->data.PS_TYPE_VECTOR_MASK_DATA[i] & PM_PATTERN_ERROR) {
+	  if (j == 0)      { B->data.F64[y][x] = NAN; }
+	  else if (j == 1) { A->data.F64[y][x] = NAN; }
+	  else if (j == 2) { C->data.F64[y][x] = NAN; }
+	  else if (j == 3) { D->data.F64[y][x] = NAN; }
+	}
+	else { // Set the value to match what we got from the edge box.
+	  if (xParity == -1) {
+	    if (j == 2)      { C->data.F64[y][x] = psStatsGetValue(bgStats,stat); }
+	    else if (j == 3) { D->data.F64[y][x] = psStatsGetValue(bgStats,stat); }
+	  }
+	  if (xParity == 1) {
+	    if (j == 3)      { C->data.F64[y][x] = psStatsGetValue(bgStats,stat); }
+	    else if (j == 2) { D->data.F64[y][x] = psStatsGetValue(bgStats,stat); }
+	  }
+	  if (yParity == 1) {
+	    if (j == 0)      { B->data.F64[y][x] = psStatsGetValue(bgStats,stat); }
+	    else if (j == 1) { A->data.F64[y][x] = psStatsGetValue(bgStats,stat); }
+	  }
+	  if (yParity == -1) {
+	    if (j == 1)      { B->data.F64[y][x] = psStatsGetValue(bgStats,stat); }
+	    else if (j == 0) { A->data.F64[y][x] = psStatsGetValue(bgStats,stat); }
+	  }
+	  
+	}
+#if (0)	
+	for (int u = 0; u < subset->numCols; u++) {
+	  for (int v = 0; v < subset->numRows; v++) {
+	    psTrace("psModules.detrend.cont",10,"BOX: %d %d (%d %d) (%d %d) %f %d",
+		    i,j,x,y,u,v,subset->data.F32[v][u],0);
+	  }
+	}
+#endif	
+	psFree(subset);
+	
+      }
+      psTrace("psModules.detrend.cont",5, "OTA: %d (%d %d) (%d %d) A: %f B: %f C: %f D: %f",
+	      i,x,y,xParity,yParity,
+	      A->data.F64[y][x],B->data.F64[y][x],C->data.F64[y][x],D->data.F64[y][x]);		
+    }
+    psFree(bgStats);
+    psFree(rng);
+    
+    // We've now allocated all the edge values, so we can now minimize the offsets.
+    // This involves solving the equation A x = b, where
+    // A is the (64x64 for GPC1) matrix containing the edges that match for each cell
+    // x is the solution vector
+    // b is the combination of offsets across each cell boundary for each cell.
+    // Below "XX" is used as the matrix A, and "solution" is used as both b and x
+    //   (due to the way psMatrixLUSolve operates).
+    psVector *solution = psVectorAlloc(64,PS_TYPE_F64);
+    psImage  *XX       = psImageAlloc(64,64,PS_TYPE_F64);
+    psVectorInit(solution,0.0);
+    psImageInit(XX,0.0);
+
+
+    for (int i = 0; i < numChips; i++) {
+      // Accumulate all the possible edge differences we can for this cell.
+      // As we do so, make a note of the correlations by incrementing the element of the matrix.
+      pmChip *chip = out->fpa->chips->data[i];
+      const char *chipName = psMetadataLookupStr(NULL,chip->concepts, "CHIP.NAME");
+      int x = chipName[2] - '0';
+      int y = chipName[3] - '0';
+
+      int j;
+      int k = 8 * x + y;
+      double critical_value = 0.0;
+
+      if (x + 1 < 8) {  // We have a neighbor adjacent in the +x direction
+	j = 8 * (x + 1) + y; // Determine that neighbor's index
+	
+	if (fabs(C->data.F64[y][x]) > fabs(D->data.F64[y][x+1])) {
+	  critical_value = 2.0 * fabs(D->data.F64[y][x+1]);
+	}
+	else {
+	  critical_value = 2.0 * fabs(C->data.F64[y][x]);
+	}
+	if (critical_value < 25) { critical_value = 25; }
+	psTrace("psModules.detrend.cont",5,"CmD %d %d %d %d %g %g %g", // diagnostic
+		i,x,y,j,
+		C->data.F64[y][x],
+		D->data.F64[y][x+1],
+		critical_value
+		);
+	if (// If there are no errors with the neighbor,
+	    (isfinite(C->data.F64[y][x]))&&(isfinite(D->data.F64[y][x+1]))&&     // and all edges have valid values,
+	    (fabs(C->data.F64[y][x] - D->data.F64[y][x+1]) < critical_value)     // and there are no large discontinuities,
+	    ) {    
+	  solution->data.F64[k] += C->data.F64[y][x] - D->data.F64[y][x+1];     // Take the difference
+	  XX->data.F64[k][k] += 1;                                              // increment our relation with ourself
+	  XX->data.F64[k][j] += -1;                                             // decrement our relation with the neighbor
+	}
+      }
+      if (x - 1 > -1) { // etc.
+	j = 8 * (x - 1) + y;
+	if (fabs(C->data.F64[y][x-1]) > fabs(D->data.F64[y][x])) {
+	  critical_value = 2.0 * fabs(D->data.F64[y][x]);
+	}
+	else {
+	  critical_value = 2.0 * fabs(C->data.F64[y][x-1]);
+	}
+	if (critical_value < 25) { critical_value = 25; }
+	psTrace("psModules.detrend.cont",5,"DmC %d %d %d %d %g %g %g",
+		i,x,y,j,
+		D->data.F64[y][x],
+		C->data.F64[y][x-1],
+		critical_value
+		);
+
+	if (
+	    (isfinite(D->data.F64[y][x]))&&(isfinite(C->data.F64[y][x-1]))&&
+	    (fabs(D->data.F64[y][x] - C->data.F64[y][x-1]) < critical_value)
+	    ) {
+	  solution->data.F64[k] += D->data.F64[y][x] - C->data.F64[y][x-1];
+	  XX->data.F64[k][k] += 1;
+	  XX->data.F64[k][j] += -1;
+	}
+      }
+      if (y + 1 < 8) {
+	j = 8 * x + (y + 1);
+	psTrace("psModules.detrend.cont",5,"AmB %d %d %d %d %g %g",
+		i,x,y,j,
+		A->data.F64[y][x],
+		B->data.F64[y+1][x]
+		);
+	if (fabs(A->data.F64[y][x]) > fabs(B->data.F64[y+1][x])) {
+	  critical_value = 2.0 * fabs(B->data.F64[y+1][x]);
+	}
+	else {
+	  critical_value = 2.0 * fabs(A->data.F64[y][x]);
+	}
+	if (critical_value < 25) { critical_value = 25; }
+	if (
+	    (isfinite(A->data.F64[y][x]))&&(isfinite(B->data.F64[y+1][x]))&&
+	    (fabs(A->data.F64[y][x] - B->data.F64[y+1][x]) < critical_value)
+	    ) {
+	  solution->data.F64[k] += A->data.F64[y][x] - B->data.F64[y+1][x];
+	  XX->data.F64[k][k] += 1;
+	  XX->data.F64[k][j] += -1;
+	}
+      }
+      if (y - 1 > -1) {
+	j = 8 * x +  (y - 1);
+	psTrace("psModules.detrend.cont",5,"BmA %d %d %d %d %g %g",
+		i,x,y,j,
+		B->data.F64[y][x],
+		A->data.F64[y-1][x]
+		);
+	if (fabs(A->data.F64[y-1][x]) > fabs(B->data.F64[y][x])) {
+	  critical_value = 2.0 * fabs(B->data.F64[y][x]);
+	}
+	else {
+	  critical_value = 2.0 * fabs(A->data.F64[y-1][x]);
+	}
+	if (critical_value < 25) { critical_value = 25; }
+	if (
+	    (isfinite(B->data.F64[y][x]))&&(isfinite(A->data.F64[y-1][x]))&&
+	    (fabs(B->data.F64[y][x] - A->data.F64[y-1][x]) < critical_value)
+	    ) {
+	  solution->data.F64[k] += B->data.F64[y][x] - A->data.F64[y-1][x];
+	  XX->data.F64[k][k] += 1;
+	  XX->data.F64[k][j] += -1;
+	}
+      }
+    }
+    double max_XX = 0;
+#if (PS_TRACE_ON)
+    double solution_V = 0;
+    int i_peak = -1;
+#endif
+    for (int i = 0; i < numChips + 4; i++) { // If any cells have no value of themself, set the matrix to 1.0.
+      if (XX->data.F64[i][i] == 0.0) {
+	XX->data.F64[i][i] = 1.0;
+      }
+      if (XX->data.F64[i][i] > max_XX) {
+	max_XX = XX->data.F64[i][i];
+#if (PS_TRACE_ON)
+	solution_V = solution->data.F64[i];
+	i_peak = i;
+#endif
+      }
+    }
+    psTrace("psModules.detrend.cont",5,"fixed point: %d %g\n",
+	    i_peak,solution_V);
+    
+#if (1)
+    for (int i = 0; i < numChips + 4; i++) { // print matrix A
+      psTrace("psModules.detrend.cont",5,"A: %3d % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f % 2.0f",
+	      i,
+	      XX->data.F64[i][0],	      XX->data.F64[i][1],	      XX->data.F64[i][2],	      XX->data.F64[i][3],
+	      XX->data.F64[i][4],	      XX->data.F64[i][5],	      XX->data.F64[i][6],	      XX->data.F64[i][7],
+	      XX->data.F64[i][8],	      XX->data.F64[i][9],	      XX->data.F64[i][10],	      XX->data.F64[i][11],
+	      XX->data.F64[i][12],	      XX->data.F64[i][13],	      XX->data.F64[i][14],	      XX->data.F64[i][15],
+	      XX->data.F64[i][16],	      XX->data.F64[i][17],	      XX->data.F64[i][18],	      XX->data.F64[i][19],
+	      XX->data.F64[i][20],	      XX->data.F64[i][21],	      XX->data.F64[i][22],	      XX->data.F64[i][23],
+	      XX->data.F64[i][24],	      XX->data.F64[i][25],	      XX->data.F64[i][26],	      XX->data.F64[i][27],
+	      XX->data.F64[i][28],	      XX->data.F64[i][29],	      XX->data.F64[i][30],	      XX->data.F64[i][31],
+	      XX->data.F64[i][32],	      XX->data.F64[i][33],	      XX->data.F64[i][34],	      XX->data.F64[i][35],
+	      XX->data.F64[i][36],	      XX->data.F64[i][37],	      XX->data.F64[i][38],	      XX->data.F64[i][39],
+	      XX->data.F64[i][40],	      XX->data.F64[i][41],	      XX->data.F64[i][42],	      XX->data.F64[i][43],
+	      XX->data.F64[i][44],	      XX->data.F64[i][45],	      XX->data.F64[i][46],	      XX->data.F64[i][47],
+	      XX->data.F64[i][48],	      XX->data.F64[i][49],	      XX->data.F64[i][50],	      XX->data.F64[i][51],
+	      XX->data.F64[i][52],	      XX->data.F64[i][53],	      XX->data.F64[i][54],	      XX->data.F64[i][55],
+	      XX->data.F64[i][56],	      XX->data.F64[i][57],	      XX->data.F64[i][58],	      XX->data.F64[i][59],
+	      XX->data.F64[i][60],	      XX->data.F64[i][61],	      XX->data.F64[i][62],	      XX->data.F64[i][63]
+	      );
+    }
+
+    for (int i = 0; i < numChips + 4; i++) { // print vector b
+      psTrace("psModules.detrend.cont",5,"b: %d %f",
+	      i,
+	      solution->data.F64[i]
+	      );
+    }
+#endif    
+    
+    // Solve the Ax=b equation
+    //    psMatrixLUSolve(XX,solution);
+    psMatrixGJSolve(XX,solution);
+#if (1)
+    for (int i = 0; i < numChips + 4; i++) { // print vector b
+      psTrace("psModules.detrend.cont",5,"x: %d %f",
+	      i,
+	      solution->data.F64[i]
+	      );
+    }
+#endif
+    
+    // Cleanup
+    psFree(XX);
+    psFree(A);
+    psFree(B);
+    psFree(C);
+    psFree(D);
+
+    // Correct cells based on the offsets calculated, and store the result in the analysis metadata.
+    for (int i = 0; i < numChips; i++) {
+	pmChip *chip = out->fpa->chips->data[i];
+        pmCell *cell = chip->cells->data[0]; // Cell of interest
+	if ((cell->readouts->n != 1)||(!chip->data_exists))  {
+	  continue;
+	}
+        pmReadout *ro = cell->readouts->data[0]; // Readout of interest
+	const char *chipName = psMetadataLookupStr(NULL,chip->concepts, "CHIP.NAME");
+	int x = chipName[2] - '0';
+	int y = chipName[3] - '0';
+	int k = 8 * x + y;
+        float correction = solution->data.F64[k];
+        psLogMsg("psModules.detrend", PS_LOG_DETAIL, "Correcting background of chip %s by %f",
+                 chipName, correction);
+        psBinaryOp(ro->image, ro->image, "-", psScalarAlloc(correction, PS_TYPE_F32));
+        psMetadataAddF32(ro->analysis, PS_LIST_TAIL, PM_PATTERN_CELL_CORRECTION, PS_META_REPLACE,
+                         "Pattern chip correction solution", correction);
+    }
+
+    psFree(solution);
+    psFree(meanMask);
+
+    return true;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmPattern.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmPattern.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmPattern.h	(revision 42651)
@@ -0,0 +1,94 @@
+/* @file pmPattern.h
+ * @brief Fit and remove pattern noise
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.16 $
+ * @date $Date: 2009-02-12 19:25:52 $
+ * Copyright 2004-2009 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_PATTERN_H
+#define PM_PATTERN_H
+
+#include <pslib.h>
+#include <pmFPA.h>
+#include <pmFPAview.h>
+#include <pmFPAfile.h>
+#include <pmFPAfileFitsIO.h>
+#include <pmFPAHeader.h>
+#include <pmHDU.h>
+#include <pmHDUUtils.h>
+
+/// @addtogroup detrend Detrend Creation and Application
+/// @{
+
+#define PM_PATTERN_ROW_CORRECTION "PATTERN.ROW.CORRECTION" // Pattern row correction on analysis metadata
+#define PM_PATTERN_CELL_CORRECTION "PATTERN.CELL.CORRECTION" // Pattern cell correction on analysis metadata
+
+/// Fit and remove pattern noise over rows
+bool pmPatternRow(
+    pmReadout *ro,                      ///< Readout to correct
+    int order,                          ///< Polynomial order
+    int iter,                           ///< Number of clipping iterations
+    float rej,                          ///< Rejection threshold for clipping
+    float thresh,                       ///< Threshold for ignoring pixels
+    psStatsOptions clipMean,            ///< Statistic to use for mean
+    psStatsOptions clipStdev,           ///< Statistic to use for standard deviation
+    psImageMaskType maskVal,            ///< Mask value to use
+    psImageMaskType maskBad             ///< Mask value to give bad pixels
+    );
+
+/// Apply previously measured row pattern correction
+bool pmPatternRowApply(pmReadout *ro,   ///< Readout to correct
+                       psImageMaskType maskBad ///< Mask value to give bad pixels
+                       );
+
+/// Fix the background on cells known to be troublesome
+bool pmPatternCell(
+    pmChip *chip,                       ///< Chip to correct
+    const psVector *tweak,              ///< U8 vector indicating whether to tweak the corresponding cell
+    psStatsOptions bgStat,              ///< Statistic to use for background measurement
+    psStatsOptions cellStat,            ///< Statistic to use for combination of cell background measurements
+    psImageMaskType maskVal,            ///< Mask value to use
+    psImageMaskType maskBad             ///< Mask value to give bad pixels
+    );
+
+/// Apply previously measured cell pattern correction
+bool pmPatternCellApply(pmReadout *ro,          ///< Readout to correct
+                        psImageMaskType maskBad ///< Mask value to give bad pixels
+                        );
+
+/// Fix the background on cells known to be troublesome
+bool pmPatternContinuity(
+    pmChip *chip,                       ///< Chip to correct
+    const psVector *tweak,              ///< U8 vector indicating whether to tweak the corresponding cell
+    psStatsOptions bgStat,              ///< Statistic to use for background measurement
+    psStatsOptions cellStat,            ///< Statistic to use for combination of cell background measurements
+    psImageMaskType maskVal,            ///< Mask value to use
+    psImageMaskType maskBad,            ///< Mask value to give bad pixels
+    int edgeWidth                       ///< Size of box to use
+    );
+
+bool pmPatternContinuityBackground(pmFPAfile *in,
+				   pmFPAfile *out,
+				   psStatsOptions bgStat,
+				   psStatsOptions cellStat,
+				   psImageMaskType maskVal,
+				   psImageMaskType maskBad,
+				   int edgeWidth);
+
+/// Apply previously measured cell pattern correction
+bool pmPatternContinuityApply(pmReadout *ro,          ///< Readout to correct
+                        psImageMaskType maskBad ///< Mask value to give bad pixels
+                        );
+
+
+
+bool pmPatternDeadCells(pmChip *chip, const psVector *backs, psImageMaskType maskVal);
+
+/// @}
+#endif
+
+
+
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmPatternIO.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmPatternIO.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmPatternIO.c	(revision 42651)
@@ -0,0 +1,652 @@
+#include <stdio.h>
+#include <pslib.h>
+#include <string.h>
+
+#include "pmHDU.h"
+#include "pmHDUUtils.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPAUtils.h"
+#include "pmFPAfileFitsIO.h"
+#include "pmFPAHeader.h"
+#include "pmConceptsRead.h"
+#include "pmConceptsWrite.h"
+
+#include "pmPattern.h"
+#include "pmPatternIO.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmReadoutWritePattern(pmReadout *ro, psFits *fits)
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    bool gotRow, gotCell;
+    psImage *rowCorr = psMetadataLookupPtr(&gotRow, ro->analysis, PM_PATTERN_ROW_CORRECTION); // Row correction
+    float cellCorr = psMetadataLookupF32(&gotCell, ro->analysis, PM_PATTERN_CELL_CORRECTION); // Cell corr.
+
+    pmCell *cell = ro->parent;          // Cell of interest
+    if (!cell) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "No cell associated with readout.");
+        return false;
+    }
+    pmChip *chip = cell->parent;    // Chip of interest
+    pmFPA *fpa = chip->parent;      // FPA of interest
+    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // HDU for readout
+    if (!hdu) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "No HDU associated with readout.");
+        return false;
+    }
+
+    psMetadata *header = psMetadataCopy(NULL, hdu->header); // Header for output
+
+    if (gotCell) {
+        psMetadataAddF32(header, PS_LIST_TAIL, PM_PATTERN_CELL_CORRECTION, PS_META_REPLACE,
+                         "Pattern cell correction value", cellCorr);
+    }
+
+    if (gotRow) {
+        if (!psFitsWriteImage(fits, header, rowCorr, 0, hdu->extname)) {
+            psError(PS_ERR_IO, false, "Unable to write pattern row correction.");
+            psFree(header);
+            return false;
+        }
+    } else {
+        if (!psFitsWriteBlank(fits, header, hdu->extname)) {
+            psError(PS_ERR_IO, false, "Unable to write pattern cell correction.");
+            psFree(header);
+            return false;
+        }
+    }
+
+    psFree(header);
+    return true;
+}
+
+static bool pmCellWritePattern(pmCell *cell, const pmFPAview *view,
+                               pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(cell->readouts, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    if (!pmConceptsWriteCell(cell, true, config)) {
+        psError(PS_ERR_IO, false, "Unable to write concepts for cell.");
+        return false;
+    }
+
+    // Only do the FIRST readout --- don't want to write lots of headers
+    pmReadout *readout = cell->readouts->data[0];
+    if (!pmReadoutWritePattern(readout, file->fits)) {
+        psError(PS_ERR_IO, false, "Failed to write readout");
+        return false;
+    }
+    return true;
+}
+
+static bool pmChipWritePattern(pmChip *chip, const pmFPAview *view,
+                               pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(chip->cells, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    if (!pmConceptsWriteChip(chip, true, true, config)) {
+        psError(PS_ERR_IO, false, "Unable to write concepts for chip.\n");
+        return false;
+    }
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        thisView->cell = i;
+        if (!pmCellWritePattern(cell, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write %dth cell", i);
+            psFree(thisView);
+            return false;
+        }
+    }
+    psFree(thisView);
+    return true;
+}
+
+static bool pmFPAWritePattern(pmFPA *fpa, const pmFPAview *view,
+                              pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fpa->chips, false);
+
+    if (!pmConceptsWriteFPA(fpa, true, config)) {
+        psError(PS_ERR_IO, false, "Unable to write concepts for FPA.\n");
+        return false;
+    }
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        thisView->chip = i;
+        if (!pmChipWritePattern(chip, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write %dth chip", i);
+            psFree(thisView);
+            return false;
+        }
+    }
+    psFree(thisView);
+    return true;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+bool pmReadoutReadPattern(pmReadout *ro, psFits *fits)
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    pmCell *cell = ro->parent;          // Cell of interest
+    if (!cell) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "No cell associated with readout.");
+        return false;
+    }
+    pmChip *chip = cell->parent;    // Chip of interest
+    pmFPA *fpa = chip->parent;      // FPA of interest
+    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // HDU for readout
+    if (!hdu) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "No HDU associated with readout.");
+        return false;
+    }
+
+    if (!psFitsMoveExtName(fits, hdu->extname)) {
+        psError(PS_ERR_IO, false, "Unable to move to pattern correction.");
+        return false;
+    }
+
+    psMetadata *header = psFitsReadHeader(NULL, fits); // Header
+    if (!header) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to read header for pattern correction.");
+        return false;
+    }
+
+    bool data = true;                  // Did we find any data?
+
+    psMetadataItem *cellCorr = psMetadataLookup(header, PM_PATTERN_CELL_CORRECTION); // Cell pattern correction
+    if (cellCorr) {
+        psMetadataAddItem(ro->analysis, cellCorr, PS_LIST_TAIL, PS_META_REPLACE);
+        data = true;
+    }
+
+    int naxis = psMetadataLookupS32(NULL, header, "NAXIS"); // Number of axes
+    if (naxis > 0) {
+        psImage *rowCorr = psFitsReadImage(fits, psRegionSet(0, 0, 0, 0), 0); // Row pattern correction
+        psMetadataAddImage(ro->analysis, PS_LIST_TAIL, PM_PATTERN_ROW_CORRECTION, PS_META_REPLACE,
+                           "Pattern row correction", rowCorr);
+        psFree(rowCorr);
+        data = true;
+    }
+
+    if (data) {
+        ro->data_exists = true;
+        ro->parent->data_exists = true;
+        ro->parent->parent->data_exists = true;
+    }
+
+    psFree(header);
+
+    return true;
+}
+
+static bool pmCellReadPattern(pmCell *cell, const pmFPAview *view,
+                              pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(cell->readouts, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    // Create a readout if none exists
+    if (!cell->readouts || cell->readouts->n == 0) {
+        pmReadout *readout = pmReadoutAlloc(cell); // New readout
+        psFree(readout);                // Drop reference
+    }
+
+    cell->data_exists = false;
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        thisView->readout = i;
+        if (!pmReadoutReadPattern(readout, file->fits)) {
+            psError(PS_ERR_IO, false, "Unable to read pattern correction.");
+            return false;
+        }
+    }
+    psFree(thisView);
+
+    if (!pmCellReadHeader(cell, file->fits, config)) {
+        psError(PS_ERR_IO, false, "Unable to read header for cell.");
+        return false;
+    }
+    // load in the concept information for this cell
+    if (!pmConceptsReadCell(cell, PM_CONCEPT_SOURCE_HEADER, true, NULL)) {
+        psErrorClear();
+        psWarning("Difficulty reading concepts for cell; attempting to proceed.");
+    }
+
+    return true;
+}
+
+static bool pmChipReadPattern(pmChip *chip, const pmFPAview *view,
+                              pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(chip->cells, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    chip->data_exists = false;
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        thisView->cell = i;
+        pmCellReadPattern(cell, thisView, file, config);
+        if (!cell->data_exists) {
+            continue;
+        }
+        chip->data_exists = true;
+    }
+    psFree(thisView);
+
+    if (!pmChipReadHeader(chip, file->fits, config)) {
+        psError(PS_ERR_IO, false, "Unable to read header for cell.");
+        return false;
+    }
+    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;
+}
+
+static bool pmFPAReadPattern(pmFPA *fpa, const pmFPAview *view,
+                             pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa->chips, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        thisView->chip = i;
+        pmChipReadPattern(chip, thisView, file, config);
+    }
+    psFree(thisView);
+
+    if (!pmFPAReadHeader(fpa, file->fits, config)) {
+        psError(PS_ERR_IO, false, "Unable to read header for cell.");
+        return false;
+    }
+    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;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmPatternWrite(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // Suitable FPA for writing
+
+    if (view->chip == -1) {
+        if (!pmFPAWritePattern(fpa, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write pattern correction from fpa");
+            psFree(fpa);
+            return false;
+        }
+        psFree(fpa);
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        psError(PS_ERR_UNKNOWN, false, "Writing chip == %d (>= chips->n == %ld)", view->chip, fpa->chips->n);
+        psFree(fpa);
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        if (!pmChipWritePattern(chip, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write pattern correction from chip");
+            psFree(fpa);
+            return false;
+        }
+        psFree(fpa);
+        return true;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        psError(PS_ERR_UNKNOWN, false, "Writing cell == %d (>= cells->n == %ld)",
+                view->cell, chip->cells->n);
+        psFree(fpa);
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        if (!pmCellWritePattern(cell, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write pattern correction from cell");
+            psFree(fpa);
+            return false;
+        }
+        psFree(fpa);
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        psError(PS_ERR_UNKNOWN, false, "Writing readout == %d (>= readouts->n == %ld)",
+                view->readout, cell->readouts->n);
+        psFree(fpa);
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    if (!pmReadoutWritePattern(readout, file->fits)) {
+        psError(PS_ERR_IO, false, "Failed to write pattern correction from readout %d", view->readout);
+        psFree(fpa);
+        return false;
+    }
+
+    psFree(fpa);
+    return true;
+}
+
+bool pmPatternWritePHU(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    if (file->wrote_phu) {
+        return true;
+    }
+
+    // find the FPA phu
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // Suitable FPA for writing
+    pmHDU *phu = psMemIncrRefCounter(pmFPAviewThisPHU(view, fpa));
+    psFree(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);
+    }
+    psFree(phu);
+
+    pmConfigConformHeader(outhead, file->format);
+
+    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 pmPatternRead(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        pmFPAReadPattern(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) {
+        pmChipReadPattern(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) {
+        pmCellReadPattern(cell, view, file, config);
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    return pmReadoutReadPattern(readout, file->fits);
+}
+
+/**************** PatternRowAmp(litude) I/O *************************/
+
+bool pmPatternRowAmpRead (const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+    {
+        // read the full model in one pass: require the level to be FPA
+        if (view->chip != -1) {
+            psError(PS_ERR_IO, false, "Pattern Row Amplitude must be read at the FPA level");
+            return false;
+        }
+
+        if (!pmPatternRowAmpReadFPA (file)) {
+            psError(PS_ERR_IO, false, "Failed to read Pattern Row Amplitude for fpa");
+            return false;
+        }
+        return true;
+    }
+
+// read in all chip-level Pattern Row Amplitude data for this FPA
+bool pmPatternRowAmpReadFPA (pmFPAfile *file) {
+
+    if (!pmPatternRowAmpReadChips (file)) {
+        psError(PS_ERR_IO, false, "Failed to read Pattern Row Amplitude for chips");
+        return false;
+    }
+
+    return true;
+}
+
+// Read the set of tables, one for each chip.  The values are saved on the cell->analysis
+// metadata of the pmFPAfile associated with the pattern file.  Later, when this is used (e.g.,
+// ppImageDetrendPatternRowApply), the values are transferred to the cell->analysis metadata of
+// the pmFPAfile for the image being processed.
+bool pmPatternRowAmpReadChips (pmFPAfile *file) {
+
+    bool haveData, status;
+
+    // loop over the extensions
+    // for each extension, use the extname (eg, XY01.ptn) to assign to a chip
+
+    // move to the start of the file
+    haveData = psFitsMoveExtNum (file->fits, 1, false);
+    if (!haveData) {
+        psError(PS_ERR_IO, false, "Failed to read even the first extension?");
+        return false;
+    }
+
+    int nGood = 0;
+    int nTotal = 0;
+    while (haveData) {
+
+	// load the header
+	psMetadata *header = psFitsReadHeader(NULL, file->fits); // The FITS header
+	if (!header) psAbort("cannot read model header");
+
+	// load the full model in one shot
+	psArray *model = psFitsReadTable (file->fits);
+	if (!model) psAbort("cannot read model");
+	
+	// determine the chip:
+	char *extname = psMetadataLookupStr (&status, header, "EXTNAME");
+	psLogMsg ("psModules.detrend", 8, "read %ld rows from Pattern Row Amplitude file, extname %s\n", model->n, extname);
+	nTotal += model->n;
+
+	// I expect to find a name of the form: chipName.ptn (eg, XY01.ptn)
+	// where chipName like 'XY01'
+	psAssert (strlen(extname) == 8, "invalid extension %s", extname);
+	psAssert (extname[5] == 'p', "invalid extension %s", extname);
+	psAssert (extname[6] == 't', "invalid extension %s", extname);
+	psAssert (extname[7] == 'n', "invalid extension %s", extname);
+
+	char chipName[5];
+	strncpy (chipName, extname, 4);
+	chipName[4] = 0;
+
+	pmChip *chip = pmConceptsChipFromName (file->fpa, chipName);
+	if (!chip) psAbort ("invalid chip?");
+
+	// parse the model entries
+	for (int i = 0; i < model->n; i++) {
+	    psMetadata *row = model->data[i];
+	    psAssert (row, "missing model row");
+
+	    char *cellName = psMetadataLookupStr(&status, row, "CELL_NAME");
+	    if (!cellName) continue;
+
+	    float amplitude = psMetadataLookupF32(&status, row, "VALUE_MEDIAN");
+
+	    int cellNumber = pmChipFindCell (chip, cellName);
+	    psAssert ((cellNumber >=0) && (cellNumber < chip->cells->n), "invalid cell number");
+
+	    pmCell *cell = chip->cells->data[cellNumber];
+	    if (!cell) continue;
+
+	    psAssert (cell->analysis, "oops");
+
+	    psMetadataAddF32 (cell->analysis, PS_LIST_TAIL, "PTN.ROW.AMP", PS_META_REPLACE, "", amplitude);
+	    nGood ++;
+	}
+
+	psFree (model);
+	psFree (header);
+
+	// move to the next extension
+	haveData = psFitsMoveExtNum (file->fits, 1, true);
+    }
+    psLogMsg ("psModules.detrend", 4, "read %d of %d rows from Pattern Row Amplitude file\n", nGood, nTotal);
+
+    return true;
+}
+
+/**************** PatternDeadCells I/O *************************/
+
+bool pmPatternDeadCellsRead (const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+    {
+        // read the full model in one pass: require the level to be FPA
+        if (view->chip != -1) {
+            psError(PS_ERR_IO, false, "Pattern Dead Cells must be read at the FPA level");
+            return false;
+        }
+
+        if (!pmPatternDeadCellsReadFPA (file)) {
+            psError(PS_ERR_IO, false, "Failed to read Pattern Dead Cells for fpa");
+            return false;
+        }
+        return true;
+    }
+
+// read in all chip-level Pattern Dead Cells data for this FPA
+bool pmPatternDeadCellsReadFPA (pmFPAfile *file) {
+
+    if (!pmPatternDeadCellsReadChips (file)) {
+        psError(PS_ERR_IO, false, "Failed to read Pattern Dead Cells for chips");
+        return false;
+    }
+
+    return true;
+}
+
+// Read the set of dead cell image cubes, one for each chip.  The values are saved on the
+// chip->analysis metadata of the pmFPAfile associated with the pattern file.  Later, when this
+// is used (e.g., ppImageDetrendPatternDeadCellsApply), the values are transferred to the
+// chip->analysis metadata of the pmFPAfile for the image being processed.
+bool pmPatternDeadCellsReadChips (pmFPAfile *file) {
+
+    bool haveData, status;
+
+    // loop over the extensions
+    // for each extension, use the extname (eg, XY01.ded) to assign to a chip
+
+    // move to the start of the file
+    haveData = psFitsMoveExtNum (file->fits, 1, false);
+    if (!haveData) {
+        psError(PS_ERR_IO, false, "Failed to read even the first extension?");
+        return false;
+    }
+
+    int nGood = 0;
+    while (haveData) {
+
+	// load the header
+	psMetadata *header = psFitsReadHeader(NULL, file->fits); // The FITS header
+	if (!header) psAbort("cannot read dead cell header");
+
+	// load the full model in one shot
+	psImage *deadCellData = psFitsReadImage(file->fits, psRegionSet(0,0,0,0), 0); // dead cell patterns
+	if (!deadCellData) psAbort("cannot read dead cell pattern");
+	
+	// determine the chip (not all chips have DEAD CELL patterns)
+	char *extname = psMetadataLookupStr (&status, header, "EXTNAME");
+	psLogMsg ("psModules.detrend", 8, "read dead cell pattern for extname %s\n", extname);
+
+	// I expect to find a name of the form: chipName.ded (eg, XY01.ded)
+	// where chipName like 'XY01'
+	psAssert (strlen(extname) == 8, "invalid extension %s", extname);
+	psAssert (extname[5] == 'd', "invalid extension %s", extname);
+	psAssert (extname[6] == 'e', "invalid extension %s", extname);
+	psAssert (extname[7] == 'd', "invalid extension %s", extname);
+
+	char chipName[5];
+	strncpy (chipName, extname, 4);
+	chipName[4] = 0;
+
+	pmChip *chip = pmConceptsChipFromName (file->fpa, chipName);
+	if (!chip) psAbort ("invalid chip?");
+
+	psMetadataAddImage (chip->analysis, PS_LIST_TAIL, "PTN.DEAD.CELL", PS_META_REPLACE, "", deadCellData);
+	psFree (deadCellData);
+	psFree (header);
+
+	// move to the next extension
+	haveData = psFitsMoveExtNum (file->fits, 1, true);
+	nGood ++;
+    }
+    psLogMsg ("psModules.detrend", 4, "read patterns for %d chips from Pattern Dead Cells file\n", nGood);
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmPatternIO.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmPatternIO.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmPatternIO.h	(revision 42651)
@@ -0,0 +1,46 @@
+#ifndef PM_PATTERN_IO_H
+#define PM_PATTERN_IO_H
+
+#include <pslib.h>
+
+#include <pmHDU.h>
+#include <pmFPA.h>
+
+/// Write pattern correction within a readout to a FITS file
+bool pmReadoutWritePattern(
+    pmReadout *readout,                 ///< Readout for which to write pattern correction (in analysis MD)
+    psFits *fits                        ///< FITS file to which to write
+    );
+
+
+bool pmReadoutReadPattern(
+    pmReadout *readout,                 ///< Readout for which to read pattern correction (into analysis MD)
+    psFits *fits                        ///< FITS file to which to write
+    );
+
+
+bool pmPatternRead(const pmFPAview *view, ///< View into which to read
+                   pmFPAfile *file, ///< File from which to read
+                   pmConfig *config ///< Configuration
+    );
+
+bool pmPatternWrite(const pmFPAview *view, ///< View from which to write
+                    pmFPAfile *file, ///< File to which to write
+                    pmConfig *config ///< Configuration
+    );
+
+bool pmPatternWritePHU(const pmFPAview *view, // View to PHU
+                       pmFPAfile *file, ///< File to which to write
+                       pmConfig *config ///< Configuration
+    );
+
+
+bool pmPatternRowAmpRead (const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmPatternRowAmpReadFPA (pmFPAfile *file);
+bool pmPatternRowAmpReadChips (pmFPAfile *file);
+
+bool pmPatternDeadCellsRead (const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmPatternDeadCellsReadFPA (pmFPAfile *file);
+bool pmPatternDeadCellsReadChips (pmFPAfile *file);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmRemnance.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmRemnance.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmRemnance.c	(revision 42651)
@@ -0,0 +1,104 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmFPA.h"
+#include "pmRemnance.h"
+
+#define SIZE 30                         // Size of accumulation patch
+#define THRESHOLD 20.0                   // Threshold above background
+
+bool pmRemnance(pmReadout *ro,           ///< Readout with input image
+                psImageMaskType maskVal,      ///< Value of mask
+                psImageMaskType maskRem,       ///< Value to give remance
+                int size,               ///< Size of accumulation patches
+                float threshold         ///< Threshold for masking
+    )
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PM_ASSERT_READOUT_IMAGE(ro, false);
+    PM_ASSERT_READOUT_MASK(ro, false);
+
+    psImage *image = ro->image;
+    psImage *mask = ro->mask; // Mask and image from readout
+
+    int numCols = image->numCols, numRows = image->numRows; // Size of image
+
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS); // Random number generator
+
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV); // Statistics
+    if (!psImageBackground(stats, NULL, image, mask, maskVal, rng)) {
+        // Probably means the entire readout is bad
+        psErrorClear();
+        psWarning("Unable to calculate image statistics: masking entire readout.");
+        psBinaryOp(mask, mask, "|", psScalarAlloc(maskRem, PS_TYPE_IMAGE_MASK));
+        psFree(stats);
+        psFree(rng);
+        return true;
+    }
+    psFree(rng);
+    float bgMean = stats->robustMedian; // Background level
+    float bgStdev = stats->robustStdev; // Background stdev
+
+    stats->options = PS_STAT_SAMPLE_MEDIAN;
+
+    int numMasked = 0;                  // Number of pixels masked
+    int number = ceil(numRows / (float)SIZE); // Number of steps up the columns
+    psVector *values = psVectorAlloc(numRows, PS_TYPE_F32); // Values below center
+    for (int x = 0; x < numCols; x++) {
+        int maxMask = 0;                // Maximum row to which to mask
+        int numValues = 0;              // Number of values
+        bool done = false;              // Are we done yet?
+        for (int i = 0, min = 0, max = size; i < number && !done; i++, min += size, max += size) {
+
+            if (max > numRows) {
+                max = numRows;
+            }
+            for (int y = min; y < max; y++) {
+                if (mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskVal) {
+                    continue;
+                }
+                values->data.F32[numValues++] = image->data.F32[y][x];
+            }
+            values->n = numValues;
+            if (!psVectorStats(stats, values, NULL, NULL, 0)) {
+		psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+		return false;
+	    }
+	    if (isnan(stats->sampleMedian)) {
+                maxMask = max;
+                continue;
+            }
+            float median = stats->sampleMedian;
+
+            if (median > bgMean + threshold * bgStdev / sqrtf(numValues)) {
+                maxMask = max;
+            } else {
+                done = true;
+            }
+        }
+
+        if (maxMask > 0) {
+            maxMask += size;
+            if (maxMask > numRows) {
+                maxMask = numRows;
+            }
+            for (int y = 0; y < maxMask; y++) {
+                mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= maskRem;
+            }
+            numMasked += maxMask;
+        }
+
+    }
+    psFree(values);
+    psFree(stats);
+
+
+    psMetadataAddS32(ro->analysis, PS_LIST_TAIL, PM_REMNANCE_ANALYSIS_NUM, 0,
+                     "Number of remnance pixels masked", numMasked);
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmRemnance.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmRemnance.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmRemnance.h	(revision 42651)
@@ -0,0 +1,21 @@
+#ifndef PM_REMNANCE_H
+#define PM_REMNANCE_H
+
+#include <pslib.h>
+#include "pmFPA.h"
+
+// Analysis metadata names
+#define PM_REMNANCE_ANALYSIS_NUM "DETREND.REMNANCE.NUM" // Number of masked remnance pixels
+
+// Mask remnance pixels
+//
+// By "remnance", we mean pixels left over from previous exposures.
+// GPC1 leaves remnance that flows down from where the annoyed pixels are.
+bool pmRemnance(pmReadout *ro,           ///< Readout with input image
+                psImageMaskType maskVal,      ///< Value of mask
+                psImageMaskType maskRem,       ///< Value to give remance
+                int size,               ///< Size of accumulation patches
+                float threshold         ///< Threshold for masking
+    );
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmShifts.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmShifts.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmShifts.c	(revision 42651)
@@ -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, psImageMaskType 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_branches/psModules.20240412/src/detrend/pmShifts.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmShifts.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmShifts.h	(revision 42651)
@@ -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
+                      psImageMaskType maskVal ///< Mask value to smear
+                      );
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmShutterCorrection.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmShutterCorrection.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmShutterCorrection.c	(revision 42651)
@@ -0,0 +1,1186 @@
+#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 "pmDetrendThreads.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(void)
+{
+    pmShutterCorrection *corr = (pmShutterCorrection*)psAlloc(sizeof(pmShutterCorrection));
+    psMemSetDeallocator(corr, (psFreeFunc)pmShutterCorrectionFree);
+
+    corr->scale  = 0.0;
+    corr->offset = 0.0;
+    corr->offref = 0.0;
+    corr->num = 0;
+    corr->stdev = NAN;
+    corr->valid = true;
+
+    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;
+
+    // Iterate only
+    for (index = 0; !isfinite(exptime->data.F32[index]) && index < N - 1; index++);
+
+    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)
+{
+    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, 0xff, 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;
+    }
+
+    pmShutterCorrection *corr = pmShutterCorrectionAlloc();
+    corr->offref = offref;
+    corr->scale  = line->coeff[1][0];
+    corr->offset = line->coeff[0][1] / line->coeff[1][0];
+    corr->num = stats->clippedNvalues;
+    corr->stdev = stats->clippedStdev;
+
+    psFree(stats);
+    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, 1.0); // 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];
+
+    // apply the correction and measure the residual scatter
+    psVector *resid = psVectorAlloc (exptime->n, PS_TYPE_F32);
+    for (int i = 0; i < exptime->n; i++) {
+        float fitCounts = corr->scale * (exptime->data.F32[i] + corr->offset) / (exptime->data.F32[i] + corr->offref);
+        resid->data.F32[i] = counts->data.F32[i] - fitCounts;
+    }
+
+    psStats *rawStats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    psStats *resStats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    if (!psVectorStats (rawStats, counts, NULL, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+        return NULL;
+    }
+    if (!psVectorStats (resStats, resid, NULL, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+        return NULL;
+    }
+
+    // XXX temporary hard-wired minimum stdev improvement factor
+    psTrace("psModules.detrend", 3, "raw scatter %f vs res scatter %f\n", rawStats->sampleStdev, resStats->sampleStdev);
+    if (rawStats->sampleStdev / resStats->sampleStdev < 1.5) corr->valid = false;
+    if (isnan(rawStats->sampleStdev) || isnan(resStats->sampleStdev)) corr->valid = false;
+
+    psFree (rawStats);
+    psFree (resStats);
+    psFree (resid);
+
+    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, psImageMaskType 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 *variances = NULL; // Array of variances
+    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->variance)
+        {
+            variances = psArrayAlloc(num);
+        }
+    }
+
+    // Check input sizes, generate first-pass statistics
+    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;
+            // define the reference region : a box of size 'size' at the center
+            // XXX unused psRegion refRegion = psRegionForSquare(0.5 * numCols, 0.5 * numRows, size);
+            // Set up the sample regions : boxes of size 'size' at the 4 image corners
+            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_IMAGE_MASK) {
+                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 *variance = readout->variance; // Variance map of interest
+        if (variance) {
+            if (!variances) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Not all readouts have variances.\n");
+                goto MEASURE_ERROR;
+            }
+            variances->data[i] = psMemIncrRefCounter(variance);
+
+            if (variance->type.type != PS_TYPE_F32) {
+                psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Bad type for variances: %x\n", variance->type.type);
+                goto MEASURE_ERROR;
+            }
+            if (variance->numRows != numRows || variance->numCols != numCols) {
+                psError(PS_ERR_BAD_PARAMETER_SIZE, true,
+                        "Variance sizes don't match: %dx%d vs %dx%d\n", variance->numCols, variance->numRows,
+                        numCols, numRows);
+                goto MEASURE_ERROR;
+            }
+        } else if (variances) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Not all readouts have variances.\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 variances
+    if (variances && nIter > 1) {
+        for (int i = 0; i < variances->n && nIter > 1; i++) {
+            psImage *variance = variances->data[i]; // Variance image
+            if (!variance) {
+                // We don't have variances, so no realistic errors: turn off iteration
+                if (nIter > 0) {
+                    psWarning("Not all images have variances --- 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_VECTOR_MASK); // 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.PS_TYPE_VECTOR_MASK_DATA[i] = (maskImage->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskVal);
+                }
+                psImage *variance;        // Variance image
+                if (variances && (variance = variances->data[i])) {
+                    errors->data.F32[i] = sqrtf(variance->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);
+            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(variances);
+    psFree(refs);
+    psFree(regions);
+    psFree(stats);
+    psFree(samplesMean);
+    psFree(samplesStdev);
+    return false;
+}
+
+
+bool pmShutterCorrectionApplyScan_Threaded(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    psImage *image = job->args->data[0];
+    psImage *mask  = job->args->data[1];
+    psImage *var   = job->args->data[2];
+    const psImage *shutterImage = job->args->data[3];
+
+    float exptime    = PS_SCALAR_VALUE(job->args->data[4], F32);
+    psImageMaskType blank = PS_SCALAR_VALUE(job->args->data[5], PS_TYPE_IMAGE_MASK_DATA);
+    int rowStart     = PS_SCALAR_VALUE(job->args->data[6], S32);
+    int rowStop      = PS_SCALAR_VALUE(job->args->data[7], S32);
+    return pmShutterCorrectionApplyScan(image, mask, var, shutterImage, exptime, blank, rowStart, rowStop);
+}
+
+bool pmShutterCorrectionApplyScan(psImage *image, psImage *mask, psImage *var,
+                                  const psImage *shutterImage, float exptime,
+                                  psImageMaskType blank, int rowStart, int rowStop)
+{
+    // Neglecting asserts because inputs should have been checked already
+
+    int numCols = image->numCols;       // Number of columns
+
+    for (int y = rowStart; y < rowStop; y++) {
+        for (int x = 0; x < numCols; x++) {
+            if (mask && !isfinite(shutterImage->data.F32[y][x])) {
+                mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= blank;
+                image->data.F32[y][x] = NAN;
+                if (var) {
+                    var->data.F32[y][x] = NAN;
+                }
+                continue;
+            }
+            float correction = exptime / (exptime + shutterImage->data.F32[y][x]); // Correction factor
+            image->data.F32[y][x] *= correction;
+            if (var) {
+                var->data.F32[y][x] *= PS_SQR(correction);
+            }
+        }
+    }
+    return true;
+}
+
+bool pmShutterCorrectionApply(pmReadout *readout, const pmReadout *shutter, psImageMaskType blank)
+{
+    PM_ASSERT_READOUT_NON_NULL(readout, false);
+    PM_ASSERT_READOUT_NON_NULL(shutter, false);
+    PM_ASSERT_READOUT_IMAGE(readout, false);
+    PM_ASSERT_READOUT_IMAGE(shutter, 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
+    psImage *mask = readout->mask;      // Corresponding mask
+    psImage *var = readout->variance;   // Corresponding variance map
+
+    bool threaded = true;
+    int scanRows = pmDetrendGetScanRows();
+    if (scanRows == 0) {
+        threaded = false;
+        scanRows = image->numRows;
+    }
+
+    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++) {
+                if (mask && !isfinite(shutterImage->data.F32[y][x])) {
+                    mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= blank;
+                    image->data.F32[y][x] = NAN;
+                    continue;
+                }
+                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 rowStart = 0; rowStart < image->numRows; rowStart += scanRows) {
+            int rowStop = PS_MIN (rowStart + scanRows, image->numRows);
+
+            if (threaded) {
+                // allocate a job, construct the arguments for this job
+                psThreadJob *job = psThreadJobAlloc("PSMODULES_DETREND_SHUTTER");
+                psArrayAdd(job->args, 1, image);
+                psArrayAdd(job->args, 1, mask);
+                psArrayAdd(job->args, 1, var);
+                psArrayAdd(job->args, 1, shutterImage);
+                PS_ARRAY_ADD_SCALAR(job->args, exptime, PS_TYPE_F32);
+                PS_ARRAY_ADD_SCALAR(job->args, blank, PS_TYPE_IMAGE_MASK);
+                PS_ARRAY_ADD_SCALAR(job->args, rowStart, PS_TYPE_S32);
+                PS_ARRAY_ADD_SCALAR(job->args, rowStop, PS_TYPE_S32);
+
+                if (!psThreadJobAddPending(job)) {
+                    return false;
+                }
+            } else if (!pmShutterCorrectionApplyScan(image, mask, var, shutterImage, exptime, blank,
+                                                     rowStart, rowStop)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to apply shutter correction.");
+                psFree(shutterImage);
+                return false;
+            }
+        }
+        if (threaded) {
+            // wait here for the threaded jobs to finish
+            if (!psThreadPoolWait(true, true)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to apply shutter correction.");
+                psFree(shutterImage);
+                return false;
+            }
+        }
+    }
+    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
+                                   psImageMaskType 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_IMAGE_MASK, NULL);
+        PS_ASSERT_IMAGE_SIZE(readout->mask, data->numCols, data->numRows, NULL);
+    }
+    if (readout->variance) {
+        PS_ASSERT_IMAGE_NON_NULL(readout->variance, NULL);
+        PS_ASSERT_IMAGE_TYPE(readout->variance, PS_TYPE_F32, NULL);
+        PS_ASSERT_IMAGE_SIZE(readout->variance, 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);
+    } 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 & exptime for shutter image : %f cnts %f sec\n", refValue, exptime);
+    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
+        psRegion adjusted = *region;    // Adjusted region, compensating for offsets
+        adjusted.x0 += readout->image->col0;
+        adjusted.x1 += readout->image->col0;
+        adjusted.y0 += readout->image->row0;
+        adjusted.y1 += readout->image->row0;
+        psImage *subImage = psImageSubset(readout->image, adjusted); // Sub-image
+        psImage *subMask = NULL;        // Sub-image of mask
+        if (readout->mask) {
+            subMask = psImageSubset(readout->mask, adjusted);
+        }
+        if (!psImageStats(stats, subImage, subMask, maskVal)) {
+            psString regionString = psRegionToString(adjusted);
+            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;
+
+        psTrace("psModules.detrend", 5, "input shutter image sample value %d: %f +/- %f  ->  %f +/- %f\n", j,
+                psStatsGetValue(stats, meanStat), psStatsGetValue(stats, stdevStat),
+                mean->data.F32[mean->n], stdev->data.F32[stdev->n]);
+
+        data->mean->data[j] = psVectorExtend(mean, IMAGES_BUFFER, 1);
+        data->stdev->data[j] = psVectorExtend(stdev, IMAGES_BUFFER, 1);
+    }
+
+    data->num++;
+
+    return true;
+}
+
+float pmShutterCorrectionReference(pmShutterCorrectionData *data)
+{
+    PS_ASSERT_PTR_NON_NULL(data, NAN);
+    PS_ASSERT_INT_POSITIVE(data->num, NAN);
+
+    // supply counts sorted by exptime
+
+    // generate the index for the exptimes vector
+    psVector *index = psVectorSortIndex (NULL, data->exptimes);
+    psVector *newtimes = psVectorAlloc (data->exptimes->n, PS_TYPE_F32);
+
+    // reshuffle exptimes to new sequence (this is only a local value)
+    for (int j = 0; j < newtimes->n; j++) {
+        newtimes->data.F32[j] = data->exptimes->data.F32[index->data.S32[j]];
+    }
+
+    float meanRef = 0.0;                // Mean reference offset
+    int numGood = 0;                    // Number of good measurements
+    for (int i = 0; i < MEASURE_SAMPLES; i++) {
+        psVector *newcounts = psVectorAlloc (data->exptimes->n, PS_TYPE_F32);
+        psVector *newerrors = psVectorAlloc (data->exptimes->n, PS_TYPE_F32);
+        psVector *counts = data->mean->data[i];
+        psVector *errors = data->stdev->data[i];
+
+        for (int j = 0; j < newcounts->n; j++) {
+            newcounts->data.F32[j] = counts->data.F32[index->data.S32[j]];
+            newerrors->data.F32[j] = errors->data.F32[index->data.S32[j]];
+        }
+
+        // use the sorted exptime, counts, and errors for the measurements
+        pmShutterCorrection *guess = pmShutterCorrectionGuess(newtimes, newcounts); // Guess at correction
+        psTrace("psModules.detrend", 5, "Shutter correction guess: scale: %f, offset: %f, offref: %f\n", guess->scale, guess->offset, guess->offref);
+
+        pmShutterCorrection *corr = pmShutterCorrectionFullFit(newtimes, newcounts, newerrors, guess); // The actual correction
+
+        if (corr) {
+            psTrace("psModules.detrend", 5, "Shutter correction fit: scale: %f, offset: %f, offref: %f\n", corr->scale, corr->offset, corr->offref);
+            if (isfinite(corr->offref) && corr->valid) {
+                psTrace("psModules.detrend", 5, "Sample reference value: %f\n", corr->offref);
+                meanRef += corr->offref;
+                numGood++;
+            }
+        } else {
+            psTrace("psModules.detrend", 5, "failed Shutter correction fit\n");
+        }
+
+        psFree(corr);
+        psFree(guess);
+        psFree (newcounts);
+        psFree (newerrors);
+    }
+    psFree (newtimes);
+    psFree (index);
+
+    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 pmShutterCorrectionGeneratePrepare(pmReadout *shutter, pmReadout *pattern, const psArray *inputs,
+                                        psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(shutter, false);
+    PS_ASSERT_PTR_NON_NULL(pattern, false);
+    PS_ASSERT_ARRAY_NON_NULL(inputs, false);
+
+    // determine the output image size based on the input images
+    int row0, col0, numCols, numRows;
+    if (!pmReadoutStackSetOutputSize(&col0, &row0, &numCols, &numRows, inputs)) {
+        psError(PS_ERR_UNKNOWN, false, "problem setting output readout size.");
+        return false;
+    }
+
+    // generate the required output image based on the specified sizes
+    pmReadoutStackDefineOutput(shutter, col0, row0, numCols, numRows, false, false, maskVal);
+    if (pattern) {
+        pmReadoutStackDefineOutput(pattern, col0, row0, numCols, numRows, false, false, maskVal);
+    }
+
+    psImage *nums = pmReadoutSetAnalysisImage(shutter, PM_READOUT_STACK_ANALYSIS_COUNT, numCols, numRows,
+                                              PS_TYPE_U16, 0); // Image with number fitted per pixel
+    if (!nums) {
+        return false;
+    }
+    psImage *sigma = pmReadoutSetAnalysisImage(shutter, PM_READOUT_STACK_ANALYSIS_SIGMA, numCols, numRows,
+                                               PS_TYPE_F32, NAN); // Image with stdev per pixel
+    if (!sigma) {
+        return false;
+    }
+
+    // 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;
+
+    pattern->data_exists = true;
+    if (pattern->parent) {
+        pattern->parent->data_exists = true;
+        if (pattern->parent->parent) {
+            pattern->parent->parent->data_exists = true;
+        }
+    }
+
+    return success;
+}
+
+bool pmShutterCorrectionGenerate(pmReadout *shutter, pmReadout *pattern, const psArray *inputs,
+                                 float reference, const pmShutterCorrectionData *data,
+                                 int nIter, float rej, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(shutter, false);
+    PS_ASSERT_PTR_NON_NULL(pattern, false);
+    PS_ASSERT_ARRAY_NON_NULL(inputs, false);
+    PS_ASSERT_INT_EQUAL(data->num, inputs->n, false);
+    PS_ASSERT_INT_NONNEGATIVE(nIter, false);
+    PS_ASSERT_FLOAT_LARGER_THAN(rej, 0.0, 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;
+    }
+
+    psImage *nums = pmReadoutGetAnalysisImage(shutter, PM_READOUT_STACK_ANALYSIS_COUNT);
+    if (!nums) {
+        return false;
+    }
+    psImage *sigma = pmReadoutGetAnalysisImage(shutter, PM_READOUT_STACK_ANALYSIS_SIGMA);
+    if (!sigma) {
+        return false;
+    }
+
+    psImage *shutterImage = shutter->image; // Shutter correction image
+    psImage *patternImage = pattern->image; // Illumination pattern
+
+    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_VECTOR_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_VECTOR_MASK_DATA[r] = (readout->mask->data.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn] & maskVal);
+                }
+                if (readout->variance) {
+                    errors->data.F32[r] = sqrtf(readout->variance->data.F32[yIn][xIn]) * ref;
+                } else {
+                    // XXX guess that the input data is Poisson distributed; if we go negative, force high
+                    errors->data.F32[r] = sqrtf(fabs(image->data.F32[yIn][xIn])) * ref;
+                }
+            }
+
+            pmShutterCorrection *corr = pmShutterCorrectionLinFit(data->exptimes, counts, errors, mask, reference, nIter, rej);
+            if (!corr) {
+                // Nothing we can do about it
+                psErrorClear();
+                shutterImage->data.F32[yOut][xOut] = NAN;
+                patternImage->data.F32[yOut][xOut] = NAN;
+                nums->data.U16[yOut][xOut] = 0;
+                sigma->data.F32[yOut][xOut] = NAN;
+                continue;
+            }
+            shutterImage->data.F32[yOut][xOut] = corr->offset;
+            patternImage->data.F32[yOut][xOut] = corr->scale;
+            nums->data.U16[yOut][xOut] = corr->num;
+            sigma->data.F32[yOut][xOut] = corr->stdev;
+            psFree(corr);
+        }
+    }
+    psFree(mask);
+    psFree(errors);
+    psFree(counts);
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmShutterCorrection.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmShutterCorrection.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmShutterCorrection.h	(revision 42651)
@@ -0,0 +1,211 @@
+/* @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.24 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-16 22:27:49 $
+ * 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(t) is the (variable) overall intensity of the
+ *  illuminating source and f(x,y) is the spatial illumination pattern 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)
+ *  -- a normalization for the flat-field is not specified here.  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) or f'(x,y)
+    double offset;                      ///< The time offset, dTk
+    double offref;                      ///< The reference time offset, dTo
+    int num;                            ///< Number of points used
+    float stdev;                        ///< Standard deviation
+    bool valid;                         // is the fitted shutter correction valid (produce a significant improvement?)
+} pmShutterCorrection;
+
+/// Allocator for shutter correction parameters
+pmShutterCorrection *pmShutterCorrectionAlloc(void);
+
+/// 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)
+    );
+
+/// 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)
+    psImageMaskType maskVal                  ///< Mask value
+    );
+
+/// Thread entry point for applying a shutter correction
+bool pmShutterCorrectionApplyScan_Threaded(
+    psThreadJob *job                    ///< Job to execute
+    );
+
+/// Apply the shutter correction to a scan
+bool pmShutterCorrectionApplyScan(
+    psImage *image,                     ///< Input image to correct
+    psImage *mask,                      ///< Input mask image
+    psImage *var,                       ///< Input variance image
+    const psImage *shutterImage,        ///< Shutter correction image
+    float exptime,                      ///< Exposure time to which to correct
+    psImageMaskType blank,                   ///< Mask value to give blank pixels
+    int rowStart, int rowStop           ///< Range of scan
+    );
+
+/// 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
+    psImageMaskType blank                    ///< Value to give blank pixels
+    );
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// 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
+    psImageMaskType maskVal,                 ///< Mask value
+    psRandom *rng                       ///< Random number generator
+    );
+
+/// Calculate the reference shutter time from the correction data
+float pmShutterCorrectionReference(
+    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)
+    psImageMaskType maskVal                  ///< Mask value
+    );
+
+// prepare outputs for shutter correction
+bool pmShutterCorrectionGeneratePrepare(pmReadout *shutter, pmReadout *pattern, const psArray *inputs,
+                                        psImageMaskType maskVal);
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/detrend/pmSkySubtract.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmSkySubtract.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmSkySubtract.c	(revision 42651)
@@ -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.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-01-27 06:39:38 $
+ *
+ *  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_VECTOR_MASK);
+    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.PS_TYPE_VECTOR_MASK_DATA[count] = 0;
+                    } else {
+                        binVector->data.F32[count] = 0.0;
+                        binMask->data.PS_TYPE_VECTOR_MASK_DATA[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_IMAGE_MASK, 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.PS_TYPE_IMAGE_MASK_DATA[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 = psMatrixLUDecomposition(Aout, &outPerm, A);
+    PS_ASSERT_IMAGE_NON_NULL(Aout, NULL);
+    PS_ASSERT_IMAGE_NON_EMPTY(Aout, NULL);
+    psVector *C = psVectorAlloc(localPolyTerms, PS_TYPE_F64);
+    psMatrixLUSolution(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_IMAGE_MASK);
+            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_IMAGE_MASK);
+            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_IMAGE_MASK);
+        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.PS_TYPE_IMAGE_MASK_DATA[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_branches/psModules.20240412/src/detrend/pmSkySubtract.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/detrend/pmSkySubtract.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/detrend/pmSkySubtract.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/extras/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/Makefile.am	(revision 42651)
@@ -0,0 +1,28 @@
+noinst_LTLIBRARIES = libpsmodulesextras.la
+
+libpsmodulesextras_la_CPPFLAGS = $(SRCINC) $(PSMODULES_CFLAGS) -I../pslib/
+libpsmodulesextras_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+libpsmodulesextras_la_SOURCES  = \
+	psVectorBracket.c \
+	psPipe.c \
+	psIOBuffer.c \
+	pmKapaPlots.c \
+	pmVisual.c \
+	pmVisualUtils.c \
+	pmThreadTools.c \
+	ippStages.c \
+        pmCensor.c
+
+pkginclude_HEADERS = \
+	psVectorBracket.h \
+	psPipe.h \
+	psIOBuffer.h \
+	pmKapaPlots.h \
+	pmVisual.h \
+	pmVisualUtils.h \
+	pmThreadTools.h \
+	ippDiffMode.h \
+	ippStages.h \
+        pmCensor.h
+
+CLEANFILES = *~
Index: /branches/eam_branches/psModules.20240412/src/extras/ippDiffMode.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/ippDiffMode.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/ippDiffMode.h	(revision 42651)
@@ -0,0 +1,20 @@
+/* @file ippDiffMode.h
+ * @brief some macro defintions for the stages of the pipeline
+ * @author Bill Sweeney, IfA
+ *
+ * Copyright 2010 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef IPP_DIFF_MODE_H
+#define IPP_DIFF_MODE_H
+
+typedef enum {
+    IPP_DIFF_MODE_UNDEFINED   = 0,
+    IPP_DIFF_MODE_WARP_WARP   = 1,
+    IPP_DIFF_MODE_WARP_STACK  = 2,
+    IPP_DIFF_MODE_STACK_WARP  = 3,
+    IPP_DIFF_MODE_STACK_STACK = 4
+} ippDiffMode;
+
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/extras/ippStages.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/ippStages.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/ippStages.c	(revision 42651)
@@ -0,0 +1,35 @@
+/** Functions that describe the various stages of the IPP
+ *  @author Bill Sweeney, IfA
+ **/
+
+#include <pslib.h>
+#include "ippStages.h"
+#include <string.h>
+
+ippStage ippStringToStage(const psString stageString)
+{
+    PS_ASSERT_PTR_NON_NULL(stageString, IPP_STAGE_NONE);
+
+    if (!strcmp(stageString, "raw")) {
+        return IPP_STAGE_RAW;
+    } else if (!strcmp(stageString, "chip")) {
+        return IPP_STAGE_CHIP;
+    } else if (!strcmp(stageString, "chip_bg")) {
+        return IPP_STAGE_CHIP_BG;
+    } else if (!strcmp(stageString, "camera")) {
+        return IPP_STAGE_CAMERA;
+    } else if (!strcmp(stageString, "warp")) {
+        return IPP_STAGE_WARP;
+    } else if (!strcmp(stageString, "warp_bg")) {
+        return IPP_STAGE_WARP_BG;
+    } else if (!strcmp(stageString, "fake")) {
+        return IPP_STAGE_FAKE;
+    } else if (!strcmp(stageString, "diff")) {
+        return IPP_STAGE_DIFF;
+    } else if (!strcmp(stageString, "stack")) {
+        return IPP_STAGE_STACK;
+    } else {
+        psError(PS_ERR_PROGRAMMING, true, "%s is not a valid IPP stage", stageString);
+        return IPP_STAGE_NONE;
+    }
+}
Index: /branches/eam_branches/psModules.20240412/src/extras/ippStages.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/ippStages.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/ippStages.h	(revision 42651)
@@ -0,0 +1,29 @@
+/* @file ippStages.h
+ * @brief some macro defintions for the stages of the pipeline
+ * @author Bill Sweeney, IfA
+ *
+ * Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef IPP_STAGES_H
+#define IPP_STAGES_H
+
+typedef enum {
+    IPP_STAGE_NONE = -1,
+    IPP_STAGE_RAW = 0,
+    IPP_STAGE_CHIP,
+    IPP_STAGE_CHIP_BG,
+    IPP_STAGE_CAMERA,
+    IPP_STAGE_FAKE,
+    IPP_STAGE_WARP,
+    IPP_STAGE_WARP_BG,
+    IPP_STAGE_DIFF,
+    IPP_STAGE_STACK,
+} ippStage;
+
+/** return the ippStage represented by a string
+ * @return the corresponding value or IPP_STAGE_NONE if invalid
+ */
+ippStage ippStringToStage(const psString stageString);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/extras/pmCensor.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/pmCensor.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/pmCensor.c	(revision 42651)
@@ -0,0 +1,115 @@
+#include <pslib.h>
+#include <pmCensor.h>
+
+bool
+pmCensorGetMasks(pmConfig *config, psU32 *pMaskCensor, psU32 *pMaskStreak)
+{
+    bool status;
+    psMetadata *masks = psMetadataLookupMetadata(&status, config->recipes, "MASKS");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "failed to lookup MASKS in recipes.\n");
+        return false;
+    }
+
+
+    // Older sets of mask values had SPIKE and STARCORE with the same value as STREAK
+    // handle this case by making sure the streak bit is set
+    psU32 streak = psMetadataLookupU32(&status, masks, "STREAK");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "failed to lookup mask value for STREAK in recipes.\n");
+        return false;
+    }
+    *pMaskStreak = streak;
+
+    // first look for the value in the recipes
+    psU32 maskNoCensor = psMetadataLookupU32(&status, config->camera, "MASK.NO.CENSOR");
+    if (status) {
+        // All done
+        *pMaskCensor = ~maskNoCensor;
+        return true;
+    }
+
+    // Not defined. So we must compute it.
+
+    // preserve pixels that are only CONV.POOR
+    // check old name first
+    psU32 convPoor = psMetadataLookupU32(&status, masks, "POOR.WARP");
+    if (!status) {
+        convPoor = psMetadataLookupU32(&status, masks, "CONV.POOR");
+        if (!status) {
+            psError(PS_ERR_UNKNOWN, false, "failed to lookup either CONV.POOR and POOR.WARP in mask recipe.\n");
+            return false;
+        }
+    }
+    // preserve pixels that are only suspect
+    psU32 suspect = psMetadataLookupU32(&status, masks, "SUSPECT");
+    if (!status) {
+        psWarning("failed to lookup SUSPECT in mask recipe.\n");
+        suspect = 0;
+    }
+    // preserve pixels that are in a diffraction spike
+    psU32 spike = psMetadataLookupU32(&status, masks, "SPIKE");
+    if (!status) {
+        psWarning("failed to lookup SPIKE in mask recipe.\n");
+        spike = 0;
+    }
+    // preserve pixels that are in core of a bright star
+    psU32 starcore = psMetadataLookupU32(&status, masks, "STARCORE");
+    if (!status) {
+        psWarning("failed to lookup STARCORE in mask recipe.\n");
+        starcore = 0;
+    }
+
+    // The value we return is the set of bits to censor. 
+    // Including STREAK handles the case where any of these other bits have the same value as STREAK.
+    // That was true in older 8 bit mask configurations.
+
+    *pMaskCensor = ~(convPoor | suspect | starcore | spike) | streak;
+
+    return true;
+}
+
+bool pmCensorMasked(pmConfig *config, psImage *image, psImage *mask, psImage *variance, long *pNumCensored)
+{
+    *pNumCensored = 0;
+
+    psU32 censorMask;
+    psU32 streakMask;
+    if (!pmCensorGetMasks(config, &censorMask, &streakMask)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to lookup set of mask bits to censor.\n");
+        return false;
+    }
+
+    if (~censorMask == 0) {
+        // censoring masked pixels is not required
+        return true;
+    }
+
+    double exciseValue;
+    if (image->type.type == PS_TYPE_U16) {
+        exciseValue = 0x7fff;
+    } else if (image->type.type == PS_TYPE_U16) {
+        exciseValue = 0xffff;
+    } else if (image->type.type == PS_TYPE_F32) {
+        exciseValue = NAN;
+    } else {
+        // XXX: sooner or later we'll get passed another image type
+        psError(PS_ERR_PROGRAMMING, true, "unexpected image type: %d\n", image->type.type);
+        return false;
+    }
+
+    for (int y=0; y<image->numRows; y++) {
+        for (int x=0; x<image->numCols; x++) {
+            psU16 maskVal = psImageGet(mask, x, y);
+            if (maskVal & censorMask) {
+                (*pNumCensored)++;
+                psImageSet(image, x, y, exciseValue);
+                if (variance) {
+                    psImageSet(variance, x, y, exciseValue);
+                }
+            }
+        }
+    }
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/extras/pmCensor.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/pmCensor.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/pmCensor.h	(revision 42651)
@@ -0,0 +1,9 @@
+#ifndef _PM_CENSOR_H
+#define _PM_CENSOR_H
+
+#include <pmConfig.h>
+
+bool pmCensorGetMasks(pmConfig *config, psU32 *pMaskCensor, psU32 *pMaskStreak);
+bool pmCensorMasked(pmConfig *config, psImage *image, psImage *mask, psImage *variance, long *pNumCensored);
+
+#endif // _PM_CENSOR_H
Index: /branches/eam_branches/psModules.20240412/src/extras/pmKapaPlots.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/pmKapaPlots.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/pmKapaPlots.c	(revision 42651)
@@ -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 (void)
+{
+
+    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 = KAPA_POINT_BOX_SOLID;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+    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_branches/psModules.20240412/src/extras/pmKapaPlots.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/pmKapaPlots.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/pmKapaPlots.h	(revision 42651)
@@ -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(void);
+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_branches/psModules.20240412/src/extras/pmThreadTools.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/pmThreadTools.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/pmThreadTools.c	(revision 42651)
@@ -0,0 +1,153 @@
+/** These utility functions help threading source analysis on readouts
+ *  @author Eugene Magnier, IfA
+ *  @date June 05, 2013
+ */
+
+/* Include Files  */
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAMaskWeight.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourcePhotometry.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+
+// the strategy here is to divide the image into 2x2 blocks of cells and cycle through
+// the four discontiguous sets of cells, threading all within a set and blocking between
+// sets 
+
+// we divide the image region into 2*2 blocks of size Nx*Ny, the image will have 
+// Cx*Cy blocks so that (2Nx)Cx = numCols, (2Ny)Cy = numRows.  We want to choose Cx and
+// Cy so that (2Nx)Cx * (2Ny)Cy = 4 * NFILL * nThreads -- each of the four sets of cells
+// has enough cells to allow NFILL cells for each thread (to better distribute heavy and
+// light load cells
+    
+// the array runs from readout->image->col0 to readout->image->col0 + readout->image->numCols 
+
+// we save these for threaded analysis runs
+static int Xo = 0;
+static int Yo = 0;
+static int Nx = 1;
+static int Ny = 1;
+
+bool pmReadoutChooseCellSizes (int *Cx, int *Cy, pmReadout *readout, int nThreads) {
+
+    int nCells = nThreads * 2*2; // number of cells in a single set
+    int C = sqrt(nCells) + 0.5;
+    
+    // we need to assign Cx and Cy based on the dimensionality of the image
+    // crude way to find most evenly balanced factors of nCells:
+    for (int i = C; i >= 1; i--) {
+	int C1 = nCells / C;
+	int C2 = nCells / C1;
+	if (C1*C2 != nCells) continue;
+
+	if (readout->image->numRows > readout->image->numCols) {
+	    *Cx = PS_MAX (C1, C2);
+	    *Cy = PS_MIN (C1, C2);
+	} else {
+	    *Cx = PS_MAX (C1, C2);
+	    *Cy = PS_MIN (C1, C2);
+	}
+
+	Xo = readout->image->col0;
+	Yo = readout->image->row0;
+	Nx = readout->image->numCols / (*Cx*2);
+	Ny = readout->image->numRows / (*Cy*2);
+
+	return true;
+    }
+    *Cx = 1;
+    *Cy = 1; 
+
+    Xo = readout->image->col0;
+    Yo = readout->image->row0;
+    Nx = readout->image->numCols / (*Cx*2);
+    Ny = readout->image->numRows / (*Cy*2);
+
+    return true;
+}
+
+bool pmReadoutCoordToCell (int *group, int *cell, float x, float y, int Cx, int Cy) {
+  
+    // XXX need to handle edges
+    int ix = (x - Xo)/(2*Nx);
+    ix = PS_MAX (0, PS_MIN (ix, Cx - 1));
+
+    int iy = (y - Yo)/(2*Ny);
+    iy = PS_MAX (0, PS_MIN (iy, Cy - 1));
+
+    int jx = (((int)(x - Xo))%(2*Nx))/Nx;
+    jx = PS_MAX (0, PS_MIN (jx, Nx - 1));
+
+    int jy = (((int)(y - Yo))%(2*Ny))/Ny;
+    jy = PS_MAX (0, PS_MIN (jy, Ny - 1));
+
+    *group = jx + 2*jy;
+    *cell  = ix + Cx*iy;
+
+    return true;
+}
+
+// we have 2x2 * Cx*Cy cells for the image.  we need a function to convert an x,y
+// coordinate pair into the index for these cells
+
+// first, how shall we number them?  is there one index for all cells, or one set of
+// indices for each of the 2x2 cell groups?  
+
+psArray *pmReadoutAssignSourcesToCells (int Cx, int Cy, psArray *sources) {
+
+    psArray *cellGroups = psArrayAlloc (4);
+    for (int i = 0; i < cellGroups->n; i++) {
+	psArray *cells = psArrayAlloc (Cx*Cy);
+	cellGroups->data[i] = cells;
+	for (int j = 0; j < cells->n; j++) {
+	    psArray *cellSources = psArrayAllocEmpty (50);
+	    cells->data[j] = cellSources;
+	}
+    }
+
+    for (int i = 0; i < sources->n; i++) {
+    
+	int group = 0;
+	int cell = 0;
+
+	pmSource *source = sources->data[i];
+
+	pmReadoutCoordToCell (&group, &cell, source->peak->xf, source->peak->yf, Cx, Cy);
+       
+	psArray *cells = cellGroups->data[group];
+	psArray *cellSources = cells->data[cell];
+	
+	psArrayAdd (cellSources, 100, source);
+    }
+	
+    return cellGroups;
+}
Index: /branches/eam_branches/psModules.20240412/src/extras/pmThreadTools.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/pmThreadTools.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/pmThreadTools.h	(revision 42651)
@@ -0,0 +1,14 @@
+/* @file pmVisual.h
+ * @brief functions to create visual diagnostics with the help of 'kapa'
+ * @author Chris Beaumont, IfA
+ *
+ * Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_THREAD_TOOLS_H
+#define PM_THREAD_TOOLS_H
+
+psArray *pmReadoutAssignSourcesToCells (int Cx, int Cy, psArray *sources);
+bool     pmReadoutChooseCellSizes (int *Cx, int *Cy, pmReadout *readout, int nThreads);
+
+#endif //ndef PM_THREAD_TOOLS_H
Index: /branches/eam_branches/psModules.20240412/src/extras/pmVisual.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/pmVisual.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/pmVisual.c	(revision 42651)
@@ -0,0 +1,507 @@
+/** The following are a collection of core procedures to aid the creation of visual diagnostics
+ *  @author Chris Beaumont, IfA
+ *  @date January 23, 2008
+ **/
+
+/* Include Files  */
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <pslib.h>
+
+bool pmSubtractionVisualClose(void);
+bool pmAstromVisualClose(void);
+bool pmSubtractionVisualClose(void);
+bool pmStackVisualClose(void);
+bool pmSourceVisualClose(void);
+
+#include "pmSourceSatstar.h"
+
+# if (HAVE_KAPA)
+# include <kapa.h>
+#include "pmVisual.h"
+#include "pmKapaPlots.h"
+
+# define KAPAX 700
+# define KAPAY 700
+
+//#define TESTING
+
+static bool isVisual = false;
+
+
+bool pmVisualSetVisual(bool value) {
+    isVisual = value;
+    return true;
+}
+
+
+bool pmVisualIsVisual(void) {return isVisual;}
+
+bool pmVisualClose(void) {
+    pmAstromVisualClose();
+    pmSubtractionVisualClose();
+    pmStackVisualClose();
+    pmSourceVisualClose();
+    return true;
+}
+
+bool pmVisualInitWindow (int *kapid, char *name) {
+
+    if (*kapid == -1) {
+        *kapid = KapaOpenNamedSocket("kapa", name);
+        if (*kapid == -1) {
+            fprintf (stderr, "Failure to open kapa; visual mode disabled.\n");
+	    pmVisualSetVisual(false);
+            return false;
+        }
+        KapaResize (*kapid, KAPAX, KAPAY);
+    }
+    return true;
+}
+
+
+bool pmVisualInitGraph (int kapa, KapaSection *section, Graphdata *graphdata)
+{
+    KapaSetSection (kapa, section);
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaSetLimits (kapa, graphdata);
+    KapaBox (kapa, graphdata);
+    return true;
+}
+
+
+// ask the user to continue or not.  give up after 2 seconds.
+// XXX add option to turn on/off timeouts?
+bool pmVisualAskUser(bool *plotFlag)
+{
+    struct timeval timeout;
+    fd_set fdSet;
+    int status;
+
+    char key[10];
+    if (plotFlag) {
+	fprintf (stderr, "[p]ause? [c]ontinue? [s]kip the rest of these plots? [a]bort all visual plots? (c) ");
+    } else {
+	fprintf (stderr, "[p]ause? [c]ontinue? [a]bort all visual plots? (c) ");
+    }
+
+    /* Wait up to 1.0 second for a response, then continue */
+    timeout.tv_sec = 10;
+    timeout.tv_usec = 0;
+
+    FD_ZERO (&fdSet);
+    FD_SET (STDIN_FILENO, &fdSet);
+
+    status = select (1, &fdSet, NULL, NULL, &timeout);
+    if (status <= 0) {
+	fprintf (stderr, "\n");
+	return true; // if no data, give up
+    }
+
+    while (true) {
+	if (!fgets(key, 8, stdin)) {
+	    psWarning("Unable to read option");
+	}
+	switch (key[0]) {
+	  case 's':
+	    if (plotFlag) *plotFlag = false;
+	    return true;
+	  case 'a':
+	    isVisual = false;
+	    return true;
+	  case 'c':
+	  case '\n':
+	    return true;
+	  default:
+	    break;
+	}
+	
+	if (plotFlag) {
+	    fprintf (stderr, "[c]ontinue? [s]kip the rest of these plots? [a]bort all visual plots? (c) ");
+	} else {
+	    fprintf (stderr, "[c]ontinue? [a]bort all visual plots? (c) ");
+	}
+    }
+    return true;
+}
+
+
+// ask the user to continue or not.  give up after 2 seconds.
+bool pmVisualAskUserOrDump(bool *plotFlag, bool *dumpData)
+{
+    struct timeval timeout;
+    fd_set fdSet;
+    int status;
+
+    if (dumpData) *dumpData = false;
+
+    char key[10];
+    if (plotFlag && dumpData) {
+	fprintf (stderr, "[p]ause? [c]ontinue? [s]kip the rest of these plots? [d]ump the data? [a]bort all visual plots? (c) ");
+    } 
+    if (plotFlag && !dumpData) {
+	fprintf (stderr, "[p]ause? [c]ontinue? [s]kip the rest of these plots? [a]bort all visual plots? (c) ");
+    } 
+    if (!plotFlag && dumpData) {
+	fprintf (stderr, "[p]ause? [c]ontinue? [d]ump the data? [a]bort all visual plots? (c) ");
+    } 
+    if (!plotFlag && !dumpData) {
+	fprintf (stderr, "[p]ause? [c]ontinue? [a]bort all visual plots? (c) ");
+    }
+
+    /* Wait up to 1.0 second for a response, then continue */
+    timeout.tv_sec = 10;
+    timeout.tv_usec = 0;
+
+    FD_ZERO (&fdSet);
+    FD_SET (STDIN_FILENO, &fdSet);
+
+    status = select (1, &fdSet, NULL, NULL, &timeout);
+    if (status <= 0) {
+	fprintf (stderr, "\n");
+	return true; // if no data, give up
+    }
+
+    while (true) {
+	if (!fgets(key, 8, stdin)) {
+	    psWarning("Unable to read option");
+	}
+	switch (key[0]) {
+	  case 's':
+	    if (plotFlag) *plotFlag = false;
+	    return true;
+	  case 'd':
+	    if (dumpData) *dumpData = true;
+	    return true;
+	  case 'a':
+	    isVisual = false;
+	    return true;
+	  case 'c':
+	  case '\n':
+	    return true;
+	  default:
+	    break;
+	}
+	
+	if (plotFlag && dumpData) {
+	  fprintf (stderr, "[p]ause? [c]ontinue? [s]kip the rest of these plots? [d]ump the data? [a]bort all visual plots? (c) ");
+	} 
+	if (plotFlag && !dumpData) {
+	  fprintf (stderr, "[p]ause? [c]ontinue? [s]kip the rest of these plots? [a]bort all visual plots? (c) ");
+	} 
+	if (!plotFlag && dumpData) {
+	  fprintf (stderr, "[p]ause? [c]ontinue? [d]ump the data? [a]bort all visual plots? (c) ");
+	} 
+	if (!plotFlag && !dumpData) {
+	  fprintf (stderr, "[p]ause? [c]ontinue? [a]bort all visual plots? (c) ");
+	}
+    }
+    return true;
+}
+
+bool pmVisualImStats(psImage *image, double *mean, double *stdev, double *min, double *max) {
+
+    *min = +FLT_MAX;
+    *max = -FLT_MAX;
+    double ex = 0;  // <x>
+    double ex2 = 0; // <x^2>
+    int numPix = 0; // number of finite pixels
+    bool isDouble = image->type.type == PS_TYPE_F64;
+
+    if(!isDouble && (image->type.type != PS_TYPE_F32)) {
+        fprintf(stderr, "Image must be PS_TYPE_F32 or PS_TYPE_F64\n");
+        return false;
+    }
+
+    for(int i = 0; i < image->numRows; i++) {
+        for(int j = 0; j < image->numCols; j++) {
+            double entry;
+            if(isDouble) {
+                if (!isfinite(image->data.F64[i][j])) continue;
+                entry = image->data.F64[i][j];
+            } else {
+                if (!isfinite(image->data.F32[i][j])) continue;
+                entry = image->data.F32[i][j];
+            }
+            numPix++;
+            ex += entry;
+            ex2 += (entry * entry);
+            *min = PS_MIN(*min, entry);
+            *max = PS_MAX(*max, entry);
+        }
+    }
+
+    if (numPix == 0) return false;
+    ex /= numPix;
+    ex2 /= numPix;
+    *mean = ex;
+    *stdev = sqrt(ex2  - ex * ex);
+
+    return true;
+}
+
+
+bool pmVisualTriplePlot (int kapid, Graphdata *graphdata, psVector *xVec, psVector *yVec, psVector *zVec, bool increasing)
+{
+    pmVisualScaleGraphdata (graphdata, xVec, yVec, true);
+    //printf("%f %f %f %f \n",graphdata->xmin, graphdata->xmax, graphdata->ymin, graphdata->ymax);
+    // set the scale vector
+    psVector *zScale = psVectorAlloc (zVec->n, PS_DATA_F32);
+    pmVisualCreateScaleVec (zVec, zScale, increasing);
+
+    KapaSetFont (kapid, "helvetica", 14);
+    KapaSetLimits(kapid, graphdata);
+    KapaBox (kapid, graphdata);
+
+    // the point size will be scaled from the z vector
+    graphdata->ptype = KAPA_POINT_CIRCLE_OPEN;
+    graphdata->style = KAPA_PLOT_POINTS;
+    graphdata->size = -1;
+    KapaPrepPlot (kapid, xVec->n, graphdata);
+    KapaPlotVector (kapid, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapid, yVec->n, yVec->data.F32, "y");
+    KapaPlotVector (kapid, zVec->n, zScale->data.F32, "z");
+    psFree (zScale);
+    return true;
+}
+
+
+bool pmVisualTripleOverplot (int kapid, Graphdata *graphdata, psVector *xVec, psVector *yVec, psVector *zVec, bool increasing) {
+
+    // set the scale vector
+    psVector *zScale = psVectorAlloc (zVec->n, PS_DATA_F32);
+    pmVisualCreateScaleVec (zVec, zScale, increasing);
+
+    KapaSetFont (kapid, "helvetica", 14);
+
+    // the point size will be scaled from the z vector
+    graphdata->size = -1;
+    KapaPrepPlot (kapid, xVec->n, graphdata);
+    KapaPlotVector (kapid, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapid, yVec->n, yVec->data.F32, "y");
+    KapaPlotVector (kapid, zVec->n, zScale->data.F32, "z");
+    psFree (zScale);
+    return true;
+}
+
+
+bool pmVisualCreateScaleVec (psVector *zVec, psVector *zScale, bool increasing) {
+    // set limits based on data values
+    float zmin = +FLT_MAX;
+    float zmax = -FLT_MAX;
+
+    for (int i = 0; i < zVec->n; i++) {
+        zmin = PS_MIN (zmin, zVec->data.F32[i]);
+        zmax = PS_MAX (zmax, zVec->data.F32[i]);
+    }
+
+    if (increasing) {
+	fprintf (stderr, "plotting points scaled from %f to %f\n", zmin, zmax);
+    } else {
+	fprintf (stderr, "plotting points scaled from %f to %f\n", zmax, zmin);
+    }
+
+    float range = zmax - zmin;
+    if (range == 0.0) {
+        psVectorInit (zScale, 1.0);
+    } else {
+        for (int i = 0; i < zVec->n; i++) {
+            if (increasing) {
+                zScale->data.F32[i] = PS_MIN (1.5, PS_MAX(0.05, 1.5*(zVec->data.F32[i] - zmin)/range));
+            } else {
+                zScale->data.F32[i] = PS_MIN (1.5, PS_MAX(0.05, 1.5*(zmax - zVec->data.F32[i])/range));
+            }
+        }
+    }
+    return true;
+}
+
+
+bool pmVisualScaleImage(int kapaFD, psImage *inImage, const char *name, int channel, bool clip) {
+    KiiImage image;
+    KapaImageData data;
+    Coords coords;
+
+    //make sure we have a compatible image
+    if (inImage == NULL) {
+        fprintf(stderr, "Image is NULL, and cannot be displayed\n");
+        return false;
+    }
+
+    if(inImage->type.type != PS_TYPE_F32) {
+        fprintf(stderr, "Cannot display this image (imcompatible data type)\n");
+        return false;
+    }
+
+    strcpy (coords.ctype, "RA---TAN");
+
+
+    double min, max, stdev, mean;
+    if(!pmVisualImStats(inImage, &mean, &stdev, &min, &max)) return false;
+
+    image.data2d = inImage->data.F32;
+    image.Nx = inImage->numCols;
+    image.Ny = inImage->numRows;
+    strcpy (data.name, name);
+    strcpy (data.file, name);
+
+    data.zero = clip ? mean - 3 * stdev : min;
+    data.range = clip ? 6 * stdev : max - min;
+    data.logflux = 0;
+
+    KiiSetChannel (kapaFD, channel);
+    KiiNewPicture2D (kapaFD, &image, &data, &coords);
+
+    return true;
+}
+
+bool pmVisualRangeImage (int kapaFD, psImage *inImage, const char *name, int channel, float min, float max) {
+
+    KiiImage image;
+    KapaImageData data;
+    Coords coords;
+
+    strcpy (coords.ctype, "RA---TAN");
+
+    image.data2d = inImage->data.F32;
+    image.Nx = inImage->numCols;
+    image.Ny = inImage->numRows;
+
+    strcpy (data.name, name);
+    strcpy (data.file, name);
+    data.zero = min;
+    data.range = max - min;
+    data.logflux = 0;
+
+    KiiSetChannel (kapaFD, channel);
+    KiiNewPicture2D (kapaFD, &image, &data, &coords);
+
+    return true;
+}
+
+psImage* pmVisualImageToFloat(psImage *image) {
+    psImage *result = psImageAlloc(image->numCols, image->numRows, PS_TYPE_F32);
+
+    if (image->type.type == PS_TYPE_F32) {
+        psImageOverlaySection(result, image, 0, 0, "=");
+        return image;
+    } else if (image->type.type == PS_TYPE_F64) {
+        for (int i = 0; i < image->numRows; i++) {
+            for (int j = 0; j < image->numCols; j++) {
+                result->data.F32[i][j] = (float) image->data.F64[i][j];
+            }
+        }
+    } else {
+        fprintf(stderr, "Unsupported psImage data type (F32 or F64)");
+        return NULL;
+    }
+    return result;
+}
+
+
+bool pmVisualScaleGraphdata(Graphdata *graphdata, psVector *xVec, psVector *yVec, bool clip) {
+
+    graphdata->xmin = +FLT_MAX;
+    graphdata->xmax = -FLT_MAX;
+    graphdata->ymin = +FLT_MAX;
+    graphdata->ymax = -FLT_MAX;
+
+    //determine standard deviation of xVec and yVec
+    psStats *statsX = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    psStats *statsY = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    if (!psVectorStats (statsX, xVec, NULL, NULL, 0)) {
+	psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	return false;
+    }
+    if (!psVectorStats (statsY, yVec, NULL, NULL, 0)) {
+	psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	return false;
+    }
+
+    float xhi  = statsX->sampleMedian + 3 *statsX->sampleStdev;
+    float xlo = statsX->sampleMedian - 3 *statsX->sampleStdev;
+    float yhi = statsY->sampleMedian + 3 *statsY->sampleStdev;
+    float ylo = statsY->sampleMedian - 3 *statsY->sampleStdev;
+
+    // don't sigma clip
+    if (!clip) {
+        xhi = +FLT_MAX;
+        xlo = -FLT_MAX;
+        yhi = +FLT_MAX;
+        ylo = -FLT_MAX;
+    }
+
+    for(int i = 0; i < xVec->n; i++) {
+        if (!isfinite(xVec->data.F32[i])) continue;
+        if (xVec->data.F32[i] > xhi || xVec->data.F32[i] < xlo) continue;
+        graphdata->xmin = PS_MIN (graphdata->xmin, xVec->data.F32[i]);
+        graphdata->xmax = PS_MAX (graphdata->xmax, xVec->data.F32[i]);
+    }
+
+    for (int i = 0; i < yVec->n; i++) {
+        if (!isfinite(xVec->data.F32[i])) continue;
+        if (yVec->data.F32[i] > yhi || yVec->data.F32[i] < ylo) continue;
+        graphdata->ymin = PS_MIN (graphdata->ymin, yVec->data.F32[i]);
+        graphdata->ymax = PS_MAX (graphdata->ymax, yVec->data.F32[i]);
+    }
+
+
+    // abort if there is no good data
+    if (!isfinite(xhi) || !isfinite(xlo) || !isfinite(yhi) || !isfinite(ylo)) {
+        graphdata->xmin = -1;
+        graphdata->ymin  = -1;
+        graphdata->xmax = 1;
+        graphdata->ymax = 1;
+        psFree(statsX);
+        psFree(statsY);
+        return false;
+    }
+
+    // add a whitespace border
+    float range = graphdata->xmax - graphdata->xmin;
+    if (range == 0) range = 1;
+    graphdata->xmin -= .05 * range;
+    graphdata->xmax += .05 * range;
+
+    range = graphdata->ymax - graphdata->ymin;
+    if (range == 0) range = 1;
+    graphdata->ymin -= .05 * range;
+    graphdata->ymax += .05 * range;
+
+    psFree (statsX);
+    psFree (statsY);
+    return true;
+}
+
+#else
+bool pmVisualSetVisual(bool value) {return true;}
+bool pmVisualIsVisual(void) {return false;}
+bool pmVisualClose(void) {return true;}
+bool pmVisualInitWindow(int *kapid, char *name){return true;}
+bool pmVisualInitGraph (int kapa, void *section, void *graphdata){return true;}
+
+bool pmVisualAskUser(bool *plotFlag){return true;}
+bool pmVisualImStats(psImage *image, double *mean,
+                     double *stdev, double *min, double *max){return true;}
+bool pmVisualTriplePlot (int kapid, void *graphdata, psVector *xVec,
+                         psVector *yVec, psVector *zVec, bool increasing){return true;}
+bool pmVisualTripleOverplot (int kapid, void *graphdata, psVector *xVec,
+                             psVector *yVec, psVector *zVec, bool increasing){return true;}
+bool pmVisualCreateScaleVec (psVector *zVec, psVector *zScale, bool increasing){return true;}
+bool pmVisualResidPlot (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe, char *title, int *kapa, int *kapa2){return true;}
+bool pmVisualScaleImage(int kapaFD, psImage *inImage,
+                        const char *name, int channel, bool clip){return true;}
+bool pmVisualScaleGraphdata(void *graphdata, psVector *xVec,
+                            psVector *yVec, bool clip){return true;}
+
+psImage* pmVisualImageToFloat(psImage *image){return NULL;}
+
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/extras/pmVisual.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/pmVisual.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/pmVisual.h	(revision 42651)
@@ -0,0 +1,166 @@
+/* @file pmVisual.h
+ * @brief functions to create visual diagnostics with the help of 'kapa'
+ * @author Chris Beaumont, IfA
+ *
+ * Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_VISUAL_H
+#define PM_VISUAL_H
+
+#if (HAVE_KAPA)
+
+
+/** Globally enable or disable plotting
+ * @param value - true to enable plotting
+ * @return true
+ */
+bool pmVisualSetVisual(bool value);
+
+
+/** Check whether plotting is enabled
+ * @retrun true if plots should be generated
+ */
+bool pmVisualIsVisual(void);
+
+
+/** Destroy plotting windows at the end of a run
+ * @return true for success */
+bool pmVisualClose(void);
+
+
+/** Open, name, and resize a window for plotting.
+ * @param kapid an identifier for this window. Initialize to -1. Its value will be updated.
+ * @param name What to name the window. Seems not to like spaces.
+ * @return true for successful completion
+*/
+bool pmVisualInitWindow (int *kapid, char *name);
+
+
+/** Initialize a graph, set the appropriate section and font.
+ * @param kapa the index of the kapa window to plot to
+ * @param section the section to use
+ * @graphdata to use
+ * @return true for successful completion
+ */
+bool pmVisualInitGraph (int kapa, KapaSection *section, Graphdata *graphdata);
+
+
+/** Ask the user how to proceed.
+ * At the user's request, this will disable diagnostic plotting.
+ * @param plotFlag, set to false if this plot should be disabled in the future
+ */
+bool pmVisualAskUser(bool *plotFlag);
+
+
+/** Ask the user how to proceed.
+ * At the user's request, this will disable diagnostic plotting.
+ * @param plotFlag, set to false if this plot should be disabled in the future
+ * @param dumpData, set to true if user requests a data dump
+ */
+bool pmVisualAskUserOrDump(bool *plotFlag, bool *dumpData);
+
+
+/** Scale and display an image.
+ * @param kapaFD the index of the Kapa window to draw to
+ * @param inImage the image to display
+ * @param name the image label
+ * @param which channel to draw to (zero works)
+ * @param clip set to true to sigma clip the image when scaling
+ * @return true for successful completion */
+bool pmVisualScaleImage(int kapaFD, psImage *inImage,
+                        const char *name, int channel, bool clip);
+
+bool pmVisualRangeImage (int kapaFD, psImage *inImage, const char *name, int channel, float min, float max);
+
+/** Calculate statistics on an image.
+ *  This can handle non-finite input pixels
+ *  @param image to calculate statistics for
+ *  @param mean  stores the calculated mean
+ *  @param stdev stores the calculated standard deviation
+ *  @param min stores the calculated minimum
+ *  @param max stores the calculated maximum
+ *  @return true for successful completion */
+bool pmVisualImStats(psImage *image, double *mean,
+                     double *stdev, double *min, double *max);
+
+
+/** Create a PS_TYPE_F32 image out of a PS_TYPE_F32 or PS_TYPE_F64 image.
+ * This is needed when displaying PS_TYPE_F64 images, which will not work wtih Kii.
+ * @param image to convert to type PS_TYPE_F32
+ * @return a copy of the image of type PS_TYPE_F32, or NULL upon failure */
+psImage* pmVisualImageToFloat(psImage *image);
+
+
+/** Create a scaled vector of plot point sizes from an unscaled, raw vector.
+ *  Used for input into pmVisualTriplePlot
+ * @param zVec the raw data
+ * @param zScale the scaled vector of plot point sizes
+ * @return true for successful completion
+ */
+bool pmVisualCreateScaleVec (psVector *zVec, psVector *zScale, bool increasing);
+
+
+/** Use x and y data to determine appropriate values for a Graphdata structure.
+ * This procedure sets the max and min keywords of a Graphdata structure to encompass
+ * the data coordinates given in xVec and yVec. Optionally, it will try to ignore outliers.
+ * @param graphdata structure to set up
+ * @param xVec X coordinates of data points
+ * @param yVec Y coordinates of data points
+ * @clip  set to true to attempt to clip out outliers
+ * @return true for successful completion
+ */
+bool pmVisualScaleGraphdata(Graphdata *graphdata, psVector *xVec,
+                            psVector *yVec, bool clip);
+
+
+/** Create a scatter plot form a set of (x,y) positions, whose plot points
+ *  are scaled by the size of a z vector
+ * @param kapid the index of the kapa window to plot to
+ * @param graphdata describing the plotting window
+ * @param xVec x positions
+ * @param yVec y positions
+ * @param zVec plot point sizes
+ * @param increasing whether the points in x,y,zvec are sorted
+ */
+bool pmVisualTriplePlot (int kapid, Graphdata *graphdata, psVector *xVec,
+                         psVector *yVec, psVector *zVec, bool increasing);
+
+/** The same as pmVisualTriplePlot, but draws new points without erasing the old plot window
+ * @param kapid the index of the kapa window to plot to
+ * @param xVec x positions
+ * @param yVec y positions
+ * @param zVec plot point sizes
+ * @param increasing whether the x,y,zvectors are listed in increasing order
+ */
+bool pmVisualTripleOverplot (int kapid, Graphdata *graphdata, psVector *xVec,
+                             psVector *yVec, psVector *zVec, bool increasing);
+
+#else
+
+// kapa-specific data types are changed to void
+bool pmVisualSetVisual(bool value);
+bool pmVisualIsVisual(void);
+bool pmVisualClose(void);
+bool pmVisualInitWindow (int *kapid, char *name);
+bool pmVisualInitGraph (int kapa, void *section, void *graphdata);
+bool pmVisualAskUser(bool *plotFlag);
+bool pmVisualAskUserOrDump(bool *plotFlag, bool *dumpData);
+bool pmVisualScaleImage(int kapaFD, psImage *inImage,
+                        const char *name, int channel, bool clip);
+bool pmVisualImStats(psImage *image, double *mean,
+                     double *stdev, double *min, double *max);
+psImage* pmVisualImageToFloat(psImage *image);
+bool pmVisualCreateScaleVec (psVector *zVec, psVector *zScale, bool increasing);
+bool pmVisualResidPlot (psArray *rawstars, psArray *refstars, psArray *match,
+                        psMetadata *recipe, char *title, int *kapa, int *kapa2);
+bool pmVisualScaleGraphdata(void *graphdata, psVector *xVec,
+                            psVector *yVec, bool clip);
+bool pmVisualTriplePlot (int kapid, void *graphdata, psVector *xVec,
+                         psVector *yVec, psVector *zVec, bool increasing);
+bool pmVisualTripleOverplot (int kapid, void *graphdata, psVector *xVec,
+                             psVector *yVec, psVector *zVec, bool increasing);
+
+#endif //HAVE_KAPA
+
+#endif //ndef PM_VISUAL_H
Index: /branches/eam_branches/psModules.20240412/src/extras/pmVisualUtils.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/pmVisualUtils.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/pmVisualUtils.c	(revision 42651)
@@ -0,0 +1,516 @@
+/** These utility functions manage the visual level information (equivalent to pmVisual levels)
+ *  @author Eugene Magnier, IfA
+ *  @date June 18, 2010
+ */
+
+/* Include Files  */
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <pslib.h>
+
+#include <pmVisual.h>
+#include <pmVisualUtils.h>
+
+#define PM_UNKNOWN_VISUAL_LEVEL -9999   // we don't know this name's level
+#define PM_DEFAULT_VISUAL_LEVEL -1
+#define PM_THE_OTHER_DEFAULT_VISUAL_LEVEL 0 // ???
+#define MAX_COMPONENT_LENGTH 1024
+
+/** Basic structure for the component tree.  A component is a string of the
+    form aaa.bbb.ccc, and may itself contain further subcomponents.  The
+    Component structure doesn't in fact contain it's full name, but only the
+    last part. */
+
+static pmVisualComponent* cRoot = NULL; // The root of the visual component tree
+
+/** static function prototypes **/
+static void componentFree(pmVisualComponent* comp);
+static pmVisualComponent* componentAlloc(const char *name, int level);
+static int getLevel(const char *facil);
+static void initVisualLevels(void);
+static void componentFree(pmVisualComponent* comp);
+static pmVisualComponent* componentAlloc(const char *name, int level);
+static void doGetVisualLevels(psMetadata *out, const pmVisualComponent* comp, psString parent, int defLevel);
+static void doPrintVisualLevels(const pmVisualComponent* comp, FILE *output, psS32 depth, psS32 defLevel);
+static psS32 doGetVisualLevel(const char *aname);
+static bool componentAdd(const char *addNodeName, psS32 level);
+
+/*****************************************************************************
+Set all visual levels at or below the specified node to zero.
+ *****************************************************************************/
+void pmVisualReset(pmVisualComponent* currentNode)
+{
+    psAssert(currentNode, "impossible");
+
+    psS32 i = 0;
+
+    if (NULL == currentNode) {
+        return;
+    }
+
+    currentNode->level = 0;
+    for (i = 0; i < currentNode->n; i++) {
+        if (!currentNode->subcomp[i]) {
+            psLogMsg("pmVisualReset", PS_LOG_WARN, _("Sub-component %d of node %s in visual tree is NULL."), i, currentNode->name);
+        } else {
+            pmVisualReset(currentNode->subcomp[i]);
+        }
+    }
+    return;
+}
+
+/*****************************************************************************
+Free all visual levels
+ *****************************************************************************/
+void pmVisualCleanup()
+{
+    psFree(cRoot);
+    cRoot = NULL;
+}
+
+// psSetVisualLevel(): add the component named "comp" to the component tree,
+int pmVisualSetLevel(const char *comp,   // component of interest
+                    int level)  // desired visual level
+{
+    PS_ASSERT_STRING_NON_EMPTY(comp, 0);
+
+    char *compName = NULL;
+    int prevLevel = -1;
+
+    // If the root component tree does not exist, then initialize it.
+    if (cRoot == NULL) {
+        initVisualLevels();
+    }
+
+    // turn on overall visualization
+    pmVisualSetVisual(true);
+
+    // If the component name has no leading dot, then supply it.
+    if (comp[0] != '.') {
+        compName = (char *) psAlloc(10 + strlen(comp));
+        strcpy(compName, ".");
+        compName = strcat(compName, comp);
+    } else {
+        compName = (char *) comp;
+    }
+    prevLevel = getLevel(compName);
+    // Add the new component to the component tree.
+    if ( !componentAdd(compName, level) ) {
+        psError(PS_ERR_UNKNOWN, false,
+                _("Failed to set visual level (%d) to '%s'."),
+                level,
+                compName);
+
+        if (comp[0] != '.') {
+            psFree(compName);
+        }
+        //        return false;
+        return -1;
+    }
+
+    if (comp[0] != '.') {
+        psFree(compName);
+    }
+
+    //    return true;
+    return prevLevel;
+}
+
+// Append the function name to the facility
+// NB: declares TARGET!
+#define FACILITY(TARGET, FUNC, FACIL) \
+    size_t _facilLength = strlen(FACIL); /* Length of facility name */ \
+    size_t _funcLength = strlen(FUNC);   /* Length of function name */ \
+    char TARGET[_facilLength + _funcLength + 2]; /* facility + the function name */ \
+    strcpy(&TARGET[0], FACIL); \
+    TARGET[_facilLength] = '.'; \
+    strcpy(&TARGET[_facilLength + 1], FUNC);
+
+int p_pmVisualGetLevel(const char *func, const char *name)
+{
+    PS_ASSERT_STRING_NON_EMPTY(name, 0);
+    PS_ASSERT_STRING_NON_EMPTY(func, 0);
+
+    FACILITY(facility, func, name);
+    return getLevel(facility);
+}
+
+/** the main tool in the pmVisual system : check that the specified level matches the desired level **/
+bool p_pmVisualTestLevel(const char *func, const char *name, int level)
+{
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+    PS_ASSERT_STRING_NON_EMPTY(func, false);
+
+    // if visualization is turned off, just skip
+    if (!pmVisualIsVisual()) return false;
+
+    // return true if level is set to be shown
+    FACILITY(facility, func, name);
+
+    bool valid = level <= getLevel(facility);
+
+    if (valid) {
+	psLogMsg ("psModules.visual", PS_LOG_DETAIL, "visualization %s (%d)\n", facility, level);
+    }
+
+    return (valid);
+}
+
+// psPrintVisualLevels(): Simply print all the visual levels in the visual level
+void pmVisualPrintLevels(FILE *output)
+{
+    if (cRoot == NULL) {
+        return;
+    }
+
+    doPrintVisualLevels(cRoot, output, 0, PM_THE_OTHER_DEFAULT_VISUAL_LEVEL);
+}
+
+// generate a metadata of the visual levels (is this really needed?)
+psMetadata *pmVisualLevels(void)
+{
+    if (cRoot == NULL) {
+        return psMetadataAlloc();
+    }
+
+    psMetadata *out = psMetadataAlloc();// Output metadata with the levels
+    doGetVisualLevels(out, cRoot, NULL, PM_THE_OTHER_DEFAULT_VISUAL_LEVEL);
+
+    return out;
+}
+
+/****************************** static functions ******************************/
+
+/*****************************************************************************
+componentAdd(): Adds the component named "addNodeName" to the root tree.
+ *****************************************************************************/
+static bool componentAdd(const char *addNodeName, psS32 level)
+{
+    psAssert(addNodeName, "impossible");
+
+    psS32 i = 0;                        // Loop index variable.
+    char name[MAX_COMPONENT_LENGTH]; // buffer for writeable copy.
+    char *firstComponent = NULL;        // first component of name
+    pmVisualComponent* currentNode = cRoot;
+    psS32 nodeExists = 0;
+
+    // XXX: Verify that this is the correct behavior.
+    if (strcmp("", addNodeName) == 0) {
+        psError(PS_ERR_BAD_PARAMETER_NULL,true,
+                _("Failed to add null component to visual tree."));
+        return false;
+    }
+
+    // Is this the root node? If so, simply set level and return.
+    if (strcmp(".", addNodeName) == 0) {
+        cRoot->level = level;
+        return true;
+    }
+
+    if (addNodeName[0] != '.') {
+        psError(PS_ERR_BAD_PARAMETER_VALUE,true,
+                _("Failed to add '%s' to the root component tree; component must start with '.'."),
+                addNodeName);
+        return false;
+    }
+
+    ps_strncpy_nowarn(name, addNodeName, MAX_COMPONENT_LENGTH);
+    char *pname = name+1;               // Take off the period
+    // Iterate through the components of addNodeName.  Strip off the first
+    // component of the name, find that in the root tree, or add it if it
+    // does not exist, then move to the next component in the name.
+
+    while (pname != NULL) {
+        firstComponent = pname;
+        pname = strchr(firstComponent, '.');
+        if (pname != NULL) {
+            *pname = '\0';
+            pname++;
+        }
+        nodeExists = 0;
+        for (i = 0; i < currentNode->n; i++) {
+            if (strcmp(currentNode->subcomp[i]->name, firstComponent) == 0) {
+                currentNode = currentNode->subcomp[i];
+                nodeExists = 1;
+                if (pname == NULL) {
+                    currentNode->level = level;
+                }
+            }
+        }
+
+        if (nodeExists == 0) {
+            currentNode->subcomp = psRealloc(currentNode->subcomp,
+                                             (currentNode->n + 1) * sizeof(pmVisualComponent* ));
+            psMemSetPersistent(currentNode->subcomp,true);
+
+            currentNode->n = (currentNode->n) + 1;
+
+            if (pname == NULL) {
+                // This is the final component to add.
+                currentNode->subcomp[(currentNode->n) - 1] =
+                    componentAlloc(firstComponent, level);
+            } else {
+                // We are adding an intermediate component.  The visual level is not defined.
+                // An undefined visual level inherits the visual level of it's parent.  However,
+                // we do not set that specifically here since that would result in a static,
+                // one-time, type of behavior.
+
+                currentNode->subcomp[(currentNode->n) - 1] =
+                    componentAlloc(firstComponent, PM_DEFAULT_VISUAL_LEVEL);
+            }
+            currentNode = currentNode->subcomp[(currentNode->n) - 1];
+        }
+    }
+
+    return true;
+}
+
+/*****************************************************************************
+    doGetVisualLevel()
+ This function recursively searches the root component tree for the
+ component named "name", which is supplied by a parameter.  If it
+ finds that component, it returns the level of that component.
+ Otherwise, it returns ???.
+
+    NOTE: We modified this so that the user may omit the leading "," in a
+    component name.  Since the code was already implemented assuming the "."
+    was required, rather than change all that code, in this function, I
+    simply add a leading "." to the component name if there is none.
+
+    Inputs:
+ name:
+    Outputs:
+ none
+    Returns:
+ The visual level of the "name" component.
+ *****************************************************************************/
+static psS32 doGetVisualLevel(const char *aname)
+{
+    psAssert(aname, "impossible");
+    char name[strlen(aname) + 1];       // need a writeable copy: for strsep()
+    char *pname = name;
+    char *firstComponent = NULL;        // first component of name
+    pmVisualComponent* currentNode = cRoot;
+    psS32 i = 0;
+    psS32 defaultLevel = 0;
+
+    if (NULL == currentNode) {
+        return (PM_UNKNOWN_VISUAL_LEVEL);
+    }
+
+    if (strcmp(".", aname) == 0) {
+        return (cRoot->level);
+    }
+
+    if (aname[0] != '.') {
+        return (PM_UNKNOWN_VISUAL_LEVEL);
+    }
+
+    defaultLevel = cRoot->level;
+    strcpy(name, aname);
+    pname = name+1;
+    while (pname != NULL) {
+        firstComponent = pname;
+        pname = strchr(firstComponent, '.');
+        if (pname != NULL) {
+            *pname = '\0';
+            pname++;
+        }
+        for (i = 0; i < currentNode->n; i++) {
+            if (NULL == currentNode->subcomp[i]) {
+                psLogMsg("p_pmVisualReset", PS_LOG_WARN,
+                         _("Sub-component %d of node %s in visual tree is NULL."),
+                         i, currentNode->name);
+            }
+
+            if (strcmp(currentNode->subcomp[i]->name, firstComponent) == 0) {
+                currentNode = currentNode->subcomp[i];
+                // For level inheritance purpose, we save the level of this
+                // component if it is not DEFAULT.
+                if (currentNode->level != PM_DEFAULT_VISUAL_LEVEL) {
+                    defaultLevel = currentNode->level;
+                }
+                // Determine if this is the last component:
+                if (pname == NULL) {
+                    if (currentNode->level != PM_DEFAULT_VISUAL_LEVEL) {
+                        return (currentNode->level);
+                    } else {
+                        return(defaultLevel);
+                    }
+                }
+            }
+        }
+    }
+    return(defaultLevel);
+}
+
+/*****************************************************************************
+    getLevel()
+ Return a visual level of "name" in the root component tree.  If the
+ exact string of components in "name" does not exist in the root
+ tree, we return the deepest level of the match.
+ *****************************************************************************/
+static int getLevel(const char *name)
+{
+    if (cRoot == NULL) {
+        return (PM_UNKNOWN_VISUAL_LEVEL);
+    }
+
+    psS32 visualLevel;
+
+    // If the component name has no leading dot, then supply it.
+    if (name[0] != '.') {
+        char compName[strlen(name) + 2];
+        compName[0] = '.';
+        strcpy(&compName[1], name);
+
+        visualLevel = doGetVisualLevel(compName);
+    } else {
+        // Search the component root tree, determine the visual level.
+        visualLevel = doGetVisualLevel(name);
+    }
+
+    // XXX: The default visual level is currently set at -1, which is not a
+    // valid visual level.  This is convenient in determining whether or not
+    // a component should inherit the visual level from parent nodes.  However,
+    // it's not clear that -1 should ever be returned by this function.
+    // The SDR is unclear on this point and we should probably request IfA
+    // comment.
+    if (visualLevel == PM_DEFAULT_VISUAL_LEVEL) {
+        visualLevel = PM_THE_OTHER_DEFAULT_VISUAL_LEVEL;
+    }
+
+    return(visualLevel);
+}
+
+/*****************************************************************************
+    doPrintVisualLevels()
+ This function recursively searches the component tree supplied by the
+ parameter "comp" and prints the name and level of each component.
+    Inputs:
+ comp: a node in the component tree.
+ level: the level of that node
+    Outputs:
+ none
+    Returns:
+ null
+ *****************************************************************************/
+static void doPrintVisualLevels(const pmVisualComponent* comp,
+				FILE *output,
+				psS32 depth,
+				psS32 defLevel)
+{
+    psAssert(comp, "impossible");
+
+    char line[1024];
+    psS32 i = 0;
+
+    if (comp->name[0] == '\0') {
+        return;
+    } else {
+        int nwritten = 0;
+        if (comp->level == PM_DEFAULT_VISUAL_LEVEL) {
+	    sprintf(line,"%*s%-*s %d\n", depth, "", 20 - depth, comp->name, defLevel);
+	    nwritten = fwrite (line, 1, strlen(line), output);
+        } else {
+	    sprintf(line, "%*s%-*s %d\n", depth, "", 20 - depth, comp->name, comp->level);
+	    nwritten = fwrite (line, 1, strlen(line), output);
+        }
+	if (nwritten < 1) {
+	}
+    }
+
+    for (i = 0; i < comp->n; i++) {
+        if (comp->level == PM_DEFAULT_VISUAL_LEVEL) {
+            doPrintVisualLevels(comp->subcomp[i], output, depth + 1, defLevel);
+        } else {
+            doPrintVisualLevels(comp->subcomp[i], output, depth + 1, comp->level);
+        }
+    }
+}
+
+static void doGetVisualLevels(psMetadata *out, // Output metadata with the visual levels
+                             const pmVisualComponent* comp, // Component to add
+                             psString parent, // Name of parent level
+                             int defLevel // Default level
+                            )
+{
+    if (comp->name[0] == '\0') {
+        return;
+    }
+
+    psString name = psStringCopy(parent); // Name of this level
+    if (comp->name[0] == '.') {
+        psStringAppend(&name, "%s", comp->name + 1);
+    } else if (!parent) {
+        psStringAppend(&name, "%s", comp->name);
+    } else {
+        psStringAppend(&name, ".%s", comp->name);
+    }
+
+    int level = (comp->level == PM_DEFAULT_VISUAL_LEVEL) ? defLevel : comp->level; // Level for component
+    if (name) {
+        psMetadataAddS32(out, PS_LIST_TAIL, name, 0, NULL, level);
+    }
+    for (int i = 0; i < comp->n; i++) {
+        doGetVisualLevels(out, comp->subcomp[i], name, level);
+    }
+
+    psFree(name);
+
+    return;
+}
+
+/*****************************************************************************
+componentAlloc(): allocate memory for a new node, and initialize members.
+ *****************************************************************************/
+static pmVisualComponent* componentAlloc(const char *name, int level)
+{
+    psAssert(name, "impossible");
+
+    pmVisualComponent* comp = psAlloc(sizeof(pmVisualComponent));
+    psMemSetDeallocator(comp, (psFreeFunc) componentFree);
+
+    comp->name = psStringCopy(name);
+    comp->level = level;
+    comp->n = 0;
+    comp->specified = false;
+    comp->subcomp = NULL;
+    return comp;
+}
+
+/*****************************************************************************
+componentFree(): free the current node in the root tree, and all children
+nodes as well.
+ *****************************************************************************/
+static void componentFree(pmVisualComponent* comp)
+{
+    if (comp == NULL) {
+        return;
+    }
+
+    if (comp->subcomp != NULL) {
+        for (psS32 i = 0; i < comp->n; i++) {
+            psFree(comp->subcomp[i]);
+        }
+        psFree(comp->subcomp);
+    }
+    psFree(comp->name);
+}
+
+/*****************************************************************************
+initVisualLevels(): simply initialize the component root tree.
+*****************************************************************************/
+static void initVisualLevels(void)
+{
+    if (cRoot == NULL) {
+        cRoot = componentAlloc(".", PM_DEFAULT_VISUAL_LEVEL);
+    }
+}
Index: /branches/eam_branches/psModules.20240412/src/extras/pmVisualUtils.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/pmVisualUtils.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/pmVisualUtils.h	(revision 42651)
@@ -0,0 +1,33 @@
+/* @file pmVisualUtils.h
+ * @brief functions to create visual diagnostics with the help of 'kapa'
+ * @author Chris Beaumont, IfA
+ *
+ * Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_VISUAL_UTILS_H
+#define PM_VISUAL_UTILS_H
+
+typedef struct pmVisualComponent
+{
+    const char *name;			///< last part of name of component
+    psS32 level;			///< visual level for this component
+    bool specified;			///< whether the component is specified
+    psS32 n;				///< number of subcomponents
+    struct pmVisualComponent* *subcomp;	///< next level of subcomponents
+}
+pmVisualComponent;
+
+psMetadata *pmVisualLevels(void);
+void pmVisualPrintLevels(FILE *output);
+int pmVisualSetLevel(const char *comp, int level);
+void pmVisualCleanup();
+void pmVisualReset(pmVisualComponent* currentNode);
+
+int p_pmVisualGetLevel(const char *func, const char *name);
+# define pmVisualGetLevel(facil) p_pmVisualGetLevel(__func__, facil)
+
+bool p_pmVisualTestLevel(const char *func, const char *name, int level);
+# define pmVisualTestLevel(facil, level) p_pmVisualTestLevel(__func__, facil, level)
+
+#endif // PM_VISUAL_UTILS_H
Index: /branches/eam_branches/psModules.20240412/src/extras/psIOBuffer.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/psIOBuffer.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/psIOBuffer.c	(revision 42651)
@@ -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_branches/psModules.20240412/src/extras/psIOBuffer.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/psIOBuffer.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/psIOBuffer.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/extras/psPipe.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/psPipe.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/psPipe.c	(revision 42651)
@@ -0,0 +1,209 @@
+#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 (void)
+{
+    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);
+        if (status < 0) { psWarning ("error running exec for child process"); }
+        exit (1); // this statement exits the child, not the parent, process
+    }
+    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_branches/psModules.20240412/src/extras/psPipe.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/psPipe.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/psPipe.h	(revision 42651)
@@ -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 (void);
+psPipe *psPipeOpen (char *command);
+int     psPipeClose (psPipe *pipe);
+
+/// @}
+# endif
Index: /branches/eam_branches/psModules.20240412/src/extras/psVectorBracket.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/psVectorBracket.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/psVectorBracket.c	(revision 42651)
@@ -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;
+        }
+    }
+    N = (Nhi >= index->n) ? Nhi - 1 : Nhi;
+    // at this point, index[N] >= key > index[Nlo]
+    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_branches/psModules.20240412/src/extras/psVectorBracket.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/extras/psVectorBracket.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/extras/psVectorBracket.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/imcombine/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/Makefile.am	(revision 42651)
@@ -0,0 +1,50 @@
+noinst_LTLIBRARIES = libpsmodulesimcombine.la
+
+libpsmodulesimcombine_la_CPPFLAGS = $(SRCINC) $(PSMODULES_CFLAGS)
+libpsmodulesimcombine_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+
+libpsmodulesimcombine_la_SOURCES = \
+	pmReadoutCombine.c	\
+	pmStack.c		\
+	pmStackReject.c		\
+	pmSubtraction.c		\
+	pmSubtractionAnalysis.c	\
+	pmSubtractionEquation.c	\
+	pmSubtractionIO.c	\
+	pmSubtractionKernels.c	\
+	pmSubtractionHermitian.c	\
+	pmSubtractionDeconvolve.c	\
+	pmSubtractionMask.c	\
+	pmSubtractionMatch.c	\
+	pmSubtractionParams.c	\
+	pmSubtractionStamps.c	\
+	pmSubtractionThreads.c	\
+	pmSubtractionSimple.c   \
+	pmPSFEnvelope.c         \
+	pmSubtractionVisual.c   \
+	pmStackVisual.c
+
+pkginclude_HEADERS = \
+	pmImageCombine.h \
+	pmReadoutCombine.h	\
+	pmStack.h		\
+	pmStackReject.h		\
+	pmSubtraction.h		\
+	pmSubtractionTypes.h		\
+	pmSubtractionAnalysis.h	\
+	pmSubtractionEquation.h	\
+	pmSubtractionIO.h	\
+	pmSubtractionKernels.h	\
+	pmSubtractionHermitian.h	\
+	pmSubtractionDeconvolve.h	\
+	pmSubtractionMask.h	\
+	pmSubtractionMatch.h	\
+	pmSubtractionParams.h	\
+	pmSubtractionStamps.h	\
+	pmSubtractionThreads.h	\
+	pmSubtractionSimple.h   \
+	pmPSFEnvelope.h         \
+	pmSubtractionVisual.h   \
+	pmStackVisual.h
+
+CLEANFILES = *~
Index: /branches/eam_branches/psModules.20240412/src/imcombine/README
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/README	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/README	(revision 42651)
@@ -0,0 +1,9 @@
+
+pmSubtractionEquation.v0.c
+
+  This version of the code includes the old-style form of the matrix
+  equations, in which the normalization and background were calculated
+  as part of the matrix inversion.  We've decided that this method is
+  too sensitive to errors in the normalization and no longer use this
+  approach.
+
Index: /branches/eam_branches/psModules.20240412/src/imcombine/demo_psftool.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/demo_psftool.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/demo_psftool.c	(revision 42651)
@@ -0,0 +1,194 @@
+// This is a very quick and dirty test program for pmPSFEnvelope
+//
+// gcc -g demo_psftool.c -o demo_psftool `psmodules-config --cflags --libs` --std=gnu99 -Wall
+//
+// ./demo_psftool test MOPS.skycell.0198767.wrp*.psf
+//
+
+#include <stdio.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+// Add a single filename to the arguments as an array, so that it can be used with pmFPAfileBindFromArgs, etc
+static void fileList(const char *file, // The symbolic name for the file
+                     const char *name, // The name of the file
+                     const char *comment, // Description of the file
+                     pmConfig *config // Configuration
+    )
+{
+    psArray *files = psArrayAlloc(1); // Array with file names
+    files->data[0] = psStringCopy(name);
+    psMetadataAddArray(config->arguments, PS_LIST_TAIL, file, 0, comment, files);
+    psFree(files);
+    return;
+}
+
+
+
+int main(int argc, char *argv[])
+{
+    psLibInit(NULL);
+    pmConfig *config = pmConfigRead(&argc, argv, "PSF");
+    if (!config) {
+        psErrorStackPrint(stderr, "Error reading configuration.");
+        exit(PS_EXIT_CONFIG_ERROR);
+    }
+
+    psTraceSetLevel("psModules.imcombine", 5);
+    psTraceSetLevel("psModules.objects", 0);
+    psTraceSetLevel("psLib.math", 0);
+
+    pmModelClassInit();
+
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "Name of the output", argv[1]);
+
+    psArray *files = psArrayAlloc(argc - 2);
+    for (int i = 2; i < argc; i++) {
+        psString name = NULL;           // Name of file list
+        psStringAppend(&name, "INPUT_%d", i);
+
+        fileList(name, argv[i], "Input PSF", config);
+
+        pmFPAfile *file = pmFPAfileDefineFromArgs(NULL, config, "PSPHOT.PSF.LOAD", name);
+        psFree(name);
+        if (!file) {
+            psErrorStackPrint(stderr, "Can't define PSF file from %s --> %s", name, argv[i]);
+            psFree(files);
+            psFree(config);
+            exit(PS_EXIT_SYS_ERROR);
+        }
+
+        files->data[i - 2] = psMemIncrRefCounter(file);
+    }
+
+    pmFPAfile *outFile = pmFPAfileDefineOutput(config, NULL, "PSPHOT.PSF.SAVE");
+    if (!outFile) {
+        psErrorStackPrint(stderr, "Can't define output PSF file");
+        psFree(files);
+        psFree(config);
+        exit(PS_EXIT_SYS_ERROR);
+    }
+    outFile->save = true;
+
+    // XXX This is a bit dodgy; should be more rigorous for a real system
+    {
+        pmFPAview *phuView = pmFPAviewAlloc(0);
+        phuView->chip = 0;
+        if (!pmFPAAddSourceFromView(outFile->fpa, "Envelope PSF", phuView, outFile->format)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to add PHU to output.");
+            psFree(phuView);
+            return false;
+        }
+        psFree(phuView);
+    }
+
+    pmFPAview *view = pmFPAviewAlloc(0);
+
+    if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
+        psErrorStackPrint(stderr, "Problem in I/O");
+        psFree(view);
+        psFree(files);
+        psFree(config);
+        exit(PS_EXIT_SYS_ERROR);
+    }
+
+    pmChip *chip;
+    while ((chip = pmFPAviewNextChip(view, outFile->fpa, 1)) != NULL) {
+        if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
+            psErrorStackPrint(stderr, "Problem in I/O");
+            psFree(view);
+            psFree(files);
+            psFree(config);
+            exit(PS_EXIT_SYS_ERROR);
+        }
+
+#if 0
+        pmFPAfileActivate(config->files, "PSPHOT.PSF.SAVE", false);
+        pmCell *cell;
+        while ((cell = pmFPAviewNextCell(view, outFile->fpa, 1)) != NULL) {
+            if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
+                psErrorStackPrint(stderr, "Problem in I/O");
+                psFree(view);
+                psFree(files);
+                psFree(config);
+                exit(PS_EXIT_SYS_ERROR);
+            }
+        }
+#endif
+
+        psArray *inputs = psArrayAlloc(files->n);
+        int numCols = 4501, numRows = 4751;
+        for (int i = 0; i < files->n; i++) {
+            pmFPAfile *file = files->data[i];
+            pmChip *chip = pmFPAviewThisChip(view, file->fpa);
+
+            pmPSF *psf = psMetadataLookupPtr(NULL, chip->analysis, "PSPHOT.PSF");
+            if (!psf) {
+                psErrorStackPrint(stderr, "Can't find PSF in file %d", i);
+                psFree(inputs);
+                psFree(files);
+                psFree(config);
+                exit(PS_EXIT_PROG_ERROR);
+            }
+
+#if 0
+            pmHDU *hdu = pmHDUGetLowest(file->fpa, chip, NULL);
+            int imaxis1 = psMetadataLookupS32(NULL, hdu->header, "IMAXIS1");
+            int imaxis2 = psMetadataLookupS32(NULL, hdu->header, "IMAXIS2");
+            if (imaxis1 == 0 || imaxis2 == 0) {
+                psErrorStackPrint(stderr, "Size of image %d can't be determined.", i);
+                psFree(inputs);
+                psFree(files);
+                psFree(config);
+                exit(PS_EXIT_SYS_ERROR);
+            }
+            if (numCols == 0 && numRows == 0) {
+                numCols = imaxis1;
+                numRows = imaxis2;
+            } else if (imaxis1 != numCols || imaxis2 != numRows) {
+                psErrorStackPrint(stderr, "Image %d differs in size: %dx%d vs %dx%d",
+                                  i, imaxis1, imaxis2, numCols, numRows);
+                psFree(inputs);
+                psFree(files);
+                psFree(config);
+                exit(PS_EXIT_SYS_ERROR);
+            }
+#endif
+
+            inputs->data[i] = psMemIncrRefCounter(psf);
+        }
+
+        pmPSF *psf = pmPSFEnvelope(numCols, numRows, inputs, 5, 20, "PS_MODEL_RGAUSS", 3, 3);
+        psFree(inputs);
+        if (!psf) {
+            psErrorStackPrint(stderr, "Can't generate envelope PSF.");
+            psFree(config);
+            exit(PS_EXIT_SYS_ERROR);
+        }
+
+        psMetadataAddPtr(chip->analysis, PS_LIST_TAIL, "PSPHOT.PSF", PS_DATA_UNKNOWN, "Envelope PSF", psf);
+        psFree(psf);
+        chip->data_exists = true;
+
+        if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
+            psErrorStackPrint(stderr, "Problem in I/O");
+            psFree(view);
+            psFree(files);
+            psFree(config);
+            exit(PS_EXIT_SYS_ERROR);
+            return false;
+        }
+    }
+
+    if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
+        psErrorStackPrint(stderr, "Problem in I/O");
+        psFree(view);
+        psFree(files);
+        psFree(config);
+        exit(PS_EXIT_SYS_ERROR);
+    }
+
+    psFree(config);
+
+    exit(PS_EXIT_SUCCESS);
+}
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmImageCombine.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmImageCombine.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmImageCombine.c	(revision 42651)
@@ -0,0 +1,692 @@
+/** @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.13 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-01-27 06:39:38 $
+ *
+ *  XXX: pmRejectPixels() has a known bug with the pmImageTransform() call.
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+// XXX this is somewhat messy and unclear on the masking.
+
+#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_VECTOR_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
+                          psImageMaskType 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.PS_TYPE_VECTOR_MASK_DATA[i] = (mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskVal);
+        }
+        // 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, 0xff)) {
+            psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+            return false;
+        }
+        if (isnan(stats->sampleMean)) {
+            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.PS_TYPE_VECTOR_MASK_DATA[i] & 0xff) &&
+                    fabs(pixelData->data.F32[i] - combinedPixel) > sigmaClip * stdev) {
+                // Reject pixel as questionable
+                numRejects++;
+                pixelMasks->data.PS_TYPE_IMAGE_MASK_DATA[i] = 0xff;
+                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
+    psImageMaskType 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_IMAGE_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_VECTOR_MASK); // 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.PS_TYPE_IMAGE_MASK_DATA[j][i])) {
+                    pixels->data.F32[num] = image->data.F32[j][i];
+                    mask->data.PS_TYPE_VECTOR_MASK_DATA[num] = 0;
+                    num++;
+                } else {
+                    mask->data.PS_TYPE_VECTOR_MASK_DATA[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.PS_TYPE_VECTOR_MASK_DATA[num] = 0;
+                    num++;
+                } else {
+                    mask->data.PS_TYPE_VECTOR_MASK_DATA[num] = 1;
+                }
+            }
+        }
+    }
+
+    pixels->n = num;
+    mask->n = num;
+
+    // Get the median
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN);
+    if (!psVectorStats(stats, pixels, NULL, mask, 1)) {
+        psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+    }
+    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.PS_TYPE_IMAGE_MASK_DATA[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); // XXX really F32??
+            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 psImageMaskType 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_branches/psModules.20240412/src/imcombine/pmImageCombine.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmImageCombine.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmImageCombine.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/imcombine/pmPSFEnvelope.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmPSFEnvelope.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmPSFEnvelope.c	(revision 42651)
@@ -0,0 +1,502 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+
+#include "pmReadoutFake.h"
+#include "pmPSFEnvelope.h"
+#include "pmStackVisual.h"
+
+
+// #define TESTING                         // Enable test output
+// #define PEAK_NORM                       // Normalise peaks?
+#define PEAK_FLUX 1.0e4                 // Peak flux for each source
+#define SKY_VALUE 0.0e0                 // Sky value for fake image
+#define VARIANCE_VAL 3.0                // Variance for image
+#define VARIANCE_FACTOR 10.0            // Factor to multiply image by to get variance
+#define PSF_STATS PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV // Statistics options for measuring PSF
+#define SOURCE_FIT_ITERATIONS 100       // Number of iterations for source fitting
+#define MODEL_MASK (PM_MODEL_STATUS_NONCONVERGE | PM_MODEL_STATUS_OFFIMAGE | \
+                    PM_MODEL_STATUS_BADARGS | PM_MODEL_STATUS_LIMITS) // Mask to apply to models
+
+
+// XXX To do:
+//
+// * PSF variation when only a portion of the image is present (e.g., the edge of an FPA overlapping a
+// skycell) may mean a disastrously weird PSF in the missing regions.  To counter this, get a region of
+// validity for each PSF (perhaps from an associated mask, or have the user work it out), and taper the PSF
+// when outside this region (perhaps multiply the peak flux by a Gaussian whose arguments are the distance
+// from the valid region, and a width that the user supplies).
+
+
+// We deliberately do not include the calculation of and storing of residuals (data - model) for the PSF
+// model, because (1) there is no code in psModules to do this, and we're not going to implement it here; and
+// (2) this is intended to generate "nice" or "ideal" PSFs to feed into pmSubtraction (PSF matching code), so
+// any residuals will hopefully be dealt with by that.
+
+
+pmPSF *pmPSFEnvelope(int numCols, int numRows, // Size of original image
+                     const psArray *inputs, // Input PSF models
+                     int instances, // Number of instances per dimension
+                     int radius,        // Radius of each PSF
+                     const char *modelName,// Name of PSF model to use
+                     int xOrder, int yOrder, // Order for PSF variation fit
+                     psImageMaskType maskVal
+                     )
+{
+    PS_ASSERT_INT_POSITIVE(numCols, NULL);
+    PS_ASSERT_INT_POSITIVE(numRows, NULL);
+    PS_ASSERT_ARRAY_NON_NULL(inputs, NULL);
+    PS_ASSERT_INT_POSITIVE(instances, NULL);
+    PS_ASSERT_INT_POSITIVE(radius, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(modelName, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(xOrder, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(yOrder, NULL);
+
+    float xOrigSpacing = (float)(numCols - 2 * radius) / (float)(instances - 1); // Spacing between instances
+    float yOrigSpacing = (float)(numRows - 2 * radius) / (float)(instances - 1); // Spacing between instances
+    int fakeSpacing = 2 * radius + 1;   // Spacing between instances (x and y) in the fake image
+    int fakeSize = instances * fakeSpacing; // Size of fake image
+
+    // Generate list of fake sources (instances of the PSF)
+    int numFakes = PS_SQR(instances);   // Number of fake sources
+    psArray *fakes = psArrayAlloc(numFakes); // Fake sources
+    psVector *xOffset = psVectorAlloc(numFakes, PS_TYPE_S32); // X offset from fake position to image
+    psVector *yOffset = psVectorAlloc(numFakes, PS_TYPE_S32); // Y offset from fake position to image
+    for (int j = 0, index = 0; j < instances; j++) {
+        float yOrig = j * yOrigSpacing + radius; // Source position in original image
+        float yFake = j * fakeSpacing + radius; // Position in fake image
+        int dy = yFake - yOrig;         // Difference between fake and original position
+
+        for (int i = 0; i < instances; i++, index++) {
+            float xOrig = i * xOrigSpacing + radius; // Source position in original image
+            float xFake = i * fakeSpacing + radius; // Position in fake image
+            int dx = xFake - xOrig;     // Difference between fake and original position
+
+            pmSource *fake = pmSourceAlloc(); // Fake source
+            fake->peak = pmPeakAlloc(xFake - dx, yFake - dy, PEAK_FLUX, PM_PEAK_LONE);
+            fake->type = PM_SOURCE_TYPE_STAR;
+            fake->psfMag = -2.5 * log10(PEAK_FLUX);
+
+            psTrace("psModules.imcombine", 5, "Source %d: %.2f,%.2f\n",
+                    index, xOrig, yOrig);
+
+            fakes->data[index] = fake;
+            xOffset->data.S32[index] = dx;
+            yOffset->data.S32[index] = dy;
+        }
+    }
+
+    // Generate fake images with each PSF, and take the envelope
+    psImage *envelope = psImageAlloc(fakeSize, fakeSize, PS_TYPE_F32); // Image with envelope of PSFs
+    psImageInit(envelope, SKY_VALUE);
+    pmReadout *fakeRO = pmReadoutAlloc(NULL); // Fake readout
+    float maxRadius = 0.0;              // Maximum radius for sources
+    psVector *numbers = psVectorAlloc(numFakes, PS_TYPE_S32); // Number of detections for each source
+    psVectorInit(numbers, 0);
+    for (int i = 0; i < inputs->n; i++) {
+        pmPSF *psf = inputs->data[i];   // PSF of interest
+        if (!psf) {
+            continue;
+        }
+
+        if (psTraceGetLevel("psModules.imcombine") >= 1) {
+            psString string = NULL;     // String with values
+            psStringAppend(&string, "PSF %d: ", i);
+            float x = numCols / 2.0, y = numRows / 2.0; // Coordinates of interest
+            for (int j = 4; j < psf->params->n; j++) {
+                pmTrend2D *trend = psf->params->data[j]; // Trend of interest
+                double val = pmTrend2DEval(trend, x, y);
+                double err;
+                switch (trend->mode) {
+                  case PM_TREND_POLY_ORD:
+                  case PM_TREND_POLY_CHEB:
+                    err = NAN;
+                    break;
+                  case PM_TREND_MAP:
+                    err = psImageUnbinPixel(x, y, trend->map->error, trend->map->binning);
+                    break;
+                  default:
+                    psAbort("Unsupported mode: %x", trend->mode);
+                }
+                psStringAppend(&string, "%lf %lf   ", val, err);
+            }
+            psTrace("psModules.imcombine", 1, "%s\n", string);
+            psFree(string);
+        }
+
+        // Test PSF
+        {
+            bool goodPSF = false;       // Is there a PSF that we can use?
+            int xNum = PS_MAX(psf->trendNx, 1), yNum = PS_MAX(psf->trendNy, 1); // Number of positions to check
+            for (int j = 0; j < yNum && !goodPSF; j++) {
+                float y = ((float)j + 0.5) / (float)yNum * numRows; // Position on image
+                for (int i = 0; i < xNum && !goodPSF; i++) {
+                    float x = ((float)i + 0.5) / (float)xNum * numCols; // Position on image
+                    pmModelClassSetLimits(PM_MODEL_LIMITS_IGNORE);
+                    pmModel *model = pmModelFromPSFforXY(psf, x, y, PEAK_FLUX); // Test model
+                    if (!model) {
+                        continue;
+                    }
+                    model->class->modelSetLimits(PM_MODEL_LIMITS_MODERATE);
+                    bool limits = true; // Model within limits?
+                    for (int j = 0; j < model->params->n && limits; j++) {
+                        if (!model->class->modelLimits(PS_MINIMIZE_PARAM_MIN, j, model->params->data.F32, NULL) ||
+                            !model->class->modelLimits(PS_MINIMIZE_PARAM_MAX, j, model->params->data.F32, NULL)) {
+                            limits = false;
+                        }
+                    }
+                    psFree(model);
+                    if (limits) {
+                        goodPSF = true;
+                    }
+                }
+            }
+            if (!goodPSF) {
+                psWarning("PSF %d is completely bad --- not including in envelope calculation.", i);
+                continue;
+            }
+        }
+
+        pmResiduals *resid = psf->residuals;// PSF residuals
+        psf->residuals = NULL;
+        pmModelClassSetLimits(PM_MODEL_LIMITS_MODERATE);
+	psLogMsg("psModules",PS_LOG_INFO,"Matching Input %d",i);
+#define CIRCULARIZE true
+        if (!pmReadoutFakeFromSources(fakeRO, fakeSize, fakeSize, fakes, 0, xOffset, yOffset, psf,
+                                      NAN, radius, CIRCULARIZE, false)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to generate fake readout.");
+            psFree(envelope);
+            psFree(yOffset);
+            psFree(xOffset);
+            psFree(fakes);
+            psFree(numbers);
+            psf->residuals = resid;
+            return NULL;
+        }
+        psf->residuals = resid;
+
+        // Need to renormalise sources so they all have the same peak.  You would think they do have the same
+        // peak already, but it seems that the residual map messes things up by adding extra flux
+        for (int j = 0; j < numFakes; j++) {
+            pmSource *source = fakes->data[j]; // Fake source
+            float x = source->peak->xf + xOffset->data.S32[j]; // x coordinate of source
+            float y = source->peak->yf + yOffset->data.S32[j]; // y coordinate of source
+
+#ifdef PEAK_NORM
+            // Perhaps I'm being paranoid, but specify a range to check
+            int uMax = PS_MIN(x + radius, numCols - 1), uMin = PS_MAX(x - radius, 0);
+            int vMax = PS_MIN(y + radius, numRows - 1), vMin = PS_MAX(y - radius, 0);
+
+            double flux = -INFINITY;    // Peak flux
+            for (int v = vMin; v <= vMax; v++) {
+                for (int u = uMin; u <= uMax; u++) {
+                    if (fakeRO->image->data.F32[v][u] > flux) {
+                        flux = fakeRO->image->data.F32[v][u];
+                    }
+                }
+            }
+            if (!isfinite(flux) || flux < 0) {
+                continue;
+            }
+            float norm = PEAK_FLUX / flux; // Normalisation for source
+#endif
+            psRegion region = psRegionSet(x - radius, x + radius, y - radius, y + radius); // PSF region
+            psImage *subImage = psImageSubset(fakeRO->image, region); // Subimage of fake PSF
+            psImage *subEnv = psImageSubset(envelope, region); // Subimage of envelope
+#ifdef PEAK_NORM
+            psBinaryOp(subImage, subImage, "*", psScalarAlloc(norm, PS_TYPE_F32));
+#endif
+            psBinaryOp(subEnv, subEnv, "MAX", subImage);
+            psFree(subImage);
+            psFree(subEnv);
+
+            // Get the radius
+            pmModel *model = pmModelFromPSFforXY(psf, source->peak->xf, source->peak->yf, PEAK_FLUX); // Model for source
+            if (!model || (model->flags & MODEL_MASK)) {
+                continue;
+            }
+            float srcRadius = model->class->modelRadius(model->params, PS_SQR(VARIANCE_VAL)); // Radius for source
+            psFree(model);
+            if (srcRadius == 0) {
+                continue;
+            }
+            if (srcRadius > maxRadius) {
+                maxRadius = srcRadius;
+            }
+
+            // If we got this far, the source is decent
+            numbers->data.S32[j]++;
+        }
+
+#ifdef TESTING
+        {
+            // Write out the PSF field
+            psString name = NULL;
+            psStringAppend(&name, "psf_field_%03d.fits", i);
+            psFits *fits = psFitsOpen(name, "w");
+            pmStackVisualPlotTestImage(fakeRO->image, name);
+            psFitsWriteImage(fits, NULL, fakeRO->image, 0, NULL);
+            psFitsClose(fits);
+            psFree(name);
+        }
+#endif
+
+    }
+    psFree(fakeRO);
+
+#ifdef TESTING
+    {
+        // Write out the envelope
+        psFits *fits = psFitsOpen("psf_field_envelope.fits", "w");
+        pmStackVisualPlotTestImage(envelope, "psf_field_envelope.fits");
+        psFitsWriteImage(fits, NULL, envelope, 0, NULL);
+        psFitsClose(fits);
+    }
+#endif
+
+    // Put the fake sources onto a full-size image
+    psArray *goodFakes = psArrayAllocEmpty(numFakes); // Good fake sources
+    pmReadout *readout = pmReadoutAlloc(NULL); // Readout to contain envelope pixels
+    readout->image = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psImageInit(readout->image, 0.0);
+    for (int i = 0; i < numFakes; i++) {
+        pmSource *source = fakes->data[i]; // Fake source
+        if (numbers->data.S32[i] > 0) {
+            psArrayAdd(goodFakes, goodFakes->n, source);
+        }
+
+        // Position of source on fake image
+        int xFake = source->peak->x + xOffset->data.S32[i];
+        int yFake = source->peak->y + yOffset->data.S32[i];
+        psRegion region = psRegionSet(xFake - radius, xFake + radius,
+                                      yFake - radius, yFake + radius); // PSF region
+        psImage *subImage = psImageSubset(envelope, region); // Subimage of fake PSF
+
+        // Position of source on "real" image
+        int x0 = source->peak->x - radius;
+        int y0 = source->peak->y - radius;
+
+        if (!psImageOverlaySection(readout->image, subImage, x0, y0, "=")) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to overlay PSF");
+            psFree(subImage);
+            psFree(readout);
+            psFree(xOffset);
+            psFree(yOffset);
+            psFree(fakes);
+            psFree(numbers);
+            return NULL;
+        }
+        psFree(subImage);
+    }
+    psFree(xOffset);
+    psFree(yOffset);
+    psFree(envelope);
+    psFree(numbers);
+
+    psFree(fakes);
+    fakes = goodFakes;
+    numFakes = fakes->n;
+
+    if (numFakes == 0) {
+        psError(PS_ERR_UNKNOWN, false, "No fake sources are suitable for PSF fitting.");
+        psFree(fakes);
+        psFree(readout);
+        return false;
+    }
+
+    // XXX Setting the variance seems to be an art
+    // Can't set it too high so that pixels are rejected as insignificant
+    // Can't set it too low so that it's hard to get to the minimum
+    // Have also tried:
+    // *** readout->variance = (psImage*)psBinaryOp(NULL, readout->image, "*", readout->image);
+    // *** readout->variance = (psImage*)psBinaryOp(NULL, readout->image, "*", psScalarAlloc(VARIANCE_FACTOR, PS_TYPE_F32));
+    readout->variance = (psImage*)psBinaryOp(NULL, readout->image, "+",
+                                             psScalarAlloc(VARIANCE_VAL, PS_TYPE_F32));
+    readout->mask = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK);
+    psImageInit(readout->mask, 0);
+
+    if (maxRadius > radius) {
+        maxRadius = radius;
+    }
+
+#ifdef TESTING
+    {
+        // Write out the envelope
+        psFits *fits = psFitsOpen("psf_field_full.fits", "w");
+        pmStackVisualPlotTestImage(readout->image, "psf_field_full.fits");
+        psFitsWriteImage(fits, NULL, readout->image, 0, NULL);
+        psFitsClose(fits);
+    }
+#endif
+
+    // Reset the sources to point to the new pixels, and measure the moments in preparation for PSF fitting
+    int numMoments = 0;                 // Number of moments measured
+    for (int i = 0; i < numFakes; i++) {
+        pmSource *source = fakes->data[i]; // Fake source
+        float x = source->peak->xf;     // x coordinates of source
+        float y = source->peak->yf;     // y coordinates of source
+
+        psFree(source->pixels);
+        psFree(source->variance);
+        psFree(source->maskView);
+        psFree(source->maskObj);
+        source->pixels = NULL;
+        source->variance = NULL;
+        source->maskView = NULL;
+        source->maskObj = NULL;
+
+        if (!pmSourceDefinePixels(source, readout, x, y, radius)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to define pixels for source.");
+            psFree(readout);
+            psFree(fakes);
+            return NULL;
+        }
+
+        // measure the source moments: tophat windowing, no pixel S/N cutoff
+        if (!pmSourceMoments(source, maxRadius, 0.25*maxRadius, 0.0, 0.0, maskVal)) {
+            // Can't do anything about it; limp along as best we can
+            psErrorClear();
+            continue;
+        }
+        numMoments++;
+    }
+
+    if (numMoments == 0) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to measure moments for sources.");
+        psFree(fakes);
+        psFree(readout);
+        return NULL;
+    }
+
+    // Don't assume Poisson errors
+    pmPSFOptions *options = pmPSFOptionsAlloc(); // Options for fitting a PSF
+    options->poissonErrorsPhotLMM = true;
+    options->poissonErrorsPhotLin = false;
+    options->poissonErrorsParams = true;
+    options->stats = psStatsAlloc(PSF_STATS);
+    options->fitRadius = maxRadius;
+    options->apRadius = maxRadius; // XXX need to decide if aperture mags need a different radius
+    options->psfTrendMode = PM_TREND_MAP;
+    options->psfTrendNx = xOrder;
+    options->psfTrendNy = yOrder;
+    options->psfFieldNx = numCols;
+    options->psfFieldNy = numRows;
+    options->psfFieldXo = 0;
+    options->psfFieldYo = 0;
+    options->chiFluxTrend = false;      // All sources have similar flux, so fitting a trend often fails
+
+    // options which modify the behavior of the model fitting
+    options->fitOptions                = pmSourceFitOptionsAlloc();
+    options->fitOptions->nIter         = SOURCE_FIT_ITERATIONS;
+    options->fitOptions->minTol        = 0.01;
+    options->fitOptions->maxTol        = 1.00;
+    options->fitOptions->poissonErrors = true;
+    options->fitOptions->weight        = VARIANCE_VAL;
+    options->fitOptions->mode          = PM_SOURCE_FIT_PSF;
+
+    pmModelClassSetLimits(PM_MODEL_LIMITS_STRICT); // Important for getting a good stack target PSF
+
+    pmPSFtry *try = pmPSFtryModel(fakes, modelName, options, 0, 0xff);
+    psFree(options);
+    if (!try) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to fit PSF model to PSF envelope.");
+        psFree(readout);
+        psFree(fakes);
+        return NULL;
+    }
+
+    pmPSF *psf = psMemIncrRefCounter(try->psf); // Output PSF
+    psFree(try);
+
+    if (psTraceGetLevel("psModules.imcombine") >= 1) {
+        psString string = NULL;     // String with values
+        psStringAppend(&string, "Envelope PSF: ");
+        float x = numCols / 2.0, y = numRows / 2.0; // Coordinates of interest
+        for (int j = 4; j < psf->params->n; j++) {
+            pmTrend2D *trend = psf->params->data[j]; // Trend of interest
+            double val = pmTrend2DEval(trend, x, y);
+            double err;
+            switch (trend->mode) {
+              case PM_TREND_POLY_ORD:
+              case PM_TREND_POLY_CHEB:
+                err = NAN;
+                break;
+              case PM_TREND_MAP:
+                err = psImageUnbinPixel(x, y, trend->map->error, trend->map->binning);
+                break;
+              default:
+                psAbort("Unsupported mode: %x", trend->mode);
+            }
+            psStringAppend(&string, "%lf %lf   ", val, err);
+        }
+        psTrace("psModules.imcombine", 1, "%s\n", string);
+        psFree(string);
+    }
+
+#ifdef TESTING
+    {
+        // Need to translate peak flux --> integrated flux
+        pmModel *fakeModel = pmModelFromPSFforXY(psf, (float)numCols / 2.0, (float)numRows / 2.0,
+                                                 1.0); // Fake model, with central intensity of 1.0
+        psAssert (fakeModel, "failed to generate model: should this be an error or not?");
+        float flux0 = fakeModel->modelFlux(fakeModel->params); // Flux for central intensity of 1.0
+        for (int i = 0; i < numFakes; i++) {
+            pmSource *source = fakes->data[i]; // Fake source
+            source->psfMag -= 2.5 * log10(flux0);
+        }
+
+        pmReadout *generated = pmReadoutAlloc(NULL); // Generated image
+        pmReadoutFakeFromSources(generated, numCols, numRows, fakes, 0, NULL, NULL, psf, NAN, radius,
+                                 false, true);
+        {
+            psFits *fits = psFitsOpen("psf_field_model.fits", "w");
+            pmStackVisualPlotTestImage(generated->image, "psf_field_model.fits");
+            psFitsWriteImage(fits, NULL, generated->image, 0, NULL);
+            psFitsClose(fits);
+        }
+        psBinaryOp(generated->image, generated->image, "-", readout->image);
+        {
+            psFits *fits = psFitsOpen("psf_field_resid.fits", "w");
+            pmStackVisualPlotTestImage(generated->image, "psf_field_resid.fits");
+            psFitsWriteImage(fits, NULL, generated->image, 0, NULL);
+            psFitsClose(fits);
+        }
+        psFree(generated);
+    }
+#endif
+
+    psFree(fakes);
+    psFree(readout);
+
+    return psf;
+}
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmPSFEnvelope.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmPSFEnvelope.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmPSFEnvelope.h	(revision 42651)
@@ -0,0 +1,23 @@
+#ifndef PM_PSF_ENVELOPE_H
+#define PM_PSF_ENVELOPE_H
+
+#include <pslib.h>
+#include <pmMoments.h>
+#include <pmResiduals.h>
+#include <pmGrowthCurve.h>
+#include <pmTrend2D.h>
+#include <pmPSF.h>
+
+/// Generate a PSF which is the envelope of an array of PSFs
+///
+/// Generates multiple instances of the PSFs (distributed over an image)
+pmPSF *pmPSFEnvelope(int numCols, int numRows, // Size of original image
+                     const psArray *inputs, // Input PSF models
+                     int instances,     // Number of instances per dimension
+                     int radius,        // Radius of each PSF
+                     const char *modelName, // Name of PSF model to use
+                     int xOrder, int yOrder, // Order for PSF variation
+		     psImageMaskType maskVal
+    );
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmReadoutCombine.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmReadoutCombine.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmReadoutCombine.c	(revision 42651)
@@ -0,0 +1,546 @@
+#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->fracLow = 0.0;
+    params->fracHigh = 0.0;
+    params->iter = 1;
+    params->rej = INFINITY;
+    params->variances = false;
+
+    return params;
+}
+
+// check the input parameters and set up the output images
+bool pmReadoutCombinePrepare(pmReadout *output, const psArray *inputs, 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);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(params->fracLow, 0.0, 1.0, false);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(params->fracHigh, 0.0, 1.0, false);
+
+    // valid combintion statistic?
+    bool valid = false;
+    valid |= (params->combine == PS_STAT_SAMPLE_MEAN);
+    valid |= (params->combine == PS_STAT_SAMPLE_MEDIAN);
+    valid |= (params->combine == PS_STAT_ROBUST_MEDIAN);
+    valid |= (params->combine == PS_STAT_FITTED_MEAN);
+    valid |= (params->combine == PS_STAT_CLIPPED_MEAN);
+    if (!valid) {
+        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;
+    }
+
+    pmHDU *hdu = pmHDUFromReadout(output); // Output HDU
+    if (!hdu) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find HDU for readout.\n");
+        return false;
+    }
+
+    //  set the output header metadata
+    psString comment = NULL;        // Comment to add to header
+    psStringAppend(&comment, "Combining using statistic: %x", params->combine);
+    if (!hdu->header) {
+        hdu->header = psMetadataAlloc();
+    }
+    psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, comment, "");
+    psFree(comment);
+
+    // note the clipping parameters, if used
+    if (params->combine == PS_STAT_CLIPPED_MEAN) {
+        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);
+    }
+
+    // note the use of variances
+    if (params->variances) {
+        psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK,
+                         "Using input variances to combine images", "");
+    }
+
+    // note the rejection fraction
+    float keepFrac = 1.0 - params->fracLow - params->fracHigh; // Fraction of pixels to keep
+    if (keepFrac != 1.0) {
+        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);
+    }
+
+    // note the mask value actually used
+    psImageMaskType maskVal = params->maskVal; // The mask value
+    if (maskVal) {
+        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);
+    }
+
+    // determine the output image size based on the input images
+    int row0, col0, numCols, numRows;
+    if (!pmReadoutStackSetOutputSize(&col0, &row0, &numCols, &numRows, inputs)) {
+        psError(PS_ERR_UNKNOWN, false, "problem setting output readout size.");
+        return false;
+    }
+
+    // generate the required output images based on the specified sizes
+    pmReadoutStackDefineOutput(output, col0, row0, numCols, numRows, true, params->variances, params->blank);
+    psTrace("psModules.imcombine", 7, "Output minimum: %d,%d\n", output->col0, output->row0);
+
+    // these calls allocate and save the requested images on the output analysis metadata
+    psImage *counts = pmReadoutSetAnalysisImage(output, PM_READOUT_STACK_ANALYSIS_COUNT, numCols, numRows, PS_TYPE_U16, 0);
+    if (!counts) {
+        return false;
+    }
+    psImage *sigma = pmReadoutSetAnalysisImage(output, PM_READOUT_STACK_ANALYSIS_SIGMA, numCols, numRows, PS_TYPE_F32, NAN);
+    if (!sigma) {
+        return false;
+    }
+
+    // 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);
+
+    // set these even though the values are not yet set
+    output->data_exists = true;
+    output->parent->data_exists = true;
+    output->parent->parent->data_exists = true;
+
+    return success;
+}
+
+// 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);
+
+    // does required/desired data exist?
+    for (int i = 0; i < inputs->n; i++) {
+        pmReadout *readout = inputs->data[i]; // Readout of interest
+	psAssert(readout, "readout was not defined");
+	if (!readout->process) continue;
+        if (!readout->image) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "Image data is missing for image %d.\n", i);
+            return false;
+        }
+        if (params->variances && !readout->variance) {
+            psError(PS_ERR_UNEXPECTED_NULL, true,
+                    "Rejection based on variances requested, but no variances supplied for image %d.\n", i);
+            return false;
+        }
+    }
+
+    pmHDU *hdu = pmHDUFromReadout(output); // Output HDU
+    if (!hdu) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find HDU for readout.\n");
+        return false;
+    }
+
+#if 0
+    pthread_t id = pthread_self();
+    char name[64];
+    sprintf(name, "%x", (unsigned int)id);
+    psTimerStart(name);
+#endif
+
+    psStatsOptions combineStdev = 0; // Statistics option for variances
+    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");
+    }
+
+    psStats *stats = psStatsAlloc(params->combine | combineStdev); // The statistics to use in the combination
+    if (params->combine == PS_STAT_CLIPPED_MEAN) {
+        stats->clipSigma = params->rej;
+        stats->clipIter = params->iter;
+    }
+
+    psImage *counts = pmReadoutGetAnalysisImage(output, PM_READOUT_STACK_ANALYSIS_COUNT);
+    if (!counts) {
+        return false;
+    }
+    psImage *sigma = pmReadoutGetAnalysisImage(output, PM_READOUT_STACK_ANALYSIS_SIGMA);
+    if (!sigma) {
+        return false;
+    }
+
+    stats->options |= combineStdev;
+
+    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;
+    }
+
+    // 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_VECTOR_MASK); // Mask for stack
+    psVectorMaskType *maskData = mask->data.PS_TYPE_VECTOR_MASK_DATA;     // Dereference mask
+
+    psVector *variances = NULL;           // Stack of variances
+    psVector *errors = NULL;            // Stack of errors (sqrt of variance), for psVectorStats
+    psF32 *variancesData = NULL;          // Dereference variances
+    if (params->variances) {
+        variances = psVectorAlloc(inputs->n, PS_TYPE_F32); // Stack of variances
+        variancesData = variances->data.F32;
+    }
+    psVector *index = NULL;             // The indices to sort the pixels
+
+    float keepFrac = 1.0 - params->fracLow - params->fracHigh; // Fraction of pixels to keep
+    psImageMaskType maskVal = params->maskVal; // The mask value
+
+    #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
+            if (!readout->process) continue; 
+            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
+
+    // set up windows for visualization (if selected)
+    pmReadoutCombineVisualInit();
+
+    // Dereference output products
+    psF32 **outputImage  = output->image->data.F32; // Output image
+    psImageMaskType **outputMask   = output->mask->data.PS_TYPE_IMAGE_MASK_DATA; // Output mask
+    psF32 **outputVariance = NULL; // Output variance map
+    if (output->variance) {
+        outputVariance = output->variance->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(psVectorMaskType)); // Reset the mask
+            for (int r = 0; r < inputs->n; r++) {
+                pmReadout *readout = inputs->data[r]; // Input readout
+		if (!readout->process) {
+		    maskData[r] = 1;
+		    continue;
+		}
+                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
+
+                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.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn] & maskVal) {
+                    maskData[r] = 1;
+                    continue;
+                }
+
+                if (params->variances) {
+                    variancesData[r] = readout->variance->data.F32[yIn][xIn];
+                }
+
+                if (zero) {
+                    pixelsData[r] -= zero->data.F32[r];
+                }
+                if (scale) {
+                    pixelsData[r] *= invScale->data.F32[r];
+                    if (params->variances) {
+                        variancesData[r] *= invScale->data.F32[r] * invScale->data.F32[r];
+                    }
+                }
+
+                numValid++;
+            }
+
+            if (numValid == 0) {
+                outputMask[yOut][xOut] = params->blank;
+                outputImage[yOut][xOut] = NAN;
+                counts->data.U16[yOut][xOut] = 0;
+                sigma->data.F32[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++;
+                        numValid--;
+                    }
+                }
+                // 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++;
+                        numValid--;
+                    }
+                }
+            }
+            counts->data.U16[yOut][xOut] = numValid;
+
+            // XXXXX this step probably is very expensive : convert errors to variance everywhere?
+            if (params->variances) {
+                errors = (psVector*)psUnaryOp(errors, variances, "sqrt");
+            }
+
+            // Combination
+            if (!psVectorStats(stats, pixels, errors, mask, 1)) {
+		psError(PS_ERR_UNKNOWN, false, "error in pixel stats");
+		return false;
+	    }
+	    
+	    outputImage[yOut][xOut] = psStatsGetValue(stats, params->combine);
+
+	    if (!isfinite(outputImage[yOut][xOut])) {
+		pmReadoutCombineVisualPixels(pixels, mask, outputImage[yOut][xOut]);
+	    }
+
+	    if (isnan(outputImage[yOut][xOut])) {
+                outputImage[yOut][xOut] = NAN;
+                outputMask[yOut][xOut] = params->blank;
+                sigma->data.F32[yOut][xOut] = NAN;
+                if (params->variances) {
+                    outputVariance[yOut][xOut] = NAN;
+                }
+		continue;
+	    }
+	    outputMask[yOut][xOut] = 0;
+	    sigma->data.F32[yOut][xOut] = psStatsGetValue(stats, combineStdev);
+	    if (params->variances) {
+		float stdev = psStatsGetValue(stats, combineStdev);
+		outputVariance[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(variances);
+    psFree(errors);
+    psFree(stats);
+    psFree(invScale);
+
+    // fprintf (stderr, "done with combine %x : %f sec\n", (unsigned int) id, psTimerMark (name));
+    return true;
+}
+
+#if (HAVE_KAPA)
+#include <kapa.h>
+#include "pmKapaPlots.h"
+#include "pmVisual.h"
+
+static int kapa = -1;
+static bool plotFlag = true;
+
+// this init function only gets the ordinates for the first readout...
+bool pmReadoutCombineVisualInit(void) {
+    
+    if (!pmVisualIsVisual()) return true;
+
+    // skip if we have already opened the windows (or if none are requested...)
+    if (kapa != -1) return true;
+
+    pmVisualInitWindow(&kapa, "ppmerge");
+    return true;
+}
+
+bool pmReadoutCombineVisualPixels(psVector *pixels, psVector *mask, float mean) {
+
+    Graphdata graphdata;
+    float xline[2], yline[2];
+    
+    if (!pmVisualIsVisual()) return true;
+
+    if (!plotFlag) return true;
+
+    KapaInitGraph(&graphdata);
+
+    psVector *xAll = psVectorAlloc(pixels->n, PS_TYPE_F32);
+    psVector *xSub = psVectorAlloc(pixels->n, PS_TYPE_F32);
+    psVector *ySub = psVectorAlloc(pixels->n, PS_TYPE_F32);
+
+    // generate vectors of the unmasked values
+    int nSub = 0;
+    for (int j = 0; j < pixels->n; j++) {
+	xAll->data.F32[j] = j;
+	if (mask && mask->data.PS_TYPE_VECTOR_MASK_DATA[j]) continue;
+	xSub->data.F32[nSub] = j;
+	ySub->data.F32[nSub] = pixels->data.F32[j];
+	nSub ++;
+    }
+    xSub->n = ySub->n = nSub;
+    xAll->n = pixels->n;
+	
+    xline[0] = 0;
+    xline[1] = pixels->n;
+    yline[0] = mean;
+    yline[1] = mean;
+
+    // plot the unmasked values
+    pmVisualScaleGraphdata (&graphdata, xAll, pixels, false);
+    KapaSetGraphData(kapa, &graphdata);
+    KapaSetLimits(kapa, &graphdata);
+    KapaClearPlots (kapa);
+
+    KapaSetFont (kapa, "courier", 14);
+    KapaBox (kapa, &graphdata);
+    KapaSendLabel (kapa, "ordinate", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "pixel values", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName("black");
+    graphdata.style = KAPA_PLOT_POINTS;
+    graphdata.ptype = KAPA_POINT_CROSS;
+    KapaPrepPlot  (kapa, xSub->n, &graphdata);
+    KapaPlotVector(kapa, xSub->n, xSub->data.F32, "x");
+    KapaPlotVector(kapa, xSub->n, ySub->data.F32, "y");
+
+    graphdata.color = KapaColorByName("red");
+    graphdata.style = KAPA_PLOT_POINTS;
+    graphdata.ptype = KAPA_POINT_CIRCLE_OPEN;
+    KapaPrepPlot  (kapa, xAll->n, &graphdata);
+    KapaPlotVector(kapa, xAll->n, xAll->data.F32, "x");
+    KapaPlotVector(kapa, xAll->n, pixels->data.F32, "y");
+
+    graphdata.color = KapaColorByName("blue");
+    graphdata.style = KAPA_PLOT_CONNECT;
+    graphdata.ptype = KAPA_POINT_CIRCLE_OPEN;
+    KapaPrepPlot  (kapa, 2, &graphdata);
+    KapaPlotVector(kapa, 2, xline, "x");
+    KapaPlotVector(kapa, 2, yline, "y");
+
+    pmVisualAskUser (&plotFlag);
+    return true;
+}
+
+bool pmReadoutCombineVisualCleanup(void) {
+
+    if (!pmVisualIsVisual()) return true;
+    if (kapa == -1) return true;
+
+    KapaClose(kapa);
+    return true;
+}
+
+# else
+
+bool pmReadoutCombineVisualInit(void) { return true; }
+bool pmReadoutCombineVisualPixels(psVector *pixels, psVector *mask, float mean) { return true; }
+bool pmReadoutCombineVisualCleanup(void) { return true; }
+
+# endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmReadoutCombine.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmReadoutCombine.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmReadoutCombine.h	(revision 42651)
@@ -0,0 +1,54 @@
+/* @file  pmReadoutCombine.h
+ * @brief Combine multiple readouts
+ *
+ * @author George Gusciora, MHPCC
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.16 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-06 02:31:25 $
+ * 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
+    psImageMaskType maskVal;            ///< Mask value
+    psImageMaskType 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 variances;                     ///< Use the supplied variances (instead of calculated stdev)?
+} pmCombineParams;
+
+// Allocator for pmCombineParams
+pmCombineParams *pmCombineParamsAlloc(psStatsOptions statsOptions ///< Statistic to use for combination
+                                     );
+
+// check the input parameters and set up the output images
+bool pmReadoutCombinePrepare(pmReadout *output, const psArray *inputs, const pmCombineParams *params);
+
+/// 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
+                      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
+                     );
+
+bool pmReadoutCombineVisualInit(void);
+bool pmReadoutCombineVisualPixels(psVector *pixels, psVector *mask, float mean);
+bool pmReadoutCombineVisualCleanup(void);
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmStack.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmStack.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmStack.c	(revision 42651)
@@ -0,0 +1,2002 @@
+/** @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.48.2.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-19 17:59:50 $
+ *  Copyright 2004-2007 Institute for Astronomy, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h> // for memset
+#include <pslib.h>
+
+#include <gsl/gsl_cdf.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmReadoutStack.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
+//#define ADD_VARIANCE                    // Allow additional variance (besides variance factor)?
+#define NUM_DIRECT_STDEV 5              // For less than this number of values, measure stdev directly
+
+
+# if (0)
+#define TESTING                         Enable test output
+/* #define TEST_X 5745                       // x coordinate to examine */
+/* #define TEST_Y 5331                       // y coordinate to examine */
+// #define TEST_X 972
+// #define TEST_Y 3213
+//#define TEST_X 3289
+//#define TEST_Y 4810
+//#define TEST_RADIUS 0.5                 // Radius to examine
+//MEH -- streak-like junk md04s065i
+#define TEST_X 1129 
+#define TEST_Y 4256
+#define TEST_RADIUS 2.0                 // Radius to examine
+# endif
+
+# ifdef TESTING
+# define CHECKPIX(XPIX,YPIX,MSG,...) { if (PS_SQR(XPIX - TEST_X) + PS_SQR(YPIX - TEST_Y) <= PS_SQR(TEST_RADIUS)) { fprintf(stderr,MSG,__VA_ARGS__); } }
+# else
+# define CHECKPIX(XPIX,YPIX,MSG,...) { }
+# endif    
+
+// 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 *variances;                // Pixel variances
+    psVector *weights;                  // Pixel weightings
+    psVector *exps;                     // Pixel exposures
+    psVector *sources;                  // Pixel sources (which image did they come from?)
+    psVector *limits;                   // Rejection limits
+    psVector *suspects;                 // Pixel is suspect?
+    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->variances);
+    psFree(buffer->weights);
+    psFree(buffer->exps);
+    psFree(buffer->sources);
+    psFree(buffer->limits);
+    psFree(buffer->suspects);
+    psFree(buffer->sort);
+    return;
+}
+
+static 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->variances = psVectorAlloc(numImages, PS_TYPE_F32);
+    buffer->weights = psVectorAlloc(numImages, PS_TYPE_F32);
+    buffer->exps = psVectorAlloc(numImages, PS_TYPE_F32);
+    buffer->sources = psVectorAlloc(numImages, PS_TYPE_U16);
+    buffer->limits = psVectorAlloc(numImages, PS_TYPE_F32);
+    buffer->suspects = psVectorAlloc(numImages, PS_TYPE_U8);
+    buffer->sort = psVectorAlloc(numImages, PS_TYPE_F32);
+    return buffer;
+}
+
+
+// Deallocator for the stack data
+static void stackDataFree(pmStackData *data)
+{
+    psFree(data->readout);
+    psFree(data->reject);
+    psFree(data->inspect);
+    return;
+}
+
+// KMM functions to do bimodality rejection of pixels
+double gaussian(float x, float m, float s) {
+    return(pow(s * sqrt(2 * M_PI),-1) * exp(-0.5 * pow( (x - m) / s, 2)));
+}
+
+static void KMMcalculate(const psVector *values,
+			 float *Punimodal,int *iter,
+			 float *mU, float *sU,
+			 float *pi1, float *m1, float *s1,
+			 float *pi2, float *m2, float *s2,
+                         int xyrdebug) {
+    assert(values);
+    assert(values->type.type == PS_TYPE_F32);
+  
+    double logL_bimodal = 0, logL_unimodal;
+    psVector *P1 = psVectorAlloc(values->n,PS_TYPE_F32);
+    psVector *P2 = psVectorAlloc(values->n,PS_TYPE_F32);
+    int i;
+    int discrepant_index = -1;
+    double discrepant_value = 0;
+/*   int debug = 0; */
+  
+    // Calculate unimodal properties
+    *mU = 0;
+    *sU = 0;
+    logL_unimodal = 0;
+    for (i = 0; i < values->n; i++) { // Calculate mean
+	*mU += values->data.F32[i];
+    }
+    *mU /= values->n;
+    for (i = 0; i < values->n; i++) { // Calculate sigma
+	*sU += pow(values->data.F32[i] - *mU,2);
+
+	// Attempt to guess better starting values
+	if (pow(values->data.F32[i] - *mU,2) > discrepant_value) {
+	    discrepant_value = pow(values->data.F32[i] - *mU,2);
+	    discrepant_index = i;
+	}
+    
+    }
+    *sU = sqrt(*sU / values->n);
+    for (i = 0; i < values->n; i++) { // Calculate log likelihood
+	logL_unimodal += log(gaussian(values->data.F32[i],*mU,*sU));
+    }
+
+    if (xyrdebug == 1) { 
+	fprintf(stderr,"KMM uni: %d %f %d (%f %f)\n", 
+		xyrdebug,logL_unimodal,discrepant_index, 
+		*mU,*sU); 
+    } 
+
+    // Do EM loop
+    float dL = 0;
+    float oldL = -999;
+    *iter = 0;
+    logL_bimodal = logL_unimodal;
+
+    if (discrepant_index == -1) {
+	*m1 = *mU - 3 * *sU;
+	*m2 = *mU + 3 * *sU;
+	*s1 = *sU / 2;
+	*s2 = *sU / 2;
+    }
+    else {
+	// This is an attempt to speed up convergence. Find the largest contributor to sigma, and set one mean
+	// to that value.  Set the other mean to the mean of all other points with this one removed.  Next,
+	// set the sigmas to be equal to each other.  Take the value of sigma to be such that a point equidistant
+	// to the initial values of the two modes is equally not believed by either mode (2.5 sigma away).
+    
+	discrepant_value = values->data.F32[discrepant_index];
+    
+	if (discrepant_value >  *mU) {
+	    *m1 = ((*mU * values->n) - discrepant_value) / (values->n - 1);
+	    *m2 = discrepant_value;
+	}
+	else {
+	    *m1 = discrepant_value;
+	    *m2 = ((*mU * values->n) - discrepant_value) / (values->n - 1);
+	}
+	*s1 = fabs((*m1 - *m2) / 5);
+	*s2 = *s1;
+    }
+    
+    *pi1 = 0.5;
+    *pi2 = 0.5;
+
+    //MEH -- need to be double to help avoid 0 in norm
+    double g1,g2,norm;
+    float w1,w2;
+
+    // These should be options.
+    float KMM_TOLERANCE = 1e-6;
+    int KMM_MAX_ITERATIONS = 30;
+    float KMM_SMALL_NUMBER = 1e-5;
+    while (((dL > KMM_TOLERANCE)||(*iter < 3))&&(*iter < KMM_MAX_ITERATIONS)) {
+	*iter += 1;
+	dL = fabs(logL_bimodal - oldL);
+	oldL = logL_bimodal;
+/*     if (debug == 1) { */
+/*       fprintf(stderr,"KMM: %d %f %f %f %f (%f %f %f) (%f %f %f)\n", */
+/* 	      *iter,logL_unimodal,logL_bimodal,oldL,dL, */
+/* 	      *m1,*s1,*pi1, */
+/* 	      *m2,*s2,*pi2); */
+/*     } */
+
+	if (xyrdebug == 1) { 
+	    fprintf(stderr,"KMM EM iter: %d %f %f %f %f (%f %f %e) (%f %f %e)\n", 
+		    *iter,logL_unimodal,logL_bimodal,oldL,dL, 
+		    *m1,*s1,*pi1, 
+		    *m2,*s2,*pi2); 
+	} 
+
+	// Expectation/P-stage
+	for (i = 0; i < values->n; i++) { // Calculate probabilities for each mode
+	    g1 = gaussian(values->data.F32[i],*m1,*s1);
+	    g2 = gaussian(values->data.F32[i],*m2,*s2);
+	    norm = (*pi1 * g1 + *pi2 * g2);
+	    //MEH -- must protect denom from norm=0
+	    if (norm > 0) {
+		P1->data.F32[i] = (*pi1 * g1) / norm;
+		P2->data.F32[i] = (*pi2 * g2) / norm;
+	    } else {
+		P1->data.F32[i] = 0.0;
+		P2->data.F32[i] = 0.0;
+	    }	 
+ 
+	    if (xyrdebug == 1) {
+		fprintf(stderr,"KMM EM-P loop: %d %d %le %le %le\n",
+			*iter,i,norm,g1,g2);
+	    }
+
+	}
+	// Maximization/M-stage
+	logL_bimodal = 0;
+	w1 = 0;
+	w2 = 0;
+	for (i = 0; i < values->n; i++) { // Calculate log likelihood
+	    if (!((*pi1 == 0)||(*pi2 == 0))) {
+		logL_bimodal += log(*pi1 * gaussian(values->data.F32[i],*m1,*s1) +
+				    *pi2 * gaussian(values->data.F32[i],*m2,*s2));
+	    }
+	}
+	*m1 = 0;
+	*m2 = 0;
+	*s1 = 0;
+	*s2 = 0;
+	for (i = 0; i < values->n; i++) { // Calculate new means and weights
+	    *m1 += values->data.F32[i] * P1->data.F32[i];
+	    *m2 += values->data.F32[i] * P2->data.F32[i];
+
+	    w1 += P1->data.F32[i];
+	    w2 += P2->data.F32[i];
+
+	    if (xyrdebug == 1) {
+		fprintf(stderr,"KMM EM-M loop: %d %d (%f %f %f %e) (%f %f %f %e)\n",
+			*iter,i,*m1,values->data.F32[i],w1,P1->data.F32[i],*m2,values->data.F32[i],w2,P2->data.F32[i]);
+	    }
+
+	}
+	*m1 /= w1;
+	*m2 /= w2;
+	for (i = 0; i < values->n; i++) { // Calculate new sigmas
+	    *s1 += pow(values->data.F32[i] - *m1,2) * P1->data.F32[i];
+	    *s2 += pow(values->data.F32[i] - *m2,2) * P2->data.F32[i];
+	}
+	*s1 = sqrt(*s1 / w1);
+	*s2 = sqrt(*s2 / w2);
+
+	*pi1 = w1 / values->n;
+	*pi2 = w2 / values->n;
+
+	if (!isfinite(*pi1)) { // finite checks
+	    *pi1 = 0.0;
+	}
+	if (!isfinite(*pi2)) { // finite checks
+	    *pi2 = 0.0;
+	}
+	if (*s1 == 0) { // sigma may not be zero -- MEH -- nor <0 and need additive offset if m~0
+	    *s1 = fabsf(KMM_SMALL_NUMBER * *m1) + KMM_SMALL_NUMBER;
+	}
+	if (*s2 == 0) { // sigma may not be zero 
+	    *s2 = fabsf(KMM_SMALL_NUMBER * *m2) + KMM_SMALL_NUMBER;
+	}
+
+	if (xyrdebug == 1) { 
+	    fprintf(stderr,"KMM EM end: %d %f %f %f %f (%f %e %e %f) (%f %e %e %f)\n", 
+		    *iter,logL_unimodal,logL_bimodal,oldL,dL, 
+		    *m1,*s1,*pi1,w1, 
+		    *m2,*s2,*pi2,w2); 
+	} 
+
+    } // End EM phase
+
+    // Calculate Punimodal
+    double lambda = -2.0 * (logL_unimodal - logL_bimodal);
+    int    df     = 2 + 2 * 1; // I can't find my reference on this. 
+    if (lambda > 0) {
+	*Punimodal = gsl_cdf_chisq_Q(lambda,df);
+    }
+    else { // If lambda <= 0, then logL_unimodal > logL_bimodal, so Punimodal must be by definition 1.0
+	*Punimodal = 1.0;
+    }
+
+    if (xyrdebug == 1) { 
+	fprintf(stderr,"KMM calc Puni: %d %f %d %f\n", 
+		xyrdebug,lambda,df,*Punimodal); 
+    } 
+
+    psFree(P1);
+    psFree(P2);
+}
+
+static void KMMFindPopular(const psVector *values, float *Punimodal, float *mean, float *sigma, float *pi, int xyrdebug) {
+    float KMM_MINIMUM_PVALUE = 0.05; // Should be an option.
+    float mU,sU;
+    float pi1,m1,s1,pi2,m2,s2;
+    int iter;
+
+    assert(values);
+    assert(values->type.type == PS_TYPE_F32);
+  
+    KMMcalculate(values,Punimodal,&iter,
+		 &mU,&sU,
+		 &pi1,&m1,&s1,
+		 &pi2,&m2,&s2,xyrdebug);
+/*   fprintf(stdout,"%g %g : %g %g %g : %g %g %g : %g %d\t", */
+/* 	  mU,sU, */
+/* 	  m1,s1,pi1, */
+/* 	  m2,s2,pi2, */
+/* 	  *Punimodal,iter); */
+/*   if (iter > 3) { */
+/*     for (int i = 0; i < values->n; i++) { */
+/*       fprintf(stdout," %f ",values->data.F32[i]); */
+/*     } */
+/*   } */
+/*   fprintf(stdout,"\n"); */
+    if (*Punimodal > KMM_MINIMUM_PVALUE) {
+	// Is unimodal
+	*mean = mU;
+	*sigma = sU;
+	*pi = 1.0;
+    }
+    else {
+	// Is bimodal. Select most popular mode.
+	if (pi1 >= pi2) {
+	    *mean = m1;
+	    *sigma = s1;
+	    *pi = pi1;
+	}
+	else {
+	    *mean = m2;
+	    *sigma = s2;
+	    *pi = pi2;
+	}
+    }  
+}
+
+			       
+
+
+// Determine a mean value and variance for the combination
+// Not using psVectorStats because it assumes that the "weights" are errors, and weights by 1/error^2
+static bool combinationMeanVariance(float *mean, // Mean value, to return
+                                    float *var, // Variance value, to return
+                                    float *exp, // Exposure time, to return
+                                    float *expWeight,          // Weighted exposure time, to return
+                                    const psVector *values, // Values to combine
+                                    const psVector *variances, // Pixel variances to combine
+                                    const psVector *exps,      // Exposure times to combine
+                                    const psVector *weights // Weights to apply
+    )
+{
+    assert(mean);
+    assert(values && weights);
+    assert(values->n == weights->n);
+    assert((var && variances) || !var);
+    assert(!variances || variances->n == values->n);
+    assert(values->type.type == PS_TYPE_F32);
+    assert(!values || values->type.type == PS_TYPE_F32);
+    assert(weights->type.type == PS_TYPE_F32);
+
+    // 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).
+    // NOTE: in 2012.07.13, the variance calculation was changed without justification to the variance
+    // appropriate to a weighted mean, while the pixel mean was kept as the average unweighted by pixel variance
+
+    float sumValueWeight = 0.0;         // Sum of the value multiplied by the weight
+    float sumVarianceWeight = 0.0;     // Sum of the pixel variances multiplied by the global weights
+    float sumWeight = 0.0;              // Sum of the image weights
+    float sumExp = 0.0;                 // Sum of the exposure time
+    float sumExpWeight = 0.0;           // Sum of the exposure time multiplied by the global weights
+    int numGood = 0;                    // Number of good exposures
+    for (int i = 0; i < values->n; i++) {
+        sumValueWeight += values->data.F32[i] * weights->data.F32[i];
+        sumWeight += weights->data.F32[i];
+        if (variances) {
+	    //            sumVarianceWeight += variances->data.F32[i] * PS_SQR(weights->data.F32[i]);
+	    sumVarianceWeight += 1 / variances->data.F32[i];
+        }
+        if (exps) {
+            sumExp += exps->data.F32[i];
+            sumExpWeight += exps->data.F32[i] * weights->data.F32[i];
+            numGood++;
+        }
+    }
+
+    if (sumWeight <= 0) {
+        return false;
+    }
+
+    *mean = sumValueWeight / sumWeight;
+    if (var) {
+	//*var = sumVarianceWeight / PS_SQR(sumWeight);
+	*var = 1 / sumVarianceWeight;
+    }
+    if (exp) {
+        *exp = sumExp;
+    }
+    if (expWeight) {
+        *expWeight = sumExpWeight;
+    }
+    return true;
+}
+
+// Return the median and standard deviation for the pixels
+// Not using psVectorStats because it has additional allocations which slow things down
+static bool combinationMedianStdev(float *median, // Median value, to return
+                                   float *stdev, // Standard deviation value, to return
+                                   const psVector *values, // Values to combine
+                                   psVector *sortBuffer // Buffer for sorting
+    )
+{
+    assert(values);
+    assert(values->type.type == PS_TYPE_F32);
+    assert(sortBuffer && sortBuffer->nalloc >= values->n && sortBuffer->type.type == PS_TYPE_F32);
+
+    int num = values->n;                // Number of values
+    sortBuffer = psVectorSortIndex(sortBuffer, values);
+    if (!sortBuffer) {
+        *median = NAN;
+        *stdev = NAN;
+        return false;
+    }
+
+    if (num == 3) {
+        // Attempt to measure standard deviation with only three values (and one of those possibly corrupted)
+        *median = values->data.F32[sortBuffer->data.S32[1]];
+        if (stdev) {
+            float diff1 = values->data.F32[sortBuffer->data.S32[0]] - *median;
+            float diff2 = values->data.F32[sortBuffer->data.S32[2]] - *median;
+            // This factor of sqrt(2) might not be exact, but it's about right
+            *stdev = M_SQRT2 * PS_MIN(fabsf(diff1), fabsf(diff2));
+        }
+    } else {
+        *median = num % 2 ? values->data.F32[sortBuffer->data.S32[num / 2]] :
+            (values->data.F32[sortBuffer->data.S32[num / 2 - 1]] +
+             values->data.F32[sortBuffer->data.S32[num / 2]]) / 2.0;
+        if (stdev) {
+            if (num <= NUM_DIRECT_STDEV) {
+                // If there are not many values, the direct standard deviation is better
+                double sum = 0.0;
+                for (int i = 0; i < num; i++) {
+                    sum += PS_SQR(values->data.F32[sortBuffer->data.S32[i]] - *median);
+                }
+                *stdev = sqrt(sum / (double)(num - 1));
+            } else {
+                // Standard deviation from the interquartile range
+                *stdev = 0.74 * (values->data.F32[sortBuffer->data.S32[(int)(0.75 * num)]] -
+                                 values->data.F32[sortBuffer->data.S32[(int)(0.25 * num)]]);
+            }
+        }
+    }
+
+    return true;
+}
+
+// Return the weighted Olympic mean for the pixels
+static float combinationWeightedOlympic(const psVector *values, // Values to combine
+                                        const psVector *weights, // Weights to combine
+                                        float frac, // Fraction to discard
+                                        psVector *sortBuffer // Buffer for sorting
+    )
+{
+    int numGood = values->n;            // Number of good values
+
+    int numBad = frac * numGood + 0.5;  // Number of bad values
+    int low = numBad / 2, high = low + numGood - numBad; // Indices (modulo masked pixels)
+
+    sortBuffer = psVectorSortIndex(sortBuffer, values);
+
+    double sumValues = 0.0, sumWeight = 0.0; // Sums for weighted mean
+    for (int i = 0, j = 0; i < values->n; i++) {
+        int index = sortBuffer->data.S32[i]; // Index of interest
+        j++;
+        if (j > high) {
+            break;
+        }
+        if (j <= low) {
+            continue;
+        }
+        sumValues += values->data.F32[index] * weights->data.F32[index];
+        sumWeight += weights->data.F32[index];
+    }
+
+    return sumValues / sumWeight;
+}
+
+// Mark a pixel for inspection
+// Value in pixel doesn't seem to agree with the stack, so need to look closer
+static inline void combineMarkInspect(const psArray *inputs, // Stack data
+                                      int x, int y, // Pixel
+                                      int source // Source image index
+    )
+{
+    CHECKPIX(x, y, "Marking image %d, pixel %d,%d for inspection\n", source, x, y);
+    pmStackData *data = inputs->data[source]; // Stack data of interest
+    if (!data) {
+        psWarning("Can't find input data for source %d", source);
+        return;
+    }
+    data->inspect = psPixelsAdd(data->inspect, data->inspect->nalloc, x, y);
+    return;
+}
+
+// Mark a pixel for rejection
+// Cannot possibly inspect this pixel and confirm that it's good.
+// e.g., Only a single input
+static inline void combineMarkReject(const psArray *inputs, // Stack data
+                                     int x, int y, // Pixel
+                                     int source // Source image index
+    )
+{
+    CHECKPIX(x, y, "Marking pixel image %d, pixel %d,%d for rejection\n", source, x, y);
+    pmStackData *data = inputs->data[source]; // Stack data of interest
+    if (!data) {
+        psWarning("Can't find input data for source %d", source);
+        return;
+    }
+    data->reject = psPixelsAdd(data->reject, data->reject->nalloc, x, y);
+    return;
+}
+#if (0)
+// Currently unused function to reject inputs for a given pixel(x,y) based on the selecting the
+// most popular mode after running the KMM test, and rejecting all inputs that belong to the
+// least popular mode.
+static void KMMRejectUnpopular(const psArray *inputs, int x, int y) {
+    float KMM_MINIMUM_PVALUE = 0.05;
+    float mU,sU;
+    float Punimodal,pi1,m1,s1,pi2,m2,s2;
+    int iter;
+    int j,k;
+
+    psVector *values = psVectorAlloc(inputs->n, PS_TYPE_F32);
+    k = 0;
+    for (j = 0; j < inputs->n; j++) {
+	pmStackData *data = inputs->data[j]; // Stack data of interest
+	if (!data) {
+	    k++;
+	    continue;
+	}
+	psImage *image = data->readout->image; // Image of interest
+	int xIn = x - data->readout->col0, yIn = y - data->readout->row0; // Coordinates on input readout
+	values->data.F32[j - k] = image->data.F32[yIn][xIn];
+    }
+  
+    KMMcalculate(values,&Punimodal,&iter,
+		 &mU,&sU,
+		 &pi1,&m1,&s1,
+		 &pi2,&m2,&s2);
+
+    CHECKPIX(x, y, 
+	     "KMM Unpopular Test: %d,%d: Puni: %g in %d",x,y,Punimodal,iter);  
+    if (Punimodal < KMM_MINIMUM_PVALUE) {
+	int i;
+	float g1,g2,norm;
+	float P1,P2;
+
+	for (i = 0; i < values->n; i++) { // Calculate probabilities for each mode
+	    g1 = gaussian(values->data.F32[i],m1,s1);
+	    g2 = gaussian(values->data.F32[i],m2,s2);
+	    norm = (pi1 * g1 + pi2 * g2);
+	    P1 = (pi1 * g1) / norm;
+	    P2 = (pi2 * g2) / norm;
+
+	    CHECKPIX(x, y, "KMM Unpopular Rejection: %d,%d: %f(%d): %d %f %f:(%f %f %f ) %f:(%f %f %f) rejection? %d %d\n",
+		     x, y,
+		     Punimodal,iter,
+		     i, values->data.F32[i],
+		     P1,m1,s1,pi1,
+		     P2,m2,s2,pi2,
+		     (pi1 > pi2)&&(P1 < P2),
+		     (pi1 < pi2)&&(P1 > P2));
+	    if ((pi1 > pi2)&&(P1 < P2)) { // mode 1 is more popular, but this element belongs to mode 2
+		combineMarkReject(inputs,x,y,i);
+	    }
+	    if ((pi1 < pi2)&&(P1 > P2)) { // mode 2 is more popular, but this element belongs to mode 1
+		combineMarkReject(inputs,x,y,i);
+	    }
+	}
+    }
+    psFree(values);
+    // else do nothing.
+}
+
+// Currently unused function to reject inputs for a given pixel(x,y) based on the selecting the
+// faintest mode as determined by the KMM test, and rejecting all inputs that belong to the brighest.
+static void KMMRejectBright(const psArray *inputs, int x, int y) {
+    float KMM_MINIMUM_PVALUE = 0.05;
+    float mU,sU;
+    float Punimodal,pi1,m1,s1,pi2,m2,s2;
+    int iter;
+    int j;
+
+    psVector *values = psVectorAlloc(inputs->n, PS_TYPE_F32);
+    for (j = 0; j < inputs->n; j++) {
+	pmStackData *data = inputs->data[j]; // Stack data of interest
+	psImage *image = data->readout->image; // Image of interest
+	int xIn = x - data->readout->col0, yIn = y - data->readout->row0; // Coordinates on input readout
+	values->data.F32[j] = image->data.F32[yIn][xIn];
+    }
+  
+    KMMcalculate(values,&Punimodal,&iter,
+		 &mU,&sU,
+		 &pi1,&m1,&s1,
+		 &pi2,&m2,&s2);
+    if (Punimodal < KMM_MINIMUM_PVALUE) {
+	int i;
+	float g1,g2,norm;
+	float P1,P2;
+
+	for (i = 0; i < values->n; i++) { // Calculate probabilities for each mode
+	    g1 = gaussian(values->data.F32[i],m1,s1);
+	    g2 = gaussian(values->data.F32[i],m2,s2);
+	    norm = (pi1 * g1 + pi2 * g2);
+	    P1 = (pi1 * g1) / norm;
+	    P2 = (pi2 * g2) / norm;
+
+	    if ((m1 > m2)&&(P1 > P2)) { // m1 is larger, and this element belongs to mode 1
+		combineMarkReject(inputs,x,y,i);
+	    }
+	    if ((m1 < m2)&&(P1 < P2)) { // m2 is larger, and this element belongs to mode 2
+		combineMarkReject(inputs,x,y,i);
+	    }
+	}
+    }
+    psFree(values);
+    // else do nothing.
+}
+#endif // End if(0) to prevent KMMReject{Unpopular|Bright} from being defined.
+
+// Extract vectors for simple combination/rejection operations
+static void combineExtract(int *num,                        // Number of good pixels
+                           bool *suspect,                   // Any suspect pixels?
+			   psImageMaskType *badMask,	    // OR of all bad (masked) pixels
+			   psImageMaskType *goodMask,	    // OR of all good (unmasked) pixels
+                           combineBuffer *buffer, // Buffer with vectors
+                           psImage *image, // Combined image, for output
+                           psImage *mask, // Combined mask, for output
+                           psImage *variance, // Combined variance map, for output
+                           const psArray *inputs, // Stack data
+                           const psVector *weights, // Global (single value) weights for data, or NULL
+                           const psVector *exps,    // Exposures for data, or NULL
+                           const psVector *addVariance, // Additional variance for data
+                           const psVector *reject, // Indices of pixels to reject, or NULL
+                           int x, int y, // Coordinates of interest; frame of output image
+                           psImageMaskType badMaskBits, // Value to mask as 'bad'
+                           psImageMaskType suspectMaskBits // Value to mask as 'suspect'
+    )
+{
+    // Rudimentary error checking
+    assert(buffer);
+    assert(image);
+    assert(mask);
+    assert(inputs);
+
+    psVector *pixelData = buffer->pixels; // Values for the pixel of interest
+    psVector *pixelVariances = variance ? buffer->variances : NULL; // Variances for the pixel of interest
+    psVector *pixelWeights = buffer->weights; // Image weights for the pixel of interest
+    psVector *pixelExps = buffer->exps;       // Exposure times
+    psVector *pixelSources = buffer->sources; // Sources for the pixel of interest
+    psVector *pixelLimits = buffer->limits; // Limits for the pixel of interest
+    psVector *pixelSuspects = buffer->suspects; // Is the pixel suspect?
+
+    if (suspect) {
+        *suspect = false;
+    }
+
+    // mask values to store possible mask combinations
+    *badMask = 0;
+    *goodMask = 0xffff;
+
+    int nGoodBits[16]; // accumulate the good pixel bits here for fuzzy logic
+    psAssert (sizeof(psImageMaskType) == 2, "psImageMaskType is not the expected size");
+    memset (nGoodBits, 0, 16*sizeof(int));
+
+
+    // Extract the pixel and mask data    
+    int numGood = 0;                    // Number of good pixels
+    for (int i = 0, j = 0; i < inputs->n; i++) {
+        // Check if this pixel has been rejected.  Assumes that the rejection pixel list is sorted --- it
+        // should be because of how pixelMapGenerate works
+        if (reject && reject->data.U16[j] == i) {
+	    // pixels can be rejected because:
+	    // 1) only 1 input pixel (and 'safe' is true)
+	    // 2) only 2 input pixels were available and they were mutually inconsistent (or variance info was missing)
+	    // 3) NOTE : ifdef'ed out code for 3 inputs case 
+	    // 4) outlier from sample of N pixels
+	    // 5) some of these may have been suspect.
+	    // XXX raise a specific mask bit for these (currently results in BLANK)
+            j++;
+
+	    pmStackData *data = inputs->data[i]; // Stack data of interest
+	    if (data) {
+		int xIn = x - data->readout->col0, yIn = y - data->readout->row0; // Coordinates on input readout
+		psImage *mask = data->readout->mask; // Mask of interest
+		*badMask |= mask->data.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn]; // save the bad bits used
+		CHECKPIX(x, y, "reject: adding bit to mask: %d : %x (badMask = %x)\n", i, mask->data.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn], *badMask);
+	    } else {
+		CHECKPIX(x, y, "reject: no item in data (badMask = %x)\n", *badMask);
+	    }
+            continue;
+        }
+
+        pmStackData *data = inputs->data[i]; // Stack data of interest
+        if (!data) {
+	    CHECKPIX(x, y, "skip: no item in data (badMask = %x)\n", *badMask);
+            continue;
+        }
+	
+        int xIn = x - data->readout->col0, yIn = y - data->readout->row0; // Coordinates on input readout
+        psImage *mask = data->readout->mask; // Mask of interest
+        if (mask->data.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn] & badMaskBits) {
+	    *badMask |= (mask->data.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn] & badMaskBits); // save the bad bits used
+	    CHECKPIX(x, y, "skip: adding bit to mask: %d : %x (badMask = %x)\n", i, mask->data.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn], *badMask);
+            continue;
+        }
+
+        pixelSuspects->data.U8[numGood] = mask->data.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn] & suspectMaskBits ? true : false;
+
+	// *goodMask &= mask->data.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn]; // save the mask bits still used
+	// check for set bits and increment counter as appropriate
+	// count the number of times a given mask bit is set in the input pixels.
+	// NOTE: since we have explicitly skipped the pixels with any bad bits, these are only
+	// the suspect bits (nGoodBits is a bit of a misnomer: it is more like 'nSuspectBitsForGoodInputs'
+	{ 
+	    psImageMaskType value = 0x0001;
+	    for (int nbit = 0; nbit < 16; nbit ++) {
+		if (mask->data.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn] & value) {
+		    nGoodBits[nbit] ++;
+		}
+		value <<= 1;
+	    }
+	}
+
+        psImage *image = data->readout->image; // Image of interest
+        psImage *variance = data->readout->variance; // Variance map of interest
+        pixelData->data.F32[numGood] = image->data.F32[yIn][xIn];
+        if (variance) {
+            pixelVariances->data.F32[numGood] = variance->data.F32[yIn][xIn] * addVariance->data.F32[i];
+        }
+        pixelWeights->data.F32[numGood] = data->weight;
+        pixelExps->data.F32[numGood] = data->exp;
+        pixelSources->data.U16[numGood] = i;
+        numGood++;
+
+	CHECKPIX(x, y, "keep: %d : %x (badMask = %x)\n", i, mask->data.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn], *badMask);
+    }
+    
+    pixelData->n = numGood;
+    if (variance) {
+        pixelVariances->n = numGood;
+    }
+    pixelWeights->n = numGood;
+    pixelSources->n = numGood;
+    pixelLimits->n = numGood;
+    pixelSuspects->n = numGood;
+    *num = numGood;
+
+    // set the mask bits if nGoodBits[i] > f*numGood
+    {
+# define SUSPECT_FRACTION 0.65
+	*goodMask = 0x0000;
+	psImageMaskType value = 0x0001;
+	for (int nbit = 0; nbit < 16; nbit ++) {
+	    if (nGoodBits[nbit] > SUSPECT_FRACTION*numGood) {
+		*goodMask |= value;
+	    }
+	    value <<= 1;
+	}
+    }
+
+#ifdef TESTING
+    if (PS_SQR(x - TEST_X) + PS_SQR(y - TEST_Y) <= PS_SQR(TEST_RADIUS)) {
+        for (int i = 0; i < numGood; i++) {
+	    fprintf(stderr,"Input %d, pixel %d,%d (%" PRIu16 "): %f %f (%g) %g %f %d %x %x -> %x %x\n",
+                    i, x, y, pixelSources->data.U16[i],
+		    pixelData->data.F32[i], pixelVariances->data.F32[i],
+                    addVariance->data.F32[i],
+		    pixelWeights->data.F32[i], pixelExps->data.F32[i],
+                    pixelSuspects->data.U8[i],
+		    badMaskBits, suspectMaskBits,
+		    *badMask, *goodMask);
+        }
+    }
+#endif
+
+    return;
+}
+
+// Combine pixels
+static void combinePixels(psImage *image, // Combined image, for output
+                          psImage *mask, // Combined mask, for output
+                          psImage *variance, // Combined variance map, for output
+                          psImage *exp,   // Exposure map (time), for output
+                          psImage *expnum,       // Exposure map (number) for output
+                          psImage *expweight,    // Exposure map (weighted time) for output
+                          int num,      // Number of good pixels
+                          combineBuffer *buffer, // Buffer with vectors
+                          int x, int y, // Coordinates of interest; frame of output image
+                          psImageMaskType blankMask, // Value for empty pixels
+                          psImageMaskType badMask, // Value for bad pixels
+                          psImageMaskType goodMask, // Value for good pixels
+                          bool safe,           // Safe combination?
+			  int nminpix,         // Minimum number of input per pixel
+                          float invTotalWeight    // Inverse of total weight for all inputs
+    )
+{
+    psVector *pixelData = buffer->pixels; // Values for the pixel of interest
+    psVector *pixelVariances = variance ? buffer->variances : NULL; // Variances for the pixel of interest
+    psVector *pixelWeights = buffer->weights; // Image weights for the pixel of interest
+    psVector *pixelExps = buffer->exps;       // Exposure times
+
+    // Default option is that the pixel is bad
+    float imageValue = NAN, varianceValue = NAN; // Value for combined image and variance map
+    float expValue = 0.0, expWeightValue = NAN; // Exposure value (straight, and weighted)
+
+    // default output mask value.  badMask is the OR of all unused input pixels.  
+    // if there are no input pixels, this value will be 0, in which case we want to set the output pixel to BLANK.
+    // if there are only good input pixels, and they do not result in a valid pixel, we still want to set this to BLANK.
+    psImageMaskType maskValue = badMask ? badMask : blankMask;    // Value for combined mask 
+
+    CHECKPIX(x, y, "bad vs good : %x %x %x\n", maskValue, badMask, blankMask);
+
+    //MEH -- hackish adding of lower limit for input per pixel 
+    int numN = num;
+    if (num < nminpix) {
+        CHECKPIX(x, y, "Nmin (%d) inputs (%d) to combine, pixel %d,%d is manually set bad\n", nminpix, numN, x, y);
+        numN = 0;
+    }
+    switch (numN) {
+      case 0: {
+          // Nothing to combine: it's bad
+	  CHECKPIX(x, y, "No inputs to combine, pixel %d,%d is bad.\n", x, y);
+          break;
+      }
+      case 1: {
+          // Accept the single pixel unless we have to be safe
+          if (!safe) {
+              imageValue = pixelData->data.F32[0];
+              if (variance) {
+                  varianceValue = pixelVariances->data.F32[0];
+              }
+              if (exp) {
+                  expValue = pixelExps->data.F32[0];
+                  expWeightValue = pixelExps->data.F32[0];
+              }
+              maskValue = goodMask;
+	      CHECKPIX(x, y, "Single input to combine, safety off, pixel %d,%d --> %f\n", x, y, imageValue);
+          } else {
+	      CHECKPIX(x, y, "Single input to combine, safety on, pixel %d,%d is bad.\n", x, y);
+	  }
+          break;
+      }
+      case 2: {
+          // Automatically accept the mean of the pixels only if we're not playing safe
+          if (!safe) {
+              if (combinationMeanVariance(&imageValue, &varianceValue, &expValue, &expWeightValue, pixelData, pixelVariances, pixelExps, pixelWeights)) {
+		  maskValue = goodMask;
+		  CHECKPIX(x, y, "Two inputs to combine using unsafe, pixel %d,%d --> %f %f\n", x, y, imageValue, varianceValue);
+              }
+          } else {
+	      CHECKPIX(x, y, "Two inputs to combine, safety on, pixel %d,%d is bad\n", x, y);
+          }
+          break;
+      }
+      default: {
+          // Can combine without too much worrying
+          if (!combinationMeanVariance(&imageValue, &varianceValue, &expValue, &expWeightValue,
+                                       pixelData, pixelVariances, pixelExps, pixelWeights)) {
+              break;
+          }
+	  maskValue = goodMask;
+	  CHECKPIX(x, y, "Combined inputs, pixel %d,%d --> %f %f\n", x, y, imageValue, varianceValue);
+          break;
+      }
+    }
+
+    image->data.F32[y][x] = imageValue;
+    mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] = maskValue;
+    if (variance) {
+        variance->data.F32[y][x] = varianceValue;
+    }
+    if (exp) {
+        exp->data.F32[y][x] = expValue;
+    }
+    if (expnum) {
+        expnum->data.PS_TYPE_IMAGE_MASK_DATA[y][x] = num;
+    }
+    if (expweight) {
+        expweight->data.F32[y][x] = expWeightValue * invTotalWeight;
+    }
+
+    return;
+}
+
+
+// Test pixels to be combined
+// Returns false to repeat without suspect pixels
+static bool combineTest(int num,      // Number of good pixels
+                        bool suspect, // Does the stack contain suspect pixels?
+                        psArray *inputs,       // Original inputs (for flagging)
+                        combineBuffer *buffer, // Buffer with vectors
+                        int x, int y, // Coordinates of interest; frame of output image
+                        float iter, // Number of rejection iterations per input
+                        float rej, // Number of standard deviations at which to reject
+                        float sys,    // Relative systematic error
+                        float olympic,// Fraction of values to discard (Olympic weighted mean)
+                        bool useVariance, // Use variance for rejection when combining?
+                        bool safe    // Combine safely?
+    )
+{
+    if (iter <= 0) {
+        return true;
+    }
+
+    int numIter = PS_MAX(iter * num, 1); // Number of iterations
+
+    CHECKPIX(x, y, "Testing pixel %d,%d: %d %f %f %f %d %d\n", x, y, numIter, rej, sys, olympic, useVariance, safe);
+
+    psVector *pixelData = buffer->pixels; // Values for the pixel of interest
+    psVector *pixelWeights = buffer->weights; // Is the pixel suspect?
+    psVector *pixelVariances = buffer->variances; // Variances for the pixel of interest
+    psVector *pixelSources = buffer->sources; // Sources for the pixel of interest
+    psVector *pixelSuspects = buffer->suspects; // Is the pixel suspect?
+    psVector *pixelLimits = buffer->limits; // Is the pixel suspect?
+    //MEH -- adding a debug option for TESTING xyr position but could be better..
+    int xyrdebug = 0;
+
+    // KMM values;
+    float Punimodal = 1.0, KMMmean = NAN, KMMsigma = NAN, KMMpi = NAN;
+    int KMM_MINIMUM_INPUTS = 6;
+    bool useKMM = false;
+    if (num >= KMM_MINIMUM_INPUTS) {
+	useKMM = true;
+    }
+    
+    // Set up rejection limits
+    float rej2 = PS_SQR(rej); // Rejection level squared
+    if (num > 2 && useVariance) {
+        // Convert rejection limits --- saves doing it later multiple times
+        // Using squared rejection limit because it's cheaper than sqrts
+        double sumWeights = 0.0;
+
+	// Determine the systematic error from the most popular population in the sample
+	// This should probably be an option
+	if (useKMM) {
+#ifdef TESTING
+            if (PS_SQR(x - TEST_X) + PS_SQR(y - TEST_Y) <= PS_SQR(TEST_RADIUS)) {
+                xyrdebug = 1;
+            }
+# endif
+	    KMMFindPopular(pixelData,&Punimodal,&KMMmean,&KMMsigma,&KMMpi,xyrdebug);
+	    CHECKPIX(x,y,"KMM Popularity Contest: (%d,%d) Puni: %g Mean: %f Sigma %f Pi: %f\n",
+		     x,y,Punimodal,KMMmean,KMMsigma,KMMpi);
+	}
+        for (int i = 0; i < num; i++) {
+            sumWeights += pixelWeights->data.F32[i];
+        }
+        for (int i = 0; i < num; i++) {
+	    // Systematic error contributes to the rejection level
+	    float sysVar;
+	    if (useKMM) { // If we can trust KMM results, set the systematic variance
+		sysVar = PS_SQR(KMMsigma);
+	    }
+	    else { // Otherwise, use the 10% systematic variance we've done in the past.
+		sysVar = PS_SQR(sys * pixelData->data.F32[i]);
+	    }
+
+	    CHECKPIX(x, y, "Variance %d (%d), pixel %d,%d: %f %f %f\n", 
+		     i, pixelSources->data.U16[i], x, y, 
+		     pixelVariances->data.F32[i], sysVar, 1.0 - pixelWeights->data.F32[i] / sumWeights);
+            pixelLimits->data.F32[i] = rej2 * (pixelVariances->data.F32[i] + sysVar);
+        }
+    }
+
+    int maskIndex = 0;                  // Index of pixel to mask
+    int totalClipped = 0;               // Total number of pixels clipped
+    for (int i = 0; i < numIter && maskIndex >= 0; i++) {
+        maskIndex = -1;                 // Nothing to reject
+
+        switch (num) {
+          case 0:
+            break;
+          case 1:
+            if (i == 0 && safe) {
+                combineMarkReject(inputs, x, y, pixelSources->data.U16[0]);
+            }
+            break;
+          case 2: {
+              if (useVariance) {
+                  // Use variance to check that the two are consistent
+                  float diff = 0.5 * (pixelData->data.F32[0] - pixelData->data.F32[1]); // Mean flux difference
+                  float var1 = pixelVariances->data.F32[0]; // Variance of first
+                  float var2 = pixelVariances->data.F32[1]; // Variance of second
+                  // Systematic error contributes to the rejection level
+                  var1 += PS_SQR(sys * pixelData->data.F32[0]);
+                  var2 += PS_SQR(sys * pixelData->data.F32[1]);
+
+                  float sigma2 = var1 + var2; // Combined variance
+                  if (PS_SQR(diff) > rej2 * sigma2) {
+                      // Not consistent: don't believe either!
+                      if (i == 0 && suspect) {
+                          combineMarkReject(inputs, x, y, pixelSources->data.U16[0]);
+                          combineMarkReject(inputs, x, y, pixelSources->data.U16[1]);
+                      } else {
+                          combineMarkInspect(inputs, x, y, pixelSources->data.U16[0]);
+                          combineMarkInspect(inputs, x, y, pixelSources->data.U16[1]);
+                      }
+		      CHECKPIX(x, y, "Flagged both inputs for pixel %d,%d (%f > %f x %f\n)", x, y, diff, rej, sqrtf(sigma2));
+                  }
+              } else if (i == 0 && safe) {
+                  // Can't test them, and we want to be safe, so reject
+                  combineMarkReject(inputs, x, y, pixelSources->data.U16[0]);
+                  combineMarkReject(inputs, x, y, pixelSources->data.U16[1]);
+              }
+              break;
+          }
+#if 0
+          case 3: {
+              // Want to be a bit careful on the rejection than for a larger number of inputs
+              if (!useVariance) {
+                  return combineTestGeneral(num, suspect, inputs, buffer, x, y, numIter, rej, sys,
+                                            olympic, useVariance, safe, allowSuspect);
+              }
+
+              // Differences between pixel values
+              float diff01 = pixelData->data.F32[0] - pixelData->data.F32[1];
+              float diff12 = pixelData->data.F32[1] - pixelData->data.F32[2];
+              float diff20 = pixelData->data.F32[2] - pixelData->data.F32[0];
+              // Variance for each pixel
+              float var0 = pixelVariances->data.F32[0] + PS_SQR(sys * pixelData->data.F32[0]);
+              float var1 = pixelVariances->data.F32[1] + PS_SQR(sys * pixelData->data.F32[1]);
+              float var2 = pixelVariances->data.F32[2] + PS_SQR(sys * pixelData->data.F32[2]);
+              // Errors in pixel differences
+              float err01 = var0 + var1;
+              float err12 = var1 + var2;
+              float err20 = var2 + var0;
+
+#ifdef TESTING
+              if (PS_SQR(x - TEST_X) + PS_SQR(y - TEST_Y) <= PS_SQR(TEST_RADIUS)) {
+                  fprintf(stderr, "Diff 0-1: %f %f\n", diff01, err01);
+                  fprintf(stderr, "Diff 1-2: %f %f\n", diff12, err12);
+                  fprintf(stderr, "Diff 2-0: %f %f\n", diff20, err20);
+              }
+#endif
+
+              int badPairs = 0;         // Number of bad pairs
+              bool bad01 = false, bad12 = false, bad20 = false; // Pair is bad?
+              if (PS_SQR(diff01) > rej2 * err01) {
+                  bad01 = true;
+                  badPairs++;
+              }
+              if (PS_SQR(diff12) > rej2 * err12) {
+                  bad12 = true;
+                  badPairs++;
+              }
+              if (PS_SQR(diff20) > rej2 * err20) {
+                  bad20 = true;
+                  badPairs++;
+              }
+
+              if (badPairs > 0 && allowSuspect && suspect) {
+                  return false;
+              }
+
+              switch (badPairs) {
+                case 0:
+                  // Nothing to worry about!
+                  break;
+                case 1:
+                  // Can't tell which image is bad, so be sure to get it
+                  if (bad01) {
+                      combineMarkInspect(inputs, x, y, pixelSources->data.U16[0]);
+                      combineMarkInspect(inputs, x, y, pixelSources->data.U16[1]);
+                      break;
+                  }
+                  if (bad12) {
+                      combineMarkInspect(inputs, x, y, pixelSources->data.U16[1]);
+                      combineMarkInspect(inputs, x, y, pixelSources->data.U16[2]);
+                      break;
+                  }
+                  if (bad20) {
+                      combineMarkInspect(inputs, x, y, pixelSources->data.U16[2]);
+                      combineMarkInspect(inputs, x, y, pixelSources->data.U16[0]);
+                      break;
+                  }
+                  psAbort("Should never get here");
+                case 2:
+                  if (bad01 && bad12) {
+                      // 2 and 0 are good
+                      combineMarkInspect(inputs, x, y, pixelSources->data.U16[1]);
+                      break;
+                  }
+                  if (bad12 && bad20) {
+                      // 0 and 1 are good
+                      combineMarkInspect(inputs, x, y, pixelSources->data.U16[2]);
+                      break;
+                  }
+                  if (bad20 && bad01) {
+                      // 1 and 2 are good
+                      combineMarkInspect(inputs, x, y, pixelSources->data.U16[0]);
+                      break;
+                  }
+                  psAbort("Should never get here");
+                case 3:
+                  // Everything's bad
+                  combineMarkInspect(inputs, x, y, pixelSources->data.U16[0]);
+                  combineMarkInspect(inputs, x, y, pixelSources->data.U16[1]);
+                  combineMarkInspect(inputs, x, y, pixelSources->data.U16[2]);
+                  break;
+              }
+              break;
+          }
+#endif
+          default: {
+              if (useVariance) {
+		  float median;
+		  if ((useKMM)&&(Punimodal < 0.05)) {
+		      median = KMMmean;
+		  }
+		  else {
+		      median = combinationWeightedOlympic(pixelData, pixelWeights,
+							  olympic, buffer->sort); // Median for stack
+		  }
+		
+		  CHECKPIX(x, y, "Flag with variance pixel %d,%d: median = %f\n", x, y, median);
+                  float worst = -INFINITY; // Largest deviation
+                  for (int j = 0; j < num; j++) {
+		      float diff = pixelData->data.F32[j] - median; // Difference from expected
+		      CHECKPIX(x, y, "Testing input %d for pixel %d,%d: %f\n", j, x, y, diff);
+
+                      // Comparing squares --- cheaper than lots of sqrts
+                      // pixelVariances includes the rejection limit, from above
+                      float diff2 = PS_SQR(diff); // Square difference
+		      CHECKPIX(x,y, "Input %d, pixel %d,%d (%" PRIu16 "): %f %f (%f) %f %f :: %f %f %f %f\n",
+			       i, x, y, pixelSources->data.U16[j], pixelData->data.F32[j], pixelVariances->data.F32[j],
+			       1.0, pixelWeights->data.F32[j], 1.0,
+			       pixelLimits->data.F32[j], diff2, diff2 / pixelLimits->data.F32[j],worst);
+
+                      if (diff2 > pixelLimits->data.F32[j]) {
+                          float dev = diff2 / pixelLimits->data.F32[j]; // Deviation
+                          if (dev > worst) {
+                              worst = dev;
+                              maskIndex = j;
+                          }
+                      }
+                  }
+              } else {
+                  float median = NAN, stdev = NAN;  // Median and stdev of the combination, for rejection
+                  combinationMedianStdev(&median, &stdev, pixelData, buffer->sort);
+                  float limit = rej * stdev; // Rejection limit
+		  CHECKPIX(x, y, "Flag without variance pixel %d,%d; median = %f, stdev = %f, limit = %f\n", x, y, median, stdev, limit);
+                  float worst = -INFINITY; // Largest deviation
+                  for (int j = 0; j < num; j++) {
+                      float diff = fabsf(pixelData->data.F32[j] - median); // Difference from expected
+
+                      if (diff > limit) {
+                          float dev = diff / limit; // Deviation
+                          if (dev > worst) {
+                              worst = dev;
+                              maskIndex = j;
+                          }
+                      }
+                  }
+              }
+          }
+        }
+
+        // Do the actual rejection of the pixel
+        if (maskIndex >= 0) {
+            if (suspect) {
+		CHECKPIX(x, y, "Throwing out all suspect pixels for %d,%d\n", x, y);
+                // Throw out all suspect pixels
+                int numGood = 0;        // Number of good pixels
+                for (int j = 0; j < num; j++) {
+                    if (pixelSuspects->data.U8[j]) {
+                        combineMarkReject(inputs, x, y, pixelSources->data.U16[j]);
+                        continue;
+                    }
+                    if (numGood == j) {
+                        numGood++;
+                        continue;
+                    }
+                    pixelData->data.F32[numGood] = pixelData->data.F32[j];
+                    pixelWeights->data.F32[numGood] = pixelWeights->data.F32[j];
+                    pixelSources->data.U16[numGood] = pixelSources->data.U16[j];
+                    pixelLimits->data.F32[numGood] = pixelLimits->data.F32[j];
+                    pixelVariances->data.F32[numGood] = pixelVariances->data.F32[j];
+                    numGood++;
+                }
+                pixelData->n = numGood;
+                pixelWeights->n = numGood;
+                pixelSources->n = numGood;
+                pixelLimits->n = numGood;
+                pixelVariances->n = numGood;
+                totalClipped += num - numGood;
+                num = numGood;
+                suspect = false;
+            } else {
+                // Throw out masked pixel
+		CHECKPIX(x, y, "Throwing out input %d for pixel %d,%d\n", maskIndex, x, y);
+                combineMarkInspect(inputs, x, y, pixelSources->data.U16[maskIndex]);
+                int numGood = 0;        // Number of good pixels
+                for (int j = 0; j < num; j++) {
+                    if (j == maskIndex) {
+                        continue;
+                    }
+                    if (numGood == j) {
+                        numGood++;
+                        continue;
+                    }
+                    pixelData->data.F32[numGood] = pixelData->data.F32[j];
+                    pixelWeights->data.F32[numGood] = pixelWeights->data.F32[j];
+                    pixelSources->data.U16[numGood] = pixelSources->data.U16[j];
+                    pixelLimits->data.F32[numGood] = pixelLimits->data.F32[j];
+                    pixelVariances->data.F32[numGood] = pixelVariances->data.F32[j];
+                    numGood++;
+                }
+                pixelData->n = numGood;
+                pixelWeights->n = numGood;
+                pixelSources->n = numGood;
+                pixelLimits->n = numGood;
+                pixelVariances->n = numGood;
+                totalClipped++;
+                num--;
+            }
+        }
+    }
+
+    return true;
+}
+
+
+// Ensure the input array of pmStackData is valid, and get some details out of it
+static bool validateInputData(bool *haveVariances, // Do we have variance maps in the sky images?
+                              int *num,    // Number of inputs
+                              int *numCols, int *numRows, // Size of (sky) images
+                              const psArray *input, // Input array of pmStackData to validate
+                              const pmReadout *output, // Output readout
+                              const pmReadout *exp    // Exposure map
+    )
+{
+    PS_ASSERT_ARRAY_NON_NULL(input, false);
+    *num = input->n;
+
+    pmStackData *data = NULL;           // First image off the rank, used as a template
+    for (int i = 0; !data && i < input->n; i++) {
+        data = input->data[i];
+    }
+    PS_ASSERT_PTR_NON_NULL(data, false);
+    assert(psMemGetDeallocator(data) == (psFreeFunc)stackDataFree); // Ensure it's the right type
+    *haveVariances = 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_IMAGE_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->variance) {
+        *haveVariances = true;
+        PS_ASSERT_IMAGE_NON_NULL(data->readout->variance, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(data->readout->image, data->readout->variance, false);
+        PS_ASSERT_IMAGE_TYPE(data->readout->variance, PS_TYPE_F32, false);
+    }
+    bool haveRejects = (data->reject != NULL); // Do we have rejected pixels?
+
+    // 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
+        if (!data) {
+            continue;
+        }
+        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 ((haveRejects && !data->reject) || (data->reject && !haveRejects)) {
+            psError(PS_ERR_UNEXPECTED_NULL, true,
+                    "The rejected 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_IMAGE_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 (*haveVariances) {
+            PS_ASSERT_IMAGE_NON_NULL(data->readout->variance, false);
+            PS_ASSERT_IMAGES_SIZE_EQUAL(data->readout->image, data->readout->variance, false);
+            PS_ASSERT_IMAGE_TYPE(data->readout->variance, PS_TYPE_F32, false);
+        }
+    }
+
+    PM_ASSERT_READOUT_NON_NULL(output, false);
+    if (output->image) {
+        PS_ASSERT_IMAGE_NON_NULL(output->image, false);
+        PS_ASSERT_IMAGE_TYPE(output->image, PS_TYPE_F32, false);
+        PS_ASSERT_IMAGE_NON_NULL(output->mask, false);
+        PS_ASSERT_IMAGE_TYPE(output->mask, PS_TYPE_IMAGE_MASK, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(output->image, output->mask, false);
+    }
+
+    if (exp) {
+        PM_ASSERT_READOUT_NON_NULL(exp, false);
+        if (exp->image) {
+            PS_ASSERT_IMAGES_SIZE_EQUAL(exp->image, output->image, false);
+        }
+        if (exp->mask) {
+            PS_ASSERT_IMAGES_SIZE_EQUAL(exp->mask, output->image, false);
+        }
+    }
+
+    return true;
+}
+
+
+// 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 minCols, int maxCols, int minRows, int maxRows // Bounds of interest
+    )
+{
+    int numCols = maxCols - minCols + 1, numRows = maxRows - minRows + 1; // Size of map
+
+    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];
+        if (!data) {
+            continue;
+        }
+        assert(data->reject);
+        psPixels *pixels = data->reject; // The rejected pixels
+        for (int j = 0; j < pixels->n; j++) {
+            int x = pixels->data[j].x - minCols, y = pixels->data[j].y - minRows; // Coordinates of interest
+            if (x < 0 || x >= numCols || y < 0 || y >= numRows) {
+                continue;
+            }
+            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 x0, int y0, // Offset into map
+                               int x, int y // Coordinates of interest
+    )
+{
+    // Adjust for offset
+    x -= x0;
+    y -= y0;
+
+    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, float exp, float addVariance)
+{
+    pmStackData *data = psAlloc(sizeof(pmStackData)); // Stack data, to return
+    psMemSetDeallocator(data, (psFreeFunc)stackDataFree);
+
+    data->readout = psMemIncrRefCounter(readout);
+    data->reject = NULL;
+    data->inspect = NULL;
+    data->weight = weight;
+    data->exp = exp;
+    data->addVariance = addVariance;
+
+    return data;
+}
+
+
+bool pmStackSimpleMedianCombine(
+    pmReadout *combined,
+    psArray *input) {
+    int num = input->n;
+    //  int numCols, numRows;
+    int minInputCols, maxInputCols, minInputRows, maxInputRows; // Smallest and largest values to combine
+    int xSize, ySize;                   // Size of the output image
+
+    psArray *stack = psArrayAlloc(num); // Stack of readouts  
+    for (int i = 0; i < num; i++) {
+	//    pmStackData *data = input->data[i]; // Stack data for this input
+	pmReadout *ro = input->data[i]; // data->readout;  // Readout of interest
+	if (!ro) {
+	    continue;
+	}
+	stack->data[i] = psMemIncrRefCounter(ro);
+    }    
+
+    if (!pmReadoutStackValidate(&minInputCols, &maxInputCols, &minInputRows, &maxInputRows, &xSize, &ySize,
+				stack)) {
+	psError(psErrorCodeLast(), false, "Input stack is not valid.");
+	psFree(stack);
+	return false;
+    }
+
+    psVector *pixelData = psVectorAlloc(input->n,PS_TYPE_F32);
+    psVector *pixelMask = psVectorAlloc(input->n,PS_TYPE_VECTOR_MASK);
+    psStats  *stats     = psStatsAlloc(PS_STAT_ROBUST_MEDIAN);
+
+    for (int y = minInputRows; y < maxInputRows; y++) {
+	for (int x = minInputCols; x < maxInputCols; x++) {
+	    for (int i = 0; i < input->n; i++) {
+		pmReadout *ro  = stack->data[i];
+		psImage *image = ro->image;
+		pixelMask->data.PS_TYPE_VECTOR_MASK_DATA[i] = 0;
+		pixelData->data.F32[i] = image->data.F32[y][x];
+		if (isfinite(image->data.F32[y][x])&&
+		    (fabs(image->data.F32[y][x]) < 1e5)) {
+		    pixelMask->data.PS_TYPE_VECTOR_MASK_DATA[i] = 0;
+		}
+		else {
+		    pixelMask->data.PS_TYPE_VECTOR_MASK_DATA[i] = 1;
+		}
+#if (0)
+		if ((x == 59)&&(y > 40)&&(y < 50)) {
+		    fprintf(stderr,"%d %d %d %d %g\n",
+			    x,y,i,pixelMask->data.PS_TYPE_VECTOR_MASK_DATA[i],pixelData->data.F32[i]);
+		}
+#endif
+	    }
+	    if (!psVectorStats(stats,pixelData,NULL,pixelMask,1)) {
+		psError(PS_ERR_UNKNOWN, false, "Unable to calculate median");
+		psFree(stats);
+		psFree(pixelData);
+		psFree(pixelMask);
+		psFree(stack);
+		return(false);
+	    }
+	    combined->image->data.F32[y][x] = stats->robustMedian;
+	    if (combined->mask) {
+		combined->mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] = 0;
+	    }
+#if (0)
+	    if ((x == 59)&&(y > 40)&&(y < 50)) {
+		fprintf(stderr,"%d %d %d %d %g\n",
+			x,y,-1,combined->mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x],
+			combined->image->data.F32[y][x]);
+	    }
+#endif
+	}
+    }
+  
+    psFree(stats);
+    psFree(pixelData);
+    psFree(pixelMask);
+    psFree(stack);
+    return (true);
+}
+
+# define SUSPECT_FRACTION 0.65
+
+// Comparison and swap functions for sorting values directly
+#define SORT_VV_COMPARE(A,B) (pixelData->data.F32[A] < pixelData->data.F32[B])
+#define SORT_VV_SWAP(TYPE,A,B) {					\
+	if (A != B) {							\
+	    psF32 tempVal = pixelData->data.F32[A];			\
+	    pixelData->data.F32[A] = pixelData->data.F32[B];		\
+	    pixelData->data.F32[B] = tempVal;				\
+	    psF32 tempVar = pixelVariances->data.F32[A];		\
+	    pixelVariances->data.F32[A] = pixelVariances->data.F32[B];	\
+	    pixelVariances->data.F32[B] = tempVar;			\
+	    if (expTime) {						\
+		psF32 tempExp = expTime->data.F32[A];			\
+		expTime->data.F32[A] = expTime->data.F32[B];		\
+		expTime->data.F32[B] = tempExp;				\
+	    }								\
+	}								\
+    }
+
+// this macro uses the macros above which assume pixelData, pixelVariances, expTime
+#define SORT_VALUES(NVALUES) { PSSORT(NVALUES, SORT_VV_COMPARE, SORT_VV_SWAP, F32); }
+
+#define ESCAPE	{							\
+  combined->image->data.F32[y][x] = NAN;				\
+  combined->variance->data.F32[y][x] = NAN;				\
+  combined->mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] = blankMaskBits;	\
+  if (expmaps) {							\
+    expmaps->image->data.F32[y][x] = 0.0;				\
+    expmaps->variance->data.F32[y][x] = 0.0;				\
+    expmaps->mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] = 0;		\
+  } continue; }
+
+// combine pixels, averaging the inner XX percentile range, after rejecting 5-sigma outliers (robust 5 sigma)
+bool pmStackCombineByPercentile(
+    pmReadout *combined,		// output stacked readout
+    pmReadout *expmaps,			// output exposure map information
+    psArray *stackData,			// input exposures
+    psF64 rejectFraction,               // outlier fraction of pixels to reject
+    int nminpix,			// minimum number of input values required to generate an output value
+    psImageMaskType badMaskBits, 	// treat these bits as 'bad'
+    psImageMaskType suspectMaskBits,	// treat these bits as 'suspect'
+    psImageMaskType blankMaskBits       // use this mask value for pixels missing input data (distinguish between Ninput = 0 and Ngood = 0?)
+				) {
+    int minInputCols, maxInputCols, minInputRows, maxInputRows; // Smallest and largest values to combine
+    int xSize, ySize;                   // Size of the output image
+
+    // we need to copy the readouts to their own array for validation
+    psArray *stackReadouts = psArrayAlloc(stackData->n);
+    for (int i = 0; i < stackData->n; i++) {
+        pmStackData *data = stackData->data[i]; // Stack data for this input
+	stackReadouts->data[i] = NULL;
+	if (!data) continue;
+	pmReadout *ro = data->readout;  // Readout of interest
+	if (!ro) continue;
+	stackReadouts->data[i] = psMemIncrRefCounter(ro); // need to bump the counter since the free below will decrement
+    }
+    if (!pmReadoutStackValidate(&minInputCols, &maxInputCols, &minInputRows, &maxInputRows, &xSize, &ySize, stackReadouts)) {
+	psError(psErrorCodeLast(), false, "Input stack is not valid.");
+	psFree(stackReadouts);
+	return false;
+    }
+    psFree(stackReadouts);
+
+    // make sure the output readout matches the inputs, and set to blank by default
+    pmReadoutUpdateSize(combined, minInputCols, minInputRows, xSize, ySize, true, true, blankMaskBits);
+    if (expmaps) {
+	// if we are generating the expmaps, update to match the images, set blank mask areas to 0
+	pmReadoutUpdateSize(expmaps, minInputCols, minInputRows, xSize, ySize, expmaps->mask != NULL, expmaps->variance != NULL, 0);
+    }
+   
+    psVector *pixelData      = psVectorAlloc(stackData->n, PS_TYPE_F32);
+    psVector *pixelVariances = psVectorAlloc(stackData->n, PS_TYPE_F32);
+
+    // if we are asking for the exptime maps, generate a storage vector expTime
+    psVector *expTime        = expmaps && expmaps->image ? psVectorAlloc(stackData->n, PS_TYPE_F32) : NULL;
+
+    int nGoodBits[16]; // accumulate the good pixel bits here for fuzzy logic
+    psAssert (sizeof(psImageMaskType) == 2, "psImageMaskType is not the expected size");
+
+    for (int y = minInputRows; y < maxInputRows; y++) {
+	for (int x = minInputCols; x < maxInputCols; x++) {
+
+	    int nGood = 0; 
+	    memset (nGoodBits, 0, 16*sizeof(int));
+	    for (int i = 0; i < stackData->n; i++) {
+
+		pmStackData *data = stackData->data[i]; // Stack data for this input
+		if (!data) continue;
+		pmReadout *ro = data->readout;  // Readout of interest
+		if (!ro) continue;
+
+		psAssert (ro->mask,     "must must exist, but does not");
+		psAssert (ro->variance, "variance must exist, but does not");
+
+		psImage *image    = ro->image;
+		psImage *variance = ro->variance;
+		psImage *mask     = ro->mask;
+
+		int xIn = x - data->readout->col0;
+		int yIn = y - data->readout->row0; // Coordinates on input readout
+
+		// skip obviously bad input data
+		if (!isfinite(image->data.F32[yIn][xIn])) continue;
+		if (mask->data.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn] & badMaskBits) continue;
+		if (mask->data.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn] & suspectMaskBits) continue;
+
+		// count the number of times a given mask bit is set in the input pixels.
+		// NOTE: since we have explicitly skipped the pixels with any bad bits, these are only
+		// the suspect bits (nGoodBits is a bit of a misnomer: it is more like 'nSuspectBitsForGoodInputs'
+		// NOTE: skip the full bit-by-bit check if we know the mask byte is empty
+		psImageMaskType value = 0x0001;
+		for (int nbit = 0; mask->data.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn] && (nbit < 16); nbit ++) {
+		    if (mask->data.PS_TYPE_IMAGE_MASK_DATA[yIn][xIn] & value) {
+			nGoodBits[nbit] ++;
+		    }
+		    value <<= 1;
+		}
+
+		// accumulate pixel data and variance values:
+		pixelData->data.F32[nGood] = image->data.F32[yIn][xIn];
+		pixelVariances->data.F32[nGood] = variance->data.F32[yIn][xIn];
+
+		// accumulate exposure times if required
+		if (expTime)  { expTime->data.F32[nGood] = data->exp; }
+		nGood ++;
+	    }
+
+	    if (nGood < nminpix) ESCAPE;
+	    
+	    pixelData->n = nGood;
+
+	    // sort pixelData, pixelVariance, expTime (if it exists)
+	    SORT_VALUES (pixelData->n);
+
+	    // we now have a sorted vector of values.  We can make a very coarse outlier
+	    // cut based on the median and interquartile range.  This will let us reject
+	    // some extreme outliers that bias the signal otherwise.
+
+	    int Nlo = 0;     
+	    int Nhi = nGood; 
+
+	    if (nGood >= 5) {
+	      int midPoint = nGood / 2;
+	      float rawMedian = (nGood % 2) ? pixelData->data.F32[midPoint] : 0.5*(pixelData->data.F32[midPoint] + pixelData->data.F32[midPoint-1]);
+	      
+	      // XXX measure interquartile range
+	      int P25 = 0.25*nGood;
+	      int P75 = 0.75*nGood;
+	      float rawSigma = 0.74*(pixelData->data.F32[P75] - pixelData->data.F32[P25]);
+	      float minThresh = rawMedian - 5.0*rawSigma;
+	      float maxThresh = rawMedian + 5.0*rawSigma;
+
+	      // find the entries which are in the range
+	      // these should be safe since minThresh and maxThresh are guaranteed to contain the median
+	      while (pixelData->data.F32[Nlo    ] < minThresh) { Nlo ++; }
+	      while (pixelData->data.F32[Nhi - 1] > maxThresh) { Nhi --; }
+	    }
+
+	    // if we did not clip above (either nGood < 5 or no rejection), Nlo will be 0, Nhi will be nGood
+	    // In either case, Nlo is the offset of the first unclipped point.
+	    int nGoodClip = Nhi - Nlo;
+
+	    int isTest = true;
+	    isTest = isTest || ((x == 4743) && (y == 2903));
+	    isTest = isTest || ((x == 4953) && (y == 2919));
+	    isTest = isTest || ((x == 4751) && (y == 2725));
+	    isTest = false;
+
+	    if (isTest) {
+	      char testname[256];
+	      snprintf (testname, 256, "testpix.%04d.%04d.txt", (int) x, (int) y);
+	      FILE *f = fopen (testname, "w");
+	      int fd = fileno (f);
+
+	      fprintf (f, "# nGood: %d, Nlo: %d, Nhi: %d, nGoodClip: %d\n", nGood, Nlo, Nhi, nGoodClip);
+	      p_psVectorPrint (fd, pixelData, "pixelData");
+	      fclose (f);
+	    }
+	    // rather than define a min and max value,
+	    // what we really want is a symmetric selection about the middle,
+	    // or the output is biased.  If N % 2 = 1, then 
+	    
+	    // we are going to exclude rejectFraction*nGood measurements.  But the
+	    // rejection needs to be symmetric
+
+	    // 'AT_LEAST' means we reject at least 'rejectFraction' of values, but it could
+	    // be a higher percentage for smaller numbers of inputs.
+# define AT_LEAST 0
+
+# if (AT_LEAST)
+	    int Ns = MIN(MAX(1, 0.5*rejectFraction * nGoodClip), nGoodClip) + Nlo;
+	    int Ne = nGoodClip - Ns + Nlo;
+	    int Npt = Ne - Ns;
+# else
+	    int Ns = MIN(MAX(0, 0.5*rejectFraction * nGoodClip), nGoodClip) + Nlo;
+	    int Ne = nGoodClip - Ns + Nlo;
+	    int Npt = Ne - Ns;
+# endif
+	    if ((Npt < 1) || (nGood < nminpix)) ESCAPE;
+
+	    // Set the given (suspect) mask bit if nGoodBits[i] > f*nGood in other words
+	    // if more than 65% of the good inputs had one of these bits set, then we
+	    // should set that bit in the output mask.  Note that this analysis counts the
+	    // mask bits of pixels rejected by the clipping above.
+	    psImageMaskType value = 0x0001;
+	    psImageMaskType outputMask = 0x0000;
+	    for (int nbit = 0; nbit < 16; nbit ++) {
+		if (nGoodBits[nbit] > SUSPECT_FRACTION*nGood) {
+		    outputMask |= value;
+		}
+		value <<= 1;
+	    }
+
+	    float sum = 0.0;
+	    float varSum = 0.0;
+	    for (int n = Ns; n < Ne; n++) {
+		sum += pixelData->data.F32[n];
+		varSum += pixelVariances->data.F32[n];
+	    }
+	    float mean = sum / (float) Npt;
+	    float varValue = varSum / (float) (nGoodClip*nGoodClip);
+
+	    // alternative: calculate the stdev of the pixel values
+	    // float varSum = 0.0;
+	    // for (int n = Ns; n < Ne; n++) {
+	    // 	varSum += SQ(pixelData->data.F32[n] - mean);
+	    // }
+	    // variance on the mean (stdev / sqrt(N))^2
+
+	    // the reported variance values can be extremely high / wrong.
+	    // if we have enough measurements, let's just use the interquartile range
+	    // of the data to estimate the per-pixel variance.  NOTE: this is not valid
+	    // if the inputs have been significantly smoothed.  In that case we need
+	    // to include the covariance explicitly.  But this algorithm should be used
+	    // without convolution.
+	    // XXX How do we choose the cutoff here?
+	    if (nGoodClip >= 9) {
+	      // Measure interquartile range
+	      int P25 = 0.25*nGoodClip + Nlo;
+	      int P75 = 0.75*nGoodClip + Nlo;
+	      float rawSigma = 0.74*(pixelData->data.F32[P75] - pixelData->data.F32[P25]);
+	      varValue = PS_MIN(9e7, PS_SQR(rawSigma) / (float) nGoodClip); // sigma_mean = sigma_meas / sqrt(Nmeas) -> var_mean = var_meas / Nmeas
+	      // XXX the upper limit of 9e7 is set to match the output
+	      // format (STK_UNIONS) which can only represent values up to that limit.
+	      // perhaps it would be better to saturate the output image in psFits
+	      // rather than here.  
+	    }
+
+	    // Note: since we are calculating the average of a subset of a sorted
+	    // list of values, the denominator should not be the number of measurements
+	    // in the calculation above (Npt): in the extreme case of a median, we would
+	    // have a single value (Npt = 1), but the variance of a median is only ~1.4 x
+	    // the variance of the average / sqrt(N).  We should use nGood (the total number
+	    // of values in the sorted list), but the variance should be scaled by a factor
+	    // which depends on the fraction of values included.  
+
+	    // this coefficient varies between 1.4 (for pure median) and 1.05 for 68% range.
+
+	    combined->image->data.F32[y][x] = mean;
+	    combined->variance->data.F32[y][x] = varValue;
+	    combined->mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] = outputMask;
+
+	    // The exposure time of interest should be the total number of values, after
+	    // rejection of known bad measurements, not the sorted and clipped number.
+	    // Note that if we were to take the median, the relevant exposure time would
+	    // still be the total of all inputs, not the single exposure for which the
+	    // median was generated.
+
+	    if (expTime) { 
+		float sum = 0.0;
+		for (int n = Nlo; n < Nhi; n++) {
+		    sum += expTime->data.F32[n];
+		}
+		expmaps->image->data.F32[y][x] = sum;
+	    }
+	    if (expmaps) { expmaps->mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] = nGoodClip; }
+	}
+    }
+    psFree(pixelData);
+    psFree(pixelVariances);
+    psFree(expTime);
+
+    return (true);
+}
+
+/// Stack input images
+bool pmStackCombine(
+    pmReadout *combined,		// output stacked readout
+    pmReadout *expmaps,			// output exposure map information
+    psArray *input,			// input exposures
+    psImageMaskType badMaskBits, 	// treat these bits as 'bad'
+    psImageMaskType suspectMaskBits,	// treat these bits as 'suspect'
+    psImageMaskType blankMaskBits,      // use this mask value for pixels missing input data (distinguish between Ninput = 0 and Ngood = 0?)
+    int kernelSize,
+    float iter, 
+    float rej, 
+    float sys, 
+    float olympic,
+    bool useVariance, 
+    bool safe, 
+    int nminpix,
+    bool rejection)
+{
+    bool haveVariances;                 // Do we have the variance maps?
+    int num;                            // Number of inputs
+    int numCols, numRows;               // Size of (sky) images
+    if (!validateInputData(&haveVariances, &num, &numCols, &numRows, input, combined, expmaps)) {
+        return false;
+    }
+    PS_ASSERT_INT_NONNEGATIVE(kernelSize, false);
+    PS_ASSERT_INT_POSITIVE(blankMaskBits, false);
+    if (isnan(rej)) {
+        PS_ASSERT_FLOAT_EQUAL(iter, 0, false);
+    } else {
+        PS_ASSERT_FLOAT_LARGER_THAN(iter, 0, false);
+        PS_ASSERT_FLOAT_LARGER_THAN(rej, 0.0, false);
+    }
+    if (useVariance && !haveVariances) {
+        psWarning("Unable to use variance in rejection if no variance maps supplied --- option turned off");
+        useVariance = false;
+    }
+
+    psVector *addVariance = psVectorAlloc(num, PS_TYPE_F32); // Additional variance for each image
+    psVector *weights = psVectorAlloc(num, PS_TYPE_F32); // Relative weighting for each image
+    psVector *exps = psVectorAlloc(num, PS_TYPE_F32);    // Exposure times for each image
+    psArray *stack = psArrayAlloc(num); // Stack of readouts
+    float totalExpWeight = 0.0;           // Total value of all weighted exposure times
+    float totalExp = 0.0;                 // Total exposure time
+    for (int i = 0; i < num; i++) {
+        pmStackData *data = input->data[i]; // Stack data for this input
+        if (!data) {
+            weights->data.F32[i] = 0.0;
+            exps->data.F32[i] = NAN;
+            continue;
+        }
+        weights->data.F32[i] = data->weight;
+        exps->data.F32[i] = data->exp;
+        totalExp += exps->data.F32[i];
+        totalExpWeight += exps->data.F32[i] * weights->data.F32[i];
+        pmReadout *ro = data->readout;  // Readout of interest
+        stack->data[i] = psMemIncrRefCounter(ro);
+        addVariance->data.F32[i] = ro->covariance ? psImageCovarianceFactor(ro->covariance) : 1.0;
+#ifdef ADD_VARIANCE
+        if (isfinite(data->addVariance)) {
+            addVariance->data.F32[i] *= data->addVariance;
+        }
+#endif
+        if (!rejection) {
+            // Ensure pixels can be put on the appropriate list
+            if (!data->inspect) {
+                data->inspect = psPixelsAllocEmpty(PIXEL_LIST_BUFFER);
+            }
+            if (!data->reject) {
+                data->reject = psPixelsAllocEmpty(PIXEL_LIST_BUFFER);
+            }
+        }
+    }
+    totalExpWeight = totalExp / totalExpWeight;    // Convert to inverse
+
+    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,
+                                stack)) {
+        psError(psErrorCodeLast(), false, "Input stack is not valid.");
+        psFree(stack);
+        return false;
+    }
+    psFree(stack);
+    pmReadoutUpdateSize(combined, minInputCols, minInputRows, xSize, ySize, true, true, blankMaskBits);
+    psTrace("psModules.imcombine", 1, "Have for combination [%d:%d,%d:%d] (%dx%d)\n",
+            minInputCols, maxInputCols, minInputRows, maxInputRows, xSize, ySize);
+
+    // Reduce combination area by the size of the kernel
+    minInputCols += kernelSize;
+    maxInputCols -= kernelSize;
+    minInputRows += kernelSize;
+    maxInputRows -= kernelSize;
+    psTrace("psModules.imcombine", 1, "Combining on [%d:%d,%d:%d]\n",
+            minInputCols, maxInputCols, minInputRows, maxInputRows);
+
+
+    // Buffer for combination
+    combineBuffer *buffer = combineBufferAlloc(num);
+
+    // 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_IMAGE_MASK);
+        combinedMask = combined->mask;
+    }
+
+    psImage *combinedVariance = combined->variance; // Combined variance map
+    if (haveVariances && !combinedVariance) {
+        combined->variance = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+        combinedVariance = combined->variance;
+    }
+
+    psImage *exp = NULL, *expnum = NULL, *expweight = NULL; // Exposure map and exposure number
+    if (expmaps) {
+        if (!expmaps->image) {
+            expmaps->image = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+        }
+        exp = expmaps->image;
+
+        if (!expmaps->mask) {
+            expmaps->mask = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK);
+        }
+        expnum = expmaps->mask;
+
+        if (!expmaps->variance) {
+            expmaps->variance = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+        }
+        expweight = expmaps->variance;
+    }
+   
+    // Set up rejection list
+    psArray *pixelMap = NULL;           // Map of pixels to source
+    if (rejection) {
+        pixelMap = pixelMapGenerate(input, minInputCols, maxInputCols, minInputRows, maxInputRows);
+    }
+
+    // Combine each pixel
+    for (int y = minInputRows; y < maxInputRows; y++) {
+        for (int x = minInputCols; x < maxInputCols; x++) {
+	    // this is only for TESTING:
+	    CHECKPIX(x, y, "Combining pixel %d,%d: %x %x %f %f %f %f %d %d %d\n", x, y, badMaskBits, blankMaskBits, iter, rej, sys, olympic, useVariance, safe, rejection);
+
+            psVector *reject = NULL; // Images to reject for this pixel
+            if (rejection) {
+                reject = pixelMapQuery(pixelMap, minInputCols, minInputRows, x, y);
+#ifdef TESTING
+                if (PS_SQR(x - TEST_X) + PS_SQR(y - TEST_Y) <= PS_SQR(TEST_RADIUS)) {
+                    fprintf(stderr, "Rejected inputs for pixel %d,%d: ", x, y);
+                    if (!reject) {
+                        fprintf(stderr, "<none>\n");
+                    } else {
+                        for (int i = 0; i < reject->n; i++) {
+                            fprintf(stderr, "%d ", reject->data.U16[i]);
+                        }
+                        fprintf(stderr, "\n");
+                    }
+                }
+#endif
+            }
+ 
+            int num;			  // Number of good pixels
+            bool suspect;		  // Suspect pixels in stack?
+	    psImageMaskType badMask = 0;  // OR of mask bits in all bad input pixels
+	    psImageMaskType goodMask = 0; // OR of mask bits in all good input pixels
+            combineExtract(&num, &suspect, &badMask, &goodMask, buffer, combinedImage, combinedMask, combinedVariance, input, weights, exps, addVariance, reject, x, y, badMaskBits, suspectMaskBits);
+            combinePixels(combinedImage, combinedMask, combinedVariance, exp, expnum, expweight, num, buffer, x, y, blankMaskBits, badMask, goodMask, safe, nminpix, totalExpWeight);
+
+            if (iter > 0) {
+                combineTest(num, suspect, input, buffer, x, y, iter, rej, sys, olympic,
+                            useVariance, safe);
+            }
+        }
+    }
+
+    psFree(pixelMap);
+    psFree(weights);
+    psFree(buffer);
+    psFree(addVariance);
+
+
+#ifndef PS_NO_TRACE
+    if (!rejection && psTraceGetLevel("psModules.imcombine") >= 5) {
+        for (int i = 0; i < num; i++) {
+            pmStackData *data = input->data[i]; // Stack data for this input
+            if (!data || !data->inspect) {
+                continue;
+            }
+            psTrace("psModules.imcombine", 5, "Image %d: %ld pixels to inspect.\n", i, data->inspect->n);
+        }
+    }
+#endif
+
+    return true;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmStack.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmStack.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmStack.h	(revision 42651)
@@ -0,0 +1,79 @@
+/* @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.11 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-13 23:52:14 $
+ *
+ * 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 *reject;                   ///< Pixels to reject
+    psPixels *inspect;                  ///< Pixels to inspect
+    float weight;                       ///< Relative weighting for image
+    float exp;                          ///< Exposure time
+    float addVariance;                  ///< Additional variance when rejecting
+} pmStackData;
+
+/// Constructor
+pmStackData *pmStackDataAlloc(pmReadout *readout, ///< Warped readout (sky cell)
+                              float weight, ///< Weight to apply
+                              float exp,    ///< Exposure time
+                              float addVariance ///< Additional variance when rejecting
+    );
+/// Stack input images simply
+
+bool pmStackSimpleMedianCombine(pmReadout *combined, ///< Combined readout (output)
+				psArray *input       ///< Input array of pmStackData
+				);
+
+bool pmStackCombineByPercentile(
+    pmReadout *combined,
+    pmReadout *expmaps,
+    psArray *input,
+    psF64 rejectFraction,
+    int nminpix,		     ///< Minimum number input per pixel to combine
+    psImageMaskType badMaskBits, 	// treat these bits as 'bad'
+    psImageMaskType suspectMaskBits,	// treat these bits as 'suspect'
+    psImageMaskType blankMaskBits       // use this mask value for pixels missing input data (distinguish between Ninput = 0 and Ngood = 0?)
+  );
+
+/// Stack input images
+bool pmStackCombine(pmReadout *combined,///< Combined readout (output)
+                    pmReadout *expmaps, ///< Exposure maps (output)
+                    psArray *input,     ///< Input array of pmStackData
+                    psImageMaskType maskVal, ///< Mask value of bad pixels
+                    psImageMaskType suspect, ///< Mask value of suspect pixels
+                    psImageMaskType bad,     ///< Mask value to give rejected pixels
+                    int kernelSize,     ///< Half-size of the convolution kernel
+                    float iter,         ///< Number of iterations per input
+                    float rej,          ///< Rejection limit (standard deviations)
+                    float sys,          ///< Relative systematic error
+                    float discard,      ///< Fraction of values to discard for Olympic weighted mean
+                    bool useVariance,   ///< Use variance values for rejection?
+                    bool safe,          ///< Play safe with small numbers of input pixels (mask if N <= 2)?
+		    int nminpix,        ///< Minimum number input per pixel to combine
+                    bool rejectInspect  ///< Reject pixels instead of marking them for inspection?
+    );
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmStackReject.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmStackReject.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmStackReject.c	(revision 42651)
@@ -0,0 +1,354 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmFPA.h"
+#include "pmSubtractionTypes.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionThreads.h"
+#include "pmSubtractionKernels.h"
+
+#include "pmStackReject.h"
+
+#define PIXEL_LIST_BUFFER 100           // Number of pixels to add to list at a time
+
+//#define TESTING                         // Testing output
+
+// Mask values
+typedef enum {
+    PM_STACK_MASK_BAD      = 0x01,      // Bad pixel
+    PM_STACK_MASK_CONVOLVE = 0x02,      // Touching a bad pixel
+    PM_STACK_MASK_ALL      = 0xff,      // All mask bits
+} pmStackMask;
+
+static bool threaded = false;           // Running threaded?
+
+
+// Grow the rejection mask
+static inline bool stackRejectGrow(psImage *target,   // Target mask image (product)
+                                   psImage *source, // Source mask image (to be grown)
+                                   const pmSubtractionKernels *kernels, // Subtraction kernels
+                                   int numCols, int numRows, // Size of image
+                                   int xMin, int xMax, int yMin, int yMax, // Bounds of convolution
+                                   float poorFrac       // Fraction for "poor"
+    )
+{
+    int size = kernels->size;           // Half-size of convolution kernel
+    int x = PS_MIN(xMin + size + 1, kernels->xMax); // x coordinate of interest
+    int y = PS_MIN(yMin + size + 1, kernels->yMax); // y coordinate of interest
+
+    psImage *polyValues = p_pmSubtractionPolynomialFromCoords(NULL, kernels, x, y); // Polynomial
+    int box = p_pmSubtractionBadRadius(NULL, kernels, polyValues, false, poorFrac); // Radius of bad box
+    psTrace("psModules.imcombine", 10, "Growing by %d", box);
+    psFree(polyValues);
+
+    if (box > 0) {
+        // Convolve a subimage, then stick it in the target
+        psImage *mask = psImageSubset(source, psRegionSet(xMin - box, xMax + box,
+                                                          yMin - box, yMax + box)); // Mask to convolve
+        psImage *convolved = psImageConvolveMask(NULL, mask, PM_STACK_MASK_BAD, PM_STACK_MASK_CONVOLVE,
+                                                 -box, box, -box, box); // Convolved mask
+        psFree(mask);
+
+        int numBytes = (xMax - xMin) * PSELEMTYPE_SIZEOF(PS_TYPE_IMAGE_MASK); // Number of bytes to copy
+        psAssert(convolved->numCols - 2 * box == xMax - xMin, "Bad number of columns");
+        psAssert(convolved->numRows - 2 * box == yMax - yMin, "Bad number of rows");
+
+        for (int yTarget = yMin, ySource = box; yTarget < yMax; yTarget++, ySource++) {
+            memcpy(&target->data.PS_TYPE_IMAGE_MASK_DATA[yTarget][xMin],
+                   &convolved->data.PS_TYPE_IMAGE_MASK_DATA[ySource][box], numBytes);
+        }
+        psFree(convolved);
+    } else {
+        // Just copy over
+        int numBytes = (xMax - xMin) * PSELEMTYPE_SIZEOF(PS_TYPE_IMAGE_MASK); // Number of bytes to copy
+        for (int yTarget = yMin; yTarget < yMax; yTarget++) {
+            memcpy(&target->data.PS_TYPE_IMAGE_MASK_DATA[yTarget][xMin],
+                   &source->data.PS_TYPE_IMAGE_MASK_DATA[yTarget][xMin], numBytes);
+        }
+    }
+
+    return true;
+}
+
+// Thread entry for stackRejectGrow
+static bool stackRejectGrowThread(psThreadJob *job // Job to execute
+    )
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    psArray *args = job->args;          // Job arguments
+    psImage *target = args->data[0];    // Target mask image
+    psImage *source = args->data[1];    // Source mask image
+    const pmSubtractionKernels *kernels = args->data[2]; // Subtraction kernels
+    int numCols = PS_SCALAR_VALUE(args->data[3], S32); // Number of columns
+    int numRows = PS_SCALAR_VALUE(args->data[4], S32); // Number of rows
+    int xMin = PS_SCALAR_VALUE(args->data[5], S32); // Minimum x value
+    int xMax = PS_SCALAR_VALUE(args->data[6], S32); // Maximum x value
+    int yMin = PS_SCALAR_VALUE(args->data[7], S32); // Minimum y value
+    int yMax = PS_SCALAR_VALUE(args->data[8], S32); // Maximum y value
+    float poorFrac = PS_SCALAR_VALUE(args->data[9], F32); // Fraction for "poor"
+
+    return stackRejectGrow(target, source, kernels, numCols, numRows, xMin, xMax, yMin, yMax, poorFrac);
+}
+
+bool pmStackRejectThreadsInit(void)
+{
+    if (threaded) {
+        psAbort("Already running threaded.");
+    }
+    threaded = true;
+
+    if (!pmSubtractionThreaded()) {
+        pmSubtractionThreadsInit();
+    }
+
+    {
+        psThreadTask *task = psThreadTaskAlloc("PSMODULES_STACK_REJECT_GROW", 10);
+        task->function = &stackRejectGrowThread;
+        psThreadTaskAdd(task);
+        psFree(task);
+    }
+
+    return true;
+}
+
+
+psPixels *pmStackReject(const psPixels *in, int numCols, int numRows, float threshold, int stride,
+                        const psArray *subRegions, const psArray *subKernels)
+{
+    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);
+
+    if (!subRegions || !subKernels) {
+      psTrace("psModules.imcombine",2,"Do not have the necessary kernels and regions, returning input pixels.");
+      psPixels *out = psPixelsCopy(NULL,in);
+      return out;
+    }
+    
+    PS_ASSERT_ARRAY_NON_NULL(subRegions, NULL);
+    PS_ASSERT_ARRAY_NON_NULL(subKernels, NULL);
+    PS_ASSERT_ARRAYS_SIZE_EQUAL(subRegions, subKernels, NULL);
+
+    // Trivial case
+    if (in->n == 0) {
+        return psPixelsAllocEmpty(0);
+    }
+
+    // Check consistency of kernels
+    int numRegions = subRegions->n;     // Number of regions
+    int size = 0;                       // Size of kernel
+    for (int i = 0; i < numRegions; i++) {
+        pmSubtractionKernels *kernels = subKernels->data[i]; // Kernel of interest
+        if (size == 0) {
+            size = kernels->size;
+        } else if (kernels->size != size) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Kernel sizes are not identical: %d vs %d",
+                    size, kernels->size);
+            return NULL;
+        }
+    }
+
+    psImage *mask = psPixelsToMask(NULL, in, psRegionSet(0, numCols - 1, 0, numRows - 1), 1); // Mask
+    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
+    pmReadout *convRO = pmReadoutAlloc(NULL); // Readout with convolved image
+    pmReadout *inRO = pmReadoutAlloc(NULL); // Readout with input image
+    inRO->image = image;
+    convRO->image = psImageAlloc(image->numCols, image->numRows, PS_TYPE_F32);
+    for (int i = 0; i < numRegions; i++) {
+        psRegion *region = subRegions->data[i]; // Region of interest
+        pmSubtractionKernels *kernels = subKernels->data[i]; // Kernel of interest
+        if (!pmSubtractionConvolve(NULL, convRO, NULL, inRO, NULL, stride, 0, 0, 1.0, 0.0, 0.0,
+                                   region, kernels, false, true)) {
+            psError(psErrorCodeLast(), false, "Unable to convolve mask image in region %d.", i);
+            psFree(convRO);
+            psFree(inRO);
+            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
+        psImage *kernel = pmSubtractionKernelImage(kernels, 0.5, 0.5, false);
+        if (!kernel) {
+            psError(psErrorCodeLast(), false, "Unable to generate kernel image.");
+            psFree(convRO);
+            psFree(inRO);
+            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);
+
+        // Range for normalisation
+        int xMin = PS_MAX(0, region->x0), xMax = PS_MIN(numCols - 1, region->x1);
+        int yMin = PS_MAX(0, region->y0), yMax = PS_MIN(numRows - 1, region->y1);
+        psTrace("psModules.imcombine", 2, "Normalising convolved mask image by %f over %d:%d,%d:%d\n",
+                sum, xMin, xMax, yMin, yMax);
+        sum = 1.0 / sum;
+        for (int y = yMin; y <= yMax; y++) {
+            for (int x = xMin; x <= xMax; x++) {
+                convRO->image->data.F32[y][x] *= sum;
+            }
+        }
+    }
+    psFree(inRO);
+    psImage *convolved = psMemIncrRefCounter(convRO->image);
+    psFree(convRO);
+
+#ifdef TESTING
+    {
+        static int seqNum = 0;          // Sequence number
+        psString name = NULL;           // Name of image
+        psStringAppend(&name, "inspect_conv_%02d.fits", seqNum);
+        seqNum++;
+        psFits *fits = psFitsOpen(name, "w"); // FITS file pointer
+        psFree(name);
+        psFitsWriteImage(fits, NULL, convolved, 0, NULL);
+        psFitsClose(fits);
+    }
+#endif
+
+    // Threshold the convolved image
+    psPixels *bad = psPixelsAllocEmpty(PIXEL_LIST_BUFFER); // List of pixels that should be masked
+    for (int y = size; y < convolved->numRows - size; y++) {
+        for (int x = size; x < convolved->numCols - size; x++) {
+            if (convolved->data.F32[y][x] > threshold) {
+                bad = psPixelsAdd(bad, bad->nalloc, x, y);
+            }
+        }
+    }
+    psTrace("psModules.imcombine", 7, "Found %ld bad pixels", bad->n);
+    psFree(convolved);
+
+    return bad;
+}
+
+
+psPixels *pmStackRejectGrow(const psPixels *in, int numCols, int numRows, float poorFrac,
+                            const psArray *subRegions, const psArray *subKernels)
+{
+    PS_ASSERT_PIXELS_NON_NULL(in, NULL);
+    PS_ASSERT_ARRAY_NON_NULL(subRegions, NULL);
+    PS_ASSERT_ARRAY_NON_NULL(subKernels, NULL);
+    PS_ASSERT_ARRAYS_SIZE_EQUAL(subRegions, subKernels, NULL);
+
+    psImage *source = psPixelsToMask(NULL, in, psRegionSet(0, numCols - 1, 0, numRows - 1),
+                                     PM_STACK_MASK_BAD); // Mask image to grow
+
+#ifdef TESTING
+    {
+        static int seqNum = 0;          // Sequence number
+        psString name = NULL;           // Name of image
+        psStringAppend(&name, "reject_orig_%02d.fits", seqNum);
+        seqNum++;
+        psFits *fits = psFitsOpen(name, "w"); // FITS file pointer
+        psFree(name);
+        psFitsWriteImage(fits, NULL, source, 0, NULL);
+        psFitsClose(fits);
+    }
+#endif
+
+    // Need to set psImageConvolveMask threading OFF because that would generate threads on top of threads
+    bool oldThreads = psImageConvolveSetThreads(false); // Old value of threading for psImageColvolve
+
+    psImage *target = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK); // Grown image
+    psImageInit(target, 0);
+    for (int i = 0; i < subRegions->n; i++) {
+        psRegion *region = subRegions->data[i]; // Subtraction region
+        pmSubtractionKernels *kernels = subKernels->data[i]; // Subtraction kernel
+
+        int size = kernels->size;           // Half-size of kernel
+        int fullSize = 2 * size + 1;        // Full size of kernel
+
+        // Get region for convolution: [xMin:xMax,yMin:yMax]
+        int xMin = PS_MAX(region->x0, size), xMax = PS_MIN(region->x1, numCols - size);
+        int yMin = PS_MAX(region->y0, size), yMax = PS_MIN(region->y1, numRows - size);
+
+        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
+
+                if (threaded) {
+                    psThreadJob *job = psThreadJobAlloc("PSMODULES_STACK_REJECT_GROW"); // Job to execute
+                    psArray *args = job->args; // Job arguments
+                    psArrayAdd(args, 1, target);
+                    psArrayAdd(args, 1, source);
+                    psArrayAdd(args, 1, kernels);
+                    PS_ARRAY_ADD_SCALAR(args, numCols, PS_TYPE_S32);
+                    PS_ARRAY_ADD_SCALAR(args, numRows, PS_TYPE_S32);
+                    PS_ARRAY_ADD_SCALAR(args, i, PS_TYPE_S32);
+                    PS_ARRAY_ADD_SCALAR(args, xSubMax, PS_TYPE_S32);
+                    PS_ARRAY_ADD_SCALAR(args, j, PS_TYPE_S32);
+                    PS_ARRAY_ADD_SCALAR(args, ySubMax, PS_TYPE_S32);
+                    PS_ARRAY_ADD_SCALAR(args, poorFrac, PS_TYPE_F32);
+                    if (!psThreadJobAddPending(job)) {
+                        psFree(source);
+                        psFree(target);
+                        return NULL;
+                    }
+                } else if (!stackRejectGrow(target, source, kernels, numCols, numRows,
+                                            i, xSubMax, j, ySubMax, poorFrac)) {
+                    psError(psErrorCodeLast(), false, "Unable to grow bad pixels.");
+                    psFree(source);
+                    psFree(target);
+                    return NULL;
+                }
+            }
+        }
+    }
+
+    if (!psThreadPoolWait(false, true)) {
+        psError(psErrorCodeLast(), false, "Unable to grow bad pixels.");
+        psFree(source);
+        psFree(target);
+        return NULL;
+    }
+
+    // Harvest the jobs
+    if (threaded) {
+        psThreadJob *job;                   // Job to destroy
+        while ((job = psThreadJobGetDone())) {
+            psAssert(strcmp(job->type, "PSMODULES_STACK_REJECT_GROW") == 0,
+                     "Job has incorrect type: %s", job->type);
+            psFree(job);
+        }
+
+    }
+
+    psImageConvolveSetThreads(oldThreads);
+
+#ifdef TESTING
+    {
+        static int seqNum = 0;          // Sequence number
+        psString name = NULL;           // Name of image
+        psStringAppend(&name, "reject_grow_%02d.fits", seqNum);
+        seqNum++;
+        psFits *fits = psFitsOpen(name, "w"); // FITS file pointer
+        psFree(name);
+        psFitsWriteImage(fits, NULL, target, 0, NULL);
+        psFitsClose(fits);
+    }
+#endif
+
+    psFree(source);
+    psPixels *bad = psPixelsFromMask(NULL, target, PM_STACK_MASK_ALL); // All bad pixels
+    psFree(target);
+    psTrace("psModules.imcombine", 7, "Total %ld bad pixels", bad->n);
+
+    return bad;
+}
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmStackReject.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmStackReject.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmStackReject.h	(revision 42651)
@@ -0,0 +1,31 @@
+#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
+                        int numCols, int numRows, ///< Size of image of interest
+                        float threshold, ///< Threshold on convolved image, 0..1
+                        int stride,     ///< Size of convolution patches
+                        const psArray *regions, ///< Array of image regions for image
+                        const psArray *kernels ///< Array of kernel parameters for each region
+    );
+
+/// Given a list of pixels from the convolved image, we grow them by convolution to get the list of all pixels
+/// which should be rejected.
+psPixels *pmStackRejectGrow(const psPixels *in, ///< List of pixels in the convolved image
+                            int numCols, int numRows, ///< Size of image of interest
+                            float poorFrac, ///< Fraction for "poor"
+                            const psArray *regions, ///< Array of image regions for image
+                            const psArray *kernels ///< Array of kernel parameters for each region
+    );
+
+/// Initialise threads for stack rejection
+bool pmStackRejectThreadsInit(void);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmStackVisual.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmStackVisual.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmStackVisual.c	(revision 42651)
@@ -0,0 +1,60 @@
+/** Diagnostic plots for pmStack
+ * @author Chris Beaumont, IfA
+ */
+
+/* Include Files   */
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <pslib.h>
+
+#include "pmKapaPlots.h"
+#include "pmVisual.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmAstrometryObjects.h"
+
+# if (HAVE_KAPA)
+# include <kapa.h>
+
+//variables to determine when things are plotted
+static bool plotTestImage        = true;
+
+// variables to store plotting window indices
+static int kapa  = -1;
+static int kapa2 = -1;
+
+/** destroy windows at the end of a run*/
+bool pmStackVisualClose()
+{
+    if(kapa != -1)
+        KapaClose(kapa);
+    if(kapa2 != -1)
+        KapaClose(kapa2);
+    return true;
+}
+
+/** Display a test image
+ * @param image to plot
+ * @param name for plot. No spaces allowed.
+ */
+bool pmStackVisualPlotTestImage(psImage *image, char *name) {
+    if (!pmVisualIsVisual() || !plotTestImage) return true;
+    if (!pmVisualInitWindow(&kapa, "pmStack:Images")) return false;
+
+    if(!pmVisualScaleImage(kapa, image, (const char*)name, 0, true)) return false;
+    pmVisualAskUser(&plotTestImage);
+    return true;
+}
+
+#else
+bool pmStackVisualClose() {return true;}
+bool pmStackVisualPlotTestImage(psImage *image, char *name) {return true;}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmStackVisual.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmStackVisual.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmStackVisual.h	(revision 42651)
@@ -0,0 +1,7 @@
+#ifndef PM_STACK_VISUAL_H
+#define PM_STACK_VISUAL_H
+
+bool pmStackVisualClose(void);
+bool pmStackVisualPlotTestImage(psImage *image, char *name);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtraction.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtraction.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtraction.c	(revision 42651)
@@ -0,0 +1,1732 @@
+/** @file pmSubtraction.c
+ *
+ *  @author Paul Price, IfA
+ *  @author GLG, MHPCC
+ *
+ *  Copyright 2004-2007 Institute for Astronomy, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmErrorCodes.h"
+#include "pmHDU.h"                      // Required for pmFPA.h
+#include "pmFPA.h"
+#include "pmSubtractionTypes.h"
+#include "pmSubtractionStamps.h"
+#include "pmSubtractionEquation.h"
+#include "pmSubtractionVisual.h"
+#include "pmSubtractionThreads.h"
+
+bool psFitsWriteImageSimple (char *filename, psImage *image, psMetadata *header);
+
+#include "pmSubtraction.h"
+
+# define FFT_WINDOW 0
+//#define TESTING
+
+#define PIXEL_LIST_BUFFER 100           // Number of entries to add to pixel list at a time
+#define MIN_SAMPLE_STATS    7           // Minimum number to use sample statistics; otherwise use quartiles
+#define USE_KERNEL_ERR                  // Use kernel error image?
+#define NUM_COVAR_POS 5                 // Number of positions for covariance calculation
+//MEH -- this is causing diffim fault 5 -- seems not as robust
+#define USE_LOGFIT_REJECT
+
+// XXX we need to pass these fwhm values elsewhere.  These should go on one of the structure, but 
+// things are too confusing to do that now.  just save them here.
+static float FWHM1 = NAN;
+static float FWHM2 = NAN;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Private (file-static) functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Generate the kernel to apply to the variance from the normal kernel
+static psKernel *varianceKernel(psKernel *out, // Output kernel
+                                const psKernel *normalKernel // Normal kernel
+                                )
+{
+    // Kernel range
+    int xMin = normalKernel->xMin, xMax = normalKernel->xMax;
+    int yMin = normalKernel->yMin, yMax = normalKernel->yMax;
+
+    if (!out) {
+        out = psKernelAlloc(xMin, xMax, yMin, yMax);
+    }
+
+    // Take the square of the normal kernel
+    for (int v = yMin; v <= yMax; v++) {
+        for (int u = xMin; u <= xMax; u++) {
+            out->kernel[v][u] = PS_SQR(normalKernel->kernel[v][u]);
+        }
+    }
+    return out;
+}
+
+// Contribute to an image of the solved kernel component using the preCalculated image
+static void solvedKernelPreCalc(psKernel *kernel, // Kernel, updated
+                                const pmSubtractionKernels *kernels, // Kernel basis functions
+                                float value,                         // Normalisation value for basis function
+                                int index                  // Index of basis function of interest
+    )
+{
+    int size = kernels->size;           // Kernel half-size
+    pmSubtractionKernelPreCalc *preCalc = kernels->preCalc->data[index]; // Precalculated values
+#if 0
+    // Iterating over the kernel
+    for (int y = 0, v = -size; v <= size; y++, v++) {
+        float yValue = value * preCalc->yKernel->data.F32[y];
+        for (int x = 0, u = -size; u <= size; x++, u++) {
+            kernel->kernel[v][u] +=  yValue * preCalc->xKernel->data.F32[x];
+        }
+    }
+    // Photometric scaling for even kernels only
+    if (kernels->u->data.S32[i] % 2 == 0 && kernels->v->data.S32[i] % 2 == 0) {
+        kernel->kernel[0][0] -= value;
+    }
+#else
+    for (int v = -size; v <= size; v++) {
+        for (int u = -size; u <= size; u++) {
+            kernel->kernel[v][u] +=  value * preCalc->kernel->kernel[v][u];
+        }
+    }
+#endif
+
+    return;
+}
+
+// Generate an image of the solved kernel
+static psKernel *solvedKernel(psKernel *kernel, // Kernel, to return
+                              const pmSubtractionKernels *kernels, // Kernel basis functions
+                              const psImage *polyValues, // Spatial polynomial values
+                              bool normalise,            // Add normalisation?
+                              bool wantDual // Want the dual (second) kernel?
+                              )
+{
+    assert(kernels);
+    assert(polyValues);
+
+    int numKernels = kernels->num;      // Number of kernel basis functions
+    int size = kernels->size;           // Kernel half-size
+    if (!kernel) {
+        kernel = psKernelAlloc(-size, size, -size, size);
+    }
+    psImageInit(kernel->image, 0.0);
+
+    for (int i = 0; i < numKernels; i++) {
+        double value = p_pmSubtractionSolutionCoeff(kernels, polyValues, i, wantDual); // Polynomial value
+        if (wantDual) {
+            // The model is built with the dual convolution terms added, so to produce zero residual the
+            // equation results in negative coefficients which we must undo.
+            value *= -1.0;
+        }
+
+        switch (kernels->type) {
+          case PM_SUBTRACTION_KERNEL_POIS: {
+              int u = kernels->u->data.S32[i]; // Offset in x
+              int v = kernels->v->data.S32[i]; // Offset in y
+              kernel->kernel[v][u] += value;
+              kernel->kernel[0][0] -= value;
+              break;
+          }
+          /* SPAM and FRIES use the same method */
+          case PM_SUBTRACTION_KERNEL_SPAM:
+          case PM_SUBTRACTION_KERNEL_FRIES: {
+              int uStart = kernels->u->data.S32[i];
+              int uStop = kernels->uStop->data.S32[i];
+              int vStart = kernels->v->data.S32[i];
+              int vStop = kernels->vStop->data.S32[i];
+
+              // Normalising sum of kernel component to unity
+              float norm = 1.0 / (float)((uStop - uStart + 1) * (vStop - vStart + 1));
+
+              for (int v = vStart; v <= vStop; v++) {
+                  for (int u = uStart; u <= uStop; u++) {
+                      kernel->kernel[v][u] += norm * value;
+                      kernel->kernel[0][0] -= value;
+                  }
+              }
+              break;
+          }
+          case PM_SUBTRACTION_KERNEL_GUNK: {
+              if (i < kernels->inner) {
+                  solvedKernelPreCalc(kernel, kernels, value, i);
+              } else {
+                  // Using delta function
+                  int u = kernels->u->data.S32[i]; // Offset in x
+                  int v = kernels->v->data.S32[i]; // Offset in y
+                  kernel->kernel[v][u] += value;
+                  kernel->kernel[0][0] -= value;
+              }
+              break;
+          }
+	case PM_SUBTRACTION_KERNEL_SIMPLE: {
+              solvedKernelPreCalc(kernel, kernels, 1.0, i);
+              break;
+	}	  
+	  
+          case PM_SUBTRACTION_KERNEL_ISIS:
+          case PM_SUBTRACTION_KERNEL_ISIS_RADIAL:
+          case PM_SUBTRACTION_KERNEL_HERM:
+          case PM_SUBTRACTION_KERNEL_DECONV_HERM: {
+              solvedKernelPreCalc(kernel, kernels, value, i);
+              break;
+          }
+          case PM_SUBTRACTION_KERNEL_RINGS: {
+              pmSubtractionKernelPreCalc *preCalc = kernels->preCalc->data[i]; // Precalculated kernels
+              int num = preCalc->uCoords->n;     // Number of pixels
+
+              for (int j = 0; j < num; j++) {
+                  int u = preCalc->uCoords->data.S32[j];
+                  int v = preCalc->vCoords->data.S32[j]; // Kernel coordinates
+                  kernel->kernel[v][u] += preCalc->poly->data.F32[j] * value;
+              }
+              // Photometric scaling is built into the kernel --- no subtraction!
+              break;
+          }
+          default:
+            psAbort("Should never get here.");
+        }
+    }
+
+    /* we have three possible implementations for handling flux conservation and photometric scaling.
+       
+       1) the original implementation of Alard-Lupton subtracted the 0th kernel gaussian from
+          the rest of the kernels.  the non-zero integral of the 0th kernel allows for some
+          flux re-scaling if needed.
+
+       2) the original ppSub implementation subtracted a delta function from the kernels.  this
+          seems to make the solution sensitive to noise terms.
+
+       3) accept the measured flux normalization and make all kernels have zero integral, but
+          use a gaussian to zero the flux.  
+
+	  *** the operation below is only needed if we use option (2) ***
+    */
+
+    if (normalise) {
+        // Put in the normalisation component
+        kernel->kernel[0][0] += (wantDual ? 1.0 : p_pmSubtractionSolutionNorm(kernels));
+    }
+    return kernel;
+}
+
+// Subtract the (0,0) element to preserve photometric scaling
+static void convolveSub(psKernel *convolved, // Convolved image
+                        const psKernel *image, // Image being convolved
+                        int footprint  // Size of region of interest
+                        )
+{
+    // Can't use psBinaryOp because the images are of different size
+    for (int y = -footprint; y <= footprint; y++) {
+        for (int x = -footprint; x <= footprint; x++) {
+            convolved->kernel[y][x] -= image->kernel[y][x];
+        }
+    }
+    return;
+}
+
+// Generate the convolution given some offset
+static psKernel *convolveOffset(const psKernel *image, // Image to convolve (a kernel for convenience)
+                                int u, int v, // Offset to apply
+                                int footprint // Size of region of interest
+                                )
+{
+    psKernel *convolved = psKernelAlloc(-footprint, footprint, -footprint, footprint); // Convolved image
+    for (int y = -footprint; y <= footprint; y++) {
+        for (int x = -footprint; x <= footprint; x++) {
+            convolved->kernel[y][x] = image->kernel[y - v][x - u];
+        }
+    }
+    return convolved;
+}
+
+
+// Convolve an image using FFT
+static void convolveFFT(psImage *target,// Place the result in here
+                        psImage *image, // Image to convolve
+                        psImage *mask, // Mask image
+                        psImageMaskType maskVal, // Value to mask
+                        const psKernel *kernel, // Kernel by which to convolve
+                        psRegion region,// Region of interest
+                        float background, // Background to add
+                        int size        // Size of (square) kernel
+                        )
+{
+    psRegion border = psRegionSet(region.x0 - size, region.x1 + size,
+                                  region.y0 - size, region.y1 + size); // Add a border
+
+    psImage *subImage = image ? psImageSubset(image, border) : NULL; // Subimage to convolve
+    psImage *subMask = mask ? psImageSubset(mask, border) : NULL; // Subimage mask
+
+# if (FFT_WINDOW)
+    psImage *convolved = psImageConvolveFFTwithWindow(NULL, subImage, subMask, maskVal, kernel); // Convolution
+# else
+    psImage *convolved = psImageConvolveFFT(NULL, subImage, subMask, maskVal, kernel); // Convolution
+# endif
+
+    psFree(subImage);
+    psFree(subMask);
+
+    // Now, we have to stick it in where it belongs
+    int xMin = region.x0, xMax = region.x1, yMin = region.y0, yMax = region.y1; // Bounds of region
+    if (background != 0.0) {
+        for (int yTarget = yMin, ySource = size; yTarget < yMax; yTarget++, ySource++) {
+            for (int xTarget = xMin, xSource = size; xTarget < xMax; xTarget++, xSource++) {
+                target->data.F32[yTarget][xTarget] = convolved->data.F32[ySource][xSource] + background;
+            }
+        }
+    } else {
+        int numBytes = (xMax - xMin) * PSELEMTYPE_SIZEOF(PS_TYPE_F32); // Number of bytes to copy
+        for (int yTarget = yMin, ySource = size; yTarget < yMax; yTarget++, ySource++) {
+            memcpy(&target->data.F32[yTarget][xMin], &convolved->data.F32[ySource][size], numBytes);
+        }
+    }
+    psFree(convolved);
+
+    return;
+}
+
+
+// Convolve an image using FFT
+static void convolveVarianceFFT(psImage *target,// Place the result in here
+                                psImage *variance, // Variance map to convolve
+                                psImage *kernelErr, // Kernel error image
+                                psImage *mask, // Mask image
+                                psImageMaskType maskVal, // Value to mask
+                                const psKernel *kernel, // Kernel by which to convolve
+                                psRegion region,// Region of interest
+                                int size        // Size of (square) kernel
+                                )
+{
+    psRegion border = psRegionSet(region.x0 - size, region.x1 + size,
+                                  region.y0 - size, region.y1 + size); // Add a border
+
+    psImage *subVariance = variance ? psImageSubset(variance, border) : NULL; // Variance map
+    psImage *subKE = kernelErr ? psImageSubset(kernelErr, border) : NULL; // Kernel error image
+    psImage *subMask = mask ? psImageSubset(mask, border) : NULL; // Mask
+
+    // XXX Can trim this a little by combining the convolution: only have to take the FFT of the kernel once
+# if (FFT_WINDOW)
+    psImage *convVariance = psImageConvolveFFTwithWindow(NULL, subVariance, subMask, maskVal, kernel); // Convolved variance
+    psImage *convKE = subKE ? psImageConvolveFFTwithWindow(NULL, subKE, subMask, maskVal, kernel) : NULL; // Conv KE
+# else
+    psImage *convVariance = psImageConvolveFFT(NULL, subVariance, subMask, maskVal, kernel); // Convolved variance
+    psImage *convKE = subKE ? psImageConvolveFFT(NULL, subKE, subMask, maskVal, kernel) : NULL; // Conv KE
+# endif
+
+    psFree(subVariance);
+    psFree(subKE);
+    psFree(subMask);
+
+    // Now, we have to stick it in where it belongs
+    int xMin = region.x0, xMax = region.x1, yMin = region.y0, yMax = region.y1; // Bounds of region
+    if (convKE) {
+        for (int yTarget = yMin, ySource = size; yTarget < yMax; yTarget++, ySource++) {
+            for (int xTarget = xMin, xSource = size; xTarget < xMax; xTarget++, xSource++) {
+                target->data.F32[yTarget][xTarget] = convVariance->data.F32[ySource][xSource] +
+                    convKE->data.F32[ySource][xSource];
+            }
+        }
+    } else {
+        int numBytes = (xMax - xMin) * PSELEMTYPE_SIZEOF(PS_TYPE_F32); // Number of bytes to copy
+        for (int yTarget = yMin, ySource = size; yTarget < yMax; yTarget++, ySource++) {
+            memcpy(&target->data.F32[yTarget][xMin], &convVariance->data.F32[ySource][size], numBytes);
+        }
+    }
+
+    psFree(convVariance);
+    psFree(convKE);
+
+    return;
+}
+
+
+// Convolve an image directly
+static void convolveDirect(psImage *target, // Put the result here
+                           const psImage *image, // Image to convolve
+                           const psKernel *kernel, // Kernel by which to convolve
+                           psRegion region,// Region of interest
+                           float background, // Background to add
+                           int size        // Size of (square) kernel
+                           )
+{
+    for (int y = region.y0; y < region.y1; y++) {
+        for (int x = region.x0; x < region.x1; x++) {
+            target->data.F32[y][x] = background;
+            for (int v = -size; v <= size; v++) {
+                for (int u = -size; u <= size; u++) {
+                    target->data.F32[y][x] += kernel->kernel[v][u] * image->data.F32[y - v][x - u];
+                }
+            }
+        }
+    }
+    return;
+}
+
+// Convolve a region of an image
+static inline void convolveRegion(psImage *convImage, // Convolved image (output)
+                                  psImage *convVariance, // Convolved variance map (output), or NULL
+                                  psImage *convMask, // Convolve mask (output), or NULL
+                                  psKernel **kernelImage, // Convolution kernel for the image
+                                  psKernel **kernelVariance, // Convolution kernel for the variance map, or NULL
+                                  psImage *image, // Image to convolve
+                                  psImage *variance, // Variance map to convolve, or NULL
+                                  const psKernel *covar,               // Covariance, or NULL
+                                  psImage *kernelErr, // Kernel error image, or NULL
+                                  psImage *subMask, // Subtraction mask
+                                  const pmSubtractionKernels *kernels, // Kernels
+                                  const psImage *polyValues, // Polynomial values
+                                  float background, // Background value to apply
+                                  psRegion region, // Region to convolve
+                                  psImageMaskType maskBad, // Value to give bad pixels
+                                  psImageMaskType maskPoor, // Value to give poor pixels
+                                  float poorFrac, // Fraction for "poor"
+                                  bool useFFT,  // Use FFT to convolve?
+                                  bool wantDual // Want the dual convolution?
+    )
+{
+    *kernelImage = solvedKernel(*kernelImage, kernels, polyValues, true, wantDual);
+    if (variance || subMask) {
+        *kernelVariance = varianceKernel(*kernelVariance, *kernelImage);
+    }
+
+    psImageMaskType subBad;                  // Bad pixels in subtraction mask
+    psImageMaskType subConvBad;              // Bad pixels in subtraction mask when convolving
+    psImageMaskType subConvPoor;             // Poor pixels in subtraction mask when convolving
+    if (kernels->mode == PM_SUBTRACTION_MODE_1 || (kernels->mode == PM_SUBTRACTION_MODE_DUAL && !wantDual)) {
+        subBad = PM_SUBTRACTION_MASK_BAD_1;
+        subConvBad = PM_SUBTRACTION_MASK_CONVOLVE_BAD_1;
+        subConvPoor = PM_SUBTRACTION_MASK_CONVOLVE_1;
+    } else {
+        subBad = PM_SUBTRACTION_MASK_BAD_2;
+        subConvBad = PM_SUBTRACTION_MASK_CONVOLVE_BAD_2;
+        subConvPoor = PM_SUBTRACTION_MASK_CONVOLVE_2;
+    }
+
+    // Convolve the image and variance
+    if (useFFT) {
+        // Use Fast Fourier Transform to do the convolution
+        // This provides a big speed-up for large kernels
+        convolveFFT(convImage, image, subMask, subBad, *kernelImage, region, background, kernels->size);
+        if (variance) {
+            convolveVarianceFFT(convVariance, variance, kernelErr, subMask, subBad, *kernelVariance,
+                                region, kernels->size);
+        }
+    } else {
+        // XXX Direct convolution doesn't account for bad pixels yet
+        convolveDirect(convImage, image, *kernelImage, region, background, kernels->size);
+        if (variance) {
+            convolveDirect(convVariance, variance, *kernelVariance, region, 0.0, kernels->size);
+        }
+    }
+
+    if (variance && covar) {
+        // Apply covariance factor to variance map, to allow for spatial variation
+        float factor = psImageCovarianceCalculateFactor(*kernelImage, covar); // Factor to apply
+        for (int y = region.y0; y < region.y1; y++) {
+            for (int x = region.x0; x < region.x1; x++) {
+                convVariance->data.F32[y][x] *= factor;
+            }
+        }
+    }
+
+    // Convolve the mask for bad/poor pixels
+    if (subMask && convMask) {
+        int box = p_pmSubtractionBadRadius(*kernelImage, kernels, polyValues,
+                                           wantDual, poorFrac); // Size of bad box
+        psAssert(box >= 0, "Bad radius must be >= 0");
+
+        int colMin = region.x0, colMax = region.x1, rowMin = region.y0, rowMax = region.y1; // Bounds
+        psImage *convolved = NULL; // Convolved subtraction mask
+        if (box > 0) {
+            psRegion maskRegion = psRegionSet(colMin - box, colMax + box,
+                                              rowMin - box, rowMax + box); // Region to convolve
+            psImage *image = subMask ? psImageSubset(subMask, maskRegion) : NULL; // Mask to convolve
+            convolved = psImageConvolveMask(NULL, image, subBad, subConvBad, -box, box, -box, box);
+            psFree(image);
+        } else {
+            convolved = psImageSubset(subMask, region);
+        }
+
+        psAssert(convolved->numCols - 2 * box == colMax - colMin, "Bad number of columns");
+        psAssert(convolved->numRows - 2 * box == rowMax - rowMin, "Bad number of rows");
+
+        for (int yTarget = rowMin, ySource = box; yTarget < rowMax; yTarget++, ySource++) {
+            // Dereference images
+            psImageMaskType *target = &convMask->data.PS_TYPE_IMAGE_MASK_DATA[yTarget][colMin]; // Target values
+            psImageMaskType *source = &convolved->data.PS_TYPE_IMAGE_MASK_DATA[ySource][box]; // Source values
+            for (int xTarget = colMin; xTarget < colMax; xTarget++, target++, source++) {
+                if (*source & subConvBad) {
+                    *target |= maskBad;
+                } else if (*source & subConvPoor) {
+                    *target &= ~maskBad;
+                    *target |= maskPoor;
+                } else {
+                    *target &= ~maskBad & ~maskPoor;
+                }
+            }
+        }
+
+        psFree(convolved);
+    }
+
+    return;
+}
+
+#ifdef USE_KERNEL_ERR
+// Generate an image that can be used to track systematic errors in the kernel
+static psImage *subtractionKernelErrImage(const psImage *image, // Image from which to make kernel error image
+                                          float kernelError // Relative systematic error in kernel
+    )
+{
+    if (!isfinite(kernelError) || kernelError == 0.0) {
+        return NULL;
+    }
+
+    int numCols = image->numCols, numRows = image->numRows; // Size of image
+    psImage *kernelErr = psImageAlloc(numCols, numRows, PS_TYPE_F32); // Kernel error image
+
+    float kernelError2 = PS_SQR(kernelError); // Square of the kernel error
+    for (int y = 0; y < numRows; y++) {
+        for (int x = 0; x < numCols; x++) {
+            kernelErr->data.F32[y][x] = PS_SQR(image->data.F32[y][x]) * kernelError2;
+        }
+    }
+
+    return kernelErr;
+}
+#endif
+
+// Convolve a stamp using a pre-calculated kernel basis function
+static psKernel *convolveStampPreCalc(const psKernel *image, // Image to convolve
+                                      const pmSubtractionKernels *kernels, // Kernel basis functions
+                                      int index,                            // Index of basis function of interest
+                                      int footprint                         // Half-size of stamp
+    )
+{
+    pmSubtractionKernelPreCalc *preCalc = kernels->preCalc->data[index]; // Precalculated data
+#if 0
+    // Convolving using separable convolution
+    int size = kernels->size;     // Size of kernel
+
+    // Convolve in x
+    // Need to convolve a bit more than the footprint, for the y convolution
+    int yMin = -size - footprint, yMax = size + footprint; // Range for y
+    psKernel *temp = psKernelAlloc(yMin, yMax,
+                                   -footprint, footprint); // Temporary convolution; NOTE: wrong way!
+    for (int y = yMin; y <= yMax; y++) {
+        for (int x = -footprint; x <= footprint; x++) {
+            float value = 0.0;    // Value of convolved pixel
+            int uMin = x - size, uMax = x + size; // Range for u
+            psF32 *xKernelData = &preCalc->xKernel->data.F32[xKernel->n - 1]; // Kernel values
+            psF32 *imageData = &image->kernel[y][uMin]; // Image values
+            for (int u = uMin; u <= uMax; u++, xKernelData--, imageData++) {
+                value += *xKernelData * *imageData;
+            }
+            temp->kernel[x][y] = value; // NOTE: putting in wrong way!
+        }
+    }
+
+    // Convolve in y
+    psKernel *convolved = psKernelAlloc(-footprint, footprint, -footprint, footprint);// Convolved image
+    for (int x = -footprint; x <= footprint; x++) {
+        for (int y = -footprint; y <= footprint; y++) {
+            float value = 0.0;    // Value of convolved pixel
+            int vMin = y - size, vMax = y + size; // Range for v
+            psF32 *yKernelData = &preCalc->yKernel->data.F32[yKernel->n - 1]; // Kernel values
+            psF32 *imageData = &temp->kernel[x][vMin]; // Image values; NOTE: wrong way!
+            for (int v = vMin; v <= vMax; v++, yKernelData--, imageData++) {
+                value += *yKernelData * *imageData;
+            }
+            convolved->kernel[y][x] = value;
+        }
+    }
+    psFree(temp);
+
+    // Photometric scaling for even kernels only
+    if (kernels->u->data.S32[index] % 2 == 0 && kernels->v->data.S32[index] % 2 == 0) {
+        convolveSub(convolved, image, footprint);
+    }
+    return convolved;
+#else
+    // Convolving using precalculated kernel
+    return p_pmSubtractionConvolveStampPrecalc(image, preCalc->kernel);
+#endif
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Semi-public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+psKernel *p_pmSubtractionConvolveStampPrecalc(const psKernel *image, const psKernel *kernel)
+{
+    PS_ASSERT_KERNEL_NON_NULL(image, NULL);
+    PS_ASSERT_KERNEL_NON_NULL(kernel, NULL);
+
+# if (FFT_WINDOW)
+    psImage *conv = psImageConvolveFFTwithWindow(NULL, image->image, NULL, 0, kernel); // Convolved image
+# else
+    psImage *conv = psImageConvolveFFT(NULL, image->image, NULL, 0, kernel); // Convolved image
+# endif
+
+    // note: do not attempt to renormalize kernels here: cannot have different stars with
+    // different kernel ratios
+
+    int x0 = - image->xMin, y0 = - image->yMin; // Position of centre of convolved image
+    psKernel *convolved = psKernelAllocFromImage(conv, x0, y0); // Kernel version
+
+    // pmSubtractionVisualShowSubtraction(image->image, kernel->image, conv);
+
+    psFree(conv);
+    return convolved;
+}
+
+int p_pmSubtractionBadRadius(psKernel *preKernel, const pmSubtractionKernels *kernels,
+                             const psImage *polyValues, bool wantDual, float poorFrac)
+{
+    psKernel *kernel;                   // Kernel to use
+    if (!preKernel) {
+        kernel = solvedKernel(NULL, kernels, polyValues, true, wantDual);
+    } else {
+        kernel = psMemIncrRefCounter(preKernel);
+    }
+    PS_ASSERT_IMAGE_NON_NULL(polyValues, -1);
+
+    int xMin = kernel->xMin, xMax = kernel->xMax, yMin = kernel->yMin, yMax = kernel->yMax; // Bounds
+
+    // Determine the threshold between bad and poor
+    double sumKernel2 = 0.0;            // Sum of the kernel-squared
+    for (int y = yMin; y <= yMax; y++) {
+        for (int x = xMin; x <= xMax; x++) {
+            sumKernel2 += PS_SQR(kernel->kernel[y][x]);
+        }
+    }
+    float threshold = sumKernel2 * poorFrac; // Threshold between poor and bad
+
+    // Get bounds of threshold region
+    // Start with the entire kernel, and keep reducing the size of the box until it sum goes above threshold
+    int box = kernels->size;            // Size of box with bad pixels
+    for (double sumBox = 0.0; sumBox < threshold && box > 0; box--) {
+        for (int x = -box; x <= box; x++) {
+            sumBox += PS_SQR(kernel->kernel[-box][x]) + PS_SQR(kernel->kernel[box][x]);
+        }
+        for (int y = -box + 1; y <= box - 1; y++) {
+            // Note: not doing corners
+            sumBox += PS_SQR(kernel->kernel[y][-box]) + PS_SQR(kernel->kernel[y][box]);
+        }
+    }
+
+    psFree(kernel);
+
+    return box;
+}
+
+void p_pmSubtractionPolynomialNormCoords(float *xOut, float *yOut, float xIn, float yIn,
+                                         int xMin, int xMax, int yMin, int yMax)
+{
+    float xNormSize = xMax - xMin, yNormSize = yMax - yMin; // Size to use for normalisation
+    *xOut = 2.0 * (float)(xIn - xMin - xNormSize/2.0) / xNormSize;
+    *yOut = 2.0 * (float)(yIn - yMin - yNormSize/2.0) / yNormSize;
+    return;
+}
+
+psImage *p_pmSubtractionPolynomialFromCoords(psImage *output, const pmSubtractionKernels *kernels,
+                                             int x, int y)
+{
+    assert(kernels);
+
+    float xNorm, yNorm;                 // Normalised coordinates
+    p_pmSubtractionPolynomialNormCoords(&xNorm, &yNorm, x, y,
+                                        kernels->xMin, kernels->xMax, kernels->yMin, kernels->yMax);
+    return p_pmSubtractionPolynomial(output, kernels->spatialOrder, xNorm, yNorm);
+}
+
+psImage *p_pmSubtractionPolynomial(psImage *output, int spatialOrder, float x, float y)
+{
+    assert(spatialOrder >= 0);
+    assert(x >= -1 && x <= 1);
+    assert(y >= -1 && y <= 1);
+
+    output = psImageRecycle(output, spatialOrder + 1, spatialOrder + 1, PS_TYPE_F64);
+    output->data.F64[0][0] = 1.0;
+
+    double value = 1.0;
+    for (int i = 1; i <= spatialOrder; i++) {
+        value *= x;
+        output->data.F64[0][i] = value;
+    }
+
+    value = 1.0;
+    for (int j = 1; j <= spatialOrder; j++) {
+        value *= y;
+        output->data.F64[j][0] = value;
+    }
+
+    for (int j = 1; j <= spatialOrder; j++) {
+        for (int i = 1; i <= spatialOrder - j; i++) {
+            output->data.F64[j][i] = output->data.F64[j][0] * output->data.F64[0][i];
+        }
+    }
+
+    return output;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+// Convolve a stamp by a single kernel basis function
+static psKernel *convolveStampSingle(const pmSubtractionKernels *kernels, // Kernel basis functions
+                                     int index, // Kernel basis function index
+                                     const psKernel *image, // Image to convolve (a kernel for convenience)
+                                     int footprint // Size of region of interest
+    )
+{
+    switch (kernels->type) {
+      case PM_SUBTRACTION_KERNEL_POIS: {
+          int u = kernels->u->data.S32[index]; // Offset in x
+          int v = kernels->v->data.S32[index]; // Offset in y
+          psKernel *convolved = convolveOffset(image, u, v, footprint); // Convolved image
+          convolveSub(convolved, image, footprint);
+          return convolved;
+      }
+        // Method for SPAM and FRIES is the same
+      case PM_SUBTRACTION_KERNEL_SPAM:
+      case PM_SUBTRACTION_KERNEL_FRIES: {
+          psKernel *convolved = psKernelAlloc(-footprint, footprint,
+                                              -footprint, footprint); // Convolved image
+          int uStart = kernels->u->data.S32[index];
+          int uStop = kernels->uStop->data.S32[index];
+          int vStart = kernels->v->data.S32[index];
+          int vStop = kernels->vStop->data.S32[index];
+          float norm = 1.0 / (uStop - uStart + 1) * (vStop - vStart + 1); // Normalisation
+          for (int y = -footprint; y <= footprint; y++) {
+              for (int x = -footprint; x <= footprint; x++) {
+                  double sum = 0.0;
+                  for (int v = vStart; v <= vStop; v++) {
+                      for (int u = uStart; u <= uStop; u++) {
+                          sum += image->kernel[y - v][x - u];
+                      }
+                  }
+                  convolved->kernel[y][x] = norm * sum;
+              }
+          }
+          convolveSub(convolved, image, footprint);
+          return convolved;
+      }
+      case PM_SUBTRACTION_KERNEL_GUNK: {
+          if (index < kernels->inner) {
+              // Photometric scaling is already built in to the precalculated kernel
+              return convolveStampPreCalc(image, kernels, index, footprint);
+          }
+          // Using delta function
+          int u = kernels->u->data.S32[index]; // Offset in x
+          int v = kernels->v->data.S32[index]; // Offset in y
+          psKernel *convolved = convolveOffset(image, u, v, footprint); // Convolved image
+          convolveSub(convolved, image, footprint);
+          return convolved;
+      }
+      case PM_SUBTRACTION_KERNEL_ISIS:
+      case PM_SUBTRACTION_KERNEL_ISIS_RADIAL:
+      case PM_SUBTRACTION_KERNEL_HERM:
+      case PM_SUBTRACTION_KERNEL_DECONV_HERM: {
+            return convolveStampPreCalc(image, kernels, index, footprint);
+        }
+      case PM_SUBTRACTION_KERNEL_RINGS: {
+          psKernel *convolved = psKernelAlloc(-footprint, footprint, -footprint, footprint); // Convolved image
+          pmSubtractionKernelPreCalc *preCalc = kernels->preCalc->data[index]; // Precalculated data
+
+          int num = preCalc->uCoords->n;         // Number of pixels
+          psS32 *uData = preCalc->uCoords->data.S32; // Dereference v coordinate
+          psS32 *vData = preCalc->vCoords->data.S32; // Dereference u coordinate
+          psF32 *polyData = preCalc->poly->data.F32; // Dereference polynomial values
+          psF32 **imageData = image->kernel;  // Dereference image
+          psF32 **convData = convolved->kernel; // Dereference convolved image
+          for (int y = -footprint; y <= footprint; y++) {
+              for (int x = -footprint; x <= footprint; x++) {
+                  double sum = 0.0;             // Accumulated sum from convolution
+                  for (int j = 0; j < num; j++) {
+                      int u = uData[j], v = vData[j]; // Kernel coordinates
+                      sum += imageData[y - v][x - u] * polyData[j];
+                  }
+                  convData[y][x] = sum;
+                  // Photometric scaling is built into the kernel --- no subtraction!
+              }
+          }
+          return convolved;
+      }
+      default:
+        psAbort("Should never get here.");
+    }
+    return NULL;
+}
+
+// Convolve the stamp by each of the kernel basis functions
+static psArray *convolveStamp(psArray *convolutions, // The convolutions
+                              const psKernel *image, // Image to convolve
+                              const pmSubtractionKernels *kernels, // Kernel basis functions
+                              int footprint // Stamp half-size
+    )
+{
+    assert(image);
+    assert(kernels);
+    assert(footprint >= 0);
+
+    if (convolutions) {
+        return convolutions;
+    }
+
+    int numKernels = kernels->num;      // Number of kernels
+    convolutions = psArrayAlloc(numKernels);
+
+    for (int i = 0; i < numKernels; i++) {
+        convolutions->data[i] = convolveStampSingle(kernels, i, image, footprint);
+    }
+
+    return convolutions;
+}
+
+
+bool pmSubtractionConvolveStampThread(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    pmSubtractionStamp *stamp = job->args->data[0]; // List of stamps
+    pmSubtractionKernels *kernels = job->args->data[1]; // Kernels
+    int footprint = PS_SCALAR_VALUE(job->args->data[2], S32); // Stamp index -- MEH - it is?
+
+    return pmSubtractionConvolveStamp(stamp, kernels, footprint);
+}
+
+bool pmSubtractionConvolveStamp (pmSubtractionStamp *stamp, pmSubtractionKernels *kernels, int footprint)
+{
+    PS_ASSERT_PTR_NON_NULL(stamp, false);
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+    PS_ASSERT_INT_NONNEGATIVE(footprint, false);
+
+    if (stamp->status != PM_SUBTRACTION_STAMP_CALCULATE) {
+        psError(PM_ERR_PROG, true, "Stamp not marked for calculation.");
+        return false;
+    }
+
+    switch (kernels->mode) {
+      case PM_SUBTRACTION_MODE_1:
+        stamp->convolutions1 = convolveStamp(stamp->convolutions1, stamp->image1, kernels, footprint);
+        break;
+      case PM_SUBTRACTION_MODE_2:
+        stamp->convolutions2 = convolveStamp(stamp->convolutions2, stamp->image2, kernels, footprint);
+        break;
+      case PM_SUBTRACTION_MODE_UNSURE:
+      case PM_SUBTRACTION_MODE_DUAL:
+        stamp->convolutions1 = convolveStamp(stamp->convolutions1, stamp->image1, kernels, footprint);
+        stamp->convolutions2 = convolveStamp(stamp->convolutions2, stamp->image2, kernels, footprint);
+	if (!pmSubtractionKernelPenaltiesStamp(stamp, kernels)) {
+	    psAbort("failure in penalties");
+	}
+        break;
+      default:
+        psAbort("Unsupported subtraction mode: %x", kernels->mode);
+    }
+
+#ifdef TESTING
+    //MEH - index conflict or changed in past?
+    for (int j = 0; j < kernels->num; j++) {
+        if (stamp->convolutions1) {
+            psString convName = NULL;
+            //psStringAppend(&convName, "conv1_%03d_%03d.fits", index, j);
+            psStringAppend(&convName, "conv1_xxx_%03d.fits", j);
+	    psFits *fits = psFitsOpen(convName, "w");
+            psFree(convName);
+            psKernel *conv = stamp->convolutions1->data[j];
+            psFitsWriteImage(fits, NULL, conv->image, 0, NULL);
+            psFitsClose(fits);
+        }
+
+        if (stamp->convolutions2) {
+            psString convName = NULL;
+            //psStringAppend(&convName, "conv2_%03d_%03d.fits", index, j);
+            psStringAppend(&convName, "conv2_xxx_%03d.fits", j);
+	    psFits *fits = psFitsOpen(convName, "w");
+            psFree(convName);
+            psKernel *conv = stamp->convolutions2->data[j];
+            psFitsWriteImage(fits, NULL, conv->image, 0, NULL);
+            psFitsClose(fits);
+        }
+    }
+#endif
+
+    return true;
+}
+
+bool pmSubtractionConvolveStamps(pmSubtractionStampList *stamps, pmSubtractionKernels *kernels) 
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+
+    psTimerStart("pmSubtractionConvolveStamps");
+
+    int footprint = stamps->footprint;  // Half-size of stamps
+
+    // We iterate over each stamp and generate the convolution if needed.  We do NOT need the
+    // convolution if (a) it has already been calculated or (b) the stamp is not available for
+    // use (available = USED or CALCULATE)
+    
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+
+        bool keep = false;
+	keep |= (stamp->status == PM_SUBTRACTION_STAMP_USED);
+	keep |= (stamp->status == PM_SUBTRACTION_STAMP_CALCULATE);
+	if (!keep) continue;
+
+	bool haveConvolutions = false;
+	if (kernels->mode == PM_SUBTRACTION_MODE_1) {
+	    haveConvolutions = (stamp->convolutions1 != NULL);
+	}
+	if (kernels->mode == PM_SUBTRACTION_MODE_2) {
+	    haveConvolutions = (stamp->convolutions2 != NULL);
+	}
+	if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+	    haveConvolutions = (stamp->convolutions1 != NULL) && (stamp->convolutions2 != NULL);
+	}
+        if (haveConvolutions) {
+            continue;
+        }
+
+        if (pmSubtractionThreaded()) {
+            psThreadJob *job = psThreadJobAlloc("PSMODULES_SUBTRACTION_CONVOLVE_STAMP");
+            psArrayAdd(job->args, 1, stamp);
+            psArrayAdd(job->args, 1, kernels);
+            PS_ARRAY_ADD_SCALAR(job->args, footprint, PS_TYPE_S32);
+            if (!psThreadJobAddPending(job)) {
+                return false;
+            }
+        } else {
+            pmSubtractionConvolveStamp(stamp, kernels, footprint);
+        }
+    }
+    if (!psThreadPoolWait(true, true)) {
+        psError(psErrorCodeLast(), false, "Error waiting for threads.");
+        return false;
+    }
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Convolve stamps: %f sec", psTimerClear("pmSubtractionConvolveStamps"));
+    return true;
+}
+
+int pmSubtractionRejectStamps(pmSubtractionKernels *kernels, pmSubtractionStampList *stamps,
+                              pmSubtractionQuality *match, psImage *subMask, float sigmaRej)
+{
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, -1);
+    PS_ASSERT_IMAGE_NON_EMPTY(subMask, -1);
+    PS_ASSERT_IMAGE_TYPE(subMask, PS_TYPE_IMAGE_MASK, -1);
+
+    // Comment from PAP (r18287): I used to measure the rms deviation about zero, and use that as the
+    // sigma against which to clip, but the distribution is actually something like a chi^2 or
+    // Student's t, both of which become Gaussian-like with large N.  Therefore, let's just
+    // treat this as a Gaussian distribution.
+
+    // Comment from EAM (r29777): The residual distribution is only chisq-like if the model is
+    // a good fit to the data.  In the (likely) case that there is a systematic difference
+    // between the model and the data, the squared-residual distribution grows quadratically
+    // with increasing flux: the systematic residual flux is a constant factor times the source
+    // flux; the squared-residual is then of the form (k0 + k1*flux)^2, where k0 comes from the
+    // Gaussian distributed residual and k1*flux is the systematic residual error.
+
+    // By rejecting sources with the largest squared-residuals, the rejection biases against
+    // the brighter sources; in severe cases, this pushes the measurement to the weakest
+    // sources with the most noise.  To account for this, let's fit a 2nd order polynomial to
+    // the distribution of flux vs squared-residual, subtract that fit, and reject sources
+    // which are significantly deviant from that distribution.
+
+    // if we only have 3 or fewer stars, we have to accept them all
+    if (match->nGood < 3) {
+	kernels->mean = NAN;
+	kernels->rms = NAN;
+	kernels->numStamps = match->nGood;
+	
+	psLogMsg("psModules.imcombine", PS_LOG_INFO, "only %d stars, keeping them all (good luck!)",  kernels->numStamps);
+	return 0;
+    }
+
+    kernels->mean = NAN;
+    kernels->rms = NAN;
+    kernels->numStamps = -1;
+
+    psTrace("psModules.imcombine", 1, "Number of good stamps: %d\n", match->nGood);
+
+    // the chisq & flux vectors are calculated by pmSubtractionCalculateChisqAndMoments
+
+    // use 3hi/3lo sigma clipping on the chisq fit
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+    stats->clipSigma = 5.0;
+    stats->clipIter = 2;
+    psPolynomial1D *model = psPolynomial1DAlloc (PS_POLYNOMIAL_ORD, 2);
+
+#ifdef USE_LOGFIT_REJECT
+    // CZW: Since flux and chisq can range over many orders of magnitude, use a log-log fit to calculate the
+    // RMS scatter.  This should be more robust against outliers that can impact the fit quality.
+    psVector *logchisq = psVectorAlloc(match->chisq->n,PS_TYPE_F32);
+    psVector *logflux  = psVectorAlloc(match->fluxes->n,PS_TYPE_F32);
+    for (int qq = 0; qq < match->chisq->n; qq++) {
+      if ((match->chisq->data.F32[qq] > 0)&&
+	  (match->fluxes->data.F32[qq] > 0)) {
+	logchisq->data.F32[qq] = log10(match->chisq->data.F32[qq]);
+	logflux->data.F32[qq]  = log10(match->fluxes->data.F32[qq]);
+      }
+      else {
+	// Ignore negative values.  Maybe we could do an offset or something, but not today.
+	match->stampMask->data.PS_TYPE_VECTOR_MASK_DATA[qq] |= PM_SUBTRACTION_STAMP_REJECTED;
+      }	
+      //      if (!(match->stampMask->data.PS_TYPE_VECTOR_MASK_DATA[qq] & 0xff)) {
+      //	psLogMsg("psModules.imcombine", PS_LOG_INFO, "RRRRRR: %d %g %g %g %g\n",
+      //		 qq,match->chisq->data.F32[qq],match->fluxes->data.F32[qq],
+      //		 0.0,0.0);
+      // }
+
+    }
+
+    if (0) { 
+      FILE *f = fopen ("vector.dat", "w");
+      psAssert (f, "should not fail to open test file");
+      
+      for (int qq = 0; qq < match->chisq->n; qq++) {
+	fprintf (f, "%f %f : %f %f : %d\n", 
+		 match->chisq->data.F32[qq], match->fluxes->data.F32[qq], 
+		 logchisq->data.F32[qq], logflux->data.F32[qq], 
+		 match->stampMask->data.PS_TYPE_VECTOR_MASK_DATA[qq]);
+      }
+      fclose (f);
+    }
+
+
+    bool result = psVectorClipFitPolynomial1D(model, stats, match->stampMask, 0xff, logchisq, NULL, logflux);
+    if (!result) {
+	psError(PM_ERR_DATA, false, "Unable to measure statistics for deviations.");
+        psFree(model);
+        psFree(stats);
+	return -1;
+    }
+
+    // However, since we've done a log-log fit, we can't rely on the statistics in the stats object
+    // To accurately represent the distribution.  I've tried to massage them (as dlog10(X) = abs(1/(X log(10))) dX),
+    // but ended up deciding to just manually calculate the residual, and then pass that to a robust stats object.
+    psStats *residStats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+    psVector *residData = psVectorAlloc(match->chisq->n,PS_TYPE_F32);
+    int counter = 0;
+    for (int qq = 0; qq < match->chisq->n; qq++) {
+      if (!(match->stampMask->data.PS_TYPE_VECTOR_MASK_DATA[qq] & 0xff)) {
+	double vv = pow(10,
+			model->coeff[0] +
+			model->coeff[1] * log10(match->fluxes->data.F32[qq]) +
+			model->coeff[2] * pow(log10(match->fluxes->data.F32[qq]),2));
+	residData->data.F32[qq] = match->chisq->data.F32[qq] - vv;
+	counter++;
+	//			psLogMsg("psModules.imcombine", PS_LOG_INFO, "SSSSS: %d %g %g %g %g\n",
+	//			 qq,match->chisq->data.F32[qq],match->fluxes->data.F32[qq],
+	//			 vv,residData->data.F32[qq]);
+      }
+    }
+    psVectorStats(residStats,residData,NULL,match->stampMask,0xff);
+    if (isnan(residStats->robustMedian)) {
+	psError(PM_ERR_DATA, false, "Unable to measure statistics for deviations.");
+        psFree(model);
+        psFree(stats);
+	psFree(logchisq);
+	psFree(logflux);
+	psFree(residStats);
+	psFree(residData);
+
+        return -1;
+    }
+
+    kernels->mean = residStats->robustMedian;
+    kernels->rms =  residStats->robustStdev;
+    kernels->numStamps = counter;
+
+    psLogMsg ("pmPSFtry", 4, "chisq vs flux resid: %f +/- %f\n", residStats->robustMedian,residStats->robustStdev);
+    psFree(logchisq);
+    psFree(logflux);
+    psFree(residStats);
+    psFree(residData);
+
+#else
+    // CZW: Otherwise, use the original linear fit code.
+    bool result = psVectorClipFitPolynomial1D(model, stats, match->stampMask, 0xff, match->chisq, NULL, match->fluxes);
+    if (!result) {
+	psError(PM_ERR_DATA, false, "Unable to measure statistics for deviations.");
+        psFree(model);
+        psFree(stats);
+	return -1;
+    }
+    if (isnan(stats->sampleMean)) {
+	psError(PM_ERR_DATA, false, "Unable to measure statistics for deviations.");
+        psFree(model);
+        psFree(stats);
+        return -1;
+    }
+    kernels->mean = stats->sampleMean;
+    kernels->rms =  stats->sampleStdev;
+    kernels->numStamps = stats->clippedNvalues;
+    psLogMsg ("pmPSFtry", 4, "chisq vs flux resid: %f +/- %f\n", stats->sampleMean, stats->sampleStdev);
+#endif
+
+    psLogMsg ("pmPSFtry", 4, "chisq vs flux model: %e + %e flux + %e flux^2\n", model->coeff[0], model->coeff[1], model->coeff[2]);
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Mean deviation from %d stamps: %lf +/- %lf",  kernels->numStamps, kernels->mean, kernels->rms);
+
+    psString ds9name = NULL;            // Filename for ds9 region file
+    static int ds9num = 0;              // File number for ds9 region file
+    psStringAppend(&ds9name, "stamps_reject_%d.ds9", ds9num);
+    FILE *ds9 = pmSubtractionStampsFile(stamps, ds9name, "rejected stamps");
+    psFree(ds9name);
+    ds9num++;
+
+    int footprint = stamps->footprint;  // Half-size of stamp region of interest
+    int numRejected = 0;                // Number of stamps rejected
+    int numGood = 0;                    // Number of good stamps
+    psString log = NULL;                // Log message
+
+    // save DS9 region files for the stamps and mark for rejection and replacement
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+	if (stamp->status  != PM_SUBTRACTION_STAMP_USED) { continue; }
+        if (match->stampMask->data.PS_TYPE_VECTOR_MASK_DATA[i]) {
+            // Should we reject stars with low deviation?  Well, if this is really a Gaussian-like
+            // distribution and they're low, then we have the right to ask why.  Isn't it suspicious that
+            // they're anomalously low, compared to the rest of the population which (we hope) is indicative
+            // of normality?  Besides, the standard deviation is going to be blown up by stars that didn't
+            // subtract well, in which case very few (if any) stars will be legitimately rejected for being
+            // low.
+	    psTrace("psModules.imcombine", 3, "Rejecting stamp %d (%d,%d)\n", i,
+		    (int)(stamp->x - 0.5), (int)(stamp->y - 0.5));
+	    psStringAppend(&log, "Stamp %d (%d,%d): %f : %f : %f\n", 
+			   i, (int)(stamp->x - 0.5), (int)(stamp->y - 0.5),
+			   match->chisq->data.F32[i], match->fluxes->data.F32[i], match->chisq->data.F32[i] - psPolynomial1DEval(model, match->fluxes->data.F32[i])); 
+	    numRejected++;
+	    for (int y = stamp->y - footprint; y <= stamp->y + footprint; y++) {
+		for (int x = stamp->x - footprint; x <= stamp->x + footprint; x++) {
+		    subMask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= PM_SUBTRACTION_MASK_REJ;
+		}
+	    }
+	    pmSubtractionStampPrint(ds9, stamp->x, stamp->y, stamps->footprint, "red");
+
+	    // Set stamp for replacement
+	    stamp->x = 0;
+	    stamp->y = 0;
+	    stamp->xNorm = NAN;
+	    stamp->yNorm = NAN;
+	    stamp->status = PM_SUBTRACTION_STAMP_REJECTED;
+	    // Recalculate convolutions
+	    psFree(stamp->convolutions1);
+	    psFree(stamp->convolutions2);
+	    stamp->convolutions1 = stamp->convolutions2 = NULL;
+	    psFree(stamp->image1);
+	    psFree(stamp->image2);
+	    psFree(stamp->weight);
+	    stamp->image1 = stamp->image2 = stamp->weight = NULL;
+	    psFree(stamp->matrix);
+	    stamp->matrix = NULL;
+	    psFree(stamp->vector);
+	    stamp->vector = NULL;
+	} else {
+	    numGood++;
+	    pmSubtractionStampPrint(ds9, stamp->x, stamp->y, stamps->footprint, "green");
+        }
+    }
+
+    if (numRejected == 0) {
+        psStringAppend(&log, "<none>\n");
+    }
+    psLogMsg("psModules.imcombine", PS_LOG_DETAIL, "%s", log);
+    psFree(log);
+
+    if (ds9) {
+        fclose(ds9);
+    }
+
+    psFree(model);
+    psFree(stats);
+
+    if (numRejected > 0) {
+        psLogMsg("psModules.imcombine", PS_LOG_INFO, "%d good stamps; %d rejected.\n", numGood, numRejected);
+    } else {
+        psLogMsg("psModules.imcombine", PS_LOG_INFO, "%d good stamps; 0 rejected.\n", numGood);
+    }
+
+    return numRejected;
+}
+
+psKernel *pmSubtractionKernel(const pmSubtractionKernels *kernels, float x, float y, bool wantDual)
+{
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NULL);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(x, -1.0, 1.0, NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(y, -1.0, 1.0, NULL);
+
+    psImage *polyValues = p_pmSubtractionPolynomial(NULL, kernels->spatialOrder, x, y); // Solved polynomial
+    psKernel *kernel = solvedKernel(NULL, kernels, polyValues, true, wantDual); // The appropriate kernel
+    psFree(polyValues);
+
+    return kernel;
+}
+
+// generate an image of the convolution kernel realized at the given coordinate
+// if 'wantDual' is set, solution2 is supplied
+psImage *pmSubtractionKernelImage(const pmSubtractionKernels *kernels, float x, float y, bool wantDual)
+{
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NULL);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(x, -1.0, 1.0, NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(y, -1.0, 1.0, NULL);
+
+    psKernel *kernel = pmSubtractionKernel(kernels, x, y, wantDual); // Convolution kernel
+    psImage *image = psMemIncrRefCounter(kernel->image); // Image of the kernel
+    psFree(kernel);
+
+    return image;
+}
+
+
+float pmSubtractionVarianceFactor(const pmSubtractionKernels *kernels, float x, float y, bool wantDual)
+{
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NAN);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NAN);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(x, -1.0, 1.0, NAN);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(y, -1.0, 1.0, NAN);
+
+    // Precalulate polynomial values
+    psImage *polyValues = p_pmSubtractionPolynomial(NULL, kernels->spatialOrder, x, y);
+
+    psKernel *kernel = solvedKernel(NULL, kernels, polyValues, true, wantDual); // The appropriate kernel
+    psFree(polyValues);
+
+    double sumKernel2 = 0.0;            // Sum of the kernel squared
+    double sumKernel = 0.0;             // Sum of the kernel
+    for (int y = kernel->yMin; y <= kernel->yMax; y++) {
+        for (int x = kernel->xMin; x <= kernel->xMax; x++) {
+            sumKernel += kernel->kernel[y][x];
+            sumKernel2 += PS_SQR(kernel->kernel[y][x]);
+        }
+    }
+
+    psFree(kernel);
+
+    return sumKernel2 / PS_SQR(sumKernel);
+}
+
+#if 1
+psArray *pmSubtractionKernelSolutions(const pmSubtractionKernels *kernels, float x, float y, bool wantDual)
+{
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NULL);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(x, -1.0, 1.0, NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(y, -1.0, 1.0, NULL);
+
+    psVector *solution = wantDual ? kernels->solution2 : kernels->solution1; // Solution of interest
+    psVector *backup = psVectorCopy(NULL, solution, PS_TYPE_F64);  // Backup version
+
+    int num = kernels->num;             // Number of kernel basis functions
+
+    psImage *polyValues = p_pmSubtractionPolynomial(NULL, kernels->spatialOrder, x, y); // Solved polynomial
+    psArray *images = psArrayAlloc(num + 1); // Images of each kernel to return
+
+    // The whole kernel
+    {
+        psKernel *kernel = solvedKernel(NULL, kernels, polyValues, true, wantDual); // The appropriate kernel
+        images->data[0] = psMemIncrRefCounter(kernel->image);
+        psFree(kernel);
+    }
+
+    // The parts
+    psVectorInit(solution, 0.0);
+    for (int i = 0; i < num; i++) {
+        solution->data.F64[i] = backup->data.F64[i];
+        psKernel *kernel = solvedKernel(NULL, kernels, polyValues, false, wantDual); // The appropriate kernel
+#if 0
+        int size = kernels->size;
+        double sum = 0.0;
+        for (int v = -size; v <= size; v++) {
+            for (int u = -size; u <= size; u++) {
+                sum += kernel->kernel[v][u];
+            }
+        }
+        fprintf(stderr, "Kernel %d: %lf\n", i, sum);
+#endif
+        images->data[i + 1] = psMemIncrRefCounter(kernel->image);
+        psFree(kernel);
+        solution->data.F64[i] = 0.0;
+    }
+    psFree(polyValues);
+    psVectorCopy(solution, backup, PS_TYPE_F64);
+    psFree(backup);
+
+    return images;
+}
+#endif
+
+
+// XXX Put kernelImage, kernelVariance and polyValues on thread-dependent data
+static bool subtractionConvolvePatch(int numCols, int numRows, // Size of image
+                                     int x0, int y0, // Offsets for image
+                                     pmReadout *out1, pmReadout *out2, // Output readouts
+                                     psImage *convMask, // Output convolved mask
+                                     const pmReadout *ro1, const pmReadout *ro2, // Input readouts
+                                     psImage *kernelErr1, psImage *kernelErr2, // Kernel error images
+                                     psImage *subMask, // Input subtraction mask
+                                     psImageMaskType maskBad, // Mask value to give bad pixels
+                                     psImageMaskType maskPoor, // Mask value to give poor pixels
+                                     float poorFrac, // Fraction for "poor"
+                                     const psRegion *region, // Patch to convolve
+                                     const pmSubtractionKernels *kernels, // Kernels
+                                     bool doBG, // Add in background when convolving?
+                                     bool useFFT // Use FFT to do the convolution?
+    )
+{
+    int size = kernels->size;           // Half-size of kernel
+    int xMin = region->x0, xMax = region->x1, yMin = region->y0, yMax = region->y1; // Bounds of patch
+
+    psKernel *kernelImage = NULL;       // Kernel for the images
+    psKernel *kernelVariance = NULL;      // Kernel for the variance maps
+
+    // Only generate polynomial values every kernel footprint, since we have already assumed
+    // (with the stamps) that it does not vary rapidly on this scale.
+    psImage *polyValues = p_pmSubtractionPolynomialFromCoords(NULL, kernels, xMin + x0 + size + 1,
+                                                              yMin + y0 + size + 1);        // Polynomial
+    float background = doBG ? p_pmSubtractionSolutionBackground(kernels, polyValues) : 0.0; // Background term
+
+    if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        convolveRegion(out1->image, out1->variance, out1->mask, &kernelImage, &kernelVariance,
+                       ro1->image, ro1->variance, ro1->covariance, kernelErr1, subMask, kernels,
+                       polyValues, background, *region, maskBad, maskPoor, poorFrac, useFFT, false);
+    }
+    if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        convolveRegion(out2->image, out2->variance, out2->mask, &kernelImage, &kernelVariance,
+                       ro2->image, ro2->variance, ro2->covariance, kernelErr2, subMask, kernels,
+                       polyValues, background, *region, maskBad, maskPoor, poorFrac, useFFT,
+                       kernels->mode == PM_SUBTRACTION_MODE_DUAL);
+    }
+
+    psFree(kernelImage);
+    psFree(kernelVariance);
+    psFree(polyValues);
+
+    if ((kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) && ro1->mask) {
+        psImageMaskType **target = out1->mask->data.PS_TYPE_IMAGE_MASK_DATA; // Target mask
+        psImageMaskType **source = ro1->mask->data.PS_TYPE_IMAGE_MASK_DATA; // Source mask
+
+        for (int y = yMin; y < yMax; y++) {
+            for (int x = xMin; x < xMax; x++) {
+                target[y][x] |= source[y][x];
+            }
+        }
+    }
+    if ((kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) && ro2->mask) {
+        psImageMaskType **target = out2->mask->data.PS_TYPE_IMAGE_MASK_DATA; // Target mask
+        psImageMaskType **source = ro2->mask->data.PS_TYPE_IMAGE_MASK_DATA; // Source mask
+
+        for (int y = yMin; y < yMax; y++) {
+            for (int x = xMin; x < xMax; x++) {
+                target[y][x] |= source[y][x];
+            }
+        }
+    }
+
+    return true;
+}
+
+bool pmSubtractionConvolveThread(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    psArray *args = job->args;          // Arguments
+    int numCols = PS_SCALAR_VALUE(args->data[0], S32); // Number of columns
+    int numRows = PS_SCALAR_VALUE(args->data[1], S32); // Number of rows
+    int x0 = PS_SCALAR_VALUE(args->data[2], S32); // Offset in x
+    int y0 = PS_SCALAR_VALUE(args->data[3], S32); // Offset in x
+    pmReadout *out1 = args->data[4];    // Output readout 1
+    pmReadout *out2 = args->data[5];    // Output readout 2
+    psImage *convMask = args->data[6];  // Output convolved mask
+    const pmReadout *ro1 = args->data[7]; // Input readout 1
+    const pmReadout *ro2 = args->data[8]; // Input readout 2
+    psImage *kernelErr1 = args->data[9]; // Kernel error image 1
+    psImage *kernelErr2 = args->data[10]; // Kernel error image 2
+    psImage *subMask = args->data[11]; // Subtraction mask
+    psImageMaskType maskBad = PS_SCALAR_VALUE(args->data[12], PS_TYPE_IMAGE_MASK_DATA); // Output mask value for bad pixels
+    psImageMaskType maskPoor = PS_SCALAR_VALUE(args->data[13], PS_TYPE_IMAGE_MASK_DATA); // Output mask value for poor pixels
+    float poorFrac = PS_SCALAR_VALUE(args->data[14], F32); // Fraction for "poor"
+    const psRegion *region = args->data[15]; // Region to convolve
+    const pmSubtractionKernels *kernels = args->data[16]; // Kernels
+    bool doBG = PS_SCALAR_VALUE(args->data[17], U8); // Do background subtraction?
+    bool useFFT = PS_SCALAR_VALUE(args->data[18], U8); // Use FFT for convolution?
+
+    return subtractionConvolvePatch(numCols, numRows, x0, y0, out1, out2, convMask, ro1, ro2, kernelErr1,
+                                    kernelErr2, subMask, maskBad, maskPoor, poorFrac, region, kernels,
+                                    doBG, useFFT);
+}
+
+bool pmSubtractionConvolve(pmReadout *out1, pmReadout *out2, const pmReadout *ro1, const pmReadout *ro2,
+                           psImage *subMask, int stride, psImageMaskType maskBad, psImageMaskType maskPoor,
+                           float poorFrac, float kernelError, float covarFrac, const psRegion *region,
+                           const pmSubtractionKernels *kernels, bool doBG, bool useFFT)
+{
+    int numCols = 0, numRows = 0;       // Image dimensions
+    int x0 = 0, y0 = 0;                 // Image offset
+    if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        PM_ASSERT_READOUT_NON_NULL(out1, false);
+        PM_ASSERT_READOUT_NON_NULL(ro1, false);
+        PM_ASSERT_READOUT_IMAGE(ro1, false);
+        PM_ASSERT_READOUT_IMAGE(out1, false);
+        numCols = ro1->image->numCols;
+        numRows = ro1->image->numRows;
+        x0 = ro1->col0;
+        y0 = ro1->row0;
+    }
+    if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        PM_ASSERT_READOUT_NON_NULL(out2, false);
+        PM_ASSERT_READOUT_NON_NULL(ro2, false);
+        PM_ASSERT_READOUT_IMAGE(ro2, false);
+        PM_ASSERT_READOUT_IMAGE(out2, false);
+        if (numCols == 0 && numRows == 0) {
+            numCols = ro2->image->numCols;
+            numRows = ro2->image->numRows;
+            x0 = ro2->col0;
+            y0 = ro2->row0;
+        }
+    }
+    if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        PS_ASSERT_IMAGES_SIZE_EQUAL(ro1->image, ro2->image, false);
+    }
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, false);
+    if (subMask) {
+        PS_ASSERT_IMAGE_NON_NULL(subMask, false);
+        PS_ASSERT_IMAGE_TYPE(subMask, PS_TYPE_IMAGE_MASK, false);
+        PS_ASSERT_IMAGE_SIZE(subMask, numCols, numRows, false);
+    }
+    PS_ASSERT_INT_NONNEGATIVE(stride, false);
+    PS_ASSERT_FLOAT_LARGER_THAN_OR_EQUAL(poorFrac, 0.0, false);
+    PS_ASSERT_FLOAT_LESS_THAN_OR_EQUAL(poorFrac, 1.0, false);
+    PS_ASSERT_FLOAT_LARGER_THAN_OR_EQUAL(kernelError, 0.0, false);
+    PS_ASSERT_FLOAT_LESS_THAN_OR_EQUAL(kernelError, 1.0, false);
+    PS_ASSERT_FLOAT_LARGER_THAN_OR_EQUAL(covarFrac, 0.0, false);
+    PS_ASSERT_FLOAT_LESS_THAN(covarFrac, 1.0, false);
+    if (region && psRegionIsNaN(*region)) {
+        psString string = psRegionToString(*region);
+        psError(PM_ERR_PROG, true, "Input region (%s) contains NAN values", string);
+        psFree(string);
+        return false;
+    }
+
+    psTimerStart("pmSubtractionConvolve");
+
+    bool threaded = pmSubtractionThreaded(); // Running threaded?
+
+    // XXX This is no longer used 
+    psImage *convMask = NULL;           // Convolved mask image (common to inputs 1 and 2)
+    if (subMask) {
+        if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+            convMask = out1->mask;
+        }
+        if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+            if (!convMask) {
+                convMask = out2->mask;
+            }
+        }
+    }
+
+    psImage *kernelErr1 = NULL, *kernelErr2 = NULL; // Kernel error images
+#ifdef USE_KERNEL_ERR
+    if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        kernelErr1 = subtractionKernelErrImage(ro1->image, kernelError);
+    }
+    if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        kernelErr2 = subtractionKernelErrImage(ro2->image, kernelError);
+    }
+#endif
+
+    int size = kernels->size;           // Half-size of kernel
+
+    // Get region for convolution: [xMin:xMax,yMin:yMax]
+    int xMin = kernels->xMin + size, xMax = kernels->xMax - size;
+    int yMin = kernels->yMin + size, yMax = kernels->yMax - size;
+    if (region) {
+        xMin = PS_MAX(region->x0, xMin);
+        xMax = PS_MIN(region->x1, xMax);
+        yMin = PS_MAX(region->y0, yMin);
+        yMax = PS_MIN(region->y1, yMax);
+    }
+
+#if 0
+    // XXX Use thread-specific data to store these
+    psImage *polyValues = NULL;         // Pre-calculated polynomial values
+    psKernel *kernelImage = NULL;       // Kernel for the images
+    psKernel *kernelVariance = NULL;      // Kernel for the variance maps
+#endif
+
+    // Need to turn off threads at the psLib level --- otherwise, we end up with threads on top of threads,
+    // and everything is executing psThreadPoolWait, waiting for some other mythical thread to complete the
+    // thread's work.
+    bool oldThreads = psImageConvolveSetThreads(false); // Old value of threading for psImageConvolve
+
+    if (stride == 0) {
+        // Use the full size of the kernel
+        stride = 2 * size + 1;
+    }
+
+    for (int j = yMin; j < yMax; j += stride) {
+        int ySubMax = PS_MIN(j + stride, yMax); // Range for subregion of interest
+        for (int i = xMin; i < xMax; i += stride) {
+            int xSubMax = PS_MIN(i + stride, xMax); // Range for subregion of interest
+
+            psRegion *subRegion = psRegionAlloc(i, xSubMax, j, ySubMax); // Bounds of subtraction
+	    // for a TEST, do not run threaded for testing
+            // if (false && threaded) {
+	    if (threaded) {
+                psThreadJob *job = psThreadJobAlloc("PSMODULES_SUBTRACTION_CONVOLVE");
+                psArray *args = job->args;
+                PS_ARRAY_ADD_SCALAR(args, numCols, PS_TYPE_S32);
+                PS_ARRAY_ADD_SCALAR(args, numRows, PS_TYPE_S32);
+                PS_ARRAY_ADD_SCALAR(args, x0, PS_TYPE_S32);
+                PS_ARRAY_ADD_SCALAR(args, y0, PS_TYPE_S32);
+                psArrayAdd(args, 1, out1);
+                psArrayAdd(args, 1, out2);
+                psArrayAdd(args, 1, convMask);
+                psArrayAdd(args, 1, (pmReadout*)ro1); // Casting away const
+                psArrayAdd(args, 1, (pmReadout*)ro2); // Casting away const
+                psArrayAdd(args, 1, kernelErr1);
+                psArrayAdd(args, 1, kernelErr2);
+                psArrayAdd(args, 1, subMask);
+                PS_ARRAY_ADD_SCALAR(args, maskBad, PS_TYPE_IMAGE_MASK);
+                PS_ARRAY_ADD_SCALAR(args, maskPoor, PS_TYPE_IMAGE_MASK);
+                PS_ARRAY_ADD_SCALAR(args, poorFrac, PS_TYPE_F32);
+                psArrayAdd(args, 1, subRegion);
+                psArrayAdd(args, 1, (pmSubtractionKernels*)kernels); // Casting away const
+                PS_ARRAY_ADD_SCALAR(args, doBG, PS_TYPE_U8);
+                PS_ARRAY_ADD_SCALAR(args, useFFT, PS_TYPE_U8);
+
+                if (!psThreadJobAddPending(job)) {
+                    return false;
+                }
+            } else {
+                subtractionConvolvePatch(numCols, numRows, x0, y0, out1, out2, convMask, ro1, ro2,
+                                         kernelErr1, kernelErr2, subMask, maskBad, maskPoor, poorFrac,
+                                         subRegion, kernels, doBG, useFFT);
+            }
+            psFree(subRegion);
+        }
+    }
+
+    if (!psThreadPoolWait(false, true)) {
+        psError(psErrorCodeLast(), false, "Error waiting for threads.");
+        return false;
+    }
+
+    // We don't rely on psThreadPoolWait to harvest the jobs because the job contains a reference to the
+    // subMask, which is being changed on a thread, and psThreadPoolWait doesn't know that it needs to be
+    // locked before freeing.  After psThreadPoolWait, however, the jobs are completed, the threads are idle,
+    // and so there's no need to lock the subMask when we're blowing away the jobs.
+    if (threaded) {
+        psThreadJob *job;               // Completed job
+        while ((job = psThreadJobGetDone())) {
+            psAssert(strcmp(job->type, "PSMODULES_SUBTRACTION_CONVOLVE") == 0,
+                     "Job has incorrect type: %s", job->type);
+            psFree(job);
+        }
+    }
+    psImageConvolveSetThreads(oldThreads);
+
+    psFree(kernelErr1);
+    psFree(kernelErr2);
+
+    static int nOut1 = 0;
+    static int nOut2 = 0;
+
+    // Calculate covariances
+    // This can be fairly involved, so we only do it for a small number of instances
+    float position[NUM_COVAR_POS] = { -1.0, -0.5, 0.0, +0.5, +1.0 }; // Positions for covariance calculations
+    // Enable threads for covariance calculation, since we're not threading on top of it
+    oldThreads = psImageCovarianceSetThreads(true);
+    if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        psArray *covars = psArrayAlloc(PS_SQR(NUM_COVAR_POS)); // Covariances
+        for (int y = 0, i = 0; y < NUM_COVAR_POS; y++) {
+            for (int x = 0; x < NUM_COVAR_POS; x++, i++) {
+                psKernel *kernel = pmSubtractionKernel(kernels, position[x], position[y],
+                                                       false); // Convolution kernel
+                psKernelTruncate(kernel, covarFrac);
+                covars->data[i] = psImageCovarianceCalculate(kernel, ro1->covariance);
+		if (0) {
+		    char name[128];
+		    snprintf (name, 128, "covar.sample1.%03d.fits", nOut1);
+		    psKernel *cov = covars->data[i];
+		    psFitsWriteImageSimple (name, cov->image, NULL);
+
+		    snprintf (name, 128, "incovar.sample1.%03d.fits", nOut1);
+		    psFitsWriteImageSimple (name, ro1->covariance->image, NULL);
+
+		    snprintf (name, 128, "conv.sample1.%03d.fits", nOut1);
+		    psFitsWriteImageSimple (name, kernel->image, NULL);
+
+		    fprintf (stderr, "incov: %d,%d; kern: %d,%d, outcov: %d,%d\n", 
+			     ro1->covariance->image->numCols, ro1->covariance->image->numRows, 
+			     kernel->image->numCols, kernel->image->numRows,
+			     cov->image->numCols, cov->image->numRows);
+
+		    nOut1 ++;
+		}
+                psFree(kernel);
+            }
+        }
+	psFree(out1->covariance);
+        out1->covariance = psImageCovarianceAverage(covars);
+        psFree(covars);
+        if (!out1->covariance) {
+            psError(PM_ERR_UNKNOWN, false, "psImageCovarianceAverage returned NULL for out1.");
+            return false;
+        }
+        // Remove covariance factor from covariance, since we've put it in the variance map already
+        float factor = psImageCovarianceFactor(out1->covariance);
+        psBinaryOp(out1->covariance->image, out1->covariance->image, "/", psScalarAlloc(factor, PS_TYPE_F32));
+    }
+    if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        psArray *covars = psArrayAlloc(PS_SQR(NUM_COVAR_POS)); // Covariances
+        for (int y = 0, i = 0; y < NUM_COVAR_POS; y++) {
+            for (int x = 0; x < NUM_COVAR_POS; x++, i++) {
+                psKernel *kernel = pmSubtractionKernel(kernels, position[x], position[y],
+                                                       kernels->mode == PM_SUBTRACTION_MODE_DUAL); // Convolution kernel
+                psKernelTruncate(kernel, covarFrac);
+                covars->data[i] = psImageCovarianceCalculate(kernel, ro2->covariance);
+		if (0) {
+		    char name[128];
+		    snprintf (name, 128, "covar.sample2.%03d.fits", nOut2);
+		    psKernel *cov = covars->data[i];
+		    psFitsWriteImageSimple (name, cov->image, NULL);
+
+		    snprintf (name, 128, "incovar.sample2.%03d.fits", nOut2);
+		    psFitsWriteImageSimple (name, ro2->covariance->image, NULL);
+
+		    snprintf (name, 128, "conv.sample2.%03d.fits", nOut2);
+		    psFitsWriteImageSimple (name, kernel->image, NULL);
+
+		    fprintf (stderr, "incov: %d,%d; kern: %d,%d, outcov: %d,%d\n", 
+			     ro2->covariance->image->numCols, ro2->covariance->image->numRows, 
+			     kernel->image->numCols, kernel->image->numRows,
+			     cov->image->numCols, cov->image->numRows);
+
+		    nOut2 ++;
+		}
+                psFree(kernel);
+            }
+        }
+	psFree(out2->covariance);
+        out2->covariance = psImageCovarianceAverage(covars);
+        psFree(covars);
+        if (!out2->covariance) {
+            psError(PM_ERR_UNKNOWN, false, "psImageCovarianceAverage returned NULL for out2.");
+            return false;
+        }
+        // Remove covariance factor from covariance, since we've put it in the variance map already
+        float factor = psImageCovarianceFactor(out2->covariance);
+        psBinaryOp(out2->covariance->image, out2->covariance->image, "/", psScalarAlloc(factor, PS_TYPE_F32));
+    }
+    psImageCovarianceSetThreads(oldThreads);
+
+    // Copy anything that wasn't convolved (they may have been allocated though, so free them)
+    switch (kernels->mode) {
+      case PM_SUBTRACTION_MODE_1:
+        if (out2) {
+	    psFree(out2->image);
+	    psFree(out2->variance);
+	    psFree(out2->mask);
+	    psFree(out2->covariance);
+            out2->image = psMemIncrRefCounter(ro2->image);
+            out2->variance = psMemIncrRefCounter(ro2->variance);
+            out2->mask = psMemIncrRefCounter(ro2->mask);
+            out2->covariance = psMemIncrRefCounter(ro2->covariance);
+        }
+        break;
+      case PM_SUBTRACTION_MODE_2:
+        if (out1) {
+	    psFree(out1->image);
+	    psFree(out1->variance);
+	    psFree(out1->mask);
+	    psFree(out1->covariance);
+            out1->image = psMemIncrRefCounter(ro1->image);
+            out1->variance = psMemIncrRefCounter(ro1->variance);
+            out1->mask = psMemIncrRefCounter(ro1->mask);
+            out1->covariance = psMemIncrRefCounter(ro1->covariance);
+        }
+        break;
+      case PM_SUBTRACTION_MODE_DUAL:
+        break;
+      default:
+        psAbort("Should never get here.");
+    }
+
+    // Data exists on the outputs now
+    if (out1) {
+        out1->data_exists = true;
+        if (out1->parent) {
+            out1->parent->data_exists = out1->parent->parent->data_exists = true;
+        }
+    }
+    if (out2) {
+        out2->data_exists = true;
+        if (out2->parent) {
+            out2->parent->data_exists = out2->parent->parent->data_exists = true;
+        }
+    }
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Convolve image: %f sec",
+             psTimerClear("pmSubtractionConvolve"));
+
+    return true;
+}
+
+bool pmSubtractionGetFWHMs(float *fwhm1, float *fwhm2) {
+
+  *fwhm1 = FWHM1;
+  *fwhm2 = FWHM2;
+  return true;
+}
+
+bool pmSubtractionSetFWHMs(float fwhm1, float fwhm2) {
+
+  FWHM1 = fwhm1;
+  FWHM2 = fwhm2;
+  return true;
+}
+
+static void pmSubtractionQualityFree(pmSubtractionQuality *quality) {
+
+    psFree (quality->fluxes);
+    psFree (quality->chisq);
+    psFree (quality->moments);
+    psFree (quality->stampMask);
+}    
+
+pmSubtractionQuality *pmSubtractionQualityAlloc() {
+
+    pmSubtractionQuality *quality = psAlloc(sizeof(pmSubtractionQuality)); // Stamp list to return
+    psMemSetDeallocator(quality, (psFreeFunc)pmSubtractionQualityFree);
+
+    quality->fluxes = NULL;
+    quality->chisq = NULL;
+    quality->moments = NULL;
+    quality->stampMask = NULL;
+
+    quality->score = NAN;
+    quality->mode = PM_SUBTRACTION_MODE_ERR;
+    quality->spatialOrder = -1;
+    quality->nGood = 0;
+    
+    return quality;
+}
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtraction.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtraction.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtraction.h	(revision 42651)
@@ -0,0 +1,161 @@
+/* @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.36 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-06 02:31:25 $
+ * Copyright 2004-207 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_SUBTRACTION_H
+#define PM_SUBTRACTION_H
+
+// #include <pslib.h>
+// #include <pmHDU.h>
+// #include <pmFPA.h>
+// #include <pmSubtractionKernels.h>
+// #include <pmSubtractionStamps.h>
+
+// if we use the original ppSub implementation, we subtract a central delta-function for all
+// kernels to for a zero integral.  in the Alard-Lupton style zeroing, we can either subtract a
+// Gaussian from all kernels (ZERO_KERNEL_ZERO_FLUX), or we can subtract it from all but the
+// first kernel.
+# define CENTRAL_DELTA 0
+# define ZERO_KERNEL_ZERO_FLUX 1
+
+/// @addtogroup imcombine Image Combinations
+/// @{
+
+/// Number of terms in a polynomial
+#define PM_SUBTRACTION_POLYTERMS(ORDER) (((ORDER) + 1) * ((ORDER) + 2) / 2)
+
+/// Set the indices for the normalisation and background terms
+#define PM_SUBTRACTION_INDICES(NORM,BG,KERNELS) { \
+    int numSpatial = PM_SUBTRACTION_POLYTERMS((KERNELS)->spatialOrder); /* Number of spatial terms */ \
+    NORM = (KERNELS)->num * numSpatial; \
+    BG = NORM + 1; \
+}
+
+/// Return the index for the start of the normalisation terms
+#define PM_SUBTRACTION_INDEX_NORM(KERNELS) \
+    ((KERNELS)->num * PM_SUBTRACTION_POLYTERMS((KERNELS)->spatialOrder))
+
+/// Return the index for the start of the background terms
+#define PM_SUBTRACTION_INDEX_BG(KERNELS) \
+    (((KERNELS)->num * PM_SUBTRACTION_POLYTERMS((KERNELS)->spatialOrder)) + 1)
+
+
+/// Convolve the reference stamp with the kernel components
+bool pmSubtractionConvolveStamp(pmSubtractionStamp *stamp, ///< Stamp to convolve
+                                pmSubtractionKernels *kernels, ///< Kernel parameters
+                                int footprint ///< Half-size of region over which to calculate equation
+    );
+
+bool pmSubtractionConvolveStamps(pmSubtractionStampList *stamps, pmSubtractionKernels *kernels);
+
+bool pmSubtractionConvolveStampThread(psThreadJob *job);
+
+/// Reject stamps
+int pmSubtractionRejectStamps(pmSubtractionKernels *kernels, ///< Kernel parameters to update
+                              pmSubtractionStampList *stamps, ///< Stamps
+                              pmSubtractionQuality *match, ///< data on the subtraction quality
+                              psImage *subMask, ///< Subtraction mask
+                              float sigmaRej ///< Number of RMS deviations above zero at which to reject
+    );
+
+/// Generate the convolution kernel
+psKernel *pmSubtractionKernel(const pmSubtractionKernels *kernels, ///< Kernel parameters
+                              float x, float y, ///< Normalised position [-1,1] for which to generate image
+                              bool wantDual ///< Calculate for the dual kernel?
+    );
+
+/// Generate an image of the convolution kernel
+psImage *pmSubtractionKernelImage(const pmSubtractionKernels *kernels, ///< Kernel parameters
+                                  float x, float y,///< Normalised position [-1,1] for which to generate image
+                                  bool wantDual ///< Calculate for the dual kernel?
+                                  );
+
+/// Return the variance factor for a kernel
+///
+/// The variance factor allows conversion from the large-scale variance (which is what is calculated by
+/// pmSubtractionConvolve) and the small-scale (pixel-to-pixel) variance.
+float pmSubtractionVarianceFactor(const pmSubtractionKernels *kernels, ///< Kernel parameters
+                                  float x, float y, ///< Normalised position [-1,1]
+                                  bool wantDual ///< Calculate for the dual kernel?
+    );
+
+/// Generate images of the convolution kernel elements
+psArray *pmSubtractionKernelSolutions(const pmSubtractionKernels *kernels, ///< Kernel parameters
+                                      float x, float y, ///< Normalised position [-1,1] for images
+                                      bool wantDual ///< Calculate for the dual kernel?
+    );
+
+
+/// Execute a thread job to convolve a patch of the image
+bool pmSubtractionConvolveThread(psThreadJob *job ///< Job to execute
+    );
+
+/// Convolve image in preparation for subtraction
+bool pmSubtractionConvolve(pmReadout *out1, ///< Output image 1
+                           pmReadout *out2, ///< Output image 2 (DUAL mode only)
+                           const pmReadout *ro1, // Input image 1
+                           const pmReadout *ro2, // Input image 2
+                           psImage *subMask, ///< Subtraction mask (or NULL)
+                           int stride,  ///< Size of convolution patches
+                           psImageMaskType maskBad, ///< Mask value to give bad pixels
+                           psImageMaskType maskPoor, ///< Mask value to give poor pixels
+                           float poorFrac, ///< Fraction for "poor"
+                           float kernelError, ///< Relative systematic error in kernel
+                           float covarFrac,  ///< Truncation fraction for kernel before covariance calculation
+                           const psRegion *region, ///< Region to convolve (or NULL)
+                           const pmSubtractionKernels *kernels, ///< Kernel parameters
+                           bool doBG,   ///< Apply background term?
+                           bool useFFT  ///< Use Fast Fourier Transform for the convolution?
+    );
+
+/// 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
+    );
+
+/// Return normalised coordinates
+void p_pmSubtractionPolynomialNormCoords(
+    float *xOut, float *yOut,           ///< Normalised coordinates, returned
+    float xIn, float yIn,               ///< Input coordinates
+    int xMin, int xMax, int yMin, int yMax ///< Bounds of validity
+    );
+
+/// Given (normalised) coordinates (x,y), generate a matrix where the elements (i,j) are x^i * y^j
+psImage *p_pmSubtractionPolynomial(psImage *output, ///< Output matrix, or NULL
+                                   int spatialOrder, ///< Maximum spatial polynomial order
+                                   float x, float y ///< Normalised position of interest, [-1,1]
+    );
+
+/// Given pixel coordinates (x,y), generate a matrix where the elements (i,j) are x^i * y^j
+///
+/// Same as p_pmSubtractionPolynomial except that the normalisation is applied
+psImage *p_pmSubtractionPolynomialFromCoords(psImage *output, ///< Output matrix, or NULL
+                                             const pmSubtractionKernels *kernels, ///< Kernel parameters
+                                             int x, int y ///< Position of interest
+    );
+
+/// Return the radius from the centre of the convolution kernel that distinguishes "bad" and "poor" pixels
+int p_pmSubtractionBadRadius(psKernel *preKernel, ///< Pre-calculated convolution kernel
+                             const pmSubtractionKernels *kernels, ///< Kernel parameters
+                             const psImage *polyValues, ///< Polynomial values
+                             bool wantDual, ///< Calculate for the dual kernel?
+                             float poorFrac ///< Fraction for "poor"
+    );
+
+bool pmSubtractionGetFWHMs(float *fwhm1, float *fwhm2);
+bool pmSubtractionSetFWHMs(float fwhm1, float fwhm2);
+
+pmSubtractionQuality *pmSubtractionQualityAlloc();
+
+/// @}
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionAnalysis.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionAnalysis.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionAnalysis.c	(revision 42651)
@@ -0,0 +1,265 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmFPA.h"
+#include "pmSubtractionTypes.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionKernels.h"
+#include "pmSubtractionEquation.h"
+
+#include "pmSubtractionAnalysis.h"
+#include "pmSubtractionVisual.h"
+
+//#define TESTING
+
+// save information about the kernel in the output header.  this function also generates the
+// image normalization, used by ppSubMatchPSFs.c to rescale the output image.
+bool pmSubtractionAnalysis(psMetadata *analysis, psMetadata *header,
+                           pmSubtractionKernels *kernels, psRegion *region,
+                           int numCols, int numRows)
+{
+    PS_ASSERT_METADATA_NON_NULL(analysis, false);
+    PS_ASSERT_METADATA_NON_NULL(header, false);
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, false);
+
+    // Record region for subtraction
+    {
+        psRegion *subRegion;    // Region over which subtraction was performed
+        if (region) {
+            subRegion = psMemIncrRefCounter(region);
+        } else {
+            subRegion = psRegionAlloc(0, numCols, 0, numRows);
+        }
+
+        psMetadataAddPtr(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_REGION, PS_DATA_REGION | PS_META_DUPLICATE_OK, "Region over which subtraction was performed", subRegion);
+
+        psString string = psRegionToString(*subRegion);
+        psFree(subRegion);
+
+        psMetadataAddStr(header, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_REGION, PS_META_DUPLICATE_OK, "Region over which subtraction was performed", string);
+        psFree(string);
+    }
+
+    // Record kernel
+    psMetadataAddPtr(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_KERNEL, PS_DATA_UNKNOWN | PS_META_DUPLICATE_OK, "Subtraction kernels", kernels);
+    psMetadataAddS32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MODE, PS_META_DUPLICATE_OK, "Subtraction mode", kernels->mode);
+    psMetadataAddS32(header, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MODE, PS_META_DUPLICATE_OK, "Subtraction mode", kernels->mode);
+
+    // Realisations of kernel
+    psImage *convKernels = pmSubtractionKernelsImageMosaic(kernels);
+    psMetadataAddImage(analysis, PS_LIST_TAIL, "SUBTRACTION.KERNEL.IMAGE", PS_META_DUPLICATE_OK, "Realisations of kernel", convKernels);
+    psFree(convKernels);
+
+    // sample difference images
+    {
+        psMetadataAddArray(analysis, PS_LIST_TAIL, "SUBTRACTION.SAMPLE.STAMP.SET", PS_META_DUPLICATE_OK, "Sample Difference Stamps", kernels->sampleStamps);
+    }
+
+#ifdef TESTING
+    // Generate images of the kernel components
+    {
+        psMetadata *header = psMetadataAlloc(); // Header
+        for (int i = 0; i < kernels->solution1->n; i++) {
+            psString name = NULL;       // Header keyword
+            psStringAppend(&name, "SOLN%04d", i);
+            psMetadataAddF64(header, PS_LIST_TAIL, name, 0, NULL, kernels->solution1->data.F64[i]);
+            psFree(name);
+        }
+        psArray *kernelImages = pmSubtractionKernelSolutions(kernels, 0.0, 0.0, false);
+        psFits *kernelFile = psFitsOpen("kernels1.fits", "w");
+        (void)psFitsWriteImageCube(kernelFile, header, kernelImages, NULL);
+        psFitsClose(kernelFile);
+        psFree(kernelImages);
+        psFree(header);
+    }
+    if (kernels->solution2) {
+        psMetadata *header = psMetadataAlloc(); // Header
+        for (int i = 0; i < kernels->solution2->n; i++) {
+            psString name = NULL;       // Header keyword
+            psStringAppend(&name, "SOLN%04d", i);
+            psMetadataAddF64(header, PS_LIST_TAIL, name, 0, NULL, kernels->solution2->data.F64[i]);
+            psFree(name);
+        }
+        psArray *kernelImages = pmSubtractionKernelSolutions(kernels, 0.0, 0.0, true);
+        psFits *kernelFile = psFitsOpen("kernels2.fits", "w");
+        (void)psFitsWriteImageCube(kernelFile, header, kernelImages, NULL);
+        psFitsClose(kernelFile);
+        psFree(kernelImages);
+        psFree(header);
+    }
+#endif
+
+
+    // Kernel shape
+    {
+        psImage *image = pmSubtractionKernelImage(kernels, 0.5, 0.5, false); // Image of the kernel
+        if (!image) {
+            psError(psErrorCodeLast(), false, "Unable to generate image of kernel.");
+            return false;
+        }
+        int size = kernels->size;       // Half-size of kernel
+        int fullSize = 2 * size + 1;    // Full size of kernel
+
+        float norm = 0.0;               // Normalisation (kernel sum)
+        for (int y = 0; y < fullSize; y++) {
+            for (int x = 0; x < fullSize; x++) {
+                norm += image->data.F32[y][x];
+            }
+        }
+	psLogMsg("psModules.imcombine", PS_LOG_INFO, "Kernel Integral: %f", norm);
+
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_NORM, PS_META_DUPLICATE_OK, "Normalisation", norm);
+        psMetadataAddF32(header,   PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_NORM, PS_META_DUPLICATE_OK, "Normalisation", norm);
+
+        float maxCon =  0.0;          // Maximum +fraction > 0.0
+        float maxDec =  0.0;          // Maximum -fraction < 0.0
+        for (int r = 1; r < size; r++) {
+            unsigned long r2 = PS_SQR(r); // r^2
+            float sum = 0.0;            // Sum within circle
+            for (int y = 0, v = -size; y < fullSize; y++, v++) {
+                unsigned long v2 = PS_SQR(v); // y^2
+                for (int x = 0, u = -size; x < fullSize; x++, u++) {
+                    unsigned long u2 = PS_SQR(u); // u^2
+                    if (u2 + v2 <= r2) {
+                        sum += image->data.F32[y][x];
+                    }
+                }
+            }
+            float frac = sum / norm;    // Fraction of flux moving towards centre
+            psTrace("psModules.imcombine", 5, "(De)Convolution fraction at %d: %f\n", r, frac);
+            maxCon = PS_MAX(maxCon, +frac);
+            maxDec = PS_MAX(maxDec, -frac);
+        }
+        psFree(image);
+
+        {
+            psMetadataItem *item = NULL;
+	    item = psMetadataLookup(analysis, PM_SUBTRACTION_ANALYSIS_DECONV_MAX); // Previous
+            if (item) {
+                maxDec = item->data.F32 = PS_MAX(item->data.F32, maxDec);
+            } else {
+                psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_DECONV_MAX, 0, "Maximum deconvolution fraction", maxDec);
+            }
+            item = psMetadataLookup(header, PM_SUBTRACTION_ANALYSIS_DECONV_MAX); // Previous
+            if (item) {
+                item->data.F32 = maxDec;
+            } else {
+                psMetadataAddF32(header, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_DECONV_MAX, 0, "Maximum deconvolution fraction", maxDec);
+            }
+        }
+        {
+            psMetadataItem *item = NULL;
+	    item = psMetadataLookup(analysis, PM_SUBTRACTION_ANALYSIS_CONVOL_MAX); // Previous
+            if (item) {
+                maxCon = item->data.F32 = PS_MAX(item->data.F32, maxCon);
+            } else {
+                psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_CONVOL_MAX, 0, "Maximum convolution fraction", maxCon);
+            }
+            item = psMetadataLookup(header, PM_SUBTRACTION_ANALYSIS_CONVOL_MAX); // Previous
+            if (item) {
+                item->data.F32 = maxCon;
+            } else {
+                psMetadataAddF32(header, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_CONVOL_MAX, 0, "Maximum convolution fraction", maxCon);
+            }
+        }
+    }
+
+    // Kernel moments : since the kernel can be negative, calculate the absolute flux moments,
+    // eg \Sum(x|f|) / Sum(|f|), \Sum(x^2|f|) / Sum(|f|)
+    {
+        psImage *image = pmSubtractionKernelImage(kernels, 0.5, 0.5, false); // Image of the kernel
+        if (!image) {
+            psError(psErrorCodeLast(), false, "Unable to generate image of kernel.");
+            return false;
+        }
+        double m00 = 0, m10 = 0, m01 = 0, m20 = 0, m11 = 0, m02 = 0; // Moments to calculate
+        int size = kernels->size;       // Half-size of kernel
+        int fullSize = 2 * size + 1;    // Full size of kernel
+        for (int y = 0, v = -size; y < fullSize; y++, v++) {
+            for (int x = 0, u = -size; x < fullSize; x++, u++) {
+                float value = fabs(image->data.F32[y][x]); // Value of kernel
+                m00 += value;
+                m10 += u * value;
+                m01 += v * value;
+                m20 += u * u * value;
+                m11 += u * v * value;
+                m02 += v * v * value;
+            }
+        }
+        psFree(image);
+
+        // Convert first moments to centroids
+        m10 /= m00;
+        m01 /= m00;
+
+        // Convert second moments to covariance
+        m20 = m20 / m00 - PS_SQR(m10);
+        m02 = m02 / m00 - PS_SQR(m01);
+        m11 = m11 / m00 - m10 * m01;
+
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MX,
+                         PS_META_DUPLICATE_OK, "Moment in x", m10);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MY,
+                         PS_META_DUPLICATE_OK, "Moment in y", m01);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MXX,
+                         PS_META_DUPLICATE_OK, "Moment in xx", m20);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MXY,
+                         PS_META_DUPLICATE_OK, "Moment in xy", m11);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MYY,
+                         PS_META_DUPLICATE_OK, "Moment in yy", m02);
+
+        psMetadataAddF32(header, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MX,
+                         PS_META_DUPLICATE_OK, "Moment in x", m10);
+        psMetadataAddF32(header, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MY,
+                         PS_META_DUPLICATE_OK, "Moment in y", m01);
+        psMetadataAddF32(header, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MXX,
+                         PS_META_DUPLICATE_OK, "Moment in xx", m20);
+        psMetadataAddF32(header, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MXY,
+                         PS_META_DUPLICATE_OK, "Moment in xy", m11);
+        psMetadataAddF32(header, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MYY,
+                         PS_META_DUPLICATE_OK, "Moment in yy", m02);
+    }
+
+    // Difference in background
+    {
+        psImage *polyValues = p_pmSubtractionPolynomial(NULL, kernels->spatialOrder, 0.0, 0.0); // Polynomial
+        float bg = p_pmSubtractionSolutionBackground(kernels, polyValues); // Background difference
+
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_BGDIFF,
+                         PS_META_DUPLICATE_OK, "Background difference", bg);
+        psMetadataAddF32(header, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_BGDIFF,
+                         PS_META_DUPLICATE_OK, "Background difference", bg);
+        psFree(polyValues);
+    }
+
+    // Quality of fit
+    {
+        psMetadataAddS32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_STAMPS,   PS_META_REPLACE, "Number of stamps", kernels->numStamps);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_DEV_MEAN, PS_META_REPLACE, "Mean stamp deviation", kernels->mean);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_DEV_RMS,  PS_META_REPLACE, "RMS stamp deviation", kernels->rms);
+        psMetadataAddS32(header,   PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_STAMPS,   PS_META_REPLACE, "Number of stamps", kernels->numStamps);
+        psMetadataAddF32(header,   PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_DEV_MEAN, PS_META_REPLACE, "Mean stamp deviation", kernels->mean);
+        psMetadataAddF32(header,   PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_DEV_RMS,  PS_META_REPLACE, "RMS stamp deviation", kernels->rms);
+
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_FRES_SIGMA_MEAN,  PS_META_REPLACE, "Fractional Sigma of Residuals (Mean)", kernels->fResSigmaMean);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_FRES_SIGMA_STDEV, PS_META_REPLACE, "Fractional Sigma of Residuals (Stdev)", kernels->fResSigmaStdev);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_FRES_OUTER_MEAN,  PS_META_REPLACE, "Fractional Residual Flux (Mean, R > 2 pix)", kernels->fResOuterMean);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_FRES_OUTER_STDEV, PS_META_REPLACE, "Fractional Residual Flux (Stdev, R > 2 pix)", kernels->fResOuterStdev);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_FRES_TOTAL_MEAN,  PS_META_REPLACE, "Fractional Residual Flux (Mean, R > 0 pix)", kernels->fResTotalMean);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_FRES_TOTAL_STDEV, PS_META_REPLACE, "Fractional Residual Flux (Stdev, R > 0 pix)", kernels->fResTotalStdev);
+
+        psMetadataAddF32(header,   PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_FRES_SIGMA_MEAN,  PS_META_REPLACE, "Fractional Sigma of Residuals (Mean)", kernels->fResSigmaMean);
+        psMetadataAddF32(header,   PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_FRES_SIGMA_STDEV, PS_META_REPLACE, "Fractional Sigma of Residuals (Stdev)", kernels->fResSigmaStdev);
+        psMetadataAddF32(header,   PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_FRES_OUTER_MEAN,  PS_META_REPLACE, "Fractional Residual Flux (Mean, R > 2 pix)", kernels->fResOuterMean);
+        psMetadataAddF32(header,   PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_FRES_OUTER_STDEV, PS_META_REPLACE, "Fractional Residual Flux (Stdev, R > 2 pix)", kernels->fResOuterStdev);
+        psMetadataAddF32(header,   PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_FRES_TOTAL_MEAN,  PS_META_REPLACE, "Fractional Residual Flux (Mean, R > 0 pix)", kernels->fResTotalMean);
+        psMetadataAddF32(header,   PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_FRES_TOTAL_STDEV, PS_META_REPLACE, "Fractional Residual Flux (Stdev, R > 0 pix)", kernels->fResTotalStdev);
+    }
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionAnalysis.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionAnalysis.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionAnalysis.h	(revision 42651)
@@ -0,0 +1,45 @@
+#ifndef PM_SUBTRACTION_ANALYSIS_H
+#define PM_SUBTRACTION_ANALYSIS_H
+
+#include <pslib.h>
+#include <pmSubtractionKernels.h>
+
+// Names for things put on the readout analysis metadata
+#define PM_SUBTRACTION_ANALYSIS_KERNEL       "SUBTRACTION.KERNEL"       // Kernel used for convolving
+#define PM_SUBTRACTION_ANALYSIS_KERNEL_IMAGE "SUBTRACTION.KERNEL.IMAGE" // Image with kernel realisations
+#define PM_SUBTRACTION_ANALYSIS_STAMPS       "SUBTRACTION.STAMPS"       // Number of stamps
+#define PM_SUBTRACTION_ANALYSIS_DEV_MEAN     "SUBTRACTION.DEV.MEAN"     // Mean stamp deviation
+#define PM_SUBTRACTION_ANALYSIS_DEV_RMS      "SUBTRACTION.DEV.RMS"      // RMS stamp deviation
+#define PM_SUBTRACTION_ANALYSIS_MODE         "SUBTRACTION.MODE"         // Subtraction mode
+#define PM_SUBTRACTION_ANALYSIS_REGION       "SUBTRACTION.REGION"       // Subtraction region
+#define PM_SUBTRACTION_ANALYSIS_VARFACTOR_1  "SUBTRACTION.VARFACTOR.1"  // Variance factor for image 1
+#define PM_SUBTRACTION_ANALYSIS_VARFACTOR_2  "SUBTRACTION.VARFACTOR.2"  // Variance factor for image 2
+#define PM_SUBTRACTION_ANALYSIS_NORM         "SUBTRACTION.NORM"         // Normalisation
+#define PM_SUBTRACTION_ANALYSIS_BGDIFF       "SUBTRACTION.BGDIFF"       // Background difference
+#define PM_SUBTRACTION_ANALYSIS_MX           "SUBTRACTION.MX"           // Kernel moment in x
+#define PM_SUBTRACTION_ANALYSIS_MY           "SUBTRACTION.MY"           // Kernel moment in y
+#define PM_SUBTRACTION_ANALYSIS_MXX          "SUBTRACTION.MXX"          // Kernel moment in xx
+#define PM_SUBTRACTION_ANALYSIS_MXY          "SUBTRACTION.MXY"          // Kernel moment in xy
+#define PM_SUBTRACTION_ANALYSIS_MYY          "SUBTRACTION.MYY"          // Kernel moment in yy
+
+#define PM_SUBTRACTION_ANALYSIS_CONVOL_MAX   "SUBTRACTION.CONVOL.MAX"   // Maximum Convolution fraction
+#define PM_SUBTRACTION_ANALYSIS_DECONV_MAX   "SUBTRACTION.DECONV.MAX"   // Maximum Deconvolution fraction
+
+#define PM_SUBTRACTION_ANALYSIS_FRES_SIGMA_MEAN  "SUBTRACTION.FRES.MEAN" // RMS stamp deviation
+#define PM_SUBTRACTION_ANALYSIS_FRES_SIGMA_STDEV "SUBTRACTION.FRES.STDEV" // RMS stamp deviation
+#define PM_SUBTRACTION_ANALYSIS_FRES_OUTER_MEAN  "SUBTRACTION.FRES.OUTER.MEAN" // RMS stamp deviation
+#define PM_SUBTRACTION_ANALYSIS_FRES_OUTER_STDEV "SUBTRACTION.FRES.OUTER.STDEV"	// RMS stamp deviation
+#define PM_SUBTRACTION_ANALYSIS_FRES_TOTAL_MEAN  "SUBTRACTION.FRES.TOTAL.MEAN" // RMS stamp deviation
+#define PM_SUBTRACTION_ANALYSIS_FRES_TOTAL_STDEV "SUBTRACTION.FRES.TOTAL.STDEV"	// RMS stamp deviation
+
+// Derive QA information about the subtraction
+bool pmSubtractionAnalysis(
+    psMetadata *analysis,               ///< Metadata container for QA information
+    psMetadata *header,                 ///< Metadata container for QA information to put in header
+    pmSubtractionKernels *kernels,      ///< Kernels
+    psRegion *region,                   ///< Region for subtraction
+    int numCols, int numRows            ///< Size of image
+    );
+
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionDeconvolve.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionDeconvolve.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionDeconvolve.c	(revision 42651)
@@ -0,0 +1,283 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+#include <fftw3.h>
+#include <pslib.h>
+
+#include "pmFPA.h"
+#include "pmSubtractionTypes.h"
+#include "pmSubtractionKernels.h"
+#include "pmSubtractionDeconvolve.h"
+#include "pmSubtractionStamps.h"
+#include "pmSubtractionVisual.h"
+
+// Lock FFTW access
+#define FFTW_LOCK \
+if (threaded) { \
+    psFFTLock(); \
+}
+// Unlock FFTW access
+#define FFTW_UNLOCK \
+if (threaded) { \
+    psFFTUnlock(); \
+}
+
+#define FFTW_PLAN_RIGOR FFTW_ESTIMATE   // How rigorous the FFTW planning is
+
+psKernel *pmSubtractionDeconvolveGauss (int size, float sigma) {
+
+    psKernel *kernel = psKernelAlloc (-size, size, -size, size);
+
+    // build the gaussian from 2 1-D Gaussians
+    psVector *vector = pmSubtractionKernelISIS(sigma, 0, size);
+
+    // generate 2D kernel, calculate moments
+    for (int v = -size, y = 0; v <= size; v++, y++) {
+	for (int u = -size, x = 0; u <= size; u++, x++) {
+	    double value = vector->data.F32[x] * vector->data.F32[y]; // Value of kernel
+	    kernel->kernel[v][u] = value;
+	}
+    }
+
+    psFree (vector);
+    return kernel;
+}
+
+// deconvolve kernelTarget by kernelConv to get the kernel which, when convolved
+// by kernelConv results in kernelTarget...
+// XXX using complex to complex, explicitly setting the imaginary part to zero
+psKernel *pmSubtractionDeconvolveKernel (psKernel *kernelTarg, psKernel *kernelConv) {
+
+    PS_ASSERT_KERNEL_NON_NULL(kernelTarg, NULL);
+    PS_ASSERT_KERNEL_NON_NULL(kernelConv, NULL);
+
+    bool threaded = psThreadPoolSize(); // Are we running threaded?
+
+    // Size of image
+    int numCols = kernelConv->image->numCols;
+    int numRows = kernelConv->image->numRows;
+
+    // kernel sizes
+    int xMin = kernelConv->xMin;
+    int xMax = kernelConv->xMax;
+    int yMin = kernelConv->yMin;
+    int yMax = kernelConv->yMax;
+    if (xMin != kernelTarg->xMin) goto escape;
+    if (xMax != kernelTarg->xMax) goto escape;
+    if (yMin != kernelTarg->yMin) goto escape;
+    if (yMax != kernelTarg->yMax) goto escape;
+
+    int numPixels = numCols * numRows; // Number of pixels in padded image
+
+    // operation is: Kt = FFT(kernelTarg), Kc = FFT(kernelConv)
+    // Kd = (Kt * Kc) / (Kc * Kc^*)
+
+    // Create data array containing the image and kernel
+    FFTW_LOCK;
+    // psF32 *dataTarg = fftwf_malloc(numPixels * PSELEMTYPE_SIZEOF(PS_TYPE_F32)); // Data for FFTW
+    // psF32 *dataConv = fftwf_malloc(numPixels * PSELEMTYPE_SIZEOF(PS_TYPE_F32)); // Data for FFTW
+    fftwf_complex *dataTarg = fftwf_malloc(numPixels * sizeof(fftwf_complex)); // Data for FFTW
+    fftwf_complex *dataConv = fftwf_malloc(numPixels * sizeof(fftwf_complex)); // Data for FFTW
+    FFTW_UNLOCK;
+
+    // size_t numBytes = numCols * PSELEMTYPE_SIZEOF(PS_TYPE_F32); // Number of bytes per image row
+
+    // copy data from kernelTarg image to dataTarg array
+    for (int y = 0; y < numRows; y++) {
+	for (int x = 0; x < numCols; x++) {
+	    dataTarg[x + y*numCols][0] = kernelTarg->image->data.F32[y][x];
+	    dataTarg[x + y*numCols][1] = 0.0;
+	}
+    }
+    
+    // kernel must be copied to corners of image (0,0 pixel is center of kernel)
+    // copy data from kernelConv image to dataConv array
+    int oy = 0;
+    for (int iy = 0; iy <= yMax; iy++, oy++) {
+	int ox = 0;
+	for (int ix = 0; ix <= xMax; ix++, ox++) {
+	    dataConv[ox + oy*numCols][0] = kernelConv->kernel[iy][ix];
+	    dataConv[ox + oy*numCols][1] = 0.0;
+	}
+	for (int ix = xMin; ix <= -1; ix++, ox++) {
+	    dataConv[ox + oy*numCols][0] = kernelConv->kernel[iy][ix];
+	    dataConv[ox + oy*numCols][1] = 0.0;
+	}
+    }
+    for (int iy = yMin; iy <= -1; iy++, oy++) {
+	int ox = 0;
+	for (int ix = 0; ix <= xMax; ix++, ox++) {
+	    dataConv[ox + oy*numCols][0] = kernelConv->kernel[iy][ix];
+	    dataConv[ox + oy*numCols][1] = 0.0;
+	}
+	for (int ix = xMin; ix <= -1; ix++, ox++) {
+	    dataConv[ox + oy*numCols][0] = kernelConv->kernel[iy][ix];
+	    dataConv[ox + oy*numCols][1] = 0.0;
+	}
+    }
+
+    // Do the forward FFTs
+    // Note that the FFT images have different size from the input
+    FFTW_LOCK;
+    fftwf_complex *fftTarg = fftwf_malloc(numCols * numRows * sizeof(fftwf_complex)); // FFT
+    fftwf_complex *fftConv = fftwf_malloc(numCols * numRows * sizeof(fftwf_complex)); // FFT
+    FFTW_UNLOCK;
+
+    FFTW_LOCK;
+    fftwf_plan forwardTarg = fftwf_plan_dft_2d(numRows, numCols, dataTarg, fftTarg, FFTW_FORWARD, FFTW_PLAN_RIGOR);
+    fftwf_plan forwardConv = fftwf_plan_dft_2d(numRows, numCols, dataConv, fftConv, FFTW_FORWARD, FFTW_PLAN_RIGOR);
+    FFTW_UNLOCK;
+
+    fftwf_execute(forwardTarg);
+    fftwf_execute(forwardConv);
+
+    FFTW_LOCK;
+    fftwf_destroy_plan(forwardTarg);
+    fftwf_destroy_plan(forwardConv);
+    FFTW_UNLOCK;
+
+    // Combine the two transforms 
+    // Targ = Tr + iTi, Conv = Cr + iCi
+    // Deco = Dr + iDi
+    // (Dr + i Di) = (Tr + iTi) / (Cr + iCi)
+    // (Dr + i Di) = (Tr + iTi) * (Cr - iCi) / (Cr^2 - Ci^2)
+
+    // but anywhere Cr^2 - Ci^2 < 1e-7 of the max, mask it
+
+    // the X dimension is halved by FFTW
+    // int numColsOut = numCols / 2 + 1;
+
+    // generate Det = Cr^2 - Ci^2
+    float maxValue = 0.0;
+    psImage *det = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psImage *tR  = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psImage *tI  = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psImage *cR  = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psImage *cI  = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    for (int iy = 0; iy < numRows; iy++) {
+	for (int ix = 0; ix < numCols; ix++) {
+	    float convReal = fftConv[ix + iy*numCols][0];
+	    float convImag = fftConv[ix + iy*numCols][1];
+	    det->data.F32[iy][ix] = convReal*convReal - convImag*convImag;
+	    maxValue = PS_MAX(maxValue, fabs(det->data.F32[iy][ix]));
+
+	    tR->data.F32[iy][ix] = fftTarg[ix + iy*numCols][0];
+	    tI->data.F32[iy][ix] = fftTarg[ix + iy*numCols][1];
+	    cR->data.F32[iy][ix] = fftConv[ix + iy*numCols][0];
+	    cI->data.F32[iy][ix] = fftConv[ix + iy*numCols][1];
+	}
+    }
+
+    // pmSubtractionVisualShowSubtraction (det, tR, tI);
+    // pmSubtractionVisualShowSubtraction (det, cR, cI);
+
+# if 1
+# define TOL 1e-7
+    float limit = TOL*maxValue;
+    // generate Deco = targ * conv^* / (Cr^2 - Ci^2)
+    for (int iy = 0; iy < numRows; iy++) {
+	for (int ix = 0; ix < numCols; ix++) {
+	    float targReal = fftTarg[ix + iy*numCols][0];
+	    float targImag = fftTarg[ix + iy*numCols][1];
+	    float convReal = fftConv[ix + iy*numCols][0];
+	    float convImag = fftConv[ix + iy*numCols][1];
+	    if (fabs(det->data.F32[iy][ix]) < limit) {
+		fftTarg[ix + iy*numCols][0] = 0.0;
+		fftTarg[ix + iy*numCols][1] = 0.0;
+	    } else {
+		fftTarg[ix + iy*numCols][0] = (targReal*convReal + targImag*convImag) / det->data.F32[iy][ix];
+		fftTarg[ix + iy*numCols][1] = (targImag*convReal - targReal*convImag) / det->data.F32[iy][ix];
+		// fftTarg[ix + iy*numCols][0] = (targReal*convReal + targImag*convImag);
+		// fftTarg[ix + iy*numCols][1] = (targImag*convReal - targReal*convImag);
+	    }
+	}
+    }
+# else
+    for (int iy = 0; iy < numRows; iy++) {
+	for (int ix = 0; ix < numCols; ix++) {
+	    float targReal = fftTarg[ix + iy*numCols][0];
+	    float targImag = fftTarg[ix + iy*numCols][1];
+	    float convReal = fftConv[ix + iy*numCols][0];
+	    float convImag = fftConv[ix + iy*numCols][1];
+	    fftTarg[ix + iy*numCols][0] = targReal*convReal - targImag*convImag;
+	    fftTarg[ix + iy*numCols][1] = targImag*convReal + targReal*convImag;
+	}
+    }
+# endif
+
+    for (int iy = 0; iy < numRows; iy++) {
+	for (int ix = 0; ix < numCols; ix++) {
+	    tR->data.F32[iy][ix] = fftTarg[ix + iy*numCols][0];
+	    tI->data.F32[iy][ix] = fftTarg[ix + iy*numCols][1];
+	}
+    }
+    // pmSubtractionVisualShowSubtraction (det, tR, tI);
+
+    // Do the backward FFT
+    FFTW_LOCK;
+    fftwf_plan backward = fftwf_plan_dft_2d(numRows, numCols, fftTarg, dataTarg, FFTW_BACKWARD, FFTW_PLAN_RIGOR);
+    FFTW_UNLOCK;
+
+    fftwf_execute(backward);
+
+    FFTW_LOCK;
+    fftwf_destroy_plan(backward);
+    fftwf_free(fftTarg);
+    fftwf_free(fftConv);
+    FFTW_UNLOCK;
+
+    psKernel *output = psKernelAlloc (kernelTarg->xMin, kernelTarg->xMax, kernelTarg->yMin, kernelTarg->yMax);
+    for (int y = 0; y < numRows; y++) {
+	for (int x = 0; x < numCols; x++) {
+	    output->image->data.F32[y][x] = dataTarg[x + y*numCols][0];
+	}
+    }
+
+    FFTW_LOCK;
+    fftwf_free(dataTarg);
+    fftwf_free(dataConv);
+    FFTW_UNLOCK;
+
+    return output;
+
+ escape:
+    psError(PS_ERR_BAD_PARAMETER_VALUE, true, "mismatch between kernel and image");
+    return NULL;
+
+}
+
+bool pmSubtractionDeconvolutionTest (int order) {
+
+    float sigma = 1.0;
+    int size = 31;
+
+    // generate a Hermite polynomial 
+    psVector *xKernel = pmSubtractionKernelHERM(sigma, order, size); // x Kernel
+    psVector *yKernel = pmSubtractionKernelHERM(sigma, order, size); // y Kernel
+    psKernel *kernelTarget = psKernelAlloc(-size, size, -size, size);	// Kernel
+
+    // generate 2D kernel, calculate moments
+    for (int v = -size, y = 0; v <= size; v++, y++) {
+	for (int u = -size, x = 0; u <= size; u++, x++) {
+	    double value = xKernel->data.F32[x] * yKernel->data.F32[y]; // Value of kernel
+	    kernelTarget->kernel[v][u] = value;
+	}
+    }
+
+    // Gaussian convolution kernel
+    psKernel *kernelGauss = pmSubtractionDeconvolveGauss (size, 3.0);
+
+    // deconvolve the target by the gaussian:
+    psKernel *kernel = pmSubtractionDeconvolveKernel(kernelTarget, kernelGauss); // Kernel
+
+    // re-convolve the kernel
+    psImage *kernelConv = psImageConvolveFFT(NULL, kernel->image, NULL, 0, kernelGauss);
+    pmSubtractionVisualShowSubtraction (kernelTarget->image, kernel->image, kernelConv);
+
+    return true;
+
+}
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionDeconvolve.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionDeconvolve.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionDeconvolve.h	(revision 42651)
@@ -0,0 +1,15 @@
+#ifndef PM_SUBTRACTION_DECONVOLVE_H
+#define PM_SUBTRACTION_DECONVOLVE_H
+
+/* these function support deconvolution operations used to generate deconvolved kernels.  These
+   are kernels which, when convolved with the image sources, will yield a nearly orthonormal
+   basis set.  The analysis starts with an orthonormal basis set (eg, Hermitian functions) and
+   deconvolves those basis functions with a Gaussian approximating the Gaussian of the image of
+   interest */
+
+psKernel *pmSubtractionDeconvolveGauss (int size, float sigma);
+psKernel *pmSubtractionDeconvolveKernel (psKernel *kernelTarg, psKernel *kernelConv);
+
+bool pmSubtractionDeconvolutionTest (int order);
+
+# endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionEquation.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionEquation.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionEquation.c	(revision 42651)
@@ -0,0 +1,1968 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmErrorCodes.h"
+#include "pmVisual.h"
+#include "pmFPA.h"
+#include "pmSubtractionTypes.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionKernels.h"
+#include "pmSubtractionStamps.h"
+#include "pmSubtractionThreads.h"
+
+#include "pmSubtractionEquation.h"
+#include "pmSubtractionVisual.h"
+
+//# define TESTING                         // TESTING output for debugging; may not work with threads!
+# define USE_WEIGHT                      // Include weight (1/variance) in equation?
+# define USE_WINDOW                      // window to avoid neighbor contamination
+
+/* I believe we want to apply the WEIGHT to the chisq portions of the calculation (but not the WINDOW),
+ * and the WINDOW to the moments portiosn of the calculations (but not the WEIGHT)
+ *
+ */
+
+# define PENALTY false
+# define MOMENTS (!PENALTY)
+# define MOMENTS_PENALTY_SCALE 20 // up-weight the moments somewhat
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Private (file-static) functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Calculate the least-squares matrix and vector
+static bool calculateMatrixVector(psImage *matrix,	 // Least-squares matrix, updated
+                                  psVector *vector,	 // Least-squares vector, updated
+                                  double normValue,	 // Normalisation, supplied
+                                  const psKernel *input, // Input image (target)
+                                  const psKernel *reference, // Reference image (convolution source)
+                                  const psKernel *weight,  // Weight image
+                                  const psKernel *window,  // Window image
+                                  const psArray *convolutions,         // Convolutions for each kernel
+                                  const pmSubtractionKernels *kernels, // Kernels
+                                  const psImage *polyValues, // Spatial polynomial values
+                                  int footprint // (Half-)Size of stamp
+                                  )
+{
+    // (I - R * sum_i a_i k_i - g) (R * k_j) = 0
+    // I C_j = sum_i C_i C_j
+
+    // Background: C_i = 1.0
+    // Normalisation: C_i = R
+
+    int numKernels = kernels->num;                      // Number of kernels
+    int spatialOrder = kernels->spatialOrder;       // Order of spatial variation
+    int numPoly = PM_SUBTRACTION_POLYTERMS(spatialOrder); // Number of polynomial terms
+    double poly[numPoly];                                 // Polynomial terms
+    double poly2[numPoly][numPoly];                       // Polynomial-polynomial values
+    int numParams = numKernels * numPoly;
+
+    psAssert(matrix &&
+             matrix->type.type == PS_TYPE_F64 &&
+             matrix->numCols == numParams &&
+             matrix->numRows == numParams,
+             "Least-squares matrix is bad.");
+    psAssert(vector &&
+             vector->type.type == PS_TYPE_F64 &&
+             vector->n == numParams,
+             "Least-squares vector is bad.");
+
+    // Evaluate polynomial-polynomial terms
+    for (int iyOrder = 0, iIndex = 0; iyOrder <= spatialOrder; iyOrder++) {
+        for (int ixOrder = 0; ixOrder <= spatialOrder - iyOrder; ixOrder++, iIndex++) {
+            double iPoly = polyValues->data.F64[iyOrder][ixOrder]; // Value of polynomial
+            poly[iIndex] = iPoly;
+            for (int jyOrder = 0, jIndex = 0; jyOrder <= spatialOrder; jyOrder++) {
+                for (int jxOrder = 0; jxOrder <= spatialOrder - jyOrder; jxOrder++, jIndex++) {
+                    double jPoly = polyValues->data.F64[jyOrder][jxOrder];
+                    poly2[iIndex][jIndex] = iPoly * jPoly;
+                }
+            }
+        }
+    }
+
+    // initialize the matrix and vector for NOP on all coeffs.  we only fill in the coeffs we
+    // choose to calculate
+    psImageInit(matrix, 0.0);
+    psVectorInit(vector, 1.0);
+    for (int i = 0; i < matrix->numCols; i++) {
+        matrix->data.F64[i][i] = 1.0;
+    }
+
+    // the order of the elements in the matrix and vector is:
+    // [kernel 0, x^0 y^0][kernel 1 x^0 y^0]...[kernel N, x^0 y^0]
+    // [kernel 0, x^1 y^0][kernel 1 x^1 y^0]...[kernel N, x^1 y^0]
+    // [kernel 0, x^n y^m][kernel 1 x^n y^m]...[kernel N, x^n y^m]
+
+    for (int i = 0; i < numKernels; i++) {
+        psKernel *iConv = convolutions->data[i]; // Convolution for index i
+        for (int j = i; j < numKernels; j++) {
+            psKernel *jConv = convolutions->data[j]; // Convolution for index j
+
+            double sumCC = 0.0;         // Sum of convolution products
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    double cc = iConv->kernel[y][x] * jConv->kernel[y][x];
+                    if (weight) {
+                        cc *= weight->kernel[y][x];
+                    }
+		    // XXX NOTE: do NOT apply the window to the chisq portions of the calculation
+                    if (false && window) {
+                        cc *= window->kernel[y][x];
+                    }
+                    sumCC += cc;
+                }
+            }
+
+            // Spatial variation of kernel coeffs
+	    for (int iTerm = 0, iIndex = i; iTerm < numPoly; iTerm++, iIndex += numKernels) {
+		for (int jTerm = 0, jIndex = j; jTerm < numPoly; jTerm++, jIndex += numKernels) {
+		    double value = sumCC * poly2[iTerm][jTerm];
+		    matrix->data.F64[iIndex][jIndex] = value;
+		    matrix->data.F64[jIndex][iIndex] = value;
+		}
+	    }
+        }
+
+        double sumRC = 0.0;             // Sum of the reference-convolution products
+        double sumIC = 0.0;             // Sum of the input-convolution products
+        for (int y = - footprint; y <= footprint; y++) {
+            for (int x = - footprint; x <= footprint; x++) {
+                float conv = iConv->kernel[y][x];
+                float in = input->kernel[y][x];
+                float ref = reference->kernel[y][x];
+                double ic = in * conv;
+                double rc = ref * conv;
+                if (weight) {
+                    float wtVal = weight->kernel[y][x];
+                    ic *= wtVal;
+                    rc *= wtVal;
+                }
+		// XXX NOTE: do NOT apply the window to the chisq portions of the calculation
+                if (false && window) {
+                    float winVal = window->kernel[y][x];
+                    ic *= winVal;
+                    rc *= winVal;
+                }
+                sumIC += ic;
+                sumRC += rc;
+            }
+        }
+
+        // Spatial variation
+        for (int iTerm = 0, iIndex = i; iTerm < numPoly; iTerm++, iIndex += numKernels) {
+	    vector->data.F64[iIndex] = (sumIC - normValue*sumRC) * poly[iTerm];
+        }
+    }
+
+    // check for any NAN values in the result, skip if found:
+    for (int iy = 0; iy < matrix->numRows; iy++) {
+        for (int ix = 0; ix < matrix->numCols; ix++) {
+            if (!isfinite(matrix->data.F64[iy][ix])) {
+                fprintf (stderr, "WARNING: NAN in matrix\n");
+                return false;
+            }
+        }
+    }
+    for (int ix = 0; ix < vector->n; ix++) {
+        if (!isfinite(vector->data.F64[ix])) {
+            fprintf (stderr, "WARNING: NAN in vector\n");
+            return false;
+        }
+    }
+
+    return true;
+}
+
+# define RENORM_BY_FLUX 0
+
+// Calculate the least-squares matrix and vector for dual convolution
+// XXX we could avoid calculating these values on successive passes *if* the stamp has not changed.
+static bool calculateDualMatrixVector(pmSubtractionStamp *stamp,	      // stamp of interest
+                                      double normValue,	      // Normalisation, updated
+                                      double normValue2,      // Normalisation, updated
+                                      const psKernel *weight,  // Weight image
+                                      const psKernel *window,  // Window image
+                                      const pmSubtractionKernels *kernels, // Kernels
+                                      const psImage *polyValues, // Spatial polynomial values
+                                      int footprint // (Half-)Size of stamp
+                                      )
+{
+    int numKernels = kernels->num;			  // Number of kernels
+    int spatialOrder = kernels->spatialOrder;		  // Order of spatial variation
+    int numPoly = PM_SUBTRACTION_POLYTERMS(spatialOrder); // Number of polynomial terms
+    double poly[numPoly];                                 // Polynomial terms
+    double poly2[numPoly][numPoly];                       // Polynomial-polynomial values
+
+    int numParams = numKernels * numPoly;		  // Number of regular parameters
+    int numParams2 = numKernels * numPoly;	   // Number of additional parameters for dual
+    int numDual = numParams + numParams2;	   // Total number of parameters for dual
+
+    psImage *matrix = stamp->matrix;		   // Least-squares matrix, updated
+    psVector *vector = stamp->vector;		   // Least-squares vector, updated
+    psKernel *image1 = stamp->image1;		   // Image 1
+    psKernel *image2 = stamp->image2;		   // Image 2
+    psArray *convolutions1 = stamp->convolutions1; // Convolutions of image 1 for each kernel
+    psArray *convolutions2 = stamp->convolutions2; // Convolutions of image 2 for each kernel
+
+    psAssert(matrix &&
+             matrix->type.type == PS_TYPE_F64 &&
+             matrix->numCols == numDual &&
+             matrix->numRows == numDual,
+             "Least-squares matrix is bad.");
+
+    psAssert(vector &&
+             vector->type.type == PS_TYPE_F64 &&
+             vector->n == numDual,
+             "Least-squares vector is bad.");
+
+    // Evaluate polynomial-polynomial terms
+    for (int iyOrder = 0, iIndex = 0; iyOrder <= spatialOrder; iyOrder++) {
+        for (int ixOrder = 0; ixOrder <= spatialOrder - iyOrder; ixOrder++, iIndex++) {
+            double iPoly = polyValues->data.F64[iyOrder][ixOrder]; // Value of polynomial
+            poly[iIndex] = iPoly;
+            for (int jyOrder = 0, jIndex = 0; jyOrder <= spatialOrder; jyOrder++) {
+                for (int jxOrder = 0; jxOrder <= spatialOrder - jyOrder; jxOrder++, jIndex++) {
+                    double jPoly = polyValues->data.F64[jyOrder][jxOrder];
+                    poly2[iIndex][jIndex] = iPoly * jPoly;
+                }
+            }
+        }
+    }
+
+
+    // initialize the matrix and vector for NOP on all coeffs.  we only fill in the coeffs we
+    // choose to calculate
+    psImageInit(matrix, 0.0);
+    psVectorInit(vector, 1.0);
+    for (int i = 0; i < matrix->numCols; i++) {
+        matrix->data.F64[i][i] = 1.0;
+    }
+
+    // the order of the elements in the matrix and vector is:
+    // [kernel 0, x^0 y^0][kernel 1 x^0 y^0]...[kernel N, x^0 y^0]
+    // [kernel 0, x^1 y^0][kernel 1 x^1 y^0]...[kernel N, x^1 y^0]
+    // [kernel 0, x^n y^m][kernel 1 x^n y^m]...[kernel N, x^n y^m]
+
+    // for DUAL convolution analysis, we apply the normalization to I1 as follows:
+    // norm = I2 / I1
+    // 
+    // I1c = norm I1 + \Sum_i a_i norm I1 \cross k_i
+    // I2c =      I2 + \Sum_i b_i      I2 \cross k_i
+
+    // we cannot absorb the normalization into a_i until the analysis is complete, or the
+    // second moment terms are incorrectly calculated.
+
+    for (int i = 0; i < numKernels; i++) {
+        psKernel *iConv1 = convolutions1->data[i]; // Convolution 1 for index i
+        psKernel *iConv2 = convolutions2->data[i]; // Convolution 2 for index i
+        for (int j = i; j < numKernels; j++) {
+            psKernel *jConv1 = convolutions1->data[j]; // Convolution 1 for index j
+            psKernel *jConv2 = convolutions2->data[j]; // Convolution 2 for index j
+
+            double sumAA = 0.0;         // Sum of convolution products between image 1
+            double sumBB = 0.0;         // Sum of convolution products between image 2
+            double sumAB = 0.0;         // Sum of convolution products across images 1 and 2
+
+	    double MxxAA = 0.0;
+	    double MyyAA = 0.0;
+	    double MxxBB = 0.0;
+	    double MyyBB = 0.0;
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+
+		    // XXX NOTE: clipping low S/N pixels does not seem to work very well
+		    if (false && weight) {
+			float i1 = image1->kernel[y][x];
+			float i2 = image2->kernel[y][x];
+			float sn = (i1 + i2) / sqrt (weight->kernel[y][x]);
+			if (sn < 0.5) continue;
+		    }
+
+                    double aa = iConv1->kernel[y][x] * jConv1->kernel[y][x] * PS_SQR(normValue);
+                    double bb = iConv2->kernel[y][x] * jConv2->kernel[y][x];
+                    double ab = iConv1->kernel[y][x] * jConv2->kernel[y][x] * normValue;
+
+		    float wtVal = (weight) ? weight->kernel[y][x] : 1.0;
+                    sumAA += wtVal*aa;
+                    sumBB += wtVal*bb;
+                    sumAB += wtVal*ab;
+
+		    if (MOMENTS) {
+			float winVal = (window) ? window->kernel[y][x] : 1.0;
+			MxxAA += winVal*x*x*aa;
+			MyyAA += winVal*y*y*aa;
+			MxxBB += winVal*x*x*bb;
+			MyyBB += winVal*y*y*bb;
+		    }
+                }
+            }
+
+	    // XXX does normSquare1,2 mess up the relative scaling?
+	    // XXX no: normSquare1,2 is the sum of the flux^2 for the source
+	    if (MOMENTS) {
+		MxxAA /= stamp->normSquare1 * PS_SQR(normValue);
+		MyyAA /= stamp->normSquare1 * PS_SQR(normValue);
+		MxxBB /= stamp->normSquare2;
+		MyyBB /= stamp->normSquare2;
+	    }
+
+	    // XXX this makes the Chisq portion independent of the normalization and star flux
+	    // but may be mis-scaling between stars of different fluxes
+# if (RENORM_BY_FLUX)	    
+	    sumAA /= PS_SQR(stamp->normI2);
+	    sumAB /= PS_SQR(stamp->normI2);
+	    sumBB /= PS_SQR(stamp->normI2);
+# endif
+
+	    // fprintf (stderr, "i,j : %d %d : M(xx,yy)(AA,BB) : %f %f %f %f\n", i, j, MxxAA, MyyAA, MxxBB, MyyBB);
+
+            // Spatial variation of kernel coeffs
+	    for (int iTerm = 0, iIndex = i; iTerm < numPoly; iTerm++, iIndex += numKernels) {
+		for (int jTerm = 0, jIndex = j; jTerm < numPoly; jTerm++, jIndex += numKernels) {
+		    double aa = sumAA * poly2[iTerm][jTerm];
+		    double bb = sumBB * poly2[iTerm][jTerm];
+		    double ab = sumAB * poly2[iTerm][jTerm];
+
+		    matrix->data.F64[iIndex][jIndex] = aa;
+		    matrix->data.F64[jIndex][iIndex] = aa;
+
+		    matrix->data.F64[iIndex + numParams][jIndex + numParams] = bb;
+		    matrix->data.F64[jIndex + numParams][iIndex + numParams] = bb;
+
+		    // add in second moments
+		    if (MOMENTS) {
+			matrix->data.F64[iIndex][jIndex] += kernels->penalty * MxxAA * MOMENTS_PENALTY_SCALE;
+			matrix->data.F64[iIndex][jIndex] += kernels->penalty * MyyAA * MOMENTS_PENALTY_SCALE;
+			matrix->data.F64[jIndex][iIndex] += kernels->penalty * MxxAA * MOMENTS_PENALTY_SCALE;
+			matrix->data.F64[jIndex][iIndex] += kernels->penalty * MyyAA * MOMENTS_PENALTY_SCALE;
+
+			matrix->data.F64[iIndex + numParams][jIndex + numParams] += kernels->penalty * MxxBB * MOMENTS_PENALTY_SCALE;
+			matrix->data.F64[iIndex + numParams][jIndex + numParams] += kernels->penalty * MyyBB * MOMENTS_PENALTY_SCALE;
+			matrix->data.F64[jIndex + numParams][iIndex + numParams] += kernels->penalty * MxxBB * MOMENTS_PENALTY_SCALE;
+			matrix->data.F64[jIndex + numParams][iIndex + numParams] += kernels->penalty * MyyBB * MOMENTS_PENALTY_SCALE;
+		    }
+		    matrix->data.F64[iIndex][jIndex + numParams] = ab;
+		    matrix->data.F64[jIndex + numParams][iIndex] = ab;
+		}
+	    }
+        }
+
+	// we need to calculate the lower-diagonal AB elements since they are not symmetric for A <-> B
+        for (int j = 0; j < i; j++) {
+            psKernel *jConv2 = convolutions2->data[j]; // Convolution 2 for index j
+            double sumAB = 0.0;         // Sum of convolution products for matrix C
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    double ab = iConv1->kernel[y][x] * jConv2->kernel[y][x] * normValue;
+                    if (weight) {
+                        ab *= weight->kernel[y][x];
+                    }
+		    // XXX NOTE: do NOT apply the window to the chisq portions of the calculation
+                    if (false && window) {
+                        ab *= window->kernel[y][x];
+                    }
+                    sumAB += ab;
+                }
+            }
+
+	    // XXX this makes the Chisq portion independent of the normalization and star flux
+	    // but may be mis-scaling between stars of different fluxes
+# if (RENORM_BY_FLUX)
+	    sumAB /= PS_SQR(stamp->normI2);
+# endif
+
+            // Spatial variation of kernel coeffs
+	    for (int iTerm = 0, iIndex = i; iTerm < numPoly; iTerm++, iIndex += numKernels) {
+		for (int jTerm = 0, jIndex = j; jTerm < numPoly; jTerm++, jIndex += numKernels) {
+		    double ab = sumAB * poly2[iTerm][jTerm];
+		    matrix->data.F64[iIndex][jIndex + numParams] = ab;
+		    matrix->data.F64[jIndex + numParams][iIndex] = ab;
+		}
+            }
+        }
+
+        double sumAI2 = 0.0;            // Sum of A.I_2 products (for vector)
+        double sumBI2 = 0.0;            // Sum of B.I_2 products (for vector)
+        double sumAI1 = 0.0;            // Sum of A.I_1 products (for matrix, normalisation)
+        double sumBI1 = 0.0;            // Sum of B.I_1 products (for matrix, normalisation)
+
+	double MxxAI1 = 0.0;
+	double MyyAI1 = 0.0;
+	double MxxBI2 = 0.0;
+	double MyyBI2 = 0.0;
+        for (int y = - footprint; y <= footprint; y++) {
+            for (int x = - footprint; x <= footprint; x++) {
+                double a = iConv1->kernel[y][x];
+                double b = iConv2->kernel[y][x];
+                float i1 = image1->kernel[y][x];
+                float i2 = image2->kernel[y][x];
+
+		// XXX NOTE: clipping low S/N pixels does not seem to work very well
+		if (false && weight) {
+		    float sn = (i1 + i2) / sqrt (weight->kernel[y][x]);
+		    if (sn < 0.5) continue;
+		}
+
+                double ai2 = a * i2 * normValue;
+                double bi2 = b * i2;
+                double ai1 = a * i1 * PS_SQR(normValue);
+                double bi1 = b * i1 * normValue;
+
+		float wtVal = (weight) ? weight->kernel[y][x] : 1.0;
+                sumAI2 += wtVal*ai2;
+                sumBI2 += wtVal*bi2;
+                sumAI1 += wtVal*ai1;
+                sumBI1 += wtVal*bi1;
+
+		if (MOMENTS) {
+		    float winVal = (window) ? window->kernel[y][x] : 1.0;
+		    MxxAI1 += winVal*x*x*ai1;
+		    MyyAI1 += winVal*y*y*ai1;
+		    MxxBI2 += winVal*x*x*bi2;
+		    MyyBI2 += winVal*y*y*bi2;
+		}
+            }
+        }
+
+	if (MOMENTS) {
+	    MxxAI1 /= stamp->normSquare1 * PS_SQR(normValue);
+	    MyyAI1 /= stamp->normSquare1 * PS_SQR(normValue);
+	    MxxBI2 /= stamp->normSquare2;
+	    MyyBI2 /= stamp->normSquare2;
+	}
+
+	// fprintf (stderr, "i : %d : M(xx,yy)(AI1,BI2) : %f %f %f %f\n", i, MxxAI1, MyyAI1, MxxBI2, MyyBI2);
+
+	// XXX this makes the Chisq portion independent of the normalization and star flux
+	// but may be mis-scaling between stars of different fluxes
+# if (RENORM_BY_FLUX)
+	sumAI1 /= PS_SQR(stamp->normI2);
+	sumBI1 /= PS_SQR(stamp->normI2);
+	sumAI2 /= PS_SQR(stamp->normI2);
+	sumBI2 /= PS_SQR(stamp->normI2);
+# endif
+
+        // Spatial variation
+        for (int iTerm = 0, iIndex = i; iTerm < numPoly; iTerm++, iIndex += numKernels) {
+            double ai2 = sumAI2 * poly[iTerm];
+            double bi2 = sumBI2 * poly[iTerm];
+            double ai1 = sumAI1 * poly[iTerm];
+            double bi1 = sumBI1 * poly[iTerm];
+	    vector->data.F64[iIndex]             = ai2 - ai1;
+	    vector->data.F64[iIndex + numParams] = bi2 - bi1;
+
+	    // fprintf (stderr, "i : %d : V(I1,I2) : %f %f\n", i, vector->data.F64[iIndex], vector->data.F64[iIndex + numParams]);
+
+	    // add in second moments
+	    if (MOMENTS) {
+		vector->data.F64[iIndex]             -= kernels->penalty * MxxAI1 * MOMENTS_PENALTY_SCALE;
+		vector->data.F64[iIndex]             -= kernels->penalty * MyyAI1 * MOMENTS_PENALTY_SCALE;
+
+		vector->data.F64[iIndex + numParams] -= kernels->penalty * MxxBI2 * MOMENTS_PENALTY_SCALE;
+		vector->data.F64[iIndex + numParams] -= kernels->penalty * MyyBI2 * MOMENTS_PENALTY_SCALE;
+	    }
+        }
+    }
+
+    // check for any NAN values in the result, skip if found:
+    for (int iy = 0; iy < matrix->numRows; iy++) {
+        for (int ix = 0; ix < matrix->numCols; ix++) {
+            if (!isfinite(matrix->data.F64[iy][ix])) {
+                fprintf (stderr, "WARNING: NAN in matrix\n");
+                return false;
+            }
+        }
+    }
+    for (int ix = 0; ix < vector->n; ix++) {
+        if (!isfinite(vector->data.F64[ix])) {
+            fprintf (stderr, "WARNING: NAN in vector\n");
+            return false;
+        }
+    }
+
+
+    return true;
+}
+
+// Add in penalty term to least-squares vector
+bool calculatePenalty(psImage *matrix,                     // Matrix to which to add in penalty term
+		      psVector *vector,                    // Vector to which to add in penalty term
+		      const pmSubtractionKernels *kernels, // Kernel parameters
+		      float normSquare1,		   // Normalisation for image 1
+		      float normSquare2		   // Normalisation for image 2
+  )
+{
+    psAssert (kernels->mode == PM_SUBTRACTION_MODE_DUAL, "only use penalties for dual convolution");
+
+    if (kernels->penalty == 0.0) {
+        return true;
+    }
+
+    psVector *penalties1 = kernels->penalties1; // Penalties for each kernel component (input)
+    psVector *penalties2 = kernels->penalties2; // Penalties for each kernel component (ref)
+
+    int spatialOrder = kernels->spatialOrder; // Order of spatial variations
+    int numKernels = kernels->num; // Number of kernel components
+    int numSpatial = PM_SUBTRACTION_POLYTERMS(spatialOrder); // Number of spatial variations
+    int numParams = numKernels * numSpatial;                 // Number of kernel parameters
+
+    // order is :
+    // [p_0,x_0,y_0 p_1,x_0,y_0, p_2,x_0,y_0]
+    // [p_0,x_1,y_0 p_1,x_1,y_0, p_2,x_1,y_0]
+    // [p_0,x_0,y_1 p_1,x_0,y_1, p_2,x_0,y_1]
+
+    // [q_0,x_0,y_0 q_1,x_0,y_0, q_2,x_0,y_0]
+    // [q_0,x_1,y_0 q_1,x_1,y_0, q_2,x_1,y_0]
+    // [q_0,x_0,y_1 q_1,x_0,y_1, q_2,x_0,y_1]
+
+    for (int i = 0; i < numKernels; i++) {
+        for (int yOrder = 0, index = i; yOrder <= spatialOrder; yOrder++) {
+            for (int xOrder = 0; xOrder <= spatialOrder - yOrder; xOrder++, index += numKernels) {
+                // Contribution to chi^2: a_i^2 P_i
+                psAssert(isfinite(penalties1->data.F32[i]), "Invalid penalty");
+		// fprintf (stderr, "penalty: %f + %f (%f * %f)\n", matrix->data.F64[index][index], normSquare1 * penalties1->data.F32[i], normSquare1, penalties1->data.F32[i]);
+                matrix->data.F64[index][index] += normSquare1 * penalties1->data.F32[i];
+
+		// fprintf (stderr, "penalty: (x^%d y^%d fwhm %f) : %f + %f (%f * %f)\n", kernels->u->data.S32[index], kernels->v->data.S32[index], kernels->widths->data.F32[index], 
+		// matrix->data.F64[index + numParams][index + numParams], normSquare2 * penalties2->data.F32[i], normSquare2, penalties2->data.F32[i]);
+		matrix->data.F64[index + numParams][index + numParams] += normSquare2 * penalties2->data.F32[i];			     
+		// matrix[i][i] is ~ (k_i * I_1)(k_i * I_1)
+		// penalties scale with second moments
+		//
+            }
+        }
+    }
+
+    return true;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Semi-public functions
+// XXX We might like to define these functions as "extern inline" but gcc currently doesn't handle this in c99
+// mode.  See http://gcc.gnu.org/ml/gcc/2006-11/msg00006.html
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Calculate the value of a polynomial, specified by coefficients and polynomial values
+double p_pmSubtractionCalculatePolynomial(const psVector *coeff, // Coefficients
+                                          const psImage *polyValues, // Polynomial values
+                                          int order, // Order of polynomials
+                                          int index, // Index at which to begin
+                                          int step // Step between subsequent indices
+                                          )
+{
+    double sum = 0.0;                   // Value of the polynomial sum
+    for (int yOrder = 0; yOrder <= order; yOrder++) {
+        for (int xOrder = 0; xOrder <= order - yOrder; xOrder++, index += step) {
+
+            assert(index < coeff->n);
+
+            sum += coeff->data.F64[index] * polyValues->data.F64[yOrder][xOrder];
+        }
+    }
+    return sum;
+}
+
+double p_pmSubtractionSolutionCoeff(const pmSubtractionKernels *kernels, const psImage *polyValues,
+                                    int index, bool wantDual)
+{
+    // This is probably in a tight loop, so don't check inputs
+    psVector *solution = wantDual ? kernels->solution2 : kernels->solution1; // Solution vector
+    return p_pmSubtractionCalculatePolynomial(solution, polyValues, kernels->spatialOrder, index,
+                                              kernels->num);
+}
+
+double p_pmSubtractionSolutionNorm(const pmSubtractionKernels *kernels)
+{
+    // This is probably in a tight loop, so don't check inputs
+    int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+    return kernels->solution1->data.F64[normIndex];
+}
+
+double p_pmSubtractionSolutionBackground(const pmSubtractionKernels *kernels,
+                                                const psImage *polyValues)
+{
+    // This is probably in a tight loop, so don't check inputs
+    int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index for background
+    return p_pmSubtractionCalculatePolynomial(kernels->solution1, polyValues, kernels->bgOrder, bgIndex, 1);
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmSubtractionCalculateMoments(
+    pmSubtractionKernels *kernels, // Kernels
+    pmSubtractionStampList *stamps)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+
+    // XXX skip this, right?
+    return true;
+
+    // these are only used by DUAL mode
+    if (kernels->mode != PM_SUBTRACTION_MODE_DUAL) return true;
+
+    psTimerStart("pmSubtractionCalculateMoments");
+
+    int footprint = stamps->footprint;  // Half-size of stamps
+
+    // Loop over each stamp and calculate its normalization factor 
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (stamp->status == PM_SUBTRACTION_STAMP_REJECTED) continue;
+        if (stamp->status == PM_SUBTRACTION_STAMP_NONE) continue;
+
+	pmSubtractionCalculateMomentsStamp(kernels, stamp, footprint, stamps->normWindow2, stamps->normWindow1);
+    }
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Calculate moments: %f sec", psTimerClear("pmSubtractionCalculateMoments"));
+
+    return true;
+}
+
+bool pmSubtractionCalculateMomentsStamp(
+    pmSubtractionKernels *kernels, // Kernels
+    pmSubtractionStamp *stamp,		// stamp on which to save normalization)
+    int footprint,			// (Half-)Size of stamp
+    int normWindow1,			// Window (half-)size for normalisation measurement
+    int normWindow2			// Window (half-)size for normalisation measurement
+    )
+{
+    double Mxx, Myy;
+
+    int numKernels = kernels->num;
+
+    // Generate convolutions: these are generated once and saved
+    if (!pmSubtractionConvolveStamp(stamp, kernels, footprint)) {
+        psError(psErrorCodeLast(), false, "Unable to convolve stamp");
+        return false;
+    }
+
+    if (!stamp->MxxI1) {
+	stamp->MxxI1 = psVectorAlloc (numKernels, PS_TYPE_F32);
+    }
+    if (!stamp->MyyI1) {
+	stamp->MyyI1 = psVectorAlloc (numKernels, PS_TYPE_F32);
+    }
+    if (!stamp->MxxI2) {
+	stamp->MxxI2 = psVectorAlloc (numKernels, PS_TYPE_F32);
+    }
+    if (!stamp->MyyI2) {
+	stamp->MyyI2 = psVectorAlloc (numKernels, PS_TYPE_F32);
+    }
+
+    for (int i = 0; i < numKernels; i++) {
+        pmSubtractionCalculateMomentsKernel(&Mxx, &Myy, stamp->convolutions1->data[i], footprint, normWindow1);
+	stamp->MxxI1->data.F32[i] = Mxx / stamp->normI1;
+	stamp->MyyI1->data.F32[i] = Myy / stamp->normI1;
+        pmSubtractionCalculateMomentsKernel(&Mxx, &Myy, stamp->convolutions2->data[i], footprint, normWindow2);
+	stamp->MxxI2->data.F32[i] = Mxx / stamp->normI2;
+	stamp->MyyI2->data.F32[i] = Myy / stamp->normI2;
+    }
+
+    pmSubtractionCalculateMomentsKernel(&Mxx, &Myy, stamp->image1, footprint, normWindow1);
+    stamp->MxxI1raw = Mxx / stamp->normI1;
+    stamp->MyyI1raw = Myy / stamp->normI1;
+
+    pmSubtractionCalculateMomentsKernel(&Mxx, &Myy, stamp->image2, footprint, normWindow2);
+    stamp->MxxI2raw = Mxx / stamp->normI2;
+    stamp->MyyI2raw = Myy / stamp->normI2;
+
+    // fprintf (stderr, "Mxx I1: %f, Myy I1: %f, Mxx I2: %f, Myy I2: %f\n", stamp->MxxI1raw, stamp->MyyI1raw, stamp->MxxI2raw, stamp->MyyI2raw);
+
+    return true;
+}
+
+bool pmSubtractionCalculateMomentsKernel(double *Mxx, double *Myy, psKernel *image, int footprint, int window) {
+
+    double Sxx = 0.0;
+    double Syy = 0.0;
+    for (int y = - footprint; y <= footprint; y++) {
+        for (int x = - footprint; x <= footprint; x++) {
+            if (PS_SQR(x) + PS_SQR(y) > PS_SQR(window)) continue;
+
+            double flux = image->kernel[y][x];
+
+	    Sxx += PS_SQR(x) * flux;
+	    Syy += PS_SQR(y) * flux;
+        }
+    }
+    *Mxx = Sxx;
+    *Myy = Syy;
+    return true;
+}
+
+///---------
+
+bool pmSubtractionCalculateNormalization(
+    pmSubtractionStampList *stamps,
+    const pmSubtractionMode mode)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+
+    psTimerStart("pmSubtractionCalculateNormalization");
+
+    psVector *norms = psVectorAllocEmpty(stamps->num, PS_TYPE_F64); // Normalisations
+    psVector *norm2 = psVectorAllocEmpty(stamps->num, PS_TYPE_F64); // Normalisations
+
+    int footprint = stamps->footprint;  // Half-size of stamps
+
+    // Loop over each stamp and calculate its normalization factor 
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (stamp->status == PM_SUBTRACTION_STAMP_REJECTED) continue;
+        if (stamp->status == PM_SUBTRACTION_STAMP_NONE) continue;
+
+	// XXX skip this if we have already calculated it? (stamp->norm does not change, just the median statistic)
+	// XXX maybe not: the star may have changed for a given stamp -- only if norm is reset to NAN can we do this
+	if (mode == PM_SUBTRACTION_MODE_2) {
+	    pmSubtractionCalculateNormalizationStamp(stamp, stamp->image2, stamp->image1, footprint, stamps->normWindow2, stamps->normWindow1);
+	} else {
+	    pmSubtractionCalculateNormalizationStamp(stamp, stamp->image1, stamp->image2, footprint, stamps->normWindow1, stamps->normWindow2);
+	}
+	psVectorAppend(norms, stamp->norm);
+	psVectorAppend(norm2, stamp->normSquare2 / stamp->normSquare1);
+    }
+
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN); // Statistics for norm
+    if (!psVectorStats(stats, norms, NULL, NULL, 0)) {
+	psError(PM_ERR_DATA, false, "Unable to determine median normalisation");
+	psFree(stats);
+	psFree(norms);
+	psFree(norm2);
+	return false;
+    }
+    stamps->normValue = stats->robustMedian;
+
+    psStatsInit(stats);
+    if (!psVectorStats(stats, norm2, NULL, NULL, 0)) {
+	psError(PM_ERR_DATA, false, "Unable to determine median normalisation");
+	psFree(stats);
+	psFree(norms);
+	psFree(norm2);
+	return false;
+    }
+    stamps->normValue2 = stats->robustMedian;
+
+    psLogMsg ("psModules.imcombine", PS_LOG_INFO, "norm (1): %f (%f) %ld\n", stamps->normValue, stamps->normValue2,norms->n);
+
+    psFree(stats);
+    psFree(norms);
+    psFree(norm2);
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Calculate normalization: %f sec", psTimerClear("pmSubtractionCalculateNormalization"));
+
+    return true;
+}
+
+bool pmSubtractionCalculateNormalizationStamp(
+    pmSubtractionStamp *stamp,		// stamp on which to save normalization)
+    const psKernel *image1,		// Input image (target)
+    const psKernel *image2,		// Reference image (convolution source)
+    int footprint,			// (Half-)Size of stamp
+    int normWindow1,			// Window (half-)size for normalisation measurement
+    int normWindow2			// Window (half-)size for normalisation measurement
+    )
+{
+    double normI1 = 0.0;  // Sum of I_1 within the normalisation window (aperture)
+    double normI2 = 0.0;  // Sum of I_2 within the normalisation window (aperture)
+    double normSquare1 = 0.0;  // Sum of (I_1)^2 within the normalisation window (aperture)
+    double normSquare2 = 0.0;  // Sum of (I_2)^2 within the normalisation window (aperture)
+    for (int y = - footprint; y <= footprint; y++) {
+        for (int x = - footprint; x <= footprint; x++) {
+            double im1 = image1->kernel[y][x];
+            double im2 = image2->kernel[y][x];
+
+            if (PS_SQR(x) + PS_SQR(y) <= PS_SQR(normWindow1)) {
+                normI1 += im1;
+		normSquare1 += PS_SQR(im1);
+            }
+            if (PS_SQR(x) + PS_SQR(y) <= PS_SQR(normWindow2)) {
+                normI2 += im2;
+		normSquare2 += PS_SQR(im2);
+            }
+        }
+    }
+    //    psLogMsg("psModules.imcombine",PS_LOG_INFO, "stampNorm: %f %f %d %d %d %f %f",
+    //	     stamp->x,stamp->y,footprint,normWindow1,normWindow2,
+    //	     normI1,normI2);
+    stamp->norm = normI2 / normI1;
+    stamp->normI1 = normI1;
+    stamp->normI2 = normI2;
+    stamp->normSquare1 = normSquare1;
+    stamp->normSquare2 = normSquare2;
+
+    // psLogMsg ("psModules.imcombine", PS_LOG_DETAIL, "normValue: %f %f %f  (%f %f)\n", normI1, normI2, stamp->norm, normSquare1, normSquare2);
+
+    return true;
+}
+
+bool pmSubtractionCalculateEquationThread(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    pmSubtractionStampList *stamps = job->args->data[0]; // List of stamps
+    pmSubtractionKernels *kernels = job->args->data[1]; // Kernels
+    int index = PS_SCALAR_VALUE(job->args->data[2], S32); // Stamp index
+
+    return pmSubtractionCalculateEquationStamp(stamps, kernels, index);
+}
+
+bool pmSubtractionCalculateEquationStamp(pmSubtractionStampList *stamps, pmSubtractionKernels *kernels, int index)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+    PS_ASSERT_INT_NONNEGATIVE(index, false);
+    PS_ASSERT_INT_LESS_THAN(index, stamps->num, false);
+
+    int footprint = stamps->footprint;  // Half-size of stamps
+    int spatialOrder = kernels->spatialOrder; // Maximum order of spatial variation
+    int numKernels = kernels->num;      // Number of kernel basis functions
+    int numSpatial = PM_SUBTRACTION_POLYTERMS(spatialOrder); // Number of spatial variations
+
+    // numKernels is the number of unique kernel images (one for each Gaussian modified by a specific polynomial).
+    // = \sum_i^N_Gaussians [(order + 1) * (order + 2) / 2], eg for 1 Gauss and 1st order, numKernels = 3
+
+    // Total number of parameters to solve for: coefficient of each kernel basis function, multipled by the
+    // number of coefficients for the spatial polynomial, normalisation and a constant background offset.
+    // XXX we no longer solve for the normalization and background in the matrix inversion
+    int numParams = numKernels * numSpatial;
+    if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        // An additional image is convolved
+        numParams += numKernels * numSpatial;
+    }
+
+    pmSubtractionStamp *stamp = stamps->stamps->data[index]; // Stamp of interest
+    psAssert(stamp->status == PM_SUBTRACTION_STAMP_CALCULATE, "We only operate on stamps with this state.");
+
+    psImage *polyValues = p_pmSubtractionPolynomial(NULL, spatialOrder, stamp->xNorm, stamp->yNorm); // Polynomial terms
+
+    // Is this a new run? Have we allocated the correct sized vector/matrix?
+    bool new = stamp->vector ? false : true;
+    if (!new && (stamp->vector->n != numParams)) {
+	psFree (stamp->vector);
+	psFree (stamp->matrix);
+	new = true;
+    }
+
+    if (new) {
+        stamp->matrix = psImageAlloc(numParams, numParams, PS_TYPE_F64);
+        stamp->vector = psVectorAlloc(numParams, PS_TYPE_F64);
+    }
+#ifdef TESTING
+    psImageInit(stamp->matrix, NAN);
+    psVectorInit(stamp->vector, NAN);
+#endif
+
+    bool status;                    // Status of least-squares matrix/vector calculation
+
+    psKernel *weight = NULL;
+    psKernel *window = NULL;
+
+#ifdef USE_WEIGHT
+    weight = stamp->weight;
+#endif
+#ifdef USE_WINDOW
+    window = stamps->window;
+#endif
+
+    switch (kernels->mode) {
+      case PM_SUBTRACTION_MODE_1:
+        status = calculateMatrixVector(stamp->matrix, stamp->vector, stamps->normValue, stamp->image2, stamp->image1,
+                                       weight, window, stamp->convolutions1, kernels, polyValues, footprint);
+        break;
+      case PM_SUBTRACTION_MODE_2:
+        status = calculateMatrixVector(stamp->matrix, stamp->vector, stamps->normValue, stamp->image1, stamp->image2,
+                                       weight, window, stamp->convolutions2, kernels, polyValues, footprint);
+        break;
+      case PM_SUBTRACTION_MODE_DUAL:
+        status = calculateDualMatrixVector(stamp, stamps->normValue, stamps->normValue2, weight, window, kernels, polyValues, footprint);
+        break;
+      default:
+        psAbort("Unsupported subtraction mode: %x", kernels->mode);
+    }
+
+    if (!status) {
+        stamp->status = PM_SUBTRACTION_STAMP_REJECTED;
+        psWarning("Rejecting stamp %d (%d,%d) because of bad equation",
+                  index, (int)(stamp->x - 0.5), (int)(stamp->y - 0.5));
+    } else {
+        stamp->status = PM_SUBTRACTION_STAMP_USED;
+    }
+
+#ifdef TESTING
+    {
+        psString matrixName = NULL;
+        psStringAppend(&matrixName, "matrix_%d.fits", index);
+        psFits *matrixFile = psFitsOpen(matrixName, "w");
+        psFree(matrixName);
+        psFitsWriteImage(matrixFile, NULL, stamp->matrix, 0, NULL);
+        psFitsClose(matrixFile);
+
+        matrixName = NULL;
+        psStringAppend(&matrixName, "vector_%d.fits", index);
+        psImage *dummy = psImageAlloc(stamp->vector->n, 1, PS_TYPE_F64);
+        memcpy(dummy->data.F64[0], stamp->vector->data.F64,
+               PSELEMTYPE_SIZEOF(PS_TYPE_F64) * stamp->vector->n);
+        matrixFile = psFitsOpen(matrixName, "w");
+        psFree(matrixName);
+        psFitsWriteImage(matrixFile, NULL, dummy, 0, NULL);
+        psFree(dummy);
+        psFitsClose(matrixFile);
+    }
+#endif
+
+    psFree(polyValues);
+
+    return true;
+}
+
+bool pmSubtractionCalculateEquation(pmSubtractionStampList *stamps, pmSubtractionKernels *kernels)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+
+    psTimerStart("pmSubtractionCalculateEquation");
+
+    // We iterate over each stamp, allocate the matrix and vectors if
+    // necessary, and then calculate those matrix/vectors.
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+
+        if (stamp->status != PM_SUBTRACTION_STAMP_CALCULATE) {
+            continue;
+        }
+
+        if ((stamp->x <= 0.0) && (stamp->y <= 0.0)) {
+            psAbort ("bad stamp");
+        }
+        if (!isfinite(stamp->x) && !isfinite(stamp->y)) {
+            psAbort ("bad stamp");
+        }
+
+        if (pmSubtractionThreaded()) {
+            psThreadJob *job = psThreadJobAlloc("PSMODULES_SUBTRACTION_CALCULATE_EQUATION");
+            psArrayAdd(job->args, 1, stamps);
+            psArrayAdd(job->args, 1, (pmSubtractionKernels*)kernels); // Casting away const to put on array
+            PS_ARRAY_ADD_SCALAR(job->args, i, PS_TYPE_S32);
+            if (!psThreadJobAddPending(job)) {
+                return false;
+            }
+        } else {
+            pmSubtractionCalculateEquationStamp(stamps, kernels, i);
+        }
+    }
+
+    if (!psThreadPoolWait(true, true)) {
+        psError(psErrorCodeLast(), false, "Error waiting for threads.");
+        return false;
+    }
+
+    pmSubtractionVisualShowKernels((pmSubtractionKernels  *)kernels);
+    pmSubtractionVisualShowBasis(stamps);
+    pmSubtractionVisualPlotLeastSquares(stamps);
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Calculate equation: %f sec", psTimerClear("pmSubtractionCalculateEquation"));
+
+    return true;
+}
+
+// private functions used on pmSubtractionSolveEquation
+bool psVectorWriteFile (char *filename, const psVector *vector);
+bool psFitsWriteImageSimple (char *filename, psImage *image, psMetadata *header);
+
+bool pmSubtractionSolveEquation(pmSubtractionKernels *kernels, const pmSubtractionStampList *stamps)
+{
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+
+    psTimerStart("pmSubtractionSolveEquation");
+
+    // Check inputs
+    int numKernels = kernels->num;      // Number of kernel basis functions
+    int numSpatial = PM_SUBTRACTION_POLYTERMS(kernels->spatialOrder); // Number of spatial variations
+    // XXX int numBackground = PM_SUBTRACTION_POLYTERMS(kernels->bgOrder); // Number of background terms
+    // XXX int numParams = numKernels * numSpatial + 1 + numBackground;    // Number of parameters being solved for
+    int numParams = numKernels * numSpatial;	    // Number of parameters being solved for
+    int numSolution1 = numParams, numSolution2 = 0; // Number of parameters for each solution
+    if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        // An additional image is convolved
+        numSolution2 = numKernels * numSpatial;
+        numParams += numSolution2;
+    }
+
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        PS_ASSERT_PTR_NON_NULL(stamp, false);
+        if (stamp->status != PM_SUBTRACTION_STAMP_USED) {
+            continue;
+        }
+
+	if (stamp->vector->n != numParams) {
+	    fprintf (stderr, "mismatch length\n");
+	}
+        PS_ASSERT_VECTOR_NON_NULL(stamp->vector, false);
+        PS_ASSERT_VECTOR_SIZE(stamp->vector, (long)numParams, false);
+        PS_ASSERT_VECTOR_TYPE(stamp->vector, PS_TYPE_F64, false);
+        PS_ASSERT_IMAGE_NON_NULL(stamp->matrix, false);
+        PS_ASSERT_IMAGE_SIZE(stamp->matrix, numParams, numParams, false);
+        PS_ASSERT_IMAGE_TYPE(stamp->matrix, PS_TYPE_F64, false);
+    }
+
+    psString ds9name = NULL;            // Filename for ds9 region file
+    static int ds9num = 0;              // File number for ds9 region file
+    psStringAppend(&ds9name, "stamps_solution_%d.ds9", ds9num);
+    FILE *ds9 = pmSubtractionStampsFile(stamps, ds9name, "solution stamps");
+    psFree(ds9name);
+    ds9num++;
+
+    if (kernels->mode != PM_SUBTRACTION_MODE_DUAL) {
+        // Accumulate the least-squares matrices and vectors.  These are generated for the
+	// kernel elements, excluding the background and normalization.
+        psImage *sumMatrix = psImageAlloc(numParams, numParams, PS_TYPE_F64); // Combined matrix
+        psVector *sumVector = psVectorAlloc(numParams, PS_TYPE_F64); // Combined vector
+        psVectorInit(sumVector, 0.0);
+        psImageInit(sumMatrix, 0.0);
+
+        int numStamps = 0;              // Number of good stamps
+        for (int i = 0; i < stamps->num; i++) {
+            pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+            if (stamp->status == PM_SUBTRACTION_STAMP_USED) {
+		
+                (void)psBinaryOp(sumMatrix, sumMatrix, "+", stamp->matrix);
+                (void)psBinaryOp(sumVector, sumVector, "+", stamp->vector);
+
+                pmSubtractionStampPrint(ds9, stamp->x, stamp->y, stamps->footprint, "green");
+                numStamps++;
+            } else if (stamp->status == PM_SUBTRACTION_STAMP_REJECTED) {
+                pmSubtractionStampPrint(ds9, stamp->x, stamp->y, stamps->footprint, "red");
+            }
+        }
+
+	pmSubtractionVisualPlotLeastSquaresResid(stamps, sumMatrix, numStamps);
+
+#if 0
+	psImage *save = psImageCopy(NULL, sumMatrix, PS_TYPE_F32);
+        psFitsWriteImageSimple ("sumMatrix.fits", save, NULL);
+        psVectorWriteFile("sumVector.dat", sumVector);
+	psFree (save);
+#endif
+
+	// XXX TEST : print the matrix & vector
+	if (0) {
+	    for (int iy = 0; iy < sumMatrix->numRows; iy++) {
+		for (int ix = 0; ix < sumMatrix->numCols; ix++) {
+		    fprintf (stderr, "%e  ", sumMatrix->data.F64[iy][ix]);
+		}
+		fprintf (stderr, " : %e\n", sumVector->data.F64[iy]);
+	    }
+	}
+
+	psImage *invMatrix = NULL;
+        psVector *solution = NULL;                       // Solution to equation!
+        solution = psVectorAlloc(numParams, PS_TYPE_F64);
+        psVectorInit(solution, 0);
+
+	// XXX TEST: try some constraint on the svd solution
+	// solution = psMatrixSolveSVD(solution, sumMatrix, sumVector, NAN);
+	// SINGLE solution
+# if (1)
+	solution = psMatrixSolveSVD(solution, sumMatrix, sumVector, 1e-10);
+	invMatrix = psMatrixInvert(NULL, sumMatrix, NULL);
+# endif
+# if (0)
+	psMatrixLUSolve(sumMatrixLU, sumVector);
+	solution = psMemIncrRefCounter(sumVector);
+	invMatrix = psMemIncrRefCounter(sumMatrix);
+# endif
+# if (0)
+	psMatrixGJSolve(sumMatrix, sumVector);
+	invMatrix = psMemIncrRefCounter(sumMatrix);
+	solution = psMemIncrRefCounter(sumVector);
+# endif
+
+# if (0)
+        for (int i = 0; i < solution->n; i++) {
+	    psLogMsg("psModules.imcombine", PS_LOG_DETAIL, "Single solution %d: %lf +/- %lf\n", i, solution->data.F64[i], sqrt(fabs(invMatrix->data.F64[i][i])));
+        }
+# endif
+
+	// ensure we have a solution vector of the right size
+	kernels->solution1    = psVectorRecycle(kernels->solution1,    sumVector->n + 2, PS_TYPE_F64); // 1 for norm, 1 for bg
+	kernels->solution1err = psVectorRecycle(kernels->solution1err, sumVector->n + 2, PS_TYPE_F64); // 1 for norm, 1 for bg
+	psVectorInit(kernels->solution1, 0.0);
+	psVectorInit(kernels->solution1err, 0.0);
+
+	int numKernels = kernels->num;
+	int spatialOrder = kernels->spatialOrder;       // Order of spatial variation
+	int numPoly = PM_SUBTRACTION_POLYTERMS(spatialOrder); // Number of polynomial terms
+
+	for (int i = 0; i < numKernels * numPoly; i++) {
+	    kernels->solution1->data.F64[i] = solution->data.F64[i];
+	    kernels->solution1err->data.F64[i] = sqrt(invMatrix->data.F64[i][i]);
+	}
+
+	// Apply the normalisation and background separately
+	int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+	int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index for background
+	kernels->solution1->data.F64[normIndex] = stamps->normValue;
+	kernels->solution1->data.F64[bgIndex] = 0.0;
+
+        psFree(solution);
+        psFree(sumVector);
+        psFree(sumMatrix);
+        psFree(invMatrix);
+
+    } else {
+        // Dual convolution solution
+        // Accumulate the least-squares matrices and vectors.  These are generated for the
+	// kernel elements, excluding the background and normalization.
+        psImage *sumMatrix = psImageAlloc(numParams, numParams, PS_TYPE_F64);
+        psVector *sumVector = psVectorAlloc(numParams, PS_TYPE_F64);
+        psImageInit(sumMatrix, 0.0);
+        psVectorInit(sumVector, 0.0);
+
+        int numStamps = 0;	   // Number of good stamps
+	double normSquare1 = 0.0; // Sum of (I_1)^2 over stamps
+	double normSquare2 = 0.0; // Sum of (I_2)^2 over stamps
+        for (int i = 0; i < stamps->num; i++) {
+            pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+            if (stamp->status == PM_SUBTRACTION_STAMP_USED) {
+                (void)psBinaryOp(sumMatrix, sumMatrix, "+", stamp->matrix);
+                (void)psBinaryOp(sumVector, sumVector, "+", stamp->vector);
+
+		normSquare1 += stamp->normSquare1;
+		normSquare2 += stamp->normSquare2;
+
+                pmSubtractionStampPrint(ds9, stamp->x, stamp->y, stamps->footprint, "green");
+                numStamps++;
+            } else if (stamp->status == PM_SUBTRACTION_STAMP_REJECTED) {
+                pmSubtractionStampPrint(ds9, stamp->x, stamp->y, stamps->footprint, "red");
+            }
+        }
+
+	pmSubtractionVisualPlotLeastSquaresResid(stamps, sumMatrix, numStamps);
+
+#if 0
+	psImage *save = psImageCopy(NULL, sumMatrix, PS_TYPE_F32);
+        psFitsWriteImageSimple ("sumMatrix.fits", save, NULL);
+        psVectorWriteFile("sumVector.dat", sumVector);
+	psFree (save);
+#endif
+
+	if (PENALTY) {
+	    calculatePenalty(sumMatrix, sumVector, kernels, normSquare1, normSquare2);
+	}
+
+	// XXX TEST : print the matrix & vector
+	if (0) {
+	    for (int iy = 0; iy < sumMatrix->numRows; iy++) {
+		for (int ix = 0; ix < sumMatrix->numCols; ix++) {
+		    fprintf (stderr, "%e  ", sumMatrix->data.F64[iy][ix]);
+		}
+		fprintf (stderr, " : %e\n", sumVector->data.F64[iy]);
+	    }
+	}
+
+	psImage *invMatrix = NULL;
+        psVector *solution = NULL;                       // Solution to equation!
+        solution = psVectorAlloc(numParams, PS_TYPE_F64);
+        psVectorInit(solution, 0);
+
+	// DUAL solution
+# if (1)
+	solution = psMatrixSolveSVD(solution, sumMatrix, sumVector, 1e-10);
+	invMatrix = psMatrixInvert(NULL, sumMatrix, NULL);
+# endif
+# if (0)
+	psMatrixLUSolve(sumMatrix, sumVector);
+	solution = psMemIncrRefCounter(sumVector);
+	invMatrix = psMemIncrRefCounter(sumMatrix);
+# endif
+
+#if (0)
+        for (int i = 0; i < solution->n; i++) {
+            fprintf(stderr, "Dual solution %d: %lf +/- %lf\n", i, solution->data.F64[i], sqrt(invMatrix->data.F64[i][i]));
+        }
+#endif
+
+	// XXX TEST: manually set the coeffs to a desired solution
+	// solution->data.F64[0] = +1.826;
+	// solution->data.F64[1] = -0.115;
+	// solution->data.F64[2] =  0.0;
+	// solution->data.F64[3] =  0.0;
+
+	// ensure we have solution vectors of the right size
+	kernels->solution1    = psVectorRecycle(kernels->solution1,    numSolution1 + 2, PS_TYPE_F64); // 1 for norm, 1 for bg
+	kernels->solution1err = psVectorRecycle(kernels->solution1err, numSolution1 + 2, PS_TYPE_F64); // 1 for norm, 1 for bg
+	kernels->solution2    = psVectorRecycle(kernels->solution2,    numSolution2, 	 PS_TYPE_F64); // 1 for norm, 1 for bg
+	kernels->solution2err = psVectorRecycle(kernels->solution2err, numSolution2, 	 PS_TYPE_F64); // 1 for norm, 1 for bg
+
+	psVectorInit(kernels->solution1, 0.0);
+	psVectorInit(kernels->solution1err, 0.0);
+	psVectorInit(kernels->solution2, 0.0);
+	psVectorInit(kernels->solution2err, 0.0);
+
+	// for DUAL convolution analysis, we apply the normalization to I1 as follows:
+	// I1c = norm I1 + \Sum_i a_i norm I1 \cross k_i
+	// I2c =      I2 + \Sum_i b_i      I2 \cross k_i
+
+	// We absorb the normalization into a_i after the analysis is complete to be consistent
+	// with the SINGLE definitions of the convolutions
+
+	int numKernels = kernels->num;
+	for (int i = 0; i < numKernels * numSpatial; i++) {
+	    // we solve for coefficients 
+	    kernels->solution1->data.F64[i] = solution->data.F64[i] * stamps->normValue;
+	    kernels->solution2->data.F64[i] = solution->data.F64[i + numSolution1];
+
+	    kernels->solution1err->data.F64[i] = sqrt(invMatrix->data.F64[i][i]) * stamps->normValue;
+	    int i2 = i + numSolution1;
+	    kernels->solution2err->data.F64[i] = sqrt(invMatrix->data.F64[i2][i2]);
+	}
+
+	// Apply the normalisation and background separately
+	int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+	int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index for background
+	kernels->solution1->data.F64[normIndex] = stamps->normValue;
+	kernels->solution1->data.F64[bgIndex] = 0.0;
+
+        psFree(solution);
+        psFree(sumVector);
+        psFree(sumMatrix);
+        psFree(invMatrix);
+    }
+
+    if (ds9) {
+        fclose(ds9);
+    }
+
+    if (psTraceGetLevel("psModules.imcombine") >= 7) {
+        for (int i = 0; i < kernels->solution1->n; i++) {
+            psTrace("psModules.imcombine", 7, "Solution 1 %d: %f +/- %f\n", i, kernels->solution1->data.F64[i], kernels->solution1err->data.F64[i]);
+        }
+        if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+            for (int i = 0; i < kernels->solution2->n; i++) {
+                psTrace("psModules.imcombine", 7, "Solution 2 %d: %f +/- %f\n", i, kernels->solution2->data.F64[i], kernels->solution2err->data.F64[i]);
+            }
+        }
+     }
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Solve equation: %f sec", psTimerClear("pmSubtractionSolveEquation"));
+
+    // pmSubtractionVisualPlotLeastSquares((pmSubtractionStampList *) stamps); //casting away const
+    return true;
+}
+
+// measure some useful stats on the stamp residuals:
+// fResSigma : the residual stdev / total flux
+// fResOuter : the residual fabs / total flux for R > 2 pix
+// fResTotal : the residual fabs / total flux for R > 0 pix
+bool pmSubtractionResidualStats(psVector *fResSigma, psVector *fResOuter, psVector *fResTotal, psKernel *target, psKernel *source, psKernel *residual, double norm, int footprint) {
+
+    float sum = 0.0;
+    float peak = 0.0;
+    for (int y = - footprint; y <= footprint; y++) {
+        for (int x = - footprint; x <= footprint; x++) {
+            sum += 0.5*(target->kernel[y][x] + source->kernel[y][x] * norm);
+            peak = PS_MAX(peak, 0.5*(target->kernel[y][x] + source->kernel[y][x] * norm));
+        }
+    }
+
+    // init counters
+    int npix = 0;
+    float dflux1 = 0.0;
+    float dflux2 = 0.0;
+    float dOuter = 0.0;
+    float dTotal = 0.0;
+
+    for (int y = - footprint; y <= footprint; y++) {
+        for (int x = - footprint; x <= footprint; x++) {
+            dflux1 += residual->kernel[y][x];
+            dflux2 += PS_SQR(residual->kernel[y][x]);
+            dTotal += fabs(residual->kernel[y][x]);
+	    if (hypot(x,y) > 2.0) {
+	      dOuter += fabs(residual->kernel[y][x]);
+	    }
+            npix ++;
+        }
+    }
+    float sigma = sqrt(dflux2 / npix - PS_SQR(dflux1/npix));
+    if (!isfinite(sum))  return false;
+    if (!isfinite(peak)) return false;
+    if (!isfinite(dOuter)) return false;
+    if (!isfinite(dTotal)) return false;
+
+    // fprintf (stderr, "sum: %f, peak: %f, sigma: %f, fsigma: %f, fmax: %f, fmin: %f\n", sum, peak, sigma, sigma/sum, dOuter/sum, dTotal/sum);
+    psVectorAppend(fResSigma, sigma/sum);
+    psVectorAppend(fResOuter, dOuter/sum);
+    psVectorAppend(fResTotal, dTotal/sum);
+    return true;
+}
+
+// given the convolved image(s) and the residual image, calculate the second moment(s) and the chisq
+bool pmSubtractionChisqStats(psVector *fluxesVector, psVector *chisqDVector, psVector *chisqRVector, psVector *momentVector, psVector *stampMask, psKernel *convolved1, psKernel *convolved2, psKernel *difference, psKernel *residual, psKernel *weight, psKernel *window) {
+
+# ifndef USE_WEIGHT
+    psAssert(weight == NULL, "impossible!");
+# endif
+# ifndef USE_WINDOW
+    psAssert(window == NULL, "impossible!");
+# endif
+
+    int npix = 0;
+    float chisqR = 0;
+    float chisqD = 0;
+
+    // get the chisq
+    for (int y = residual->yMin; y <= residual->yMax; y++) {
+        for (int x = residual->xMin; x <= residual->xMax; x++) {
+            float valueR = PS_SQR(residual->kernel[y][x]);
+	    if (weight) {
+	     	valueR *= weight->kernel[y][x];
+	    }
+	    // XXX NOTE: do NOT apply the window to the chisq portions of the calculation (that would bias the chisq)
+            chisqR += valueR;
+
+            float valueD = PS_SQR(difference->kernel[y][x]);
+	    if (weight) {
+	     	valueD *= weight->kernel[y][x];
+	    }
+            chisqD += valueD;
+	    npix ++;
+        }
+    }
+    psVectorAppend(chisqRVector, chisqR / npix);
+    psVectorAppend(chisqDVector, chisqD / npix);
+
+    float value1 = 0;
+    float value2 = 0;
+    float flux2 = 0;
+    float fluxX = 0;
+    float fluxY = 0;
+    float fluxX2 = 0;
+    float fluxY2 = 0;
+
+    float fluxC1 = 0;
+    float fluxC2 = 0;
+
+    float moment = 0;
+
+    // get the moments from convolved1
+    if (convolved1) {
+	for (int y = residual->yMin; y <= residual->yMax; y++) {
+	    for (int x = residual->xMin; x <= residual->xMax; x++) {
+		value1  = convolved1->kernel[y][x];
+		value2  = PS_SQR(value1);
+
+		if (window) {
+		    value1 *= window->kernel[y][x];
+		    value2 *= window->kernel[y][x];
+		}
+
+		fluxC1 += value1;
+		flux2  += value2;
+		fluxX  += x * value2;
+		fluxY  += y * value2;
+		// fluxX2 += PS_SQR(x) * value2;
+		// fluxY2 += PS_SQR(y) * value2;
+		fluxX2 += PS_SQR(x) * value1;
+		fluxY2 += PS_SQR(y) * value1;
+	    }
+	}
+	// float Mx = fluxX / flux2;
+	// float My = fluxY / flux2;
+	// float Mxx = fluxX2 / flux2;
+	// float Myy = fluxY2 / flux2;
+	float Mxx = fluxX2 / fluxC1;
+	float Myy = fluxY2 / fluxC1;
+
+	// fprintf (stderr, "conv1, flux2: %f, Mx: %f, My: %f, Mxx: %f, Myy: %f, chisq: %f, npix: %d\n", flux2, Mx, My, Mxx, Myy, chisq, npix);
+	moment += Mxx + Myy;
+    }
+
+    // get the moments from convolved1
+    if (convolved2) {
+	for (int y = residual->yMin; y <= residual->yMax; y++) {
+	    for (int x = residual->xMin; x <= residual->xMax; x++) {
+		value1  = convolved2->kernel[y][x];
+		value2  = PS_SQR(value1);
+
+		// XXX NOTE: do NOT apply the weight to the moments calculation
+		if (false && weight) {
+		    value2 *= weight->kernel[y][x];
+		}
+		if (window) {
+		    value1 *= window->kernel[y][x];
+		    value2 *= window->kernel[y][x];
+		}
+
+		fluxC2 += value1;
+		flux2  += value2;
+		fluxX  += x * value2;
+		fluxY  += y * value2;
+		// fluxX2 += PS_SQR(x) * value2;
+		// fluxY2 += PS_SQR(y) * value2;
+		fluxX2 += PS_SQR(x) * value1;
+		fluxY2 += PS_SQR(y) * value1;
+	    }
+	}
+	// float Mx = fluxX / flux2;
+	// float My = fluxY / flux2;
+	// float Mxx = fluxX2 / flux2;
+	// float Myy = fluxY2 / flux2;
+	float Mxx = fluxX2 / fluxC2;
+	float Myy = fluxY2 / fluxC2;
+
+	// fprintf (stderr, "conv2, flux2: %f, Mx: %f, My: %f, Mxx: %f, Myy: %f, chisq: %f, npix: %d\n", flux2, Mx, My, Mxx, Myy, chisq, npix);
+	moment += Mxx + Myy;
+    }
+
+    float flux = fluxC1 + fluxC2;
+    
+    if (convolved1 && convolved2) {
+	moment *= 0.5;
+	flux *= 0.5;
+    }
+    psVectorAppend(momentVector, moment);
+    psVectorAppend(fluxesVector, flux);
+
+    // check that the last appended values are ok:
+    int Nelem = fluxesVector->n - 1;
+    bool valid = true;
+    valid &= isfinite(chisqRVector->data.F32[Nelem]);
+    valid &= isfinite(fluxesVector->data.F32[Nelem]);
+    valid &= isfinite(momentVector->data.F32[Nelem]);
+    if (valid) {
+      psVectorAppend(stampMask, 0);
+    } else {
+      psVectorAppend(stampMask, 0x02);
+    }
+    return true;
+}
+
+bool pmSubtractionCalculateChisqAndMoments(pmSubtractionQuality **bestMatch, 
+					   pmSubtractionStampList *stamps,
+					   pmSubtractionKernels *kernels)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, NULL);
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NULL);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NULL);
+
+    psTimerStart("pmSubtractionCalculateChisqAndMoments");
+
+    // XXX need to save these somewhere
+    psVector *fluxes = psVectorAllocEmpty(stamps->num, PS_TYPE_F32);
+    psVector *chisqD = psVectorAllocEmpty(stamps->num, PS_TYPE_F32);
+    psVector *chisqR = psVectorAllocEmpty(stamps->num, PS_TYPE_F32);
+    psVector *moments = psVectorAllocEmpty(stamps->num, PS_TYPE_F32);
+    psVector *stampMask = psVectorAllocEmpty(stamps->num, PS_TYPE_VECTOR_MASK);
+
+    int footprint = stamps->footprint; // Half-size of stamps
+    int numKernels = kernels->num;      // Number of kernels
+
+    psImage *polyValues = NULL;         // Polynomial values
+
+    // storage for the image (convolved2 is not used in SINGLE mode)
+    psKernel *residual = psKernelAlloc(-footprint, footprint, -footprint, footprint); // Residual image
+    psKernel *difference = psKernelAlloc(-footprint, footprint, -footprint, footprint); // Residual image
+    psKernel *convolved1 = psKernelAlloc(-footprint, footprint, -footprint, footprint); // Residual image
+    psKernel *convolved2 = psKernelAlloc(-footprint, footprint, -footprint, footprint); // Residual image
+
+    int nGood = 0;
+    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) {
+	    // mark this stamp as unused (note that we have to append NANs to the other vectors to keep the lengths in sync)
+	    psVectorAppend(moments, NAN);
+	    psVectorAppend(fluxes, NAN);
+	    psVectorAppend(chisqD, NAN);
+	    psVectorAppend(chisqR, NAN);
+	    psVectorAppend(stampMask, 0x01);
+            continue;
+        }
+	nGood ++;
+
+        // Calculate coefficients of the kernel basis functions
+        polyValues = p_pmSubtractionPolynomial(polyValues, kernels->spatialOrder, stamp->xNorm, stamp->yNorm);
+        double norm = p_pmSubtractionSolutionNorm(kernels); // Normalisation
+        double background = p_pmSubtractionSolutionBackground(kernels, polyValues);// Difference in background
+
+        // Calculate residuals
+        psImageInit(residual->image, 0.0);
+        psImageInit(difference->image, 0.0);
+
+	psKernel *weight = NULL;
+	psKernel *window = NULL;
+    
+#ifdef USE_WEIGHT
+    weight = stamp->weight;
+#endif
+#ifdef USE_WINDOW
+    window = stamps->window;
+#endif
+
+        if (kernels->mode != PM_SUBTRACTION_MODE_DUAL) {
+
+	    // the single-direction psf match code attempts to find the kernel such that:
+	    // source * kernel = target.  we need to assign 'source' and 'target' correctly
+	    // depending on which of image1 or image2 we asked to be convolved.
+
+            psKernel *target;           // Target postage stamp (convolve source to match the target)
+            psKernel *source;           // Source postage stamp (convolve source to match the target)
+            psArray *convolutions;      // Convolution postage stamps for each kernel basis function
+
+	    // init the accumulation image
+	    psImageInit(convolved1->image, 0.0);
+
+            switch (kernels->mode) {
+              case PM_SUBTRACTION_MODE_1:
+                target = stamp->image2;
+                source = stamp->image1;
+                convolutions = stamp->convolutions1;
+                break;
+              case PM_SUBTRACTION_MODE_2:
+                target = stamp->image1;
+                source = stamp->image2;
+                convolutions = stamp->convolutions2;
+                break;
+              default:
+                psAbort("Unsupported subtraction mode: %x", kernels->mode);
+            }
+
+	    // generate the convolved source image (sum over kernels)
+            for (int j = 0; j < numKernels; j++) {
+                psKernel *convolution = convolutions->data[j]; // Convolution
+                double coefficient = p_pmSubtractionSolutionCoeff(kernels, polyValues, j, false); // Coefficient
+                for (int y = - footprint; y <= footprint; y++) {
+                    for (int x = - footprint; x <= footprint; x++) {
+                        convolved1->kernel[y][x] += convolution->kernel[y][x] * coefficient;
+                    }
+                }
+            }
+
+	    // Generate the difference, residual, and convolved source images.  Note the we
+	    // accumulate the convolution of (A-B), so we need to replace it to generate the
+	    // images of the convolved source image.
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    difference->kernel[y][x] = target->kernel[y][x] - source->kernel[y][x] * norm - background;
+                    residual->kernel[y][x] = difference->kernel[y][x] - convolved1->kernel[y][x];
+		    convolved1->kernel[y][x] += source->kernel[y][x] * norm;
+                }
+            }
+
+	    // XXX if we want to have a weight and window, we'll need to pass through to here
+            pmSubtractionChisqStats(fluxes, chisqD, chisqR, moments, stampMask, convolved1, NULL, difference, residual, weight, window);
+
+        } else {
+
+            // Dual convolution
+            psArray *convolutions1 = stamp->convolutions1; // Convolutions of the first image
+            psArray *convolutions2 = stamp->convolutions2; // Convolutions of the second image
+            psKernel *image1 = stamp->image1; // The first image
+            psKernel *image2 = stamp->image2; // The second image
+
+	    // init the accumulation images
+	    psImageInit(convolved1->image, 0.0);
+	    psImageInit(convolved2->image, 0.0);
+
+            for (int j = 0; j < numKernels; j++) {
+                psKernel *conv1 = convolutions1->data[j]; // Convolution of first image
+                psKernel *conv2 = convolutions2->data[j]; // Convolution of second image
+                double coeff1 = p_pmSubtractionSolutionCoeff(kernels, polyValues, j, false); // Coefficient 1
+                double coeff2 = p_pmSubtractionSolutionCoeff(kernels, polyValues, j, true); // Coefficient 2
+
+                for (int y = - footprint; y <= footprint; y++) {
+                    for (int x = - footprint; x <= footprint; x++) {
+			// NOTE sign for coeff2
+                        convolved1->kernel[y][x] += +conv1->kernel[y][x] * coeff1;
+                        convolved2->kernel[y][x] += -conv2->kernel[y][x] * coeff2;
+                    }
+                }
+            }
+
+	    // Generate the difference, residual, and convolved source images.  Note the we
+	    // accumulate the convolutions of (A-B), so we need to replace (A or B) to generate
+	    // the images of the convolved source images.
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    difference->kernel[y][x] = image2->kernel[y][x] - image1->kernel[y][x] * norm - background;
+                    residual->kernel[y][x] = difference->kernel[y][x] + convolved2->kernel[y][x] - convolved1->kernel[y][x];
+		    convolved1->kernel[y][x] += image1->kernel[y][x] * norm;
+		    convolved2->kernel[y][x] += image2->kernel[y][x];
+                }
+            }
+
+	    if (0) {
+		psFitsWriteImageSimple("conv1.fits", convolved1->image, NULL);
+		psFitsWriteImageSimple("conv2.fits", convolved2->image, NULL);
+		psFitsWriteImageSimple("resid.fits", residual->image,   NULL);
+		pmVisualAskUser(NULL);
+	    } 
+
+            pmSubtractionChisqStats(fluxes, chisqD, chisqR, moments, stampMask, convolved1, convolved2, difference, residual, weight, window);
+        }
+    }
+
+    // find the mean chisq and mean moment
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+    psVectorStats (stats, chisqD, NULL, stampMask, 0xff);
+    float chisqDValue = stats->sampleMean;
+
+    psStatsInit(stats);
+    psVectorStats (stats, chisqR, NULL, stampMask, 0xff);
+    float chisqRValue = stats->sampleMean;
+
+    psStatsInit(stats);
+    psVectorStats (stats, moments, NULL, stampMask, 0xff);
+    float momentValue = stats->sampleMean;
+
+    double sumKernel1 = 0.0, sumKernel2 = 0.0; // Sum of the kernel
+
+    // calculate the variance contribution from this smoothing kernel
+    psKernel *modelKernel = pmSubtractionKernel(kernels, 0.0, 0.0, false);
+    for (int y = modelKernel->yMin; y <= modelKernel->yMax; y++) {
+        for (int x = modelKernel->xMin; x <= modelKernel->xMax; x++) {
+            if (!isfinite(modelKernel->kernel[y][x])) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Non-finite covariance matrix element in kernel at %d,%d", x, y);
+                return NULL;
+            }
+            sumKernel1 += PS_SQR(modelKernel->kernel[y][x]);
+        }
+    }
+    psFree (modelKernel);
+
+    if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+	psKernel *modelKernel = pmSubtractionKernel(kernels, 0.0, 0.0, true);
+	for (int y = modelKernel->yMin; y <= modelKernel->yMax; y++) {
+	    for (int x = modelKernel->xMin; x <= modelKernel->xMax; x++) {
+		if (!isfinite(modelKernel->kernel[y][x])) {
+		    psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Non-finite covariance matrix element in kernel at %d,%d", x, y);
+		    return NULL;
+		}
+		sumKernel2 += PS_SQR(modelKernel->kernel[y][x]);
+	    }
+	}
+	psFree (modelKernel);
+    } else {
+	sumKernel2 = 1.0;
+    }
+
+    // if we modify the chisq value by the (sumKernel1 + sumKernel2), we account for the
+    // smoothing coming from larger kernels adding additional spatial fit terms should be
+    // penalized by increasing the score somewhat.  the 0.01 value is not well-chosen.
+    float orderFactor = 0.01 * kernels->spatialOrder;
+    float score = 2.0 * chisqRValue / (sumKernel1 + sumKernel2) + orderFactor;
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "chisq: %6.3f, chisqD: %6.3f, moment: %6.3f, sumKernel_1: %6.3f, sumKernel_2, score: %6.3f: %6.3f\n", chisqRValue, chisqDValue, momentValue, sumKernel1, sumKernel2, score);
+
+    // save this result if it is the first or the best (skip if bestMatch is NULL)
+    if (bestMatch) {
+	pmSubtractionQuality *match = *bestMatch;
+	bool keep = false;
+	if (match == NULL) {
+	    *bestMatch = match = pmSubtractionQualityAlloc();
+	    keep = true;
+	} else {
+	    if (score < match->score) {
+		psFree(match->fluxes);
+		psFree(match->chisq);
+		psFree(match->moments);
+		psFree(match->stampMask);
+		keep = true;
+	    }
+	}
+	if (keep) {
+	    psLogMsg("psModules.imcombine", PS_LOG_INFO, "keeping order: %d, mode: %d, score: %f\n", kernels->spatialOrder, kernels->mode, score);
+	    match->score        = score;
+	    match->spatialOrder = kernels->spatialOrder;
+	    match->mode         = kernels->mode;
+	    match->nGood        = nGood;
+	    match->fluxes       = psMemIncrRefCounter(fluxes);
+	    match->chisq        = psMemIncrRefCounter(chisqR);
+	    match->moments      = psMemIncrRefCounter(moments);
+	    match->stampMask    = psMemIncrRefCounter(stampMask);
+	}	    
+    }
+
+    pmSubtractionVisualPlotChisqAndMoments(fluxes, chisqR, moments);
+
+    psFree(stats);
+    psFree(chisqR);
+    psFree(chisqD);
+    psFree(fluxes);
+    psFree(moments);
+    psFree(stampMask);
+
+    psFree(residual);
+    psFree(difference);
+    psFree(convolved1);
+    psFree(convolved2);
+    psFree(polyValues);
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Calculate Chisq and Moments: %f sec", psTimerClear("pmSubtractionCalculateChisqAndMoments"));
+
+    return true;
+}
+
+// XXX for now, let's not use this, and let's instead just use values from pmSubtractionCalculateChisqAndMoments
+psVector *pmSubtractionCalculateDeviations(pmSubtractionStampList *stamps, pmSubtractionKernels *kernels)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, NULL);
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NULL);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NULL);
+
+    psTimerStart("pmSubtractionCalculateDeviations");
+
+    psVector *deviations = psVectorAlloc(stamps->num, PS_TYPE_F32); // Mean deviation for stamps
+    int footprint = stamps->footprint; // Half-size of stamps
+    long numPixels = PS_SQR(2 * footprint + 1); // Number of pixels in footprint
+    double devNorm = 1.0 / (double)numPixels; // Normalisation for deviations
+    int numKernels = kernels->num;      // Number of kernels
+
+    psImage *polyValues = NULL;         // Polynomial values
+    psKernel *residual = psKernelAlloc(-footprint, footprint, -footprint, footprint); // Residual image
+
+    psVector *fResSigma = psVectorAllocEmpty(stamps->num, PS_TYPE_F32);
+    psVector *fResOuter = psVectorAllocEmpty(stamps->num, PS_TYPE_F32);
+    psVector *fResTotal = psVectorAllocEmpty(stamps->num, PS_TYPE_F32);
+
+    // we want to save the residual images for the 9 brightest stamps.
+    // identify the 9 brightest stamps
+    psVector *keepStamps  = psVectorAlloc(stamps->num, PS_TYPE_S32);
+    psVectorInit (keepStamps, 0);
+    {
+        psVector *flux  = psVectorAlloc(stamps->num, PS_TYPE_F32);
+        psVectorInit (flux, 0.0);
+
+        for (int i = 0; i < stamps->num; i++) {
+            pmSubtractionStamp *stamp = stamps->stamps->data[i];
+            if (!isfinite(stamp->flux)) continue;
+            flux->data.F32[i] = stamp->flux;
+        }
+
+        psVector *index = psVectorSortIndex(NULL, flux);
+        for (int i = 0; (i < stamps->num) && (i < 9); i++) {
+            int n = stamps->num - i - 1;
+            keepStamps->data.S32[index->data.S32[n]] = 1;
+        }
+        psFree (flux);
+        psFree (index);
+
+        // this function is called multiple times in the iteration, but
+        // we only know after the interation is done if we will try again.
+        // therefore we must save the sample each time, and blow away the old one
+        // if it exists.
+        psFree (kernels->sampleStamps);
+        kernels->sampleStamps = psArrayAllocEmpty(9);
+    }
+
+    psString log = psStringCopy("Deviations:\n");               // Log message with deviations
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // The stamp of interest
+        if (stamp->status != PM_SUBTRACTION_STAMP_USED) {
+            deviations->data.F32[i] = NAN;
+            continue;
+        }
+
+        // Calculate coefficients of the kernel basis functions
+        polyValues = p_pmSubtractionPolynomial(polyValues, kernels->spatialOrder, stamp->xNorm, stamp->yNorm);
+        double norm = p_pmSubtractionSolutionNorm(kernels); // Normalisation
+        double background = p_pmSubtractionSolutionBackground(kernels, polyValues);// Difference in background
+
+        // Calculate residuals
+        psKernel *weight = stamp->weight; // Weight postage stamp
+        psImageInit(residual->image, 0.0);
+        if (kernels->mode != PM_SUBTRACTION_MODE_DUAL) {
+            psKernel *target;           // Target postage stamp
+            psKernel *source;           // Source postage stamp
+            psArray *convolutions;      // Convolution postage stamps for each kernel basis function
+            switch (kernels->mode) {
+              case PM_SUBTRACTION_MODE_1:
+                target = stamp->image2;
+                source = stamp->image1;
+                convolutions = stamp->convolutions1;
+
+                // Having convolved image1 and changed its normalisation, we need to renormalise the residual
+                // so that it is on the scale of image1.
+                psImage *image = pmSubtractionKernelImage(kernels, stamp->xNorm, stamp->yNorm,
+                                                          false); // Kernel image
+                if (!image) {
+                    psError(psErrorCodeLast(), false, "Unable to generate image of kernel.");
+                    return false;
+                }
+                double sumKernel = 0;   // Sum of kernel, for normalising residual
+                int size = kernels->size; // Half-size of kernel
+                int fullSize = 2 * size + 1; // Full size of kernel
+                for (int y = 0; y < fullSize; y++) {
+                    for (int x = 0; x < fullSize; x++) {
+                        sumKernel += image->data.F32[y][x];
+                    }
+                }
+                psFree(image);
+                devNorm = 1.0 / sumKernel / numPixels;
+                break;
+              case PM_SUBTRACTION_MODE_2:
+                target = stamp->image1;
+                source = stamp->image2;
+                convolutions = stamp->convolutions2;
+                break;
+              default:
+                psAbort("Unsupported subtraction mode: %x", kernels->mode);
+            }
+
+            for (int j = 0; j < numKernels; j++) {
+                psKernel *convolution = convolutions->data[j]; // Convolution
+                double coefficient = p_pmSubtractionSolutionCoeff(kernels, polyValues, j,
+                                                                  false); // Coefficient
+                for (int y = - footprint; y <= footprint; y++) {
+                    for (int x = - footprint; x <= footprint; x++) {
+                        residual->kernel[y][x] += convolution->kernel[y][x] * coefficient;
+                    }
+                }
+            }
+
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    residual->kernel[y][x] += background + source->kernel[y][x] * norm - target->kernel[y][x];
+                }
+            }
+
+            if (keepStamps->data.S32[i]) {
+                psImage *sample = psImageCopy(NULL, residual->image, PS_TYPE_F32);
+                psArrayAdd (kernels->sampleStamps, 9, sample);
+                psFree (sample);
+            }
+
+            pmSubtractionResidualStats(fResSigma, fResOuter, fResTotal, target, source, residual, norm, footprint);
+
+        } else {
+            // Dual convolution
+            psArray *convolutions1 = stamp->convolutions1; // Convolutions of the first image
+            psArray *convolutions2 = stamp->convolutions2; // Convolutions of the second image
+            psKernel *image1 = stamp->image1; // The first image
+            psKernel *image2 = stamp->image2; // The second image
+
+            for (int j = 0; j < numKernels; j++) {
+                psKernel *conv1 = convolutions1->data[j]; // Convolution of first image
+                psKernel *conv2 = convolutions2->data[j]; // Convolution of second image
+                double coeff1 = p_pmSubtractionSolutionCoeff(kernels, polyValues, j, false); // Coefficient 1
+                double coeff2 = p_pmSubtractionSolutionCoeff(kernels, polyValues, j, true); // Coefficient 2
+
+                for (int y = - footprint; y <= footprint; y++) {
+                    for (int x = - footprint; x <= footprint; x++) {
+                        residual->kernel[y][x] += conv2->kernel[y][x] * coeff2 + conv1->kernel[y][x] * coeff1;
+                    }
+                }
+            }
+
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    residual->kernel[y][x] += background + image1->kernel[y][x] * norm - image2->kernel[y][x];
+                }
+            }
+            if (keepStamps->data.S32[i]) {
+                psImage *sample = psImageCopy(NULL, residual->image, PS_TYPE_F32);
+                psArrayAdd (kernels->sampleStamps, 9, sample);
+                psFree (sample);
+            }
+
+            pmSubtractionResidualStats(fResSigma, fResOuter, fResTotal, image1, image2, residual, norm, footprint);
+        }
+
+	double flux = 0.0;
+        double deviation = 0.0;         // Sum of differences
+        for (int y = - footprint; y <= footprint; y++) {
+            for (int x = - footprint; x <= footprint; x++) {
+                double dev = PS_SQR(residual->kernel[y][x]) * weight->kernel[y][x];
+                deviation += dev;
+		flux += stamp->image1->kernel[y][x] + stamp->image2->kernel[y][x];
+            }
+        }
+        deviations->data.F32[i] = devNorm * deviation;
+        psTrace("psModules.imcombine", 5, "Deviation and Flux for stamp %d (%d,%d): %f %f\n",
+                i, (int)(stamp->x - 0.5), (int)(stamp->y - 0.5), deviations->data.F32[i], flux);
+        psStringAppend(&log, "Stamp %d (%d,%d): %f\n",
+                       i, (int)(stamp->x - 0.5), (int)(stamp->y - 0.5), deviations->data.F32[i]);
+        if (!isfinite(deviations->data.F32[i])) {
+            stamp->status = PM_SUBTRACTION_STAMP_REJECTED;
+            psTrace("psModules.imcombine", 5, "Rejecting stamp %d (%d,%d) because of non-finite deviation\n", i, (int)(stamp->x - 0.5), (int)(stamp->y - 0.5));
+            continue;
+        }
+    }
+
+    psFree(keepStamps);
+
+    psLogMsg("psModules.imcombine", PS_LOG_MINUTIA, "%s", log);
+    psFree(log);
+
+    // calculate and report the normalization and background for the image center
+    {
+        polyValues = p_pmSubtractionPolynomial(polyValues, kernels->spatialOrder, 0.0, 0.0);
+        double norm = p_pmSubtractionSolutionNorm(kernels); // Normalisation
+        double background = p_pmSubtractionSolutionBackground(kernels, polyValues);// Difference in background
+        psLogMsg("psModules.imcombine", PS_LOG_INFO, "normalization: %f, background: %f", norm, background);
+
+        psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+        psVectorStats (stats, fResSigma, NULL, NULL, 0);
+        kernels->fResSigmaMean  = stats->robustMedian;
+        kernels->fResSigmaStdev = stats->robustStdev;
+
+        psStatsInit (stats);
+        psVectorStats (stats, fResOuter, NULL, NULL, 0);
+        kernels->fResOuterMean  = stats->robustMedian;
+        kernels->fResOuterStdev = stats->robustStdev;
+
+        psStatsInit (stats);
+        psVectorStats (stats, fResTotal, NULL, NULL, 0);
+        kernels->fResTotalMean  = stats->robustMedian;
+        kernels->fResTotalStdev = stats->robustStdev;
+
+        // XXX save these values somewhere
+        psLogMsg("psModules.imcombine", PS_LOG_INFO, "fResSigma: %f +/- %f, fResOuter: %f +/- %f, fResTotal: %f +/- %f",
+                 kernels->fResSigmaMean, kernels->fResSigmaStdev,
+                 kernels->fResOuterMean, kernels->fResOuterStdev,
+                 kernels->fResTotalMean, kernels->fResTotalStdev);
+
+        psFree (fResSigma);
+        psFree (fResOuter);
+        psFree (fResTotal);
+        psFree (stats);
+    }
+
+    psFree(residual);
+    psFree(polyValues);
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Calculate Deviations: %f sec", psTimerClear("pmSubtractionCalculateDeviations"));
+
+    return deviations;
+}
+
+// I get confused by the index values between the image vs matrix usage:  In terms
+// of the elements of an image A(x,y) = A->data.F64[y][x] = A_x,y, a matrix
+// multiplication is: A_k,j * B_i,k = C_i,j
+
+bool psFitsWriteImageSimple (char *filename, psImage *image, psMetadata *header) {
+
+    psFits *fits = psFitsOpen(filename, "w");
+    psFitsWriteImage(fits, header, image, 0, NULL);
+    psFitsClose(fits);
+
+    return true;
+}
+
+bool psVectorWriteFile (char *filename, const psVector *vector) {
+
+    FILE *f = fopen (filename, "w");
+    int fd = fileno(f);
+    p_psVectorPrint (fd, vector, "unnamed");
+    fclose (f);
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionEquation.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionEquation.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionEquation.h	(revision 42651)
@@ -0,0 +1,87 @@
+#ifndef PM_SUBTRACTION_EQUATION_H
+#define PM_SUBTRACTION_EQUATION_H
+
+#include "pmSubtractionStamps.h"
+#include "pmSubtractionKernels.h"
+#include "pmSubtraction.h"
+
+/// Execute a thread job to calculate the least-squares equation for a stamp
+bool pmSubtractionCalculateEquationThread(psThreadJob *job ///< Job to execute
+    );
+
+/// Calculate the least-squares equation to match the image quality for a single stamp
+bool pmSubtractionCalculateEquationStamp(pmSubtractionStampList *stamps, ///< Stamps
+                                         pmSubtractionKernels *kernels, ///< Kernel parameters
+                                         int index ///< Index of stamp
+    );
+
+/// Calculate the least-squares equation to match the image quality
+bool pmSubtractionCalculateEquation(pmSubtractionStampList *stamps, ///< Stamps
+                                    pmSubtractionKernels *kernels ///< Kernel parameters
+    );
+
+/// Solve the least-squares equation to match the image quality
+bool pmSubtractionSolveEquation(pmSubtractionKernels *kernels, ///< Kernel parameters
+                                const pmSubtractionStampList *stamps ///< Stamps
+    );
+
+/// Calculate deviations
+psVector *pmSubtractionCalculateDeviations(pmSubtractionStampList *stamps, ///< Stamps
+                                           pmSubtractionKernels *kernels ///< Kernel parameters
+    );
+
+/// Calculate the value of a polynomial, specified by coefficients and polynomial values
+double p_pmSubtractionCalculatePolynomial(const psVector *coeff, ///< Coefficients
+                                          const psImage *polyValues, ///< Polynomial values
+                                          int order, ///< Order of polynomials
+                                          int index, ///< Index at which to begin
+                                          int step ///< Step between subsequent indices
+    );
+
+/// Return the specified coefficient in the solution
+double p_pmSubtractionSolutionCoeff(const pmSubtractionKernels *kernels, ///< Kernel parameters
+                                    const psImage *polyValues, ///< Polynomial values
+                                    int index, ///< Coefficient index to calculate
+                                    bool wantDual ///< Calculate the coefficient for the dual solution?
+    );
+
+/// Return the normalisation in the solution
+double p_pmSubtractionSolutionNorm(const pmSubtractionKernels *kernels ///< Kernel parameters
+    );
+
+/// Return the background (difference) in the solution
+double p_pmSubtractionSolutionBackground(const pmSubtractionKernels *kernels, ///< Kernel parameters
+                                         const psImage *polyValues ///< Polynomial values
+    );
+
+bool pmSubtractionCalculateNormalization(
+  pmSubtractionStampList *stamps,
+  const pmSubtractionMode mode);
+
+bool pmSubtractionCalculateNormalizationStamp(
+    pmSubtractionStamp *stamp,		// stamp on which to save normalization)
+    const psKernel *input,		// Input image (target)
+    const psKernel *reference,		// Reference image (convolution source)
+    int footprint,			// (Half-)Size of stamp
+    int normWindow1,			// Window (half-)size for normalisation measurement
+    int normWindow2			// Window (half-)size for normalisation measurement
+  );
+
+bool pmSubtractionCalculateMoments(
+    pmSubtractionKernels *kernels, // Kernels
+    pmSubtractionStampList *stamps);
+
+bool pmSubtractionCalculateMomentsStamp(
+    pmSubtractionKernels *kernels, // Kernels
+    pmSubtractionStamp *stamp,		// stamp on which to save normalization)
+    int footprint,			// (Half-)Size of stamp
+    int normWindow1,			// Window (half-)size for normalisation measurement
+    int normWindow2			// Window (half-)size for normalisation measurement
+    );
+
+bool pmSubtractionCalculateMomentsKernel(double *Mxx, double *Myy, psKernel *image, int footprint, int window);
+
+bool pmSubtractionChisqStats(psVector *fluxesVector, psVector *chisqDVector, psVector *chisqRVector, psVector *momentVector, psVector *stampMask, psKernel *convolved1, psKernel *convolved2, psKernel *difference, psKernel *residual, psKernel *weight, psKernel *window);
+
+bool pmSubtractionCalculateChisqAndMoments(pmSubtractionQuality **bestMatch, pmSubtractionStampList *stamps, pmSubtractionKernels *kernels);
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionEquation.v0.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionEquation.v0.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionEquation.v0.c	(revision 42651)
@@ -0,0 +1,2239 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmErrorCodes.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionTypes.h"
+#include "pmSubtractionKernels.h"
+#include "pmSubtractionStamps.h"
+#include "pmSubtractionThreads.h"
+
+#include "pmSubtractionEquation.h"
+#include "pmSubtractionVisual.h"
+
+//#define TESTING                         // TESTING output for debugging; may not work with threads!
+
+//#define USE_WEIGHT                      // Include weight (1/variance) in equation?
+//#define USE_WINDOW                      // Include weight (1/variance) in equation?
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Private (file-static) functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Calculate the least-squares matrix and vector
+static bool calculateMatrixVector(psImage *matrix, // Least-squares matrix, updated
+                                  psVector *vector, // Least-squares vector, updated
+                                  double *norm,     // Normalisation, updated
+                                  const psKernel *input, // Input image (target)
+                                  const psKernel *reference, // Reference image (convolution source)
+                                  const psKernel *weight,  // Weight image
+                                  const psKernel *window,  // Window image
+                                  const psArray *convolutions,         // Convolutions for each kernel
+                                  const pmSubtractionKernels *kernels, // Kernels
+                                  const psImage *polyValues, // Spatial polynomial values
+                                  int footprint, // (Half-)Size of stamp
+                                  int normWindow1, // Window (half-)size for normalisation measurement
+                                  int normWindow2, // Window (half-)size for normalisation measurement
+                                  const pmSubtractionEquationCalculationMode mode
+                                  )
+{
+    // (I - R * sum_i a_i k_i - g) (R * k_j) = 0
+    // I C_j = sum_i C_i C_j
+
+    // Background: C_i = 1.0
+    // Normalisation: C_i = R
+
+    int numKernels = kernels->num;                      // Number of kernels
+    int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+    int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index in matrix for background
+    int spatialOrder = kernels->spatialOrder;       // Order of spatial variation
+    int numPoly = PM_SUBTRACTION_POLYTERMS(spatialOrder); // Number of polynomial terms
+    double poly[numPoly];                                 // Polynomial terms
+    double poly2[numPoly][numPoly];                       // Polynomial-polynomial values
+
+    // Evaluate polynomial-polynomial terms
+    // XXX we can skip this if we are not calculating kernel coeffs
+    for (int iyOrder = 0, iIndex = 0; iyOrder <= spatialOrder; iyOrder++) {
+        for (int ixOrder = 0; ixOrder <= spatialOrder - iyOrder; ixOrder++, iIndex++) {
+            double iPoly = polyValues->data.F64[iyOrder][ixOrder]; // Value of polynomial
+            poly[iIndex] = iPoly;
+            for (int jyOrder = 0, jIndex = 0; jyOrder <= spatialOrder; jyOrder++) {
+                for (int jxOrder = 0; jxOrder <= spatialOrder - jyOrder; jxOrder++, jIndex++) {
+                    double jPoly = polyValues->data.F64[jyOrder][jxOrder];
+                    poly2[iIndex][jIndex] = iPoly * jPoly;
+                }
+            }
+        }
+    }
+
+    // initialize the matrix and vector for NOP on all coeffs.  we only fill in the coeffs we
+    // choose to calculate
+    psImageInit(matrix, 0.0);
+    psVectorInit(vector, 1.0);
+    for (int i = 0; i < matrix->numCols; i++) {
+        matrix->data.F64[i][i] = 1.0;
+    }
+
+    // the order of the elements in the matrix and vector is:
+    // [kernel 0, x^0 y^0][kernel 1 x^0 y^0]...[kernel N, x^0 y^0]
+    // [kernel 0, x^1 y^0][kernel 1 x^1 y^0]...[kernel N, x^1 y^0]
+    // [kernel 0, x^n y^m][kernel 1 x^n y^m]...[kernel N, x^n y^m]
+    // normalization
+    // bg 0, bg 1, bg 2 (only 0 is currently used?)
+
+    for (int i = 0; i < numKernels; i++) {
+        psKernel *iConv = convolutions->data[i]; // Convolution for index i
+        for (int j = i; j < numKernels; j++) {
+            psKernel *jConv = convolutions->data[j]; // Convolution for index j
+
+            double sumCC = 0.0;         // Sum of convolution products
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    double cc = iConv->kernel[y][x] * jConv->kernel[y][x];
+                    if (weight) {
+                        cc *= weight->kernel[y][x];
+                    }
+                    if (window) {
+                        cc *= window->kernel[y][x];
+                    }
+                    sumCC += cc;
+                }
+            }
+
+            // Spatial variation of kernel coeffs
+            if (mode & PM_SUBTRACTION_EQUATION_KERNELS) {
+                for (int iTerm = 0, iIndex = i; iTerm < numPoly; iTerm++, iIndex += numKernels) {
+                    for (int jTerm = 0, jIndex = j; jTerm < numPoly; jTerm++, jIndex += numKernels) {
+                        double value = sumCC * poly2[iTerm][jTerm];
+                        matrix->data.F64[iIndex][jIndex] = value;
+                        matrix->data.F64[jIndex][iIndex] = value;
+                    }
+                }
+            }
+        }
+
+        double sumRC = 0.0;             // Sum of the reference-convolution products
+        double sumIC = 0.0;             // Sum of the input-convolution products
+        double sumC = 0.0;              // Sum of the convolution
+        for (int y = - footprint; y <= footprint; y++) {
+            for (int x = - footprint; x <= footprint; x++) {
+                float conv = iConv->kernel[y][x];
+                float in = input->kernel[y][x];
+                float ref = reference->kernel[y][x];
+                double ic = in * conv;
+                double rc = ref * conv;
+                double c = conv;
+                if (weight) {
+                    float wtVal = weight->kernel[y][x];
+                    ic *= wtVal;
+                    rc *= wtVal;
+                    c *= wtVal;
+                }
+                if (window) {
+                    float winVal = window->kernel[y][x];
+                    ic *= winVal;
+                    rc *= winVal;
+                    c  *= winVal;
+                }
+                sumIC += ic;
+                sumRC += rc;
+                sumC += c;
+            }
+        }
+        // Spatial variation
+        for (int iTerm = 0, iIndex = i; iTerm < numPoly; iTerm++, iIndex += numKernels) {
+            double normTerm = sumRC * poly[iTerm];
+            double bgTerm = sumC * poly[iTerm];
+            if ((mode & PM_SUBTRACTION_EQUATION_NORM) && (mode & PM_SUBTRACTION_EQUATION_KERNELS)) {
+                matrix->data.F64[iIndex][normIndex] = normTerm;
+                matrix->data.F64[normIndex][iIndex] = normTerm;
+            }
+            if ((mode & PM_SUBTRACTION_EQUATION_BG) && (mode & PM_SUBTRACTION_EQUATION_KERNELS)) {
+                matrix->data.F64[iIndex][bgIndex] = bgTerm;
+                matrix->data.F64[bgIndex][iIndex] = bgTerm;
+            }
+            if (mode & PM_SUBTRACTION_EQUATION_KERNELS) {
+                vector->data.F64[iIndex] = sumIC * poly[iTerm];
+                if (!(mode & PM_SUBTRACTION_EQUATION_NORM)) {
+                    // subtract norm * sumRC * poly[iTerm]
+                    psAssert (kernels->solution1, "programming error: define solution first!");
+                    int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+                    double norm = fabs(kernels->solution1->data.F64[normIndex]);  // Normalisation
+                    vector->data.F64[iIndex] -= norm * normTerm;
+                }
+            }
+        }
+    }
+
+    double sumRR = 0.0;                 // Sum of the reference product
+    double sumIR = 0.0;                 // Sum of the input-reference product
+    double sum1 = 0.0;                  // Sum of the background
+    double sumR = 0.0;                  // Sum of the reference
+    double sumI = 0.0;                  // Sum of the input
+    double normI1 = 0.0, normI2 = 0.0;  // Sum of I_1 and I_2 within the normalisation window
+    for (int y = - footprint; y <= footprint; y++) {
+        for (int x = - footprint; x <= footprint; x++) {
+            double in = input->kernel[y][x];
+            double ref = reference->kernel[y][x];
+            double ir = in * ref;
+            double rr = PS_SQR(ref);
+            double one = 1.0;
+
+            if (PS_SQR(x) + PS_SQR(y) <= PS_SQR(normWindow1)) {
+                normI1 += ref;
+            }
+            if (PS_SQR(x) + PS_SQR(y) <= PS_SQR(normWindow2)) {
+                normI2 += in;
+            }
+
+            if (weight) {
+                float wtVal = weight->kernel[y][x];
+                rr *= wtVal;
+                ir *= wtVal;
+                in *= wtVal;
+                ref *= wtVal;
+                one *= wtVal;
+            }
+            if (window) {
+                float  winVal = window->kernel[y][x];
+                rr      *= winVal;
+                ir      *= winVal;
+                in      *= winVal;
+                ref *= winVal;
+                one *= winVal;
+            }
+            sumRR += rr;
+            sumIR += ir;
+            sumR += ref;
+            sumI += in;
+            sum1 += one;
+        }
+    }
+
+    *norm = normI2 / normI1;
+
+    fprintf (stderr, "normValue: %f %f %f\n", normI1, normI2, *norm);
+
+    if (mode & PM_SUBTRACTION_EQUATION_NORM) {
+        matrix->data.F64[normIndex][normIndex] = sumRR;
+        vector->data.F64[normIndex] = sumIR;
+        // subtract sum over kernels * kernel solution
+    }
+    if (mode & PM_SUBTRACTION_EQUATION_BG) {
+        matrix->data.F64[bgIndex][bgIndex] = sum1;
+        vector->data.F64[bgIndex] = sumI;
+    }
+    if ((mode & PM_SUBTRACTION_EQUATION_NORM) && (mode & PM_SUBTRACTION_EQUATION_BG)) {
+        matrix->data.F64[normIndex][bgIndex] = sumR;
+        matrix->data.F64[bgIndex][normIndex] = sumR;
+    }
+
+    // check for any NAN values in the result, skip if found:
+    for (int iy = 0; iy < matrix->numRows; iy++) {
+        for (int ix = 0; ix < matrix->numCols; ix++) {
+            if (!isfinite(matrix->data.F64[iy][ix])) {
+                fprintf (stderr, "WARNING: NAN in matrix\n");
+                return false;
+            }
+        }
+    }
+    for (int ix = 0; ix < vector->n; ix++) {
+        if (!isfinite(vector->data.F64[ix])) {
+            fprintf (stderr, "WARNING: NAN in vector\n");
+            return false;
+        }
+    }
+
+    return true;
+}
+
+
+// Calculate the least-squares matrix and vector for dual convolution
+static bool calculateDualMatrixVector(psImage *matrix, // Least-squares matrix, updated
+                                      psVector *vector, // Least-squares vector, updated
+                                      double *norm,     // Normalisation, updated
+                                      const psKernel *image1, // Image 1
+                                      const psKernel *image2, // Image 2
+                                      const psKernel *weight,  // Weight image
+                                      const psKernel *window,  // Window image
+                                      const psArray *convolutions1, // Convolutions of image 1 for each kernel
+                                      const psArray *convolutions2, // Convolutions of image 2 for each kernel
+                                      const pmSubtractionKernels *kernels, // Kernels
+                                      const psImage *polyValues, // Spatial polynomial values
+                                      int footprint, // (Half-)Size of stamp
+                                      int normWindow1, // Window (half-)size for normalisation measurement
+                                      int normWindow2, // Window (half-)size for normalisation measurement
+                                      const pmSubtractionEquationCalculationMode mode
+                                      )
+{
+    int numKernels = kernels->num;                      // Number of kernels
+    int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+    int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index in matrix for background
+    int spatialOrder = kernels->spatialOrder;       // Order of spatial variation
+    int numPoly = PM_SUBTRACTION_POLYTERMS(spatialOrder); // Number of polynomial terms
+    double poly[numPoly];                                 // Polynomial terms
+    double poly2[numPoly][numPoly];                       // Polynomial-polynomial values
+
+    int numBackground = PM_SUBTRACTION_POLYTERMS(kernels->bgOrder); // Number of background terms
+    int numParams = numKernels * numPoly + 1 + numBackground;       // Number of regular parameters
+    int numParams2 = numKernels * numPoly;                          // Number of additional parameters for dual
+    int numDual = numParams + numParams2;                           // Total number of parameters for dual
+
+    psAssert(matrix &&
+             matrix->type.type == PS_TYPE_F64 &&
+             matrix->numCols == numDual &&
+             matrix->numRows == numDual,
+             "Least-squares matrix is bad.");
+    psAssert(vector &&
+             vector->type.type == PS_TYPE_F64 &&
+             vector->n == numDual,
+             "Least-squares vector is bad.");
+
+    // Evaluate polynomial-polynomial terms
+    for (int iyOrder = 0, iIndex = 0; iyOrder <= spatialOrder; iyOrder++) {
+        for (int ixOrder = 0; ixOrder <= spatialOrder - iyOrder; ixOrder++, iIndex++) {
+            double iPoly = polyValues->data.F64[iyOrder][ixOrder]; // Value of polynomial
+            poly[iIndex] = iPoly;
+            for (int jyOrder = 0, jIndex = 0; jyOrder <= spatialOrder; jyOrder++) {
+                for (int jxOrder = 0; jxOrder <= spatialOrder - jyOrder; jxOrder++, jIndex++) {
+                    double jPoly = polyValues->data.F64[jyOrder][jxOrder];
+                    poly2[iIndex][jIndex] = iPoly * jPoly;
+                }
+            }
+        }
+    }
+
+
+    // initialize the matrix and vector for NOP on all coeffs.  we only fill in the coeffs we
+    // choose to calculate
+    psImageInit(matrix, 0.0);
+    psVectorInit(vector, 1.0);
+    for (int i = 0; i < matrix->numCols; i++) {
+        matrix->data.F64[i][i] = 1.0;
+    }
+
+    for (int i = 0; i < numKernels; i++) {
+        psKernel *iConv1 = convolutions1->data[i]; // Convolution 1 for index i
+        psKernel *iConv2 = convolutions2->data[i]; // Convolution 2 for index i
+        for (int j = i; j < numKernels; j++) {
+            psKernel *jConv1 = convolutions1->data[j]; // Convolution 1 for index j
+            psKernel *jConv2 = convolutions2->data[j]; // Convolution 2 for index j
+
+            double sumAA = 0.0;         // Sum of convolution products between image 1
+            double sumBB = 0.0;         // Sum of convolution products between image 2
+            double sumAB = 0.0;         // Sum of convolution products across images 1 and 2
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    double aa = iConv1->kernel[y][x] * jConv1->kernel[y][x];
+                    double bb = iConv2->kernel[y][x] * jConv2->kernel[y][x];
+                    double ab = iConv1->kernel[y][x] * jConv2->kernel[y][x];
+                    if (weight) {
+                        float wtVal = weight->kernel[y][x];
+                        aa *= wtVal;
+                        bb *= wtVal;
+                        ab *= wtVal;
+                    }
+                    if (window) {
+                        float wtVal = window->kernel[y][x];
+                        aa *= wtVal;
+                        bb *= wtVal;
+                        ab *= wtVal;
+                    }
+                    sumAA += aa;
+                    sumBB += bb;
+                    sumAB += ab;
+                }
+            }
+
+            // Spatial variation of kernel coeffs
+            if (mode & PM_SUBTRACTION_EQUATION_KERNELS) {
+                for (int iTerm = 0, iIndex = i; iTerm < numPoly; iTerm++, iIndex += numKernels) {
+                    for (int jTerm = 0, jIndex = j; jTerm < numPoly; jTerm++, jIndex += numKernels) {
+                        double aa = sumAA * poly2[iTerm][jTerm];
+                        double bb = sumBB * poly2[iTerm][jTerm];
+                        double ab = sumAB * poly2[iTerm][jTerm];
+
+                        matrix->data.F64[iIndex][jIndex] = aa;
+                        matrix->data.F64[jIndex][iIndex] = aa;
+
+                        matrix->data.F64[iIndex + numParams][jIndex + numParams] = bb;
+                        matrix->data.F64[jIndex + numParams][iIndex + numParams] = bb;
+
+                        matrix->data.F64[iIndex][jIndex + numParams] = ab;
+                        matrix->data.F64[jIndex + numParams][iIndex] = ab;
+                    }
+                }
+            }
+        }
+        for (int j = 0; j < i; j++) {
+            psKernel *jConv2 = convolutions2->data[j]; // Convolution 2 for index j
+            double sumAB = 0.0;         // Sum of convolution products for matrix C
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    double ab = iConv1->kernel[y][x] * jConv2->kernel[y][x];
+                    if (weight) {
+                        ab *= weight->kernel[y][x];
+                    }
+                    if (window) {
+                        ab *= window->kernel[y][x];
+                    }
+                    sumAB += ab;
+                }
+            }
+
+            // Spatial variation of kernel coeffs
+            if (mode & PM_SUBTRACTION_EQUATION_KERNELS) {
+                for (int iTerm = 0, iIndex = i; iTerm < numPoly; iTerm++, iIndex += numKernels) {
+                    for (int jTerm = 0, jIndex = j; jTerm < numPoly; jTerm++, jIndex += numKernels) {
+                        double ab = sumAB * poly2[iTerm][jTerm];
+                        matrix->data.F64[iIndex][jIndex + numParams] = ab;
+                        matrix->data.F64[jIndex + numParams][iIndex] = ab;
+                    }
+                }
+            }
+        }
+
+        double sumAI2 = 0.0;            // Sum of A.I_2 products (for vector)
+        double sumBI2 = 0.0;            // Sum of B.I_2 products (for vector)
+        double sumAI1 = 0.0;            // Sum of A.I_1 products (for matrix, normalisation)
+        double sumA = 0.0;              // Sum of A (for matrix, background)
+        double sumBI1 = 0.0;            // Sum of B.I_1 products (for matrix, normalisation)
+        double sumB = 0.0;              // Sum of B products (for matrix, background)
+        double sumI2 = 0.0;             // Sum of I_2 (for vector, background)
+        for (int y = - footprint; y <= footprint; y++) {
+            for (int x = - footprint; x <= footprint; x++) {
+                double a = iConv1->kernel[y][x];
+                double b = iConv2->kernel[y][x];
+                float i1 = image1->kernel[y][x];
+                float i2 = image2->kernel[y][x];
+
+                double ai2 = a * i2;
+                double bi2 = b * i2;
+                double ai1 = a * i1;
+                double bi1 = b * i1;
+
+                if (weight) {
+                    float wtVal = weight->kernel[y][x];
+                    ai2 *= wtVal;
+                    bi2 *= wtVal;
+                    ai1 *= wtVal;
+                    bi1 *= wtVal;
+                    a *= wtVal;
+                    b *= wtVal;
+                    i2 *= wtVal;
+                }
+                if (window) {
+                    float wtVal = window->kernel[y][x];
+                    ai2 *= wtVal;
+                    bi2 *= wtVal;
+                    ai1 *= wtVal;
+                    bi1 *= wtVal;
+                    a *= wtVal;
+                    b *= wtVal;
+                    i2 *= wtVal;
+                }
+                sumAI2 += ai2;
+                sumBI2 += bi2;
+                sumAI1 += ai1;
+                sumA += a;
+                sumBI1 += bi1;
+                sumB += b;
+                sumI2 += i2;
+            }
+        }
+        // Spatial variation
+        for (int iTerm = 0, iIndex = i; iTerm < numPoly; iTerm++, iIndex += numKernels) {
+            double ai2 = sumAI2 * poly[iTerm];
+            double bi2 = sumBI2 * poly[iTerm];
+            double ai1 = sumAI1 * poly[iTerm];
+            double a   = sumA * poly[iTerm];
+            double bi1 = sumBI1 * poly[iTerm];
+            double b   = sumB * poly[iTerm];
+
+            if ((mode & PM_SUBTRACTION_EQUATION_NORM) && (mode & PM_SUBTRACTION_EQUATION_KERNELS)) {
+                matrix->data.F64[iIndex][normIndex] = ai1;
+                matrix->data.F64[normIndex][iIndex] = ai1;
+                matrix->data.F64[iIndex + numParams][normIndex] = bi1;
+                matrix->data.F64[normIndex][iIndex + numParams] = bi1;
+            }
+            if ((mode & PM_SUBTRACTION_EQUATION_BG) && (mode & PM_SUBTRACTION_EQUATION_KERNELS)) {
+                matrix->data.F64[iIndex][bgIndex] = a;
+                matrix->data.F64[bgIndex][iIndex] = a;
+                matrix->data.F64[iIndex + numParams][bgIndex] = b;
+                matrix->data.F64[bgIndex][iIndex + numParams] = b;
+            }
+            if (mode & PM_SUBTRACTION_EQUATION_KERNELS) {
+                vector->data.F64[iIndex] = ai2;
+                vector->data.F64[iIndex + numParams] = bi2;
+                if (!(mode & PM_SUBTRACTION_EQUATION_NORM)) {
+                    // subtract norm * sumRC * poly[iTerm]
+                    psAssert (kernels->solution1, "programming error: define solution first!");
+                    int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+                    double norm = fabs(kernels->solution1->data.F64[normIndex]);  // Normalisation
+                    vector->data.F64[iIndex] -= norm * ai1;
+                    vector->data.F64[iIndex + numParams] -= norm * bi1;
+                }
+            }
+        }
+    }
+
+    double sumI1 = 0.0;                 // Sum of I_1 (for matrix, background-normalisation)
+    double sumI1I1 = 0.0;               // Sum of I_1^2 (for matrix, normalisation-normalisation)
+    double sum1 = 0.0;                  // Sum of 1 (for matrix, background-background)
+    double sumI2 = 0.0;                 // Sum of I_2 (for vector, background)
+    double sumI1I2 = 0.0;               // Sum of I_1.I_2 (for vector, normalisation)
+    double normI1 = 0.0, normI2 = 0.0;  // Sum of I_1 and I_2 within the normalisation window
+    for (int y = - footprint; y <= footprint; y++) {
+        for (int x = - footprint; x <= footprint; x++) {
+            double i1 = image1->kernel[y][x];
+            double i2 = image2->kernel[y][x];
+
+            double i1i1 = i1 * i1;
+            double one = 1.0;
+            double i1i2 = i1 * i2;
+
+            if (PS_SQR(x) + PS_SQR(y) <= PS_SQR(normWindow1)) {
+                normI1 += i1;
+            }
+            if (PS_SQR(x) + PS_SQR(y) <= PS_SQR(normWindow2)) {
+                normI2 += i2;
+            }
+
+            if (weight) {
+                float wtVal = weight->kernel[y][x];
+                i1 *= wtVal;
+                i1i1 *= wtVal;
+                one *= wtVal;
+                i2 *= wtVal;
+                i1i2 *= wtVal;
+            }
+            if (window) {
+                float wtVal = window->kernel[y][x];
+                i1 *= wtVal;
+                i1i1 *= wtVal;
+                one *= wtVal;
+                i2 *= wtVal;
+                i1i2 *= wtVal;
+            }
+            sumI1 += i1;
+            sumI1I1 += i1i1;
+            sum1 += one;
+            sumI2 += i2;
+            sumI1I2 += i1i2;
+        }
+    }
+
+    *norm = normI2 / normI1;
+    fprintf (stderr, "normValue: %f %f %f\n", normI1, normI2, *norm);
+
+    if (mode & PM_SUBTRACTION_EQUATION_NORM) {
+        matrix->data.F64[normIndex][normIndex] = sumI1I1;
+        vector->data.F64[normIndex] = sumI1I2;
+    }
+    if (mode & PM_SUBTRACTION_EQUATION_BG) {
+        matrix->data.F64[bgIndex][bgIndex] = sum1;
+        vector->data.F64[bgIndex] = sumI2;
+    }
+    if ((mode & PM_SUBTRACTION_EQUATION_NORM) && (mode & PM_SUBTRACTION_EQUATION_BG)) {
+        matrix->data.F64[bgIndex][normIndex] = sumI1;
+        matrix->data.F64[normIndex][bgIndex] = sumI1;
+    }
+
+    // check for any NAN values in the result, skip if found:
+    for (int iy = 0; iy < matrix->numRows; iy++) {
+        for (int ix = 0; ix < matrix->numCols; ix++) {
+            if (!isfinite(matrix->data.F64[iy][ix])) {
+                fprintf (stderr, "WARNING: NAN in matrix\n");
+                return false;
+            }
+        }
+    }
+    for (int ix = 0; ix < vector->n; ix++) {
+        if (!isfinite(vector->data.F64[ix])) {
+            fprintf (stderr, "WARNING: NAN in vector\n");
+            return false;
+        }
+    }
+
+
+    return true;
+}
+
+#if 1
+// Add in penalty term to least-squares vector
+bool calculatePenalty(psImage *matrix,                     // Matrix to which to add in penalty term
+		      psVector *vector,                    // Vector to which to add in penalty term
+		      const pmSubtractionKernels *kernels, // Kernel parameters
+		      float norm                           // Normalisation
+  )
+{
+    if (kernels->penalty == 0.0) {
+        return true;
+    }
+
+    psVector *penalties1 = kernels->penalties1; // Penalties for each kernel component (input)
+    psVector *penalties2 = kernels->penalties2; // Penalties for each kernel component (ref)
+
+    int spatialOrder = kernels->spatialOrder; // Order of spatial variations
+    int numKernels = kernels->num; // Number of kernel components
+    int numSpatial = PM_SUBTRACTION_POLYTERMS(spatialOrder); // Number of spatial variations
+    int numParams = numKernels * numSpatial;                 // Number of kernel parameters
+
+    // order is :
+    // [p_0,x_0,y_0 p_1,x_0,y_0, p_2,x_0,y_0]
+    // [p_0,x_1,y_0 p_1,x_1,y_0, p_2,x_1,y_0]
+    // [p_0,x_0,y_1 p_1,x_0,y_1, p_2,x_0,y_1]
+    // [norm]
+    // [bg]
+    // [q_0,x_0,y_0 q_1,x_0,y_0, q_2,x_0,y_0]
+    // [q_0,x_1,y_0 q_1,x_1,y_0, q_2,x_1,y_0]
+    // [q_0,x_0,y_1 q_1,x_0,y_1, q_2,x_0,y_1]
+
+    for (int i = 0; i < numKernels; i++) {
+        for (int yOrder = 0, index = i; yOrder <= spatialOrder; yOrder++) {
+            for (int xOrder = 0; xOrder <= spatialOrder - yOrder; xOrder++, index += numKernels) {
+                // Contribution to chi^2: a_i^2 P_i
+                psAssert(isfinite(penalties1->data.F32[i]), "Invalid penalty");
+		fprintf (stderr, "penalty: %f + %f (%f * %f)\n", matrix->data.F64[index][index], norm * penalties1->data.F32[i], norm, penalties1->data.F32[i]);
+                matrix->data.F64[index][index] += norm * penalties1->data.F32[i];
+                if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+		    fprintf (stderr, "penalty: (x^%d y^%d fwhm %f) : %f + %f (%f * %f)\n", kernels->u->data.S32[index], kernels->v->data.S32[index], kernels->widths->data.F32[index], 
+			     matrix->data.F64[index + numParams + 2][index + numParams + 2], norm * penalties2->data.F32[i], norm, penalties2->data.F32[i]);
+		    matrix->data.F64[index + numParams + 2][index + numParams + 2] += norm * penalties2->data.F32[i];			     
+                    // matrix[i][i] is ~ (k_i * I_1)(k_i * I_1)
+                    // penalties scale with second moments
+                    //
+                }
+            }
+        }
+    }
+
+    return true;
+}
+# endif
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Semi-public functions
+// XXX We might like to define these functions as "extern inline" but gcc currently doesn't handle this in c99
+// mode.  See http://gcc.gnu.org/ml/gcc/2006-11/msg00006.html
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Calculate the value of a polynomial, specified by coefficients and polynomial values
+double p_pmSubtractionCalculatePolynomial(const psVector *coeff, // Coefficients
+                                          const psImage *polyValues, // Polynomial values
+                                          int order, // Order of polynomials
+                                          int index, // Index at which to begin
+                                          int step // Step between subsequent indices
+                                          )
+{
+    double sum = 0.0;                   // Value of the polynomial sum
+    for (int yOrder = 0; yOrder <= order; yOrder++) {
+        for (int xOrder = 0; xOrder <= order - yOrder; xOrder++, index += step) {
+
+            assert(index < coeff->n);
+
+            sum += coeff->data.F64[index] * polyValues->data.F64[yOrder][xOrder];
+        }
+    }
+    return sum;
+}
+
+double p_pmSubtractionSolutionCoeff(const pmSubtractionKernels *kernels, const psImage *polyValues,
+                                    int index, bool wantDual)
+{
+#if 0
+    // This is probably in a tight loop, so don't check inputs
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NAN);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NAN);
+    PS_ASSERT_IMAGE_NON_NULL(polyValues, NAN);
+    PS_ASSERT_INT_POSITIVE(index, NAN);
+#endif
+
+    psVector *solution = wantDual ? kernels->solution2 : kernels->solution1; // Solution vector
+    return p_pmSubtractionCalculatePolynomial(solution, polyValues, kernels->spatialOrder, index,
+                                              kernels->num);
+}
+
+double p_pmSubtractionSolutionNorm(const pmSubtractionKernels *kernels)
+{
+#if 0
+    // This is probably in a tight loop, so don't check inputs
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NAN);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NAN);
+    PS_ASSERT_IMAGE_NON_NULL(polyValues, NAN);
+#endif
+
+    int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+    return kernels->solution1->data.F64[normIndex];
+}
+
+double p_pmSubtractionSolutionBackground(const pmSubtractionKernels *kernels,
+                                                const psImage *polyValues)
+{
+#if 0
+    // This is probably in a tight loop, so don't check inputs
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NAN);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NAN);
+    PS_ASSERT_IMAGE_NON_NULL(polyValues, NAN);
+#endif
+
+    int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index for background
+    return p_pmSubtractionCalculatePolynomial(kernels->solution1, polyValues, kernels->bgOrder, bgIndex, 1);
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmSubtractionCalculateEquationThread(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    pmSubtractionStampList *stamps = job->args->data[0]; // List of stamps
+    pmSubtractionKernels *kernels = job->args->data[1]; // Kernels
+    int index = PS_SCALAR_VALUE(job->args->data[2], S32); // Stamp index
+    pmSubtractionEquationCalculationMode mode  = PS_SCALAR_VALUE(job->args->data[3], S32); // calculation model
+
+    return pmSubtractionCalculateEquationStamp(stamps, kernels, index, mode);
+}
+
+bool pmSubtractionCalculateEquationStamp(pmSubtractionStampList *stamps, pmSubtractionKernels *kernels,
+                                         int index, const pmSubtractionEquationCalculationMode mode)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+    PS_ASSERT_INT_NONNEGATIVE(index, false);
+    PS_ASSERT_INT_LESS_THAN(index, stamps->num, false);
+
+    int footprint = stamps->footprint;  // Half-size of stamps
+    int spatialOrder = kernels->spatialOrder; // Maximum order of spatial variation
+    int numKernels = kernels->num;      // Number of kernel basis functions
+    int numSpatial = PM_SUBTRACTION_POLYTERMS(spatialOrder); // Number of spatial variations
+    int numBackground = PM_SUBTRACTION_POLYTERMS(kernels->bgOrder); // Number of background terms
+
+    // numKernels is the number of unique kernel images (one for each Gaussian modified by a specific polynomial).
+    // = \sum_i^N_Gaussians [(order + 1) * (order + 2) / 2], eg for 1 Gauss and 1st order, numKernels = 3
+
+    // Total number of parameters to solve for: coefficient of each kernel basis function, multipled by the
+    // number of coefficients for the spatial polynomial, normalisation and a constant background offset.
+    int numParams = numKernels * numSpatial + 1 + numBackground;
+    if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        // An additional image is convolved
+        numParams += numKernels * numSpatial;
+    }
+
+    pmSubtractionStamp *stamp = stamps->stamps->data[index]; // Stamp of interest
+    psAssert(stamp->status == PM_SUBTRACTION_STAMP_CALCULATE, "We only operate on stamps with this state.");
+
+    // Generate convolutions: these are generated once and saved
+    if (!pmSubtractionConvolveStamp(stamp, kernels, footprint)) {
+        psError(psErrorCodeLast(), false, "Unable to convolve stamp %d.", index);
+        return NULL;
+    }
+
+#ifdef TESTING
+    for (int j = 0; j < numKernels; j++) {
+        if (stamp->convolutions1) {
+            psString convName = NULL;
+            psStringAppend(&convName, "conv1_%03d_%03d.fits", index, j);
+            psFits *fits = psFitsOpen(convName, "w");
+            psFree(convName);
+            psKernel *conv = stamp->convolutions1->data[j];
+            psFitsWriteImage(fits, NULL, conv->image, 0, NULL);
+            psFitsClose(fits);
+        }
+
+        if (stamp->convolutions2) {
+            psString convName = NULL;
+            psStringAppend(&convName, "conv2_%03d_%03d.fits", index, j);
+            psFits *fits = psFitsOpen(convName, "w");
+            psFree(convName);
+            psKernel *conv = stamp->convolutions2->data[j];
+            psFitsWriteImage(fits, NULL, conv->image, 0, NULL);
+            psFitsClose(fits);
+        }
+    }
+#endif
+
+    // XXX visualize the set of convolved stamps
+
+    psImage *polyValues = p_pmSubtractionPolynomial(NULL, spatialOrder,
+                                                    stamp->xNorm, stamp->yNorm); // Polynomial terms
+
+    bool new = stamp->vector ? false : true; // Is this a new run?
+    if (new) {
+        stamp->matrix = psImageAlloc(numParams, numParams, PS_TYPE_F64);
+        stamp->vector = psVectorAlloc(numParams, PS_TYPE_F64);
+    }
+#ifdef TESTING
+    psImageInit(stamp->matrix, NAN);
+    psVectorInit(stamp->vector, NAN);
+#endif
+
+    bool status;                    // Status of least-squares matrix/vector calculation
+
+    psKernel *weight = NULL;
+    psKernel *window = NULL;
+
+#ifdef USE_WEIGHT
+    weight = stamp->weight;
+#endif
+#ifdef USE_WINDOW
+    window = stamps->window;
+#endif
+
+    switch (kernels->mode) {
+      case PM_SUBTRACTION_MODE_1:
+        status = calculateMatrixVector(stamp->matrix, stamp->vector, &stamp->norm, stamp->image2, stamp->image1,
+                                       weight, window, stamp->convolutions1, kernels,
+                                       polyValues, footprint, stamps->normWindow1, stamps->normWindow2, mode);
+        break;
+      case PM_SUBTRACTION_MODE_2:
+        status = calculateMatrixVector(stamp->matrix, stamp->vector, &stamp->norm, stamp->image1, stamp->image2,
+                                       weight, window, stamp->convolutions2, kernels,
+                                       polyValues, footprint, stamps->normWindow2, stamps->normWindow1, mode);
+        break;
+      case PM_SUBTRACTION_MODE_DUAL:
+        status = calculateDualMatrixVector(stamp->matrix, stamp->vector, &stamp->norm,
+                                           stamp->image1, stamp->image2,
+                                           weight, window, stamp->convolutions1, stamp->convolutions2,
+                                           kernels, polyValues, footprint, stamps->normWindow1, stamps->normWindow2, mode);
+        break;
+      default:
+        psAbort("Unsupported subtraction mode: %x", kernels->mode);
+    }
+
+    if (!status) {
+        stamp->status = PM_SUBTRACTION_STAMP_REJECTED;
+        psWarning("Rejecting stamp %d (%d,%d) because of bad equation",
+                  index, (int)(stamp->x - 0.5), (int)(stamp->y - 0.5));
+    } else {
+        stamp->status = PM_SUBTRACTION_STAMP_USED;
+    }
+
+#ifdef TESTING
+    {
+        psString matrixName = NULL;
+        psStringAppend(&matrixName, "matrix_%d.fits", index);
+        psFits *matrixFile = psFitsOpen(matrixName, "w");
+        psFree(matrixName);
+        psFitsWriteImage(matrixFile, NULL, stamp->matrix, 0, NULL);
+        psFitsClose(matrixFile);
+
+        matrixName = NULL;
+        psStringAppend(&matrixName, "vector_%d.fits", index);
+        psImage *dummy = psImageAlloc(stamp->vector->n, 1, PS_TYPE_F64);
+        memcpy(dummy->data.F64[0], stamp->vector->data.F64,
+               PSELEMTYPE_SIZEOF(PS_TYPE_F64) * stamp->vector->n);
+        matrixFile = psFitsOpen(matrixName, "w");
+        psFree(matrixName);
+        psFitsWriteImage(matrixFile, NULL, dummy, 0, NULL);
+        psFree(dummy);
+        psFitsClose(matrixFile);
+    }
+#endif
+
+    psFree(polyValues);
+
+    return true;
+}
+
+bool pmSubtractionCalculateEquation(pmSubtractionStampList *stamps, pmSubtractionKernels *kernels,
+                                    const pmSubtractionEquationCalculationMode mode)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+
+    psTimerStart("pmSubtractionCalculateEquation");
+
+    // We iterate over each stamp, allocate the matrix and vectors if
+    // necessary, and then calculate those matrix/vectors.
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (stamp->status != PM_SUBTRACTION_STAMP_CALCULATE) {
+            continue;
+        }
+
+        if ((stamp->x <= 0.0) && (stamp->y <= 0.0)) {
+            psAbort ("bad stamp");
+        }
+        if (!isfinite(stamp->x) && !isfinite(stamp->y)) {
+            psAbort ("bad stamp");
+        }
+
+        if (pmSubtractionThreaded()) {
+            psThreadJob *job = psThreadJobAlloc("PSMODULES_SUBTRACTION_CALCULATE_EQUATION");
+            psArrayAdd(job->args, 1, stamps);
+            psArrayAdd(job->args, 1, (pmSubtractionKernels*)kernels); // Casting away const to put on array
+            PS_ARRAY_ADD_SCALAR(job->args, i, PS_TYPE_S32);
+            PS_ARRAY_ADD_SCALAR(job->args, mode, PS_TYPE_S32);
+            if (!psThreadJobAddPending(job)) {
+                return false;
+            }
+        } else {
+            pmSubtractionCalculateEquationStamp(stamps, kernels, i, mode);
+        }
+    }
+
+    if (!psThreadPoolWait(true, true)) {
+        psError(psErrorCodeLast(), false, "Error waiting for threads.");
+        return false;
+    }
+
+    pmSubtractionVisualPlotLeastSquares(stamps);
+    pmSubtractionVisualShowKernels((pmSubtractionKernels  *)kernels);
+    pmSubtractionVisualShowBasis(stamps);
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Calculate equation: %f sec",
+             psTimerClear("pmSubtractionCalculateEquation"));
+
+
+    return true;
+}
+
+// private functions used on pmSubtractionSolveEquation
+bool psVectorWriteFile (char *filename, const psVector *vector);
+bool psFitsWriteImageSimple (char *filename, psImage *image, psMetadata *header);
+
+psImage *p_pmSubSolve_wUt (psVector *w, psImage *U);
+psImage *p_pmSubSolve_VwUt (psImage *V, psImage *wUt);
+
+bool p_pmSubSolve_SetWeights (psVector *wApply, psVector *w, psVector *wMask);
+
+bool p_pmSubSolve_UtB (psVector **UtB, psImage *U, psVector *B);
+bool p_pmSubSolve_wUtB (psVector **wUtB, psVector *w, psVector *UtB);
+bool p_pmSubSolve_VwUtB (psVector **VwUtB, psImage *V, psVector *wUtB);
+
+bool p_pmSubSolve_Ax (psVector **B, psImage *A, psVector *x);
+bool p_pmSubSolve_VdV (double *value, psVector *x, psVector *y);
+bool p_pmSubSolve_y2 (double *y2, pmSubtractionKernels *kernels, const pmSubtractionStampList *stamps);
+
+psImage *p_pmSubSolve_Xvar (psImage *V, psVector *w);
+
+double p_pmSubSolve_ChiSquare (pmSubtractionKernels *kernels, const pmSubtractionStampList *stamps);
+
+bool pmSubtractionSolveEquation(pmSubtractionKernels *kernels,
+                                const pmSubtractionStampList *stamps,
+                                const pmSubtractionEquationCalculationMode mode)
+{
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+
+    // Check inputs
+    int numKernels = kernels->num;      // Number of kernel basis functions
+    int numSpatial = PM_SUBTRACTION_POLYTERMS(kernels->spatialOrder); // Number of spatial variations
+    int numBackground = PM_SUBTRACTION_POLYTERMS(kernels->bgOrder); // Number of background terms
+    int numParams = numKernels * numSpatial + 1 + numBackground;    // Number of parameters being solved for
+    int numSolution1 = numParams, numSolution2 = 0;                 // Number of parameters for each solution
+    if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        // An additional image is convolved
+        numSolution2 = numKernels * numSpatial;
+        numParams += numSolution2;
+    }
+
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        PS_ASSERT_PTR_NON_NULL(stamp, false);
+        if (stamp->status != PM_SUBTRACTION_STAMP_USED) {
+            continue;
+        }
+
+        PS_ASSERT_VECTOR_NON_NULL(stamp->vector, false);
+        PS_ASSERT_VECTOR_SIZE(stamp->vector, (long)numParams, false);
+        PS_ASSERT_VECTOR_TYPE(stamp->vector, PS_TYPE_F64, false);
+        PS_ASSERT_IMAGE_NON_NULL(stamp->matrix, false);
+        PS_ASSERT_IMAGE_SIZE(stamp->matrix, numParams, numParams, false);
+        PS_ASSERT_IMAGE_TYPE(stamp->matrix, PS_TYPE_F64, false);
+    }
+
+    psString ds9name = NULL;            // Filename for ds9 region file
+    static int ds9num = 0;              // File number for ds9 region file
+    psStringAppend(&ds9name, "stamps_solution_%d.ds9", ds9num);
+    FILE *ds9 = pmSubtractionStampsFile(stamps, ds9name, "solution stamps");
+    psFree(ds9name);
+    ds9num++;
+
+    if (kernels->mode != PM_SUBTRACTION_MODE_DUAL) {
+        // Accumulate the least-squares matricies and vectors
+        psImage *sumMatrix = psImageAlloc(numParams, numParams, PS_TYPE_F64); // Combined matrix
+        psVector *sumVector = psVectorAlloc(numParams, PS_TYPE_F64); // Combined vector
+        psVectorInit(sumVector, 0.0);
+        psImageInit(sumMatrix, 0.0);
+
+        psVector *norms = psVectorAllocEmpty(stamps->num, PS_TYPE_F64); // Normalisations
+
+        int numStamps = 0;              // Number of good stamps
+        for (int i = 0; i < stamps->num; i++) {
+            pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+            if (stamp->status == PM_SUBTRACTION_STAMP_USED) {
+		
+                (void)psBinaryOp(sumMatrix, sumMatrix, "+", stamp->matrix);
+                (void)psBinaryOp(sumVector, sumVector, "+", stamp->vector);
+
+                psVectorAppend(norms, stamp->norm);
+
+                pmSubtractionStampPrint(ds9, stamp->x, stamp->y, stamps->footprint, "green");
+                numStamps++;
+            } else if (stamp->status == PM_SUBTRACTION_STAMP_REJECTED) {
+                pmSubtractionStampPrint(ds9, stamp->x, stamp->y, stamps->footprint, "red");
+            }
+        }
+
+#if 0
+        int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index for background
+        calculatePenalty(sumMatrix, sumVector, kernels, sumMatrix->data.F64[bgIndex][bgIndex]);
+#endif
+
+        psVector *solution = NULL;                       // Solution to equation!
+        solution = psVectorAlloc(numParams, PS_TYPE_F64);
+        psVectorInit(solution, 0);
+
+#if 0
+        // Regular, straight-forward solution
+        solution = psMatrixSolveSVD(solution, sumMatrix, sumVector, NAN);
+#else
+        {
+            // Solve normalisation and background separately
+            int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+            int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index for background
+
+            psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN); // Statistics for norm
+            if (!psVectorStats(stats, norms, NULL, NULL, 0)) {
+                psError(PM_ERR_DATA, false, "Unable to determine median normalisation");
+                psFree(stats);
+                psFree(sumMatrix);
+                psFree(sumVector);
+                psFree(norms);
+                return false;
+            }
+
+            // double normValue = 1.0;
+            double normValue = stats->robustMedian;
+            // double bgValue = 0.0;
+
+            psFree(stats);
+
+#ifdef TESTING
+            fprintf(stderr, "Norm: %lf\n", normValue);
+#endif
+            // Solve kernel components
+            for (int i = 0; i < numSolution1; i++) {
+                sumVector->data.F64[i] -= normValue * sumMatrix->data.F64[normIndex][i];
+
+                sumMatrix->data.F64[i][normIndex] = 0.0;
+                sumMatrix->data.F64[normIndex][i] = 0.0;
+            }
+            sumVector->data.F64[bgIndex] -= normValue * sumMatrix->data.F64[normIndex][bgIndex];
+            sumMatrix->data.F64[bgIndex][normIndex] = 0.0;
+            sumMatrix->data.F64[normIndex][bgIndex] = 0.0;
+
+            sumMatrix->data.F64[normIndex][normIndex] = 1.0;
+            sumVector->data.F64[normIndex] = 0.0;
+
+            solution = psMatrixSolveSVD(solution, sumMatrix, sumVector, NAN);
+
+            solution->data.F64[normIndex] = normValue;
+        }
+# endif
+
+#if (1)
+        for (int i = 0; i < solution->n; i++) {
+            fprintf(stderr, "Single solution %d: %lf\n", i, solution->data.F64[i]);
+        }
+#endif
+
+        if (!kernels->solution1) {
+            kernels->solution1 = psVectorAlloc(sumVector->n, PS_TYPE_F64);
+            psVectorInit(kernels->solution1, 0.0);
+        }
+
+        // only update the solutions that we chose to calculate:
+        if (mode & PM_SUBTRACTION_EQUATION_NORM) {
+            int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+            kernels->solution1->data.F64[normIndex] = solution->data.F64[normIndex];
+        }
+        if (mode & PM_SUBTRACTION_EQUATION_BG) {
+            int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index in matrix for background
+            kernels->solution1->data.F64[bgIndex] = solution->data.F64[bgIndex];
+        }
+        if (mode & PM_SUBTRACTION_EQUATION_KERNELS) {
+            int numKernels = kernels->num;
+            int spatialOrder = kernels->spatialOrder;       // Order of spatial variation
+            int numPoly = PM_SUBTRACTION_POLYTERMS(spatialOrder); // Number of polynomial terms
+            for (int i = 0; i < numKernels * numPoly; i++) {
+                kernels->solution1->data.F64[i] = solution->data.F64[i];
+            }
+        }
+
+        psFree(norms);
+        psFree(solution);
+        psFree(sumVector);
+        psFree(sumMatrix);
+
+#ifdef TESTING
+        // XXX double-check for NAN in data:
+        for (int ix = 0; ix < kernels->solution1->n; ix++) {
+            if (!isfinite(kernels->solution1->data.F64[ix])) {
+                fprintf (stderr, "WARNING: NAN in vector\n");
+            }
+        }
+#endif
+
+    } else {
+        // Dual convolution solution
+
+        // Accumulation of stamp matrices/vectors
+        psImage *sumMatrix = psImageAlloc(numParams, numParams, PS_TYPE_F64);
+        psVector *sumVector = psVectorAlloc(numParams, PS_TYPE_F64);
+        psImageInit(sumMatrix, 0.0);
+        psVectorInit(sumVector, 0.0);
+
+        psVector *norms = psVectorAllocEmpty(stamps->num, PS_TYPE_F64); // Normalisations
+
+        int numStamps = 0;              // Number of good stamps
+        for (int i = 0; i < stamps->num; i++) {
+            pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+            if (stamp->status == PM_SUBTRACTION_STAMP_USED) {
+                (void)psBinaryOp(sumMatrix, sumMatrix, "+", stamp->matrix);
+                (void)psBinaryOp(sumVector, sumVector, "+", stamp->vector);
+
+                psVectorAppend(norms, stamp->norm);
+
+                pmSubtractionStampPrint(ds9, stamp->x, stamp->y, stamps->footprint, "green");
+                numStamps++;
+            }
+        }
+
+#if 0
+	psImage *save = psImageCopy(NULL, sumMatrix, PS_TYPE_F32);
+        psFitsWriteImageSimple ("sumMatrix.fits", save, NULL);
+        psVectorWriteFile("sumVector.dat", sumVector);
+	psFree (save);
+#endif
+
+#if 1
+        // int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index for background
+        // calculatePenalty(sumMatrix, sumVector, kernels, sumMatrix->data.F64[bgIndex][bgIndex]);
+
+        int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+        calculatePenalty(sumMatrix, sumVector, kernels, sumMatrix->data.F64[normIndex][normIndex] / 100.0);
+#endif
+
+        psVector *solution = NULL;                       // Solution to equation!
+        solution = psVectorAlloc(numParams, PS_TYPE_F64);
+        psVectorInit(solution, 0);
+
+#if 0
+        // Regular, straight-forward solution
+        solution = psMatrixSolveSVD(solution, sumMatrix, sumVector, NAN);
+#else
+        {
+            // Solve normalisation and background separately
+            int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+            int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index for background
+
+#if 0
+            psImage *normMatrix = psImageAlloc(2, 2, PS_TYPE_F64);
+            psVector *normVector = psVectorAlloc(2, PS_TYPE_F64);
+
+            normMatrix->data.F64[0][0] = sumMatrix->data.F64[normIndex][normIndex];
+            normMatrix->data.F64[1][1] = sumMatrix->data.F64[bgIndex][bgIndex];
+            normMatrix->data.F64[0][1] = normMatrix->data.F64[1][0] = sumMatrix->data.F64[normIndex][bgIndex];
+
+            normVector->data.F64[0] = sumVector->data.F64[normIndex];
+            normVector->data.F64[1] = sumVector->data.F64[bgIndex];
+
+            psVector *normSolution = psMatrixSolveSVD(NULL, normMatrix, normVector, NAN);
+
+            double normValue = normSolution->data.F64[0];
+            double bgValue = normSolution->data.F64[1];
+
+            psFree(normMatrix);
+            psFree(normVector);
+            psFree(normSolution);
+#endif
+
+            psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN); // Statistics for norm
+            if (!psVectorStats(stats, norms, NULL, NULL, 0)) {
+                psError(PM_ERR_DATA, false, "Unable to determine median normalisation");
+                psFree(stats);
+                psFree(sumMatrix);
+                psFree(sumVector);
+                psFree(norms);
+                return false;
+            }
+
+            double normValue = stats->robustMedian;
+
+            psFree(stats);
+
+#ifdef TESTING
+            fprintf(stderr, "Norm: %lf\n", normValue);
+#endif
+
+            // Solve kernel components
+            for (int i = 0; i < numSolution2; i++) {
+                sumVector->data.F64[i] -= normValue * sumMatrix->data.F64[normIndex][i];
+                sumVector->data.F64[i + numSolution1] -= normValue * sumMatrix->data.F64[normIndex][i + numSolution1];
+
+                sumMatrix->data.F64[i][normIndex] = 0.0;
+                sumMatrix->data.F64[normIndex][i] = 0.0;
+
+                sumMatrix->data.F64[i + numSolution1][normIndex] = 0.0;
+                sumMatrix->data.F64[normIndex][i + numSolution1] = 0.0;
+            }
+            sumVector->data.F64[bgIndex] -= normValue * sumMatrix->data.F64[normIndex][bgIndex];
+            sumMatrix->data.F64[bgIndex][normIndex] = 0.0;
+            sumMatrix->data.F64[normIndex][bgIndex] = 0.0;
+
+            sumMatrix->data.F64[normIndex][normIndex] = 1.0;
+
+            sumVector->data.F64[normIndex] = 0.0;
+
+// save the matrix and vector after the NULLs have been set
+#if 0
+	    psImage *save = psImageCopy(NULL, sumMatrix, PS_TYPE_F32);
+	    psFitsWriteImageSimple ("sumMatrix.fits", save, NULL);
+	    psVectorWriteFile("sumVector.dat", sumVector);
+	    psFree (save);
+#endif
+
+	    solution = psMatrixSolveSVD(solution, sumMatrix, sumVector, 1e-6);
+	    // solution = psMatrixSolveSVD(solution, sumMatrix, sumVector, 3e-4);
+	    // psVectorCopy (solution, sumVector, PS_TYPE_F64);
+            // psMatrixGJSolve(sumMatrix, solution);
+            solution->data.F64[normIndex] = normValue;
+        }
+#endif
+
+
+#if (1)
+        for (int i = 0; i < solution->n; i++) {
+            fprintf(stderr, "Dual solution %d: %lf\n", i, solution->data.F64[i]);
+        }
+#endif
+
+        psFree(sumMatrix);
+        psFree(sumVector);
+
+        psFree(norms);
+
+        if (!kernels->solution1) {
+            kernels->solution1 = psVectorAlloc(numSolution1, PS_TYPE_F64);
+            psVectorInit (kernels->solution1, 0.0);
+        }
+        if (!kernels->solution2) {
+            kernels->solution2 = psVectorAlloc(numSolution2, PS_TYPE_F64);
+            psVectorInit (kernels->solution2, 0.0);
+        }
+
+        // only update the solutions that we chose to calculate:
+        if (mode & PM_SUBTRACTION_EQUATION_NORM) {
+            int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+            kernels->solution1->data.F64[normIndex] = solution->data.F64[normIndex];
+        }
+        if (mode & PM_SUBTRACTION_EQUATION_BG) {
+            int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index in matrix for background
+            kernels->solution1->data.F64[bgIndex] = solution->data.F64[bgIndex];
+        }
+        if (mode & PM_SUBTRACTION_EQUATION_KERNELS) {
+            int numKernels = kernels->num;
+            for (int i = 0; i < numKernels * numSpatial; i++) {
+                // XXX fprintf (stderr, "keep\n");
+                kernels->solution1->data.F64[i] = solution->data.F64[i];
+                kernels->solution2->data.F64[i] = solution->data.F64[i + numSolution1];
+            }
+        }
+
+
+        memcpy(kernels->solution1->data.F64, solution->data.F64,
+               numSolution1 * PSELEMTYPE_SIZEOF(PS_TYPE_F64));
+        memcpy(kernels->solution2->data.F64, &solution->data.F64[numSolution1],
+               numSolution2 * PSELEMTYPE_SIZEOF(PS_TYPE_F64));
+
+        psFree(solution);
+
+    }
+
+    if (ds9) {
+        fclose(ds9);
+    }
+
+    if (psTraceGetLevel("psModules.imcombine") >= 7) {
+        for (int i = 0; i < kernels->solution1->n; i++) {
+            psTrace("psModules.imcombine", 7, "Solution 1 %d: %f\n", i, kernels->solution1->data.F64[i]);
+        }
+        if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+            for (int i = 0; i < kernels->solution2->n; i++) {
+                psTrace("psModules.imcombine", 7, "Solution 2 %d: %f\n", i, kernels->solution2->data.F64[i]);
+            }
+        }
+     }
+
+    // pmSubtractionVisualPlotLeastSquares((pmSubtractionStampList *) stamps); //casting away const
+    return true;
+}
+
+// measure some useful stats on the stamp residuals:
+// fResSigma : the residual stdev / total flux
+// fResOuter : the residual fabs / total flux for R > 2 pix
+// fResTotal : the residual fabs / total flux for R > 0 pix
+bool pmSubtractionResidualStats(psVector *fResSigma, psVector *fResOuter, psVector *fResTotal, psKernel *target, psKernel *source, psKernel *residual, double norm, int footprint) {
+
+    float sum = 0.0;
+    float peak = 0.0;
+    for (int y = - footprint; y <= footprint; y++) {
+        for (int x = - footprint; x <= footprint; x++) {
+            sum += 0.5*(target->kernel[y][x] + source->kernel[y][x] * norm);
+            peak = PS_MAX(peak, 0.5*(target->kernel[y][x] + source->kernel[y][x] * norm));
+        }
+    }
+
+    // init counters
+    int npix = 0;
+    float dflux1 = 0.0;
+    float dflux2 = 0.0;
+    float dOuter = 0.0;
+    float dTotal = 0.0;
+
+    for (int y = - footprint; y <= footprint; y++) {
+        for (int x = - footprint; x <= footprint; x++) {
+            dflux1 += residual->kernel[y][x];
+            dflux2 += PS_SQR(residual->kernel[y][x]);
+            dTotal += fabs(residual->kernel[y][x]);
+	    if (hypot(x,y) > 2.0) {
+	      dOuter += fabs(residual->kernel[y][x]);
+	    }
+            npix ++;
+        }
+    }
+    float sigma = sqrt(dflux2 / npix - PS_SQR(dflux1/npix));
+    if (!isfinite(sum))  return false;
+    if (!isfinite(peak)) return false;
+    if (!isfinite(dOuter)) return false;
+    if (!isfinite(dTotal)) return false;
+
+    fprintf (stderr, "sum: %f, peak: %f, sigma: %f, fsigma: %f, fmax: %f, fmin: %f\n", sum, peak, sigma, sigma/sum, dOuter/sum, dTotal/sum);
+    psVectorAppend(fResSigma, sigma/sum);
+    psVectorAppend(fResOuter, dOuter/sum);
+    psVectorAppend(fResTotal, dTotal/sum);
+    return true;
+}
+
+psVector *pmSubtractionCalculateDeviations(pmSubtractionStampList *stamps,
+                                           pmSubtractionKernels *kernels)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, NULL);
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NULL);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NULL);
+
+    psVector *deviations = psVectorAlloc(stamps->num, PS_TYPE_F32); // Mean deviation for stamps
+    int footprint = stamps->footprint; // Half-size of stamps
+    long numPixels = PS_SQR(2 * footprint + 1); // Number of pixels in footprint
+    double devNorm = 1.0 / (double)numPixels; // Normalisation for deviations
+    int numKernels = kernels->num;      // Number of kernels
+
+    psImage *polyValues = NULL;         // Polynomial values
+    psKernel *residual = psKernelAlloc(-footprint, footprint, -footprint, footprint); // Residual image
+
+    // set up holding images for the visualization
+    pmSubtractionVisualShowFitInit (stamps);
+
+    psVector *fResSigma = psVectorAllocEmpty(stamps->num, PS_TYPE_F32);
+    psVector *fResOuter = psVectorAllocEmpty(stamps->num, PS_TYPE_F32);
+    psVector *fResTotal = psVectorAllocEmpty(stamps->num, PS_TYPE_F32);
+
+    // we want to save the residual images for the 9 brightest stamps.
+    // identify the 9 brightest stamps
+    psVector *keepStamps  = psVectorAlloc(stamps->num, PS_TYPE_S32);
+    psVectorInit (keepStamps, 0);
+    {
+        psVector *flux  = psVectorAlloc(stamps->num, PS_TYPE_F32);
+        psVectorInit (flux, 0.0);
+
+        for (int i = 0; i < stamps->num; i++) {
+            pmSubtractionStamp *stamp = stamps->stamps->data[i];
+            if (!isfinite(stamp->flux)) continue;
+            flux->data.F32[i] = stamp->flux;
+        }
+
+        psVector *index = psVectorSortIndex(NULL, flux);
+        for (int i = 0; (i < stamps->num) && (i < 9); i++) {
+            int n = stamps->num - i - 1;
+            keepStamps->data.S32[index->data.S32[n]] = 1;
+        }
+        psFree (flux);
+        psFree (index);
+
+        // this function is called multiple times in the iteration, but
+        // we only know after the interation is done if we will try again.
+        // therefore we must save the sample each time, and blow away the old one
+        // if it exists.
+        psFree (kernels->sampleStamps);
+        kernels->sampleStamps = psArrayAllocEmpty(9);
+    }
+
+    psString log = psStringCopy("Deviations:\n");               // Log message with deviations
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // The stamp of interest
+        if (stamp->status != PM_SUBTRACTION_STAMP_USED) {
+            deviations->data.F32[i] = NAN;
+            continue;
+        }
+
+        // Calculate coefficients of the kernel basis functions
+        polyValues = p_pmSubtractionPolynomial(polyValues, kernels->spatialOrder, stamp->xNorm, stamp->yNorm);
+        double norm = p_pmSubtractionSolutionNorm(kernels); // Normalisation
+        double background = p_pmSubtractionSolutionBackground(kernels, polyValues);// Difference in background
+
+        // Calculate residuals
+        psKernel *weight = stamp->weight; // Weight postage stamp
+        psImageInit(residual->image, 0.0);
+        if (kernels->mode != PM_SUBTRACTION_MODE_DUAL) {
+            psKernel *target;           // Target postage stamp
+            psKernel *source;           // Source postage stamp
+            psArray *convolutions;      // Convolution postage stamps for each kernel basis function
+            switch (kernels->mode) {
+              case PM_SUBTRACTION_MODE_1:
+                target = stamp->image2;
+                source = stamp->image1;
+                convolutions = stamp->convolutions1;
+
+                // Having convolved image1 and changed its normalisation, we need to renormalise the residual
+                // so that it is on the scale of image1.
+                psImage *image = pmSubtractionKernelImage(kernels, stamp->xNorm, stamp->yNorm,
+                                                          false); // Kernel image
+                if (!image) {
+                    psError(psErrorCodeLast(), false, "Unable to generate image of kernel.");
+                    return false;
+                }
+                double sumKernel = 0;   // Sum of kernel, for normalising residual
+                int size = kernels->size; // Half-size of kernel
+                int fullSize = 2 * size + 1; // Full size of kernel
+                for (int y = 0; y < fullSize; y++) {
+                    for (int x = 0; x < fullSize; x++) {
+                        sumKernel += image->data.F32[y][x];
+                    }
+                }
+                psFree(image);
+                devNorm = 1.0 / sumKernel / numPixels;
+                break;
+              case PM_SUBTRACTION_MODE_2:
+                target = stamp->image1;
+                source = stamp->image2;
+                convolutions = stamp->convolutions2;
+                break;
+              default:
+                psAbort("Unsupported subtraction mode: %x", kernels->mode);
+            }
+
+            for (int j = 0; j < numKernels; j++) {
+                psKernel *convolution = convolutions->data[j]; // Convolution
+                double coefficient = p_pmSubtractionSolutionCoeff(kernels, polyValues, j,
+                                                                  false); // Coefficient
+                for (int y = - footprint; y <= footprint; y++) {
+                    for (int x = - footprint; x <= footprint; x++) {
+                        residual->kernel[y][x] += convolution->kernel[y][x] * coefficient;
+                    }
+                }
+            }
+
+            // XXX visualize the target, source, convolution and residual
+            pmSubtractionVisualShowFitAddStamp (target, source, residual, background, norm, i);
+
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    residual->kernel[y][x] += background + source->kernel[y][x] * norm - target->kernel[y][x];
+                }
+            }
+
+            if (keepStamps->data.S32[i]) {
+                psImage *sample = psImageCopy(NULL, residual->image, PS_TYPE_F32);
+                psArrayAdd (kernels->sampleStamps, 9, sample);
+                psFree (sample);
+            }
+
+            pmSubtractionResidualStats(fResSigma, fResOuter, fResTotal, target, source, residual, norm, footprint);
+
+        } else {
+            // Dual convolution
+            psArray *convolutions1 = stamp->convolutions1; // Convolutions of the first image
+            psArray *convolutions2 = stamp->convolutions2; // Convolutions of the second image
+            psKernel *image1 = stamp->image1; // The first image
+            psKernel *image2 = stamp->image2; // The second image
+
+            for (int j = 0; j < numKernels; j++) {
+                psKernel *conv1 = convolutions1->data[j]; // Convolution of first image
+                psKernel *conv2 = convolutions2->data[j]; // Convolution of second image
+                double coeff1 = p_pmSubtractionSolutionCoeff(kernels, polyValues, j, false); // Coefficient 1
+                double coeff2 = p_pmSubtractionSolutionCoeff(kernels, polyValues, j, true); // Coefficient 2
+
+                for (int y = - footprint; y <= footprint; y++) {
+                    for (int x = - footprint; x <= footprint; x++) {
+                        residual->kernel[y][x] += conv2->kernel[y][x] * coeff2 + conv1->kernel[y][x] * coeff1;
+                    }
+                }
+            }
+
+            // XXX visualize the target, source, convolution and residual
+            pmSubtractionVisualShowFitAddStamp (image2, image1, residual, background, norm, i);
+
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    residual->kernel[y][x] += background + image1->kernel[y][x] * norm - image2->kernel[y][x];
+                }
+            }
+            if (keepStamps->data.S32[i]) {
+                psImage *sample = psImageCopy(NULL, residual->image, PS_TYPE_F32);
+                psArrayAdd (kernels->sampleStamps, 9, sample);
+                psFree (sample);
+            }
+
+            pmSubtractionResidualStats(fResSigma, fResOuter, fResTotal, image1, image2, residual, norm, footprint);
+        }
+
+        double deviation = 0.0;         // Sum of differences
+        for (int y = - footprint; y <= footprint; y++) {
+            for (int x = - footprint; x <= footprint; x++) {
+                double dev = PS_SQR(residual->kernel[y][x]) * weight->kernel[y][x];
+                deviation += dev;
+#ifdef TESTING
+                residual->kernel[y][x] = dev;
+#endif
+            }
+        }
+        deviations->data.F32[i] = devNorm * deviation;
+        psTrace("psModules.imcombine", 5, "Deviation for stamp %d (%d,%d): %f\n",
+                i, (int)(stamp->x - 0.5), (int)(stamp->y - 0.5), deviations->data.F32[i]);
+        psStringAppend(&log, "Stamp %d (%d,%d): %f\n",
+                       i, (int)(stamp->x - 0.5), (int)(stamp->y - 0.5), deviations->data.F32[i]);
+        if (!isfinite(deviations->data.F32[i])) {
+            stamp->status = PM_SUBTRACTION_STAMP_REJECTED;
+            psTrace("psModules.imcombine", 5,
+                    "Rejecting stamp %d (%d,%d) because of non-finite deviation\n",
+                    i, (int)(stamp->x - 0.5), (int)(stamp->y - 0.5));
+            continue;
+        }
+
+#ifdef TESTING
+        {
+            psString filename = NULL;
+            psStringAppend(&filename, "resid_%03d.fits", i);
+            psFits *fits = psFitsOpen(filename, "w");
+            psFree(filename);
+            psFitsWriteImage(fits, NULL, residual->image, 0, NULL);
+            psFitsClose(fits);
+        }
+        if (stamp->image1) {
+            psString filename = NULL;
+            psStringAppend(&filename, "stamp_image1_%03d.fits", i);
+            psFits *fits = psFitsOpen(filename, "w");
+            psFree(filename);
+            psFitsWriteImage(fits, NULL, stamp->image1->image, 0, NULL);
+            psFitsClose(fits);
+        }
+        if (stamp->image2) {
+            psString filename = NULL;
+            psStringAppend(&filename, "stamp_image2_%03d.fits", i);
+            psFits *fits = psFitsOpen(filename, "w");
+            psFree(filename);
+            psFitsWriteImage(fits, NULL, stamp->image2->image, 0, NULL);
+            psFitsClose(fits);
+        }
+        if (stamp->weight) {
+            psString filename = NULL;
+            psStringAppend(&filename, "stamp_weight_%03d.fits", i);
+            psFits *fits = psFitsOpen(filename, "w");
+            psFree(filename);
+            psFitsWriteImage(fits, NULL, stamp->weight->image, 0, NULL);
+            psFitsClose(fits);
+        }
+#endif
+
+    }
+
+    psFree(keepStamps);
+
+    psLogMsg("psModules.imcombine", PS_LOG_DETAIL, "%s", log);
+    psFree(log);
+
+    // calculate and report the normalization and background for the image center
+    {
+        polyValues = p_pmSubtractionPolynomial(polyValues, kernels->spatialOrder, 0.0, 0.0);
+        double norm = p_pmSubtractionSolutionNorm(kernels); // Normalisation
+        double background = p_pmSubtractionSolutionBackground(kernels, polyValues);// Difference in background
+        psLogMsg("psModules.imcombine", PS_LOG_INFO, "normalization: %f, background: %f", norm, background);
+
+        pmSubtractionVisualShowFit(norm);
+        pmSubtractionVisualPlotFit(kernels);
+
+        psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+        psVectorStats (stats, fResSigma, NULL, NULL, 0);
+        kernels->fResSigmaMean  = stats->robustMedian;
+        kernels->fResSigmaStdev = stats->robustStdev;
+
+        psStatsInit (stats);
+        psVectorStats (stats, fResOuter, NULL, NULL, 0);
+        kernels->fResOuterMean  = stats->robustMedian;
+        kernels->fResOuterStdev = stats->robustStdev;
+
+        psStatsInit (stats);
+        psVectorStats (stats, fResTotal, NULL, NULL, 0);
+        kernels->fResTotalMean  = stats->robustMedian;
+        kernels->fResTotalStdev = stats->robustStdev;
+
+        // XXX save these values somewhere
+        psLogMsg("psModules.imcombine", PS_LOG_INFO, "fResSigma: %f +/- %f, fResOuter: %f +/- %f, fResTotal: %f +/- %f",
+                 kernels->fResSigmaMean, kernels->fResSigmaStdev,
+                 kernels->fResOuterMean, kernels->fResOuterStdev,
+                 kernels->fResTotalMean, kernels->fResTotalStdev);
+
+        psFree (fResSigma);
+        psFree (fResOuter);
+        psFree (fResTotal);
+        psFree (stats);
+    }
+
+    psFree(residual);
+    psFree(polyValues);
+
+    return deviations;
+}
+
+// we are supplied U, not Ut; w represents a diagonal matrix (also, we apply 1/w instead of w)
+psImage *p_pmSubSolve_wUt (psVector *w, psImage *U) {
+
+    psAssert (w->n == U->numCols, "w and U dimensions do not match");
+
+    // wUt has dimensions transposed relative to Ut.
+    psImage *wUt = psImageAlloc (U->numRows, U->numCols, PS_TYPE_F64);
+    psImageInit (wUt, 0.0);
+
+    for (int i = 0; i < wUt->numCols; i++) {
+        for (int j = 0; j < wUt->numRows; j++) {
+            if (!isfinite(w->data.F64[j])) continue;
+            if (w->data.F64[j] == 0.0) continue;
+            wUt->data.F64[j][i] = U->data.F64[i][j] / w->data.F64[j];
+        }
+    }
+    return wUt;
+}
+
+// XXX this is just standard matrix multiplication: use psMatrixMultiply?
+psImage *p_pmSubSolve_VwUt (psImage *V, psImage *wUt) {
+
+    psAssert (V->numCols == wUt->numRows, "matrix dimensions do not match");
+
+    psImage *Ainv = psImageAlloc (wUt->numCols, V->numRows, PS_TYPE_F64);
+
+    for (int i = 0; i < Ainv->numCols; i++) {
+        for (int j = 0; j < Ainv->numRows; j++) {
+            double sum = 0.0;
+            for (int k = 0; k < V->numCols; k++) {
+                sum += V->data.F64[j][k] * wUt->data.F64[k][i];
+            }
+            Ainv->data.F64[j][i] = sum;
+        }
+    }
+    return Ainv;
+}
+
+// we are supplied U, not Ut
+bool p_pmSubSolve_UtB (psVector **UtB, psImage *U, psVector *B) {
+
+    psAssert (U->numRows == B->n, "U and B dimensions do not match");
+
+    UtB[0] = psVectorRecycle (UtB[0], U->numCols, PS_TYPE_F64);
+
+    for (int i = 0; i < U->numCols; i++) {
+        double sum = 0.0;
+        for (int j = 0; j < U->numRows; j++) {
+            sum += B->data.F64[j] * U->data.F64[j][i];
+        }
+        UtB[0]->data.F64[i] = sum;
+    }
+    return true;
+}
+
+// w is diagonal
+bool p_pmSubSolve_wUtB (psVector **wUtB, psVector *w, psVector *UtB) {
+
+    psAssert (w->n == UtB->n, "w and UtB dimensions do not match");
+
+    // wUt has dimensions transposed relative to Ut.
+    wUtB[0] = psVectorRecycle (wUtB[0], w->n, PS_TYPE_F64);
+    psVectorInit (wUtB[0], 0.0);
+
+    for (int i = 0; i < w->n; i++) {
+        if (!isfinite(w->data.F64[i])) continue;
+        if (w->data.F64[i] == 0.0) continue;
+        wUtB[0]->data.F64[i] = UtB->data.F64[i] / w->data.F64[i];
+    }
+    return true;
+}
+
+// this is basically matrix * vector
+bool p_pmSubSolve_VwUtB (psVector **VwUtB, psImage *V, psVector *wUtB) {
+
+    psAssert (V->numCols == wUtB->n, "V and wUtB dimensions do not match");
+
+    VwUtB[0] = psVectorRecycle (*VwUtB, V->numRows, PS_TYPE_F64);
+
+    for (int j = 0; j < V->numRows; j++) {
+        double sum = 0.0;
+        for (int i = 0; i < V->numCols; i++) {
+            sum += V->data.F64[j][i] * wUtB->data.F64[i];
+        }
+        VwUtB[0]->data.F64[j] = sum;
+    }
+    return true;
+}
+
+// this is basically matrix * vector
+bool p_pmSubSolve_Ax (psVector **B, psImage *A, psVector *x) {
+
+    psAssert (A->numCols == x->n, "A and x dimensions do not match");
+
+    B[0] = psVectorRecycle (*B, A->numRows, PS_TYPE_F64);
+
+    for (int j = 0; j < A->numRows; j++) {
+        double sum = 0.0;
+        for (int i = 0; i < A->numCols; i++) {
+            sum += A->data.F64[j][i] * x->data.F64[i];
+        }
+        B[0]->data.F64[j] = sum;
+    }
+    return true;
+}
+
+// this is basically Vector * vector
+bool p_pmSubSolve_VdV (double *value, psVector *x, psVector *y) {
+
+    psAssert (x->n == y->n, "x and y dimensions do not match");
+
+    double sum = 0.0;
+    for (int i = 0; i < x->n; i++) {
+        sum += x->data.F64[i] * y->data.F64[i];
+    }
+    *value = sum;
+    return true;
+}
+
+bool p_pmSubSolve_y2 (double *y2, pmSubtractionKernels *kernels, const pmSubtractionStampList *stamps) {
+
+    int footprint = stamps->footprint; // Half-size of stamps
+
+    double sum = 0.0;
+    for (int i = 0; i < stamps->num; i++) {
+
+        pmSubtractionStamp *stamp = stamps->stamps->data[i];
+        if (stamp->status != PM_SUBTRACTION_STAMP_USED) continue;
+
+        psKernel *weight = NULL;
+        psKernel *window = NULL;
+        psKernel *input = NULL;
+
+#ifdef USE_WEIGHT
+        weight = stamp->weight;
+#endif
+#ifdef USE_WINDOW
+        window = stamps->window;
+#endif
+
+        switch (kernels->mode) {
+            // MODE_1 : convolve image 1 to match image 2 (and vice versa)
+          case PM_SUBTRACTION_MODE_1:
+            input = stamp->image2;
+            break;
+          case PM_SUBTRACTION_MODE_2:
+            input = stamp->image1;
+            break;
+          default:
+            psAbort ("programming error");
+        }
+
+        for (int y = - footprint; y <= footprint; y++) {
+            for (int x = - footprint; x <= footprint; x++) {
+                double in = input->kernel[y][x];
+                double value = in*in;
+                if (weight) {
+                    float wtVal = weight->kernel[y][x];
+                    value *= wtVal;
+                }
+                if (window) {
+                    float  winVal = window->kernel[y][x];
+                    value *= winVal;
+                }
+                sum += value;
+            }
+        }
+    }
+    *y2 = sum;
+    return true;
+}
+
+double p_pmSubSolve_ChiSquare (pmSubtractionKernels *kernels, const pmSubtractionStampList *stamps) {
+
+    int footprint = stamps->footprint; // Half-size of stamps
+    int numKernels = kernels->num;      // Number of kernels
+
+    double sum = 0.0;
+
+    psKernel *residual = psKernelAlloc(-footprint, footprint, -footprint, footprint); // Residual image
+    psImageInit(residual->image, 0.0);
+
+    psImage *polyValues = NULL;         // Polynomial values
+
+    for (int i = 0; i < stamps->num; i++) {
+
+        pmSubtractionStamp *stamp = stamps->stamps->data[i];
+        if (stamp->status != PM_SUBTRACTION_STAMP_USED) continue;
+
+        psKernel *weight = NULL;
+        psKernel *window = NULL;
+        psKernel *target = NULL;
+        psKernel *source = NULL;
+
+        psArray *convolutions = NULL;
+
+#ifdef USE_WEIGHT
+        weight = stamp->weight;
+#endif
+#ifdef USE_WINDOW
+        window = stamps->window;
+#endif
+
+        switch (kernels->mode) {
+            // MODE_1 : convolve image 1 to match image 2 (and vice versa)
+          case PM_SUBTRACTION_MODE_1:
+            target = stamp->image2;
+            source = stamp->image1;
+            convolutions = stamp->convolutions1;
+            break;
+          case PM_SUBTRACTION_MODE_2:
+            target = stamp->image1;
+            source = stamp->image2;
+            convolutions = stamp->convolutions2;
+            break;
+          default:
+            psAbort ("programming error");
+        }
+
+        // Calculate coefficients of the kernel basis functions
+        polyValues = p_pmSubtractionPolynomial(polyValues, kernels->spatialOrder, stamp->xNorm, stamp->yNorm);
+        double norm = p_pmSubtractionSolutionNorm(kernels); // Normalisation
+        double background = p_pmSubtractionSolutionBackground(kernels, polyValues);// Difference in background
+
+        psImageInit(residual->image, 0.0);
+        for (int j = 0; j < numKernels; j++) {
+            psKernel *convolution = convolutions->data[j]; // Convolution
+            double coefficient = p_pmSubtractionSolutionCoeff(kernels, polyValues, j, false); // Coefficient
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    residual->kernel[y][x] -= convolution->kernel[y][x] * coefficient;
+                }
+            }
+        }
+
+        for (int y = - footprint; y <= footprint; y++) {
+            for (int x = - footprint; x <= footprint; x++) {
+                double resid = target->kernel[y][x] - background - source->kernel[y][x] * norm + residual->kernel[y][x];
+                double value = PS_SQR(resid);
+                if (weight) {
+                    float wtVal = weight->kernel[y][x];
+                    value *= wtVal;
+                }
+                if (window) {
+                    float  winVal = window->kernel[y][x];
+                    value *= winVal;
+                }
+                sum += value;
+            }
+        }
+    }
+    psFree (polyValues);
+    psFree (residual);
+
+    return sum;
+}
+
+bool p_pmSubSolve_SetWeights (psVector *wApply, psVector *w, psVector *wMask) {
+
+    for (int i = 0; i < w->n; i++) {
+        wApply->data.F64[i] = wMask->data.U8[i] ? 0.0 : w->data.F64[i];
+    }
+    return true;
+}
+
+// we are supplied V and w; w represents a diagonal matrix (also, we apply 1/w instead of w)
+psImage *p_pmSubSolve_Xvar (psImage *V, psVector *w) {
+
+    psAssert (w->n == V->numCols, "w and U dimensions do not match");
+
+    psImage *Vn = psImageAlloc (V->numCols, V->numRows, PS_TYPE_F64);
+    psImageInit (Vn, 0.0);
+
+    // generate Vn = V * w^{-1}
+    for (int j = 0; j < Vn->numRows; j++) {
+        for (int i = 0; i < Vn->numCols; i++) {
+            if (!isfinite(w->data.F64[i])) continue;
+            if (w->data.F64[i] == 0.0) continue;
+            Vn->data.F64[j][i] = V->data.F64[j][i] / w->data.F64[i];
+        }
+    }
+
+    psImage *Xvar = psImageAlloc (V->numCols, V->numRows, PS_TYPE_F64);
+    psImageInit (Xvar, 0.0);
+
+    // generate Xvar = Vn * Vn^T
+    for (int j = 0; j < Vn->numRows; j++) {
+        for (int i = 0; i < Vn->numCols; i++) {
+            double sum = 0.0;
+            for (int k = 0; k < Vn->numCols; k++) {
+                sum += Vn->data.F64[k][i]*Vn->data.F64[k][j];
+            }
+            Xvar->data.F64[j][i] = sum;
+        }
+    }
+    return Xvar;
+}
+
+// I get confused by the index values between the image vs matrix usage:  In terms
+// of the elements of an image A(x,y) = A->data.F64[y][x] = A_x,y, a matrix
+// multiplication is: A_k,j * B_i,k = C_i,j
+
+
+bool psFitsWriteImageSimple (char *filename, psImage *image, psMetadata *header) {
+
+    psFits *fits = psFitsOpen(filename, "w");
+    psFitsWriteImage(fits, header, image, 0, NULL);
+    psFitsClose(fits);
+
+    return true;
+}
+
+bool psVectorWriteFile (char *filename, const psVector *vector) {
+
+    FILE *f = fopen (filename, "w");
+    int fd = fileno(f);
+    p_psVectorPrint (fd, vector, "unnamed");
+    fclose (f);
+
+    return true;
+}
+
+
+# if 0
+
+#ifdef TESTING
+        psFitsWriteImageSimple("A.fits", sumMatrix, NULL);
+        psVectorWriteFile ("B.dat", sumVector);
+#endif
+
+# define SVD_ANALYSIS 0
+# define COEFF_SIG 0.0
+# define SVD_TOL 0.0
+
+        // Use SVD to determine the kernel coeffs (and validate)
+        if (SVD_ANALYSIS) {
+
+            // We have sumVector and sumMatrix.  we are trying to solve the following equation:
+            // sumMatrix * x = sumVector.
+
+            // we can use any standard matrix inversion to solve this.  However, the basis
+            // functions in general have substantial correlation, so that the solution may be
+            // somewhat poorly determined or unstable.  If not numerically ill-conditioned, the
+            // system of equations may be statistically ill-conditioned.  Noise in the image
+            // will drive insignificant, but correlated, terms in the solution.  To avoid these
+            // problems, we can use SVD to identify numerically unconstrained values and to
+            // avoid statistically badly determined value.
+
+            // A = sumMatrix, B = sumVector
+            // SVD: A = U w V^T  -> A^{-1} = V (1/w) U^T
+            // x = V (1/w) (U^T B)
+            // \sigma_x = sqrt(diag(A^{-1}))
+            // solve for x and A^{-1} to get x & dx
+            // identify the elements of (1/w) that are nan (1/0.0) -> set to 0.0
+            // identify the elements of x that are insignificant (x / dx < 1.0? < 0.5?) -> set to 0.0
+
+            // If I use the SVD trick to re-condition the matrix, I need to break out the
+            // kernel and normalization terms from the background term.
+            // XXX is this true?  or was this due to an error in the analysis?
+
+            int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index in matrix for background
+
+            // now pull out the kernel elements into their own square matrix
+            psImage  *kernelMatrix = psImageAlloc  (sumMatrix->numCols - 1, sumMatrix->numRows - 1, PS_TYPE_F64);
+            psVector *kernelVector = psVectorAlloc (sumMatrix->numCols - 1, PS_TYPE_F64);
+
+            for (int ix = 0, kx = 0; ix < sumMatrix->numCols; ix++) {
+                if (ix == bgIndex) continue;
+                for (int iy = 0, ky = 0; iy < sumMatrix->numRows; iy++) {
+                    if (iy == bgIndex) continue;
+                    kernelMatrix->data.F64[ky][kx] = sumMatrix->data.F64[iy][ix];
+                    ky++;
+                }
+                kernelVector->data.F64[kx] = sumVector->data.F64[ix];
+                kx++;
+            }
+
+            psImage *U = NULL;
+            psImage *V = NULL;
+            psVector *w = NULL;
+            if (!psMatrixSVD (&U, &w, &V, kernelMatrix)) {
+                psError(psErrorCodeLast(), false, "failed to perform SVD on sumMatrix\n");
+                return NULL;
+            }
+
+            // calculate A_inverse:
+            // Ainv = V * w * U^T
+            psImage *wUt  = p_pmSubSolve_wUt (w, U);
+            psImage *Ainv = p_pmSubSolve_VwUt (V, wUt);
+            psImage *Xvar = NULL;
+            psFree (wUt);
+
+# ifdef TESTING
+            // kernel terms:
+            for (int i = 0; i < w->n; i++) {
+                fprintf (stderr, "w: %f\n", w->data.F64[i]);
+            }
+# endif
+            // loop over w adding in more and more of the values until chisquare is no longer
+            // dropping significantly.
+            // XXX this does not seem to work very well: we seem to need all terms even for
+            // simple cases...
+
+            psVector *Xsvd = NULL;
+            {
+                psVector *Ax = NULL;
+                psVector *UtB = NULL;
+                psVector *wUtB = NULL;
+
+                psVector *wApply = psVectorAlloc(w->n, PS_TYPE_F64);
+                psVector *wMask = psVectorAlloc(w->n, PS_TYPE_U8);
+                psVectorInit (wMask, 1); // start by masking everything
+
+                double chiSquareLast = NAN;
+                int maxWeight = 0;
+
+                double Axx, Bx, y2;
+
+                // XXX this is an attempt to exclude insignificant modes.
+                // it was not successful with the ISIS kernel set: removing even
+                // the least significant mode leaves additional ringing / noise
+                // because the terms are so coupled.
+                for (int k = 0; false && (k < w->n); k++) {
+
+                    // unmask the k-th weight
+                    wMask->data.U8[k] = 0;
+                    p_pmSubSolve_SetWeights(wApply, w, wMask);
+
+                    // solve for x:
+                    // x = V * w * (U^T * B)
+                    p_pmSubSolve_UtB (&UtB, U, kernelVector);
+                    p_pmSubSolve_wUtB (&wUtB, wApply, UtB);
+                    p_pmSubSolve_VwUtB (&Xsvd, V, wUtB);
+
+                    // chi-square for this system of equations:
+                    // chi-square = sum over terms of: (Ax - B)*x - b*x - y^2
+                    // y^2 = \sum_stamps \sum_pixels input->kernel[y][x]^2
+                    p_pmSubSolve_Ax (&Ax, kernelMatrix, Xsvd);
+                    p_pmSubSolve_VdV (&Axx, Ax, Xsvd);
+                    p_pmSubSolve_VdV (&Bx, kernelVector, Xsvd);
+                    p_pmSubSolve_y2 (&y2, kernels, stamps);
+
+                    // apparently, this works (compare with the brute force value below
+                    double chiSquare = Axx - 2.0*Bx + y2;
+                    double deltaChi = (k == 0) ? chiSquare : chiSquareLast - chiSquare;
+                    chiSquareLast = chiSquare;
+
+                    // fprintf (stderr, "chi square = %f, delta: %f\n", chiSquare, deltaChi);
+                    if (k && !maxWeight && (deltaChi < 1.0)) {
+                        maxWeight = k;
+                    }
+                }
+
+                // keep all terms or we get extra ringing
+                maxWeight = w->n;
+                psVectorInit (wMask, 1);
+                for (int k = 0; k < maxWeight; k++) {
+                    wMask->data.U8[k] = 0;
+                }
+                p_pmSubSolve_SetWeights(wApply, w, wMask);
+
+                // solve for x:
+                // x = V * w * (U^T * B)
+                p_pmSubSolve_UtB (&UtB, U, kernelVector);
+                p_pmSubSolve_wUtB (&wUtB, wApply, UtB);
+                p_pmSubSolve_VwUtB (&Xsvd, V, wUtB);
+
+                // chi-square for this system of equations:
+                // chi-square = sum over terms of: (Ax - B)*x - b*x - y^2
+                // y^2 = \sum_stamps \sum_pixels input->kernel[y][x]^2
+                p_pmSubSolve_Ax (&Ax, kernelMatrix, Xsvd);
+                p_pmSubSolve_VdV (&Axx, Ax, Xsvd);
+                p_pmSubSolve_VdV (&Bx, kernelVector, Xsvd);
+                p_pmSubSolve_y2 (&y2, kernels, stamps);
+
+                // apparently, this works (compare with the brute force value below
+                double chiSquare = Axx - 2.0*Bx + y2;
+                psLogMsg ("psModules.imcombine", PS_LOG_INFO, "model kernel with %d terms; chi square = %f\n", maxWeight, chiSquare);
+
+                // re-calculate A^{-1} to get new variances:
+                // Ainv = V * w * U^T
+                // XXX since we keep all terms, this is identical to Ainv
+                psImage *wUt  = p_pmSubSolve_wUt (wApply, U);
+                Xvar = p_pmSubSolve_VwUt (V, wUt);
+                psFree (wUt);
+
+                psFree (Ax);
+                psFree (UtB);
+                psFree (wUtB);
+                psFree (wApply);
+                psFree (wMask);
+            }
+
+            // copy the kernel solutions to the full solution vector:
+            solution = psVectorAlloc(sumVector->n, PS_TYPE_F64);
+            solutionErr = psVectorAlloc(sumVector->n, PS_TYPE_F64);
+
+            for (int ix = 0, kx = 0; ix < sumVector->n; ix++) {
+                if (ix == bgIndex) {
+                    solution->data.F64[ix] = 0;
+                    solutionErr->data.F64[ix] = 0.001;
+                    continue;
+                }
+                solutionErr->data.F64[ix] = sqrt(Ainv->data.F64[kx][kx]);
+                solution->data.F64[ix] = Xsvd->data.F64[kx];
+                kx++;
+            }
+
+            psFree (kernelMatrix);
+            psFree (kernelVector);
+
+            psFree (U);
+            psFree (V);
+            psFree (w);
+
+            psFree (Ainv);
+            psFree (Xsvd);
+        } else {
+            psVector *permutation = NULL;       // Permutation vector, required for LU decomposition
+            psImage *luMatrix = psMatrixLUDecomposition(NULL, &permutation, sumMatrix);
+            if (!luMatrix) {
+                psError(PM_ERR_DATA, true, "LU Decomposition of least-squares matrix failed.\n");
+                psFree(solution);
+                psFree(sumVector);
+                psFree(sumMatrix);
+                psFree(luMatrix);
+                psFree(permutation);
+                return NULL;
+            }
+
+            solution = psMatrixLUSolution(NULL, luMatrix, sumVector, permutation);
+            psFree(luMatrix);
+            psFree(permutation);
+            if (!solution) {
+                psError(PM_ERR_DATA, true, "Failed to solve the least-squares system.\n");
+                psFree(solution);
+                psFree(sumVector);
+                psFree(sumMatrix);
+                return NULL;
+            }
+
+            // XXX LUD does not provide A^{-1}?  fake the error for now
+            solutionErr = psVectorAlloc(sumVector->n, PS_TYPE_F64);
+            for (int ix = 0; ix < sumVector->n; ix++) {
+                solutionErr->data.F64[ix] = 0.1*solution->data.F64[ix];
+            }
+        }
+
+        if (!kernels->solution1) {
+            kernels->solution1 = psVectorAlloc (sumVector->n, PS_TYPE_F64);
+            psVectorInit (kernels->solution1, 0.0);
+        }
+
+        // only update the solutions that we chose to calculate:
+        if (mode & PM_SUBTRACTION_EQUATION_NORM) {
+            int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+            kernels->solution1->data.F64[normIndex] = solution->data.F64[normIndex];
+        }
+        if (mode & PM_SUBTRACTION_EQUATION_BG) {
+            int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index in matrix for background
+            kernels->solution1->data.F64[bgIndex] = solution->data.F64[bgIndex];
+        }
+        if (mode & PM_SUBTRACTION_EQUATION_KERNELS) {
+            int numKernels = kernels->num;
+            int spatialOrder = kernels->spatialOrder;       // Order of spatial variation
+            int numPoly = PM_SUBTRACTION_POLYTERMS(spatialOrder); // Number of polynomial terms
+            for (int i = 0; i < numKernels * numPoly; i++) {
+                // XXX fprintf (stderr, "%f +/- %f (%f) -> ", solution->data.F64[i], solutionErr->data.F64[i], fabs(solution->data.F64[i]/solutionErr->data.F64[i]));
+                if (fabs(solution->data.F64[i] / solutionErr->data.F64[i]) < COEFF_SIG) {
+                    // XXX fprintf (stderr, "drop\n");
+                    kernels->solution1->data.F64[i] = 0.0;
+                } else {
+                    // XXX fprintf (stderr, "keep\n");
+                    kernels->solution1->data.F64[i] = solution->data.F64[i];
+                }
+            }
+        }
+        // double chiSquare = p_pmSubSolve_ChiSquare (kernels, stamps);
+        // fprintf (stderr, "chi square Brute = %f\n", chiSquare);
+
+        psFree(solution);
+        psFree(sumVector);
+        psFree(sumMatrix);
+# endif
+
+#ifdef TESTING
+              // XXX double-check for NAN in data:
+                for (int iy = 0; iy < stamp->matrix->numRows; iy++) {
+                    for (int ix = 0; ix < stamp->matrix->numCols; ix++) {
+                        if (!isfinite(stamp->matrix->data.F64[iy][ix])) {
+                            fprintf (stderr, "WARNING: NAN in matrix\n");
+                        }
+                    }
+                }
+                for (int ix = 0; ix < stamp->vector->n; ix++) {
+                    if (!isfinite(stamp->vector->data.F64[ix])) {
+                        fprintf (stderr, "WARNING: NAN in vector\n");
+                    }
+                }
+#endif
+
+#ifdef TESTING
+        for (int ix = 0; ix < sumVector->n; ix++) {
+            if (!isfinite(sumVector->data.F64[ix])) {
+                fprintf (stderr, "WARNING: NAN in vector\n");
+            }
+        }
+#endif
+
+#ifdef TESTING
+        for (int ix = 0; ix < sumVector->n; ix++) {
+            if (!isfinite(sumVector->data.F64[ix])) {
+                fprintf (stderr, "WARNING: NAN in vector\n");
+            }
+        }
+        {
+            psImage *inverse = psMatrixInvert(NULL, sumMatrix, NULL);
+            psFitsWriteImageSimple("matrixInv.fits", inverse, NULL);
+            psFree(inverse);
+        }
+        {
+            psImage *X = psMatrixInvert(NULL, sumMatrix, NULL);
+            psImage *Xt = psMatrixTranspose(NULL, X);
+            psImage *XtX = psMatrixMultiply(NULL, Xt, X);
+            psFitsWriteImageSimple("matrixErr.fits", XtX, NULL);
+            psFree(X);
+            psFree(Xt);
+            psFree(XtX);
+        }
+#endif
+
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionHermitian.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionHermitian.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionHermitian.c	(revision 42651)
@@ -0,0 +1,130 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+
+#include "pmSubtractionTypes.h"
+#include "pmSubtractionHermitian.h"
+
+double p_pmSubtractionHermitianPolynomial (double x, int order) {
+  double value;
+    switch (order) {
+      case 0:
+	value = p_pmSubtractionHermitianPolynomial_00(x);
+	break;
+      case 1:
+	value = p_pmSubtractionHermitianPolynomial_01(x);
+	break;
+      case 2:
+	value = p_pmSubtractionHermitianPolynomial_02(x);
+	break;
+      case 3:
+	value = p_pmSubtractionHermitianPolynomial_03(x);
+	break;
+      case 4:
+	value = p_pmSubtractionHermitianPolynomial_04(x);
+	break;
+      case 5:
+	value = p_pmSubtractionHermitianPolynomial_05(x);
+	break;
+      case 6:
+	value = p_pmSubtractionHermitianPolynomial_06(x);
+	break;
+      case 7:
+	value = p_pmSubtractionHermitianPolynomial_07(x);
+	break;
+      case 8:
+	value = p_pmSubtractionHermitianPolynomial_08(x);
+	break;
+      case 9:
+	value = p_pmSubtractionHermitianPolynomial_09(x);
+	break;
+      case 10:
+	value = p_pmSubtractionHermitianPolynomial_10(x);
+	break;
+      default:
+	value = NAN;
+	break;
+    }
+    return value;
+}
+
+double p_pmSubtractionHermitianPolynomial_00(double x) {
+    double value;
+    // H_0(x) = 1
+    value = 1;
+    return value;
+}
+double p_pmSubtractionHermitianPolynomial_01(double x) {
+    double value;
+    // H_1(x) = x
+    value = x;
+    return value;
+}
+double p_pmSubtractionHermitianPolynomial_02(double x) {
+    double value, x2;
+    // H_2(x) = x^2-1
+    x2 = x*x;
+    value = x2 - 1.0;
+    return value;
+}
+double p_pmSubtractionHermitianPolynomial_03(double x) {
+    double value, x2;
+    // H_3(x) = x^3-3x
+    x2 = x*x;
+    value = x*(x2 - 3.0);
+    return value;
+}
+double p_pmSubtractionHermitianPolynomial_04(double x) {
+    double value, x2;
+    // H_4(x) = x^4-6x^2+3
+    x2 = x*x;
+    value = (x2 - 6.0)*x2 + 3.0;
+    return value;
+}
+double p_pmSubtractionHermitianPolynomial_05(double x) {
+    double value, x2;
+    // H_5(x) = x^5-10x^3+15x
+    x2 = x*x;
+    value = ((x2 - 10.0)*x2 + 15.0)*x;
+    return value;
+}
+double p_pmSubtractionHermitianPolynomial_06(double x) {
+    double value, x2;
+    // H_6(x) = x^6-15x^4+45x^2-15
+    x2 = x*x;
+    value = (((x2 - 15.0)*x2 + 45.0)*x2) - 15.0;
+    return value;
+}
+double p_pmSubtractionHermitianPolynomial_07(double x) {
+    double value, x2;
+    // H_7(x) = x^7-21x^5+105x^3-105x
+    x2 = x*x;
+    value = (((x2 - 21.0)*x2+105.0)*x2 - 105.0)*x;
+    return value;
+}
+double p_pmSubtractionHermitianPolynomial_08(double x) {
+    double value, x2;
+    // H_8(x) = x^8-28x^6+210x^4-420x^2+105
+    x2 = x*x;
+    value = ((((x2 - 28.0)*x2 + 210.0)*x2 - 420.0)*x2 + 105.0);
+    return value;
+}
+double p_pmSubtractionHermitianPolynomial_09(double x) {
+    double value, x2;
+    // H_9(x) = x^9-36x^7+378x^5-1260x^3+945x
+    x2 = x*x;
+    value = ((((x2 - 36.0)*x2 + 378.0)*x2 - 1260.0)*x2 + 945.0)*x;
+    return value;
+}
+double p_pmSubtractionHermitianPolynomial_10(double x) {
+    double value, x2;
+    // H_{10}(x) = x^{10}-45x^8+630x^6-3150x^4+4725x^2-945 
+    x2 = x*x;
+    value = (((((x2 - 45.0)*x2 + 630.0)*x2 - 3150.0)*x2 + 4725.0)*x2 - 945.0);
+    return value;
+}
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionHermitian.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionHermitian.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionHermitian.h	(revision 42651)
@@ -0,0 +1,28 @@
+#ifndef PM_SUBTRACTION_HERMITIAN_H
+#define PM_SUBTRACTION_HERMITIAN_H
+
+double p_pmSubtractionHermitianPolynomial (double x, int order);
+
+double p_pmSubtractionHermitianPolynomial_00(double x);
+
+double p_pmSubtractionHermitianPolynomial_01(double x);
+
+double p_pmSubtractionHermitianPolynomial_02(double x);
+
+double p_pmSubtractionHermitianPolynomial_03(double x);
+
+double p_pmSubtractionHermitianPolynomial_04(double x);
+
+double p_pmSubtractionHermitianPolynomial_05(double x);
+
+double p_pmSubtractionHermitianPolynomial_06(double x);
+
+double p_pmSubtractionHermitianPolynomial_07(double x);
+
+double p_pmSubtractionHermitianPolynomial_08(double x);
+
+double p_pmSubtractionHermitianPolynomial_09(double x);
+
+double p_pmSubtractionHermitianPolynomial_10(double x);
+
+# endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionIO.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionIO.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionIO.c	(revision 42651)
@@ -0,0 +1,612 @@
+#include <stdio.h>
+#include <pslib.h>
+#include <string.h>
+
+#include "pmErrorCodes.h"
+#include "pmHDU.h"
+#include "pmHDUUtils.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPAfileFitsIO.h"
+#include "pmConceptsRead.h"
+
+#include "pmSubtractionTypes.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionKernels.h"
+#include "pmSubtractionMatch.h"
+#include "pmSubtractionAnalysis.h"
+
+#include "pmSubtractionIO.h"
+#include "pmStackVisual.h"
+
+#define ARRAY_BUFFER 16                 // Number to add to array at a time
+
+// Names of FITS table columns
+#define NAME_XMIN "XMIN"                // Region of applicability: minimum x value
+#define NAME_XMAX "XMAX"                // Region of applicability: maximum x value
+#define NAME_YMIN "YMIN"                // Region of applicability: minimum y value
+#define NAME_YMAX "YMAX"                // Region of applicability: maximum y value
+#define NAME_KERNEL "KERNEL"            // Kernel description
+#define NAME_TYPE "TYPE"                // Kernel type
+#define NAME_SIZE "SIZE"                // Kernel half-size
+#define NAME_INNER "INNER"              // Size of inner region (only applicable for some kernel types)
+#define NAME_SPATIAL "SPATIAL_ORDER"    // Order of spatial polynomial
+#define NAME_BG "BG_ORDER"              // Order of background polynomial
+#define NAME_MODE "MODE"                // Matching mode
+#define NAME_COLS "COLUMNS"             // Number of columns
+#define NAME_ROWS "ROWS"                // Number of rows
+#define NAME_SOL1 "SOLUTION_1"          // Solution for convolving image 1
+#define NAME_SOL2 "SOLUTION_2"          // Solution for convolving image 2
+#define NAME_MEAN "MEAN"                // Mean of chi^2 from stamps
+#define NAME_RMS  "RMS"                 // RMS of chi^2 from stamps
+#define NAME_NUMSTAMPS "NUMSTAMPS"      // Number of good stamps
+
+#define EXTNAME_KERNEL "SUBTRACTION_KERNEL"    // Extension name for kernel
+#define EXTNAME_IMAGE  "SUBTRACTION_KERNEL_IMAGE"    // Extension name for image
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmReadoutWriteSubtractionKernels(pmReadout *ro, psFits *fits)
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    // Extract the regions and solutions used in the image matching
+    psArray *regions = psArrayAllocEmpty(ARRAY_BUFFER); // Array of regions
+    {
+        psString regex = NULL;          // Regular expression
+        psStringAppend(&regex, "^%s$", PM_SUBTRACTION_ANALYSIS_REGION);
+        psMetadataIterator *iter = psMetadataIteratorAlloc(ro->analysis, PS_LIST_HEAD, regex); // Iterator
+        psFree(regex);
+        psMetadataItem *item = NULL;// Item from iteration
+        while ((item = psMetadataGetAndIncrement(iter))) {
+            assert(item->type == PS_DATA_REGION);
+            regions = psArrayAdd(regions, ARRAY_BUFFER, item->data.V);
+        }
+        psFree(iter);
+    }
+    if (regions->n == 0) {
+        // We wrote everything we could find
+        psFree(regions);
+        return true;
+    }
+
+    psArray *kernels = psArrayAllocEmpty(ARRAY_BUFFER); // Array of kernels
+    {
+        psString regex = NULL;          // Regular expression
+        psStringAppend(&regex, "^%s$", PM_SUBTRACTION_ANALYSIS_KERNEL);
+        psMetadataIterator *iter = psMetadataIteratorAlloc(ro->analysis, PS_LIST_HEAD, regex); // Iterator
+        psFree(regex);
+        psMetadataItem *item = NULL;// Item from iteration
+        while ((item = psMetadataGetAndIncrement(iter))) {
+            assert(item->type == PS_DATA_UNKNOWN);
+            pmSubtractionKernels *kernel = item->data.V; // Kernel used in subtraction
+            kernels = psArrayAdd(kernels, ARRAY_BUFFER, kernel);
+        }
+        psFree(iter);
+    }
+
+    if (regions->n != kernels->n) {
+        psError(PM_ERR_PROG, true, "Number of regions (%ld) and kernels (%ld) don't match.\n",
+                regions->n, kernels->n);
+        psFree(regions);
+        psFree(kernels);
+        return false;
+    }
+
+    // Format for writing a table
+    int num = regions->n;              // Number of regions and kernels
+    psArray *rows = psArrayAlloc(num); // Array of FITS table rows
+    for (int i = 0; i < num; i++) {
+        psMetadata *row = rows->data[i] = psMetadataAlloc(); // Row of interest
+
+        psRegion *region = regions->data[i]; // Region of interest
+        psMetadataAddS32(row, PS_LIST_TAIL, NAME_XMIN,  0, "Applicability minimum x", region->x0);
+        psMetadataAddS32(row, PS_LIST_TAIL, NAME_XMAX,  0, "Applicability maximum x", region->x1);
+        psMetadataAddS32(row, PS_LIST_TAIL, NAME_YMIN,  0, "Applicability minimum y", region->y0);
+        psMetadataAddS32(row, PS_LIST_TAIL, NAME_YMAX,  0, "Applicability maximum y", region->y1);
+
+        pmSubtractionKernels *kernel = kernels->data[i]; // Kernel
+        psMetadataAddStr(row, PS_LIST_TAIL, NAME_KERNEL, 0, "Kernel description", kernel->description);
+
+#if 0
+        psMetadataAddS32(row, PS_LIST_TAIL, NAME_TYPE,  0, "Kernel type (enum)", kernel->type);
+        psMetadataAddS32(row, PS_LIST_TAIL, NAME_SIZE,  0, "Kernel half-size", kernel->size);
+        psMetadataAddS32(row, PS_LIST_TAIL, NAME_INNER, 0, "Size of inner region", kernel->inner);
+        psMetadataAddS32(row, PS_LIST_TAIL, NAME_SPATIAL, 0, "Polynomial order for spatial variations",
+                         kernel->spatialOrder);
+#endif
+
+        psMetadataAddS32(row, PS_LIST_TAIL, NAME_BG,  0, "Polynomial order for background fitting",
+                         kernel->bgOrder);
+        psMetadataAddS32(row, PS_LIST_TAIL, NAME_MODE,  0, "Matching mode (enum)", kernel->mode);
+        if (kernel->mode == PM_SUBTRACTION_MODE_1 || kernel->mode == PM_SUBTRACTION_MODE_2) {
+            psMetadataAddVector(row, PS_LIST_TAIL, NAME_SOL1, 0, "Solution vector 1", kernel->solution1);
+        }
+        if (kernel->mode == PM_SUBTRACTION_MODE_DUAL) {
+            psMetadataAddVector(row, PS_LIST_TAIL, NAME_SOL1, 0, "Solution vector 1", kernel->solution1);
+            psMetadataAddVector(row, PS_LIST_TAIL, NAME_SOL2, 0, "Solution vector 2", kernel->solution2);
+        }
+        psMetadataAddF32(row, PS_LIST_TAIL, NAME_MEAN,  0, "Mean of chi^2 from stamps", kernel->mean);
+        psMetadataAddF32(row, PS_LIST_TAIL, NAME_RMS,  0, "RMS of chi^2 from stamps", kernel->rms);
+        psMetadataAddS32(row, PS_LIST_TAIL, NAME_NUMSTAMPS,  0, "Number of good stamps", kernel->numStamps);
+    }
+    psFree(regions);
+    psFree(kernels);
+
+    psMetadata *header = psMetadataAlloc(); // Header for FITS file
+
+    pmCell *cell = ro->parent;          // Cell of interest
+    if (cell) {
+        pmChip *chip = cell->parent;    // Chip of interest
+        pmFPA *fpa = chip->parent;      // FPA of interest
+        pmHDU *hdu = pmHDUGetHighest(fpa, chip, cell); // HDU for readout
+        if (hdu) {
+            header = psMetadataCopy(header, hdu->header);
+        }
+    }
+
+    // CVS tags, used to identify the version of this file (in case incompatibilities are introduced)
+    psString cvsFile = psStringCopy("$RCSfile: pmSubtractionIO.c,v $");
+    psString cvsRev  = psStringCopy("$Revision: 1.9.18.1 $");
+    psString cvsDate = psStringCopy("$Date: 2009-02-19 17:59:50 $");
+    psStringSubstitute(&cvsFile, NULL, "RCSfile: ");
+    psStringSubstitute(&cvsRev,  NULL, "Revision: ");
+    psStringSubstitute(&cvsDate, NULL, "Date: ");
+
+    psString version = NULL;            // Version information, for header
+    psStringAppend(&version, "%s %s %s", cvsFile, cvsRev, cvsDate);
+    psFree(cvsFile);
+    psFree(cvsRev);
+    psFree(cvsDate);
+    psStringSubstitute(&version, NULL, "$");
+    psMetadataAddStr(header, PS_LIST_TAIL, "PSVERSION", 0, "S/W version", version);
+    psFree(version);
+
+    if (!psFitsWriteTable(fits, header, rows, EXTNAME_KERNEL)) {
+        psError(psErrorCodeLast(), false, "Unable to write subtraction kernel to FITS table.");
+        psFree(header);
+        psFree(rows);
+        return false;
+    }
+
+    psImage *image = psMetadataLookupPtr(NULL, ro->analysis, PM_SUBTRACTION_ANALYSIS_KERNEL_IMAGE); // Image
+    pmStackVisualPlotTestImage(image, "Subtraction_kernels.fits");
+
+    if (image && !psFitsWriteImage(fits, header, image, 0, EXTNAME_IMAGE)) {
+        psError(psErrorCodeLast(), false, "Unable to write subtraction kernel image.");
+        psFree(header);
+        psFree(rows);
+        return false;
+    }
+
+    psFree(header);
+    psFree(rows);
+
+    return true;
+}
+
+static bool pmCellWriteSubtractionKernels(pmCell *cell, const pmFPAview *view,
+                                          pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(cell->readouts, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        thisView->readout = i;
+        if (!pmReadoutWriteSubtractionKernels(readout, file->fits)) {
+            psError(psErrorCodeLast(), false, "Failed to write %dth readout", i);
+            psFree(thisView);
+            return false;
+        }
+    }
+    psFree(thisView);
+    return true;
+}
+
+static bool pmChipWriteSubtractionKernels(pmChip *chip, const pmFPAview *view,
+                                          pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(chip->cells, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        thisView->cell = i;
+        if (!pmCellWriteSubtractionKernels(cell, thisView, file, config)) {
+            psError(psErrorCodeLast(), false, "Failed to write %dth cell", i);
+            psFree(thisView);
+            return false;
+        }
+    }
+    psFree(thisView);
+    return true;
+}
+
+static bool pmFPAWriteSubtractionKernels(pmFPA *fpa, const pmFPAview *view,
+                                         pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fpa->chips, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        thisView->chip = i;
+        if (!pmChipWriteSubtractionKernels(chip, thisView, file, config)) {
+            psError(psErrorCodeLast(), false, "Failed to write %dth chip", i);
+            psFree(thisView);
+            return false;
+        }
+    }
+    psFree(thisView);
+    return true;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+bool pmReadoutReadSubtractionKernels(pmReadout *ro, psFits *fits)
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    if (!psFitsMoveExtName(fits, EXTNAME_KERNEL)) {
+        psError(psErrorCodeLast(), false, "Unable to move to subtraction kernel table.");
+        return false;
+    }
+
+    psArray *table = psFitsReadTable(fits); // Table of interest
+    if (!table) {
+        psError(psErrorCodeLast(), false, "Unable to read FITS table");
+        return false;
+    }
+
+    // Look up a column value for a row
+#define TABLE_LOOKUP(TYPE, SUFFIX, TARGET, NAME) \
+    TYPE TARGET; \
+    { \
+        bool mdok; \
+        TARGET = psMetadataLookup##SUFFIX(&mdok, row, NAME); \
+        if (!mdok) { \
+            psError(PM_ERR_PROG, false, "Unable to find column %s in subtraction kernel table.", NAME); \
+            psFree(table); \
+            return false; \
+        } \
+    }
+
+    for (int i = 0; i < table->n; i++) {
+        psMetadata *row = table->data[i]; // Table row
+
+        TABLE_LOOKUP(int, S32, xMin, NAME_XMIN);
+        TABLE_LOOKUP(int, S32, xMax, NAME_XMAX);
+        TABLE_LOOKUP(int, S32, yMin, NAME_YMIN);
+        TABLE_LOOKUP(int, S32, yMax, NAME_YMAX);
+
+        psRegion *region = psRegionAlloc(xMin, xMax, yMin, yMax); // Region of applicability
+        psMetadataAddPtr(ro->analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_REGION, PS_DATA_REGION,
+                         "Subtraction region", region);
+        psFree(region);
+
+        TABLE_LOOKUP(const char *, Str, description, NAME_KERNEL);
+        TABLE_LOOKUP(pmSubtractionMode, S32, mode,    NAME_MODE);
+
+#if 0
+        TABLE_LOOKUP(int, S32, size,    NAME_SIZE);
+        TABLE_LOOKUP(int, S32, inner,   NAME_INNER);
+        TABLE_LOOKUP(int, S32, spatial, NAME_SPATIAL);
+#endif
+
+        TABLE_LOOKUP(int, S32, bg,      NAME_BG);
+
+        TABLE_LOOKUP(float, F32, mean,      NAME_MEAN);
+        TABLE_LOOKUP(float, F32, rms,       NAME_RMS);
+        TABLE_LOOKUP(int,   S32, numStamps, NAME_NUMSTAMPS);
+
+        pmSubtractionKernels *kernels = pmSubtractionKernelsFromDescription(description, bg, *region, mode);
+        kernels->mean = mean;
+        kernels->rms = rms;
+        kernels->numStamps = numStamps;
+
+        bool mdok;                      // Status of MD lookup
+        if (mode == PM_SUBTRACTION_MODE_1 || mode == PM_SUBTRACTION_MODE_2) {
+            kernels->solution1 = psMemIncrRefCounter(psMetadataLookupPtr(&mdok, row, NAME_SOL1));
+            if (!mdok) {
+                psError(PM_ERR_PROG, false, "Unable to find column %s in subtraction kernel table.",
+                        NAME_SOL1);
+                psFree(kernels);
+                psFree(table);
+                return false;
+            }
+        }
+        if (mode == PM_SUBTRACTION_MODE_DUAL) {
+            kernels->solution1 = psMemIncrRefCounter(psMetadataLookupPtr(&mdok, row, NAME_SOL1));
+            if (!mdok) {
+                psError(PM_ERR_PROG, false, "Unable to find column %s in subtraction kernel table.",
+                        NAME_SOL1);
+                psFree(kernels);
+                psFree(table);
+                return false;
+            }
+            kernels->solution2 = psMemIncrRefCounter(psMetadataLookupPtr(&mdok, row, NAME_SOL2));
+            if (!mdok) {
+                psError(PM_ERR_PROG, false, "Unable to find column %s in subtraction kernel table.",
+                        NAME_SOL2);
+                psFree(kernels);
+                psFree(table);
+                return false;
+            }
+        }
+
+        psMetadataAddPtr(ro->analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_KERNEL, PS_DATA_UNKNOWN,
+                         "Subtraction kernels", kernels);
+        psFree(kernels);
+    }
+    psFree(table);
+    return true;
+}
+
+static bool pmCellReadSubtractionKernels(pmCell *cell, const pmFPAview *view,
+                                         pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(cell->readouts, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    // Create a readout if none exists
+    if (!cell->readouts || cell->readouts->n == 0) {
+        pmReadout *readout = pmReadoutAlloc(cell); // New readout
+        psFree(readout);                // Drop reference
+    }
+
+    cell->data_exists = false;
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        thisView->readout = i;
+        if (!pmReadoutReadSubtractionKernels(readout, file->fits)) {
+            psError(psErrorCodeLast(), false, "Unable to read subtraction kernels from cell");
+            psFree(thisView);
+            return false;
+        }
+        if (!readout->data_exists) {
+            continue;
+        }
+
+        // load in the concept information for this cell
+        if (!pmConceptsReadCell(cell, PM_CONCEPT_SOURCE_HEADER, true, NULL)) {
+            psErrorClear();
+            psWarning("Difficulty reading concepts for cell; attempting to proceed.");
+        }
+        cell->data_exists = true;
+    }
+    psFree(thisView);
+
+    return true;
+}
+
+static bool pmChipReadSubtractionKernels(pmChip *chip, const pmFPAview *view,
+                                         pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(chip->cells, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    chip->data_exists = false;
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        thisView->cell = i;
+        if (!pmCellReadSubtractionKernels(cell, thisView, file, config)) {
+            psError(psErrorCodeLast(), false, "Unable to read subtraction kernels from cell");
+            psFree(thisView);
+            return false;
+        }
+         if (!cell->data_exists) {
+            continue;
+        }
+        chip->data_exists = true;
+    }
+    psFree(thisView);
+
+    if (!pmConceptsReadChip(chip, PM_CONCEPT_SOURCE_HEADER, true, true, NULL)) {
+        psError(psErrorCodeLast(), false, "Failed to read concepts for chip.\n");
+        return false;
+    }
+
+    return true;
+}
+
+static bool pmFPAReadSubtractionKernels(pmFPA *fpa, const pmFPAview *view,
+                                        pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa->chips, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc(view->nRows); // Copy of input view
+    *thisView = *view;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        thisView->chip = i;
+        if (!pmChipReadSubtractionKernels(chip, thisView, file, config)) {
+            psError(psErrorCodeLast(), false, "Unable to read subtraction kernels from chip");
+            psFree(thisView);
+            return false;
+        }
+    }
+    psFree(thisView);
+
+    if (!pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_HEADER, true, NULL)) {
+        psError(psErrorCodeLast(), false, "Failed to read concepts for fpa.\n");
+        return false;
+    }
+
+    return true;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmSubtractionWriteKernels(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // Suitable FPA for writing
+
+    if (view->chip == -1) {
+        if (!pmFPAWriteSubtractionKernels(fpa, view, file, config)) {
+            psError(psErrorCodeLast(), false, "Failed to write subtraction kernels from fpa");
+            psFree(fpa);
+            return false;
+        }
+        psFree(fpa);
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        psError(PM_ERR_PROG, true, "Writing chip == %d (>= chips->n == %ld)", view->chip, fpa->chips->n);
+        psFree(fpa);
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        if (!pmChipWriteSubtractionKernels(chip, view, file, config)) {
+            psError(psErrorCodeLast(), false, "Failed to write objects from chip");
+            psFree(fpa);
+            return false;
+        }
+        psFree(fpa);
+        return true;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        psError(PM_ERR_PROG, true, "Writing cell == %d (>= cells->n == %ld)",
+                view->cell, chip->cells->n);
+        psFree(fpa);
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        if (!pmCellWriteSubtractionKernels(cell, view, file, config)) {
+            psError(psErrorCodeLast(), false, "Failed to write objects from cell");
+            psFree(fpa);
+            return false;
+        }
+        psFree(fpa);
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        psError(PM_ERR_PROG, true, "Writing readout == %d (>= readouts->n == %ld)",
+                view->readout, cell->readouts->n);
+        psFree(fpa);
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    if (!pmReadoutWriteSubtractionKernels(readout, file->fits)) {
+        psError(psErrorCodeLast(), false, "Failed to write objects from readout %d", view->readout);
+        psFree(fpa);
+        return false;
+    }
+
+    psFree(fpa);
+    return true;
+}
+
+bool pmSubtractionWritePHU(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    if (file->wrote_phu) {
+        return true;
+    }
+
+    // find the FPA phu
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // Suitable FPA for writing
+    pmHDU *phu = psMemIncrRefCounter(pmFPAviewThisPHU(view, fpa));
+    psFree(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);
+    }
+    psFree(phu);
+
+    pmConfigConformHeader(outhead, file->format);
+
+    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 pmSubtractionReadKernels(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        pmFPAReadSubtractionKernels(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) {
+        pmChipReadSubtractionKernels(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) {
+        pmCellReadSubtractionKernels(cell, view, file, config);
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    return pmReadoutReadSubtractionKernels(readout, file->fits);
+}
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionIO.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionIO.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionIO.h	(revision 42651)
@@ -0,0 +1,37 @@
+#ifndef PM_SUBTRACTION_IO_H
+#define PM_SUBTRACTION_IO_H
+
+#include <pslib.h>
+
+#include <pmHDU.h>
+#include <pmFPA.h>
+
+/// Write subtraction kernels within a readout to a FITS file
+bool pmReadoutWriteSubtractionKernels(
+    pmReadout *readout,                 ///< Readout for which to write subtraction kernels (in analysis MD)
+    psFits *fits                        ///< FITS file to which to write
+    );
+
+
+bool pmReadoutReadSubtractionKernels(
+    pmReadout *readout,                 ///< Readout for which to read subtraction kernels (into analysis MD)
+    psFits *fits                        ///< FITS file to which to write
+    );
+
+
+bool pmSubtractionReadKernels(const pmFPAview *view, ///< View into which to read
+                              pmFPAfile *file, ///< File from which to read
+                              pmConfig *config ///< Configuration
+                              );
+
+bool pmSubtractionWriteKernels(const pmFPAview *view, ///< View from which to write
+                               pmFPAfile *file, ///< File to which to write
+                               pmConfig *config ///< Configuration
+                               );
+
+bool pmSubtractionWritePHU(const pmFPAview *view, // View to PHU
+                           pmFPAfile *file, ///< File to which to write
+                           pmConfig *config ///< Configuration
+                           );
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionKernels.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionKernels.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionKernels.c	(revision 42651)
@@ -0,0 +1,1580 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+
+#include "pmFPA.h"
+#include "pmSubtractionTypes.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionKernels.h"
+#include "pmSubtractionHermitian.h"
+#include "pmSubtractionDeconvolve.h"
+#include "pmSubtractionVisual.h"
+
+#define RINGS_BUFFER 10                 // Buffer size for RINGS data
+
+// Free function for pmSubtractionKernels
+static void subtractionKernelsFree(pmSubtractionKernels *kernels)
+{
+    psFree(kernels->description);
+    psFree(kernels->fwhms);
+    psFree(kernels->orders);
+    psFree(kernels->u);
+    psFree(kernels->v);
+    psFree(kernels->widths);
+    psFree(kernels->uStop);
+    psFree(kernels->vStop);
+    psFree(kernels->preCalc);
+    psFree(kernels->penalties1);
+    psFree(kernels->penalties2);
+    psFree(kernels->solution1);
+    psFree(kernels->solution2);
+    psFree(kernels->solution1err);
+    psFree(kernels->solution2err);
+    psFree(kernels->sampleStamps);
+}
+
+// Free function for pmSubtractionPreCalcKernel
+static void pmSubtractionKernelPreCalcFree(pmSubtractionKernelPreCalc *kernel)
+{
+    psFree(kernel->xKernel);
+    psFree(kernel->yKernel);
+    psFree(kernel->kernel);
+
+    psFree(kernel->uCoords);
+    psFree(kernel->vCoords);
+    psFree(kernel->poly);
+}
+
+// Raise an integer 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;
+}
+
+// Generate 1D convolution kernel for SIMPLE
+psVector *pmSubtractionKernelSIMPLE(float sigma, // Gaussian width
+				    int order,   // Unused polynomial order
+				    int size     // Kernel half-size
+				    )
+{
+  int fullSize = 2 * size + 1;    // Full size of the kernel
+  psVector *kernel = psVectorAlloc(fullSize, PS_TYPE_F32); // Kernel to return
+  float expNorm = -0.5 / PS_SQR(sigma); // Normalization for exponential
+  float norm    = 1.0 / sqrtf(2.0 * M_PI * sigma * sigma); // Correct Normalization for Gaussian
+  if (sigma < 0.1) {
+    kernel->data.F32[size] = 1.0;
+    return(kernel);
+  }
+  for (int i = 0, x = -size; x <= size; i++,x++) {
+    kernel->data.F32[i] = norm * expf(expNorm * PS_SQR(x));
+  }
+  return(kernel);
+}
+
+
+// Generate 1D convolution kernel for ISIS
+psVector *pmSubtractionKernelISIS(float sigma, // Gaussian width
+                                       int order, // Polynomial order
+                                       int size // Kernel half-size
+    )
+{
+    int fullSize = 2 * size + 1;        // Full size of kernel
+    psVector *kernel = psVectorAlloc(fullSize, PS_TYPE_F32); // Kernel to return
+
+    float expNorm = -0.5 / PS_SQR(sigma); // Normalisation for exponential
+    float norm = 1.0 / (M_2_PI * sqrtf(sigma)); // Normalisation for Gaussian
+    for (int i = 0, x = -size; x <= size; i++, x++) {
+        kernel->data.F32[i] = norm * power(x, order) * expf(expNorm * PS_SQR(x));
+    }
+
+    return kernel;
+}
+
+// Generate 1D convolution kernel for HERM (normalized for 2D)
+psVector *pmSubtractionKernelHERM(float sigma, // Gaussian width
+                                       int order, // Polynomial order
+                                       int size // Kernel half-size
+    )
+{
+    int fullSize = 2 * size + 1;        // Full size of kernel
+    psVector *kernel = psVectorAlloc(fullSize, PS_TYPE_F32); // Kernel to return
+
+    // for now, we are only allowing equal orders and sigmas in X and Y
+    float nf = exp(lgamma(order + 1));
+    float norm = 1.0 / sqrt(nf*sigma*sqrt(M_2_PI));
+
+    for (int i = 0, x = -size; x <= size; i++, x++) {
+        float xf = x / sigma;
+        float z = -0.25*xf*xf;
+        kernel->data.F32[i] = norm * p_pmSubtractionHermitianPolynomial(xf, order) * exp(z);
+    }
+
+    return kernel;
+}
+
+// Generate 1D convolution kernel for HERM (normalized for 2D)
+psKernel *pmSubtractionKernelHERM_RADIAL(float sigma, // Gaussian width
+                                         int order, // Polynomial order
+                                         int size // Kernel half-size
+    )
+{
+    psKernel *kernel = psKernelAlloc(-size, size, -size, size); // 2D Kernel
+
+    // for now, we are only allowing equal orders and sigmas in X and Y
+    float nf = exp(lgamma(order + 1));
+    float norm = 1.0 / sqrt(nf*sigma*sqrt(M_2_PI));
+
+    // generate 2D radial hermitian
+    for (int v = -size; v <= size; v++) {
+        for (int u = -size; u <= size; u++) {
+            float r = hypot(u, v) / sigma;
+            float z = -0.25*r*r;
+            kernel->kernel[v][u] = norm * p_pmSubtractionHermitianPolynomial(r, order) * exp(z);
+        }
+    }
+
+    return kernel;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Semi-public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool p_pmSubtractionKernelsAddGrid(pmSubtractionKernels *kernels, int start, int size)
+{
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+    PS_ASSERT_INT_NONNEGATIVE(start, false);
+    PS_ASSERT_INT_NONNEGATIVE(size, false);
+
+    int numNew = PS_SQR(2 * size + 1) - 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->penalties1 = psVectorRealloc(kernels->penalties1, start + numNew);
+    kernels->penalties2 = psVectorRealloc(kernels->penalties2, start + numNew);
+
+    kernels->inner = start;
+    kernels->num += numNew;
+
+    // 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++) {
+            if (v == 0 && u == 0) {
+                // Skip normalisation component: added explicitly
+                index--;
+                continue;
+            }
+            kernels->widths->data.F32[index] = NAN;
+            kernels->u->data.S32[index] = u;
+            kernels->v->data.S32[index] = v;
+            kernels->preCalc->data[index] = NULL;
+
+	    // XXX this needs to be changed to use the *convolved* second moment
+            kernels->penalties1->data.F32[index] = kernels->penalty * PS_SQR(PS_SQR(u) + PS_SQR(v));
+            psAssert (isfinite(kernels->penalties1->data.F32[index]), "invalid penalty");
+
+            kernels->penalties2->data.F32[index] = kernels->penalty * PS_SQR(PS_SQR(u) + PS_SQR(v));
+            psAssert (isfinite(kernels->penalties2->data.F32[index]), "invalid penalty");
+
+            psTrace("psModules.imcombine", 7, "Kernel %d: %d %d\n", index, u, v);
+        }
+    }
+
+    kernels->widths->n = start + numNew;
+    kernels->u->n = start + numNew;
+    kernels->v->n = start + numNew;
+    kernels->preCalc->n = start + numNew;
+
+    kernels->penalties1->n = start + numNew;
+    kernels->penalties2->n = start + numNew;
+
+    return true;
+}
+
+# if (CENTRAL_DELTA)
+// This version of the code uses the central pixel to force zero net flux, Alard actually uses
+// kernel(0) for this purpose (for even order, kernel[i] = kernel'[i] - kernel[0])
+static bool pmSubtractionKernelPreCalcNormalize(pmSubtractionKernels *kernels, pmSubtractionKernelPreCalc *preCalc,
+						int index, int uOrder, int vOrder, float fwhm,
+						bool AlardLuptonStyle, bool forceZeroNull)
+{
+    // we have 4 cases here:
+    // 1) for odd functions, normalize the kernel by the maximum swing / Npix
+    // 2) for even functions, normalize the kernel to unity
+    // 3) for alard-lupton style normalization, subtract 1 from the 0,0 pixel for all even functions
+    // 4) for deconvolved hermitians, subtract 1 from the 0,0 pixel for the 0,0 function(s)
+
+    // Calculate moments
+    double sum = 0.0, sum2 = 0.0;           // Sum of kernel component
+    float min = INFINITY, max = -INFINITY;  // Minimum and maximum kernel value
+
+    for (int v = preCalc->kernel->yMin; v <= preCalc->kernel->yMax; v++) {
+        for (int u = preCalc->kernel->xMin; u <= preCalc->kernel->xMax; u++) {
+            double value = preCalc->kernel->kernel[v][u];
+            double value2 = PS_SQR(value);
+            sum += value;
+            sum2 += value2;
+            min = PS_MIN(value, min);
+            max = PS_MAX(value, max);
+        }
+    }
+
+#if 0
+    fprintf(stderr, "%d raw: %lf, null: %f, min: %lf, max: %lf\n", index, sum, preCalc->kernel->kernel[0][0], min, max);
+#endif
+
+    bool zeroNull = false;              // Zero out using the null position?
+    float scale2D = NAN;                // Scaling for 2-D kernels
+
+    if (AlardLuptonStyle) {
+        if (uOrder % 2 == 0 && vOrder % 2 == 0) {
+            // Even functions: normalise to unit sum and subtract null pixel so that sum is zero
+	    // Re-normalize 
+            // scale2D  = 1.0 / fabs(sum);
+            scale2D  = 1.0 / sqrt(sum2) / PS_SQR(fwhm);
+            zeroNull = true;
+        } else {
+            // Odd functions: choose normalisation so that parameters have about the same strength as for even
+            // functions, no subtraction of null pixel because the sum is already (near) zero
+            scale2D = 1.0 / sqrt(sum2) / PS_SQR(fwhm);
+            zeroNull = false;
+        }
+    }
+
+    if (!AlardLuptonStyle && (uOrder == 0 && vOrder == 0)) {
+        zeroNull = true;
+    }
+    if (forceZeroNull) {
+        // Force rescaling and subtraction of null pixel even though the order doesn't indicate it's even
+        scale2D = 1.0 / fabs(sum) / PS_SQR(fwhm);
+        zeroNull = true;
+    }
+    if (!forceZeroNull && ((uOrder % 2) || (vOrder % 2))) {
+        // Odd function
+        scale2D = 1.0 / sqrt(sum2) / PS_SQR(fwhm);
+    }
+
+    float scale1D = sqrtf(scale2D);     // Scaling for 1-D kernels
+    if (preCalc->xKernel) {
+        psBinaryOp(preCalc->xKernel, preCalc->xKernel, "*", psScalarAlloc(scale1D, PS_TYPE_F32));
+    }
+    if (preCalc->yKernel) {
+        psBinaryOp(preCalc->yKernel, preCalc->yKernel, "*", psScalarAlloc(scale1D, PS_TYPE_F32));
+    }
+
+    psBinaryOp(preCalc->kernel->image, preCalc->kernel->image, "*", psScalarAlloc(scale2D, PS_TYPE_F32));
+
+    if (zeroNull) {
+        // preCalc->kernel->kernel[0][0] -= 1.0;
+        preCalc->kernel->kernel[0][0] -= sum * scale2D;
+    }
+
+#if 0
+    {
+        double Sum = 0.0;   // Sum of kernel component
+        double Sum2 = 0.0;   // Sum of kernel component
+        float min = INFINITY, max = -INFINITY;  // Minimum and maximum kernel value
+	for (int v = preCalc->kernel->yMin; v <= preCalc->kernel->yMax; v++) {
+	    for (int u = preCalc->kernel->xMin; u <= preCalc->kernel->xMax; u++) {
+		double value = preCalc->kernel->kernel[v][u];
+                Sum += value;
+		Sum2 += PS_SQR(value);
+                min = PS_MIN(preCalc->kernel->kernel[v][u], min);
+                max = PS_MAX(preCalc->kernel->kernel[v][u], max);
+            }
+        }
+        fprintf(stderr, "%d sum: %lf, sum2: %lf, null: %f, min: %lf, max: %lf, scale: %f\n", index, Sum, Sum2, preCalc->kernel->kernel[0][0], min, max, scale2D);
+    }
+#endif
+
+    kernels->widths->data.F32[index] = fwhm;
+    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, fwhm, uOrder, vOrder);
+
+    return true;
+}
+
+# else /* CENTRAL_DELTA */
+
+static bool zeroIsNormal = false;
+
+// this code uses kernel(0) to force zero flux, and is invalid for other kinds of normalizations
+static bool pmSubtractionKernelPreCalcNormalize(pmSubtractionKernels *kernels, pmSubtractionKernelPreCalc *preCalc,
+						int index, int uOrder, int vOrder, float fwhm,
+						pmSubtractionKernelPreCalc *zeroKernel)
+{
+    // 1) for odd functions: no renormalization 
+    // 2) for even functions, normalize the kernel to unity
+    // 3) for even functions & index > 0, subtract kernel(0)
+
+    // Calculate moments
+    double sum = 0.0, sum2 = 0.0;           // Sum of kernel component
+    float min = INFINITY, max = -INFINITY;  // Minimum and maximum kernel value
+
+    for (int v = preCalc->kernel->yMin; v <= preCalc->kernel->yMax; v++) {
+        for (int u = preCalc->kernel->xMin; u <= preCalc->kernel->xMax; u++) {
+            double value = preCalc->kernel->kernel[v][u];
+            double value2 = PS_SQR(value);
+            sum += value;
+            sum2 += value2;
+            min = PS_MIN(value, min);
+            max = PS_MAX(value, max);
+        }
+    }
+
+#if 0
+    fprintf(stderr, "%d raw: %lf, null: %f, min: %lf, max: %lf\n", index, sum, preCalc->kernel->kernel[0][0], min, max);
+#endif
+
+    float scale2D = 1.0;		// Scaling for 2-D kernels
+    float scale1D = 1.0;		// Scaling for 1-D kernels
+
+    if (uOrder % 2 == 0 && vOrder % 2 == 0) {
+
+	scale2D = 1.0 / sum;		// Scaling for 2-D kernels
+	scale1D = sqrtf(scale2D);		// Scaling for 1-D kernels
+
+	for (int v = preCalc->kernel->yMin; v <= preCalc->kernel->yMax; v++) {
+	    for (int u = preCalc->kernel->xMin; u <= preCalc->kernel->xMax; u++) {
+		preCalc->kernel->kernel[v][u] *= scale2D;
+	    }
+	}
+# if (ZERO_KERNEL_ZERO_FLUX)
+	int firstZeroIndex = 0;
+# else
+	int firstZeroIndex = 1;
+# endif
+	if (index < firstZeroIndex) {
+	    zeroIsNormal = true;
+	} else {
+	    psAssert(zeroIsNormal, "failed to normalize zero kernel first");
+	    psAssert(zeroKernel, "failed to supply zero kernel");
+	    for (int v = preCalc->kernel->yMin; v <= preCalc->kernel->yMax; v++) {
+		for (int u = preCalc->kernel->xMin; u <= preCalc->kernel->xMax; u++) {
+		    preCalc->kernel->kernel[v][u] -= zeroKernel->kernel->kernel[v][u];
+		}
+	    }
+	}
+
+	// XXX why do we bother renormlizing the 1D kernels?  I don't think we use them again...
+	if (preCalc->xKernel) {
+	    psBinaryOp(preCalc->xKernel, preCalc->xKernel, "*", psScalarAlloc(scale1D, PS_TYPE_F32));
+	}
+	if (preCalc->yKernel) {
+	    psBinaryOp(preCalc->yKernel, preCalc->yKernel, "*", psScalarAlloc(scale1D, PS_TYPE_F32));
+	}
+    }
+
+#if 0
+    {
+        double Sum = 0.0;   // Sum of kernel component
+        double Sum2 = 0.0;   // Sum of kernel component
+        float min = INFINITY, max = -INFINITY;  // Minimum and maximum kernel value
+	for (int v = preCalc->kernel->yMin; v <= preCalc->kernel->yMax; v++) {
+	    for (int u = preCalc->kernel->xMin; u <= preCalc->kernel->xMax; u++) {
+		double value = preCalc->kernel->kernel[v][u];
+                Sum += value;
+		Sum2 += PS_SQR(value);
+                min = PS_MIN(preCalc->kernel->kernel[v][u], min);
+                max = PS_MAX(preCalc->kernel->kernel[v][u], max);
+            }
+        }
+        fprintf(stderr, "%d sum: %lf, sum2: %lf, null: %f, min: %lf, max: %lf, scale: %f\n", index, Sum, Sum2, preCalc->kernel->kernel[0][0], min, max, scale2D);
+    }
+#endif
+
+    if (kernels) {
+	kernels->widths->data.F32[index] = fwhm;
+	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, fwhm, uOrder, vOrder);
+
+    return true;
+}
+# endif /* Central Delta */
+
+pmSubtractionKernels *p_pmSubtractionKernelsRawISIS(int size, int spatialOrder,
+                                                    const psVector *fwhmsIN, const psVector *ordersIN,
+                                                    float penalty, psRegion bounds, pmSubtractionMode mode)
+{
+    PS_ASSERT_VECTOR_NON_NULL(fwhmsIN, NULL);
+    PS_ASSERT_VECTOR_TYPE(fwhmsIN, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(ordersIN, NULL);
+    PS_ASSERT_VECTOR_TYPE(ordersIN, PS_TYPE_S32, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(fwhmsIN, ordersIN, NULL);
+    PS_ASSERT_INT_POSITIVE(size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(spatialOrder, NULL);
+
+    // check the requested fwhm values: any values <= 0.0 should be dropped
+    psVector *fwhms  = psVectorAllocEmpty (fwhmsIN->n, PS_TYPE_F32);
+    psVector *orders = psVectorAllocEmpty (ordersIN->n, PS_TYPE_S32);
+    for (int i = 0; i < fwhmsIN->n; i++) {
+        if (fwhmsIN->data.F32[i] <= FLT_EPSILON) continue;
+        psVectorAppend(fwhms, fwhmsIN->data.F32[i]);
+        psVectorAppend(orders, ordersIN->data.S32[i]);
+    }
+
+    int numGaussians = fwhms->n;       // Number of Gaussians
+
+    int num = 0;                        // Number of basis functions
+    for (int i = 0; i < numGaussians; i++) {
+        int gaussOrder = orders->data.S32[i]; // Polynomial order to apply to Gaussian
+        num += (gaussOrder + 1) * (gaussOrder + 2) / 2;
+    }
+
+    pmSubtractionKernels *kernels = pmSubtractionKernelsAlloc(num, PM_SUBTRACTION_KERNEL_ISIS, size, fwhms, orders, spatialOrder, penalty, bounds, mode); // Kernels
+    pmSubtractionKernelsMakeDescription(kernels);
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "kernel: %s --> %d elements", kernels->description, num);
+
+# if (!CENTRAL_DELTA && !ZERO_KERNEL_ZERO_FLUX)
+    // in this case, subtract the 0th kernel from everyone else
+    float zeroFWHM = fwhms->data.F32[0];
+    float zeroSigma = zeroFWHM / (2.0 * sqrtf(2.0 * logf(2.0))); // Gaussian sigma
+    pmSubtractionKernelPreCalc *zeroKernel = pmSubtractionKernelPreCalcAlloc(PM_SUBTRACTION_KERNEL_ISIS, 0, 0, size, zeroSigma); // structure to hold precalculated values
+    pmSubtractionKernelPreCalcNormalize (NULL, zeroKernel, -1, 0, 0, zeroFWHM, NULL);
+# endif
+
+    // 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
+
+# if (!CENTRAL_DELTA && ZERO_KERNEL_ZERO_FLUX)
+	// in this case, subtract the a 1/2 width Gaussian from each series
+	float zeroFWHM = 0.50 * fwhms->data.F32[0];
+	float zeroSigma = zeroFWHM / (2.0 * sqrtf(2.0 * logf(2.0))); // Gaussian sigma
+	pmSubtractionKernelPreCalc *zeroKernel = pmSubtractionKernelPreCalcAlloc(PM_SUBTRACTION_KERNEL_ISIS, 0, 0, size, zeroSigma); // structure to hold precalculated values
+	pmSubtractionKernelPreCalcNormalize (NULL, zeroKernel, -1, 0, 0, zeroFWHM, NULL);
+# endif
+        // 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++) {
+
+                pmSubtractionKernelPreCalc *preCalc = pmSubtractionKernelPreCalcAlloc(PM_SUBTRACTION_KERNEL_ISIS, uOrder, vOrder, size, sigma); // structure to hold precalculated values
+# if (CENTRAL_DELTA)
+                pmSubtractionKernelPreCalcNormalize (kernels, preCalc, index, uOrder, vOrder, fwhms->data.F32[i], false, false);
+# else
+                pmSubtractionKernelPreCalcNormalize (kernels, preCalc, index, uOrder, vOrder, fwhms->data.F32[i], zeroKernel);
+# endif
+            }
+        }
+# if (!CENTRAL_DELTA && ZERO_KERNEL_ZERO_FLUX)
+	psFree(zeroKernel);
+# endif
+    }
+
+# if (!CENTRAL_DELTA && !ZERO_KERNEL_ZERO_FLUX)
+    psFree(zeroKernel);
+# endif
+    psFree(orders);
+    psFree(fwhms);
+
+    return kernels;
+}
+
+pmSubtractionKernels *pmSubtractionKernelsISIS_RADIAL(int size, int spatialOrder,
+                                                      const psVector *fwhmsIN, const psVector *ordersIN,
+                                                      float penalty, psRegion bounds, pmSubtractionMode mode)
+{
+    PS_ASSERT_VECTOR_NON_NULL(fwhmsIN, NULL);
+    PS_ASSERT_VECTOR_TYPE(fwhmsIN, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(ordersIN, NULL);
+    PS_ASSERT_VECTOR_TYPE(ordersIN, PS_TYPE_S32, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(fwhmsIN, ordersIN, NULL);
+    PS_ASSERT_INT_POSITIVE(size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(spatialOrder, NULL);
+
+    // check the requested fwhm values: any values <= 0.0 should be dropped
+    psVector *fwhms  = psVectorAllocEmpty (fwhmsIN->n, PS_TYPE_F32);
+    psVector *orders = psVectorAllocEmpty (ordersIN->n, PS_TYPE_S32);
+    for (int i = 0; i < fwhmsIN->n; i++) {
+        if (fwhmsIN->data.F32[i] <= FLT_EPSILON) continue;
+        psVectorAppend(fwhms, fwhmsIN->data.F32[i]);
+        psVectorAppend(orders, ordersIN->data.S32[i]);
+    }
+
+    int numGaussians = fwhms->n;       // Number of Gaussians
+
+    int num = 0;                        // Number of basis functions
+    for (int i = 0; i < numGaussians; i++) {
+        int gaussOrder = orders->data.S32[i]; // Polynomial order to apply to Gaussian
+        num += (gaussOrder + 1) * (gaussOrder + 2) / 2;
+        num += (11 - gaussOrder - 1);   // include all higher order radial terms
+    }
+
+    pmSubtractionKernels *kernels = pmSubtractionKernelsAlloc(num, PM_SUBTRACTION_KERNEL_ISIS_RADIAL, size, fwhms, orders, spatialOrder, penalty, bounds, mode); // Kernels
+    pmSubtractionKernelsMakeDescription(kernels);
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "kernel: %s --> %d elements", kernels->description, num);
+
+    // 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
+        // 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++) {
+                pmSubtractionKernelPreCalc *preCalc = pmSubtractionKernelPreCalcAlloc(PM_SUBTRACTION_KERNEL_ISIS, uOrder, vOrder, size, sigma); // structure to hold precalculated values
+# if (CENTRAL_DELTA)
+                pmSubtractionKernelPreCalcNormalize (kernels, preCalc, index, uOrder, vOrder, fwhms->data.F32[i], true, false);
+# else
+                pmSubtractionKernelPreCalcNormalize (kernels, preCalc, index, uOrder, vOrder, fwhms->data.F32[i], NULL);
+# endif
+            }
+        }
+        for (int order = orders->data.S32[i] + 1; order < 11; order ++, index ++) {
+            // XXX modify size for hermitians to account for sqrt(2) in Hermitian definition (relative to ISIS Gaussian)
+            pmSubtractionKernelPreCalc *preCalc = pmSubtractionKernelPreCalcAlloc(PM_SUBTRACTION_KERNEL_ISIS_RADIAL, order, order, size, sigma / sqrt(2.0)); // structure to hold precalculated values
+# if (CENTRAL_DELTA)
+            pmSubtractionKernelPreCalcNormalize (kernels, preCalc, index, order, order, fwhms->data.F32[i], true, true);
+# else
+            pmSubtractionKernelPreCalcNormalize (kernels, preCalc, index, order, order, fwhms->data.F32[i], NULL);
+# endif
+        }
+    }
+    return kernels;
+}
+
+pmSubtractionKernels *pmSubtractionKernelsHERM(int size, int spatialOrder,
+                                               const psVector *fwhmsIN, const psVector *ordersIN,
+                                               float penalty, psRegion bounds, pmSubtractionMode mode)
+{
+    PS_ASSERT_VECTOR_NON_NULL(fwhmsIN, NULL);
+    PS_ASSERT_VECTOR_TYPE(fwhmsIN, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(ordersIN, NULL);
+    PS_ASSERT_VECTOR_TYPE(ordersIN, PS_TYPE_S32, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(fwhmsIN, ordersIN, NULL);
+    PS_ASSERT_INT_POSITIVE(size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(spatialOrder, NULL);
+
+    // check the requested fwhm values: any values <= 0.0 should be dropped
+    psVector *fwhms  = psVectorAllocEmpty (fwhmsIN->n, PS_TYPE_F32);
+    psVector *orders = psVectorAllocEmpty (ordersIN->n, PS_TYPE_S32);
+    for (int i = 0; i < fwhmsIN->n; i++) {
+        if (fwhmsIN->data.F32[i] <= FLT_EPSILON) continue;
+        psVectorAppend(fwhms, fwhmsIN->data.F32[i]);
+        psVectorAppend(orders, ordersIN->data.S32[i]);
+    }
+
+    int numGaussians = fwhms->n;       // Number of Gaussians
+
+    int num = 0;                        // Number of basis functions
+    for (int i = 0; i < numGaussians; i++) {
+        int gaussOrder = orders->data.S32[i]; // Polynomial order to apply to Gaussian
+        num += (gaussOrder + 1) * (gaussOrder + 2) / 2;
+    }
+
+    pmSubtractionKernels *kernels = pmSubtractionKernelsAlloc(num, PM_SUBTRACTION_KERNEL_HERM, size, fwhms, orders, spatialOrder, penalty, bounds, mode); // Kernels
+    pmSubtractionKernelsMakeDescription(kernels);
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "kernel: %s --> %d elements", kernels->description, num);
+
+    // 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
+        // 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++) {
+                pmSubtractionKernelPreCalc *preCalc = pmSubtractionKernelPreCalcAlloc(PM_SUBTRACTION_KERNEL_HERM, uOrder, vOrder, size, sigma); // structure to hold precalculated values
+# if (CENTRAL_DELTA)
+                pmSubtractionKernelPreCalcNormalize (kernels, preCalc, index, uOrder, vOrder, fwhms->data.F32[i], true, false);
+# else
+                pmSubtractionKernelPreCalcNormalize (kernels, preCalc, index, uOrder, vOrder, fwhms->data.F32[i], NULL);
+# endif
+            }
+        }
+    }
+
+    return kernels;
+}
+
+pmSubtractionKernels *pmSubtractionKernelsDECONV_HERM(int size, int spatialOrder,
+                                                      const psVector *fwhmsIN, const psVector *ordersIN,
+                                                      float penalty, psRegion bounds, pmSubtractionMode mode)
+{
+    PS_ASSERT_VECTOR_NON_NULL(fwhmsIN, NULL);
+    PS_ASSERT_VECTOR_TYPE(fwhmsIN, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(ordersIN, NULL);
+    PS_ASSERT_VECTOR_TYPE(ordersIN, PS_TYPE_S32, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(fwhmsIN, ordersIN, NULL);
+    PS_ASSERT_INT_POSITIVE(size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(spatialOrder, NULL);
+
+    // check the requested fwhm values: any values <= 0.0 should be dropped
+    psVector *fwhms  = psVectorAllocEmpty (fwhmsIN->n, PS_TYPE_F32);
+    psVector *orders = psVectorAllocEmpty (ordersIN->n, PS_TYPE_S32);
+    for (int i = 0; i < fwhmsIN->n; i++) {
+        if (fwhmsIN->data.F32[i] <= FLT_EPSILON) continue;
+        psVectorAppend(fwhms, fwhmsIN->data.F32[i]);
+        psVectorAppend(orders, ordersIN->data.S32[i]);
+    }
+
+    int numGaussians = fwhms->n;       // Number of Gaussians
+
+    int num = 0;                        // Number of basis functions
+    for (int i = 0; i < numGaussians; i++) {
+        int gaussOrder = orders->data.S32[i]; // Polynomial order to apply to Gaussian
+        num += PS_SQR(gaussOrder + 1);
+    }
+
+    pmSubtractionKernels *kernels = pmSubtractionKernelsAlloc(num, PM_SUBTRACTION_KERNEL_DECONV_HERM, size, fwhms, orders, spatialOrder, penalty, bounds, mode); // Kernels
+    pmSubtractionKernelsMakeDescription(kernels);
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "kernel: %s --> %d elements", kernels->description, num);
+
+    // XXXXX hard-wired reference sigma for now of 1.7 pix (== 4.0 pix fwhm == 1.0 arcsec in simtest)
+    // generate the Gaussian deconvolution kernel
+    # define DECONV_SIGMA 1.6
+    psKernel *kernelGauss = pmSubtractionDeconvolveGauss (size, DECONV_SIGMA);
+
+# if 1
+    psArray *deconKernels = psArrayAllocEmpty(100);
+# endif
+
+    // 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
+        // Iterate over (u,v) order
+        for (int uOrder = 0; uOrder <= orders->data.S32[i]; uOrder++) {
+            for (int vOrder = 0; vOrder <= orders->data.S32[i]; vOrder++, index++) {
+
+                pmSubtractionKernelPreCalc *preCalc = pmSubtractionKernelPreCalcAlloc(PM_SUBTRACTION_KERNEL_HERM, uOrder, vOrder, size, sigma); // structure to hold precalculated values
+
+                // save the generated 2D kernel as the target, deconvolve it by Gaussian, replacing the generated 2D kernel
+                psKernel *kernelTarget = preCalc->kernel;
+                preCalc->kernel = pmSubtractionDeconvolveKernel(kernelTarget, kernelGauss); // Kernel
+
+                // XXX do we use Alard-Lupton normalization (last param true) or not?
+# if (CENTRAL_DELTA)
+                pmSubtractionKernelPreCalcNormalize (kernels, preCalc, index, uOrder, vOrder, fwhms->data.F32[i], true, false);
+# else		
+                pmSubtractionKernelPreCalcNormalize (kernels, preCalc, index, uOrder, vOrder, fwhms->data.F32[i], NULL);
+# endif
+                // XXXX test demo that deconvolved kernel is valid
+# if 1
+                psImage *kernelConv = psImageConvolveFFT(NULL, preCalc->kernel->image, NULL, 0, kernelGauss);
+                psArrayAdd (deconKernels, 100, kernelConv);
+                psFree (kernelConv);
+
+                if (!uOrder && !vOrder){
+                    pmSubtractionVisualShowSubtraction (kernelTarget->image, preCalc->kernel->image, kernelConv);
+                }
+# endif
+            }
+        }
+    }
+
+# if 1
+    psImage *dot = psImageAlloc(deconKernels->n, deconKernels->n, PS_TYPE_F32);
+    for (int i = 0; i < deconKernels->n; i++) {
+        for (int j = 0; j <= i; j++) {
+            psImage *t1 = deconKernels->data[i];
+            psImage *t2 = deconKernels->data[j];
+
+            double sum = 0.0;
+            for (int iy = 0; iy < t1->numRows; iy++) {
+                for (int ix = 0; ix < t1->numCols; ix++) {
+                    sum += t1->data.F32[iy][ix] * t2->data.F32[iy][ix];
+                }
+            }
+            dot->data.F32[j][i] = sum;
+            dot->data.F32[i][j] = sum;
+        }
+    }
+    pmSubtractionVisualShowSubtraction (dot, NULL, NULL);
+    psFree (dot);
+    psFree (deconKernels);
+# endif
+
+    return kernels;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+pmSubtractionKernels *pmSubtractionKernelsAlloc(int numBasisFunctions, pmSubtractionKernelsType type,
+                                                int size, psVector *fwhms, psVector *orders, int spatialOrder, float penalty, psRegion bounds,
+                                                pmSubtractionMode mode)
+{
+    pmSubtractionKernels *kernels = psAlloc(sizeof(pmSubtractionKernels)); // Kernels, to return
+    psMemSetDeallocator(kernels, (psFreeFunc)subtractionKernelsFree);
+
+    kernels->type = type;
+    kernels->description = NULL;
+    kernels->num = numBasisFunctions;
+    kernels->fwhms = psMemIncrRefCounter(fwhms);
+    kernels->orders = psMemIncrRefCounter(orders);
+    kernels->u = psVectorAlloc(numBasisFunctions, PS_TYPE_S32);
+    kernels->v = psVectorAlloc(numBasisFunctions, PS_TYPE_S32);
+    kernels->widths = psVectorAlloc(numBasisFunctions, PS_TYPE_F32);
+    kernels->uStop = NULL;
+    kernels->vStop = NULL;
+    kernels->xMin = bounds.x0;
+    kernels->xMax = bounds.x1;
+    kernels->yMin = bounds.y0;
+    kernels->yMax = bounds.y1;
+    kernels->preCalc = psArrayAlloc(numBasisFunctions);
+    kernels->penalty = penalty;
+    kernels->penalties1 = psVectorAlloc(numBasisFunctions, PS_TYPE_F32);
+    psVectorInit(kernels->penalties1, NAN);
+    kernels->penalties2 = psVectorAlloc(numBasisFunctions, PS_TYPE_F32);
+    psVectorInit(kernels->penalties2, NAN);
+    kernels->havePenalties = false;
+    kernels->size = size;
+    kernels->inner = 0;
+    kernels->binning = 0;
+    kernels->ringsOrder = 0;
+    kernels->spatialOrder = spatialOrder;
+    kernels->bgOrder = 0;
+    kernels->mode = mode;
+    kernels->solution1 = NULL;
+    kernels->solution2 = NULL;
+    kernels->solution1err = NULL;
+    kernels->solution2err = NULL;
+    kernels->mean = NAN;
+    kernels->rms = NAN;
+    kernels->numStamps = 0;
+    kernels->sampleStamps = NULL;
+
+    kernels->fResSigmaMean  = NAN;
+    kernels->fResSigmaStdev = NAN;
+    kernels->fResOuterMean  = NAN;
+    kernels->fResOuterStdev = NAN;
+    kernels->fResTotalMean  = NAN;
+    kernels->fResTotalStdev = NAN;
+
+    return kernels;
+}
+
+pmSubtractionKernelPreCalc *pmSubtractionKernelPreCalcAlloc(pmSubtractionKernelsType type, int uOrder, int vOrder, int size, float sigma) {
+
+    pmSubtractionKernelPreCalc *preCalc = psAlloc(sizeof(pmSubtractionKernelPreCalc)); // Kernels, to return
+    psMemSetDeallocator(preCalc, (psFreeFunc)pmSubtractionKernelPreCalcFree);
+
+    // 1D kernel realizations:
+    switch (type) {
+      case PM_SUBTRACTION_KERNEL_ISIS:
+        preCalc->xKernel = pmSubtractionKernelISIS(sigma, uOrder, size);
+        preCalc->yKernel = pmSubtractionKernelISIS(sigma, vOrder, size);
+        preCalc->uCoords = NULL;
+        preCalc->vCoords = NULL;
+        preCalc->poly    = NULL;
+        break;
+      case PM_SUBTRACTION_KERNEL_HERM:
+        preCalc->xKernel = pmSubtractionKernelHERM(sigma, uOrder, size);
+        preCalc->yKernel = pmSubtractionKernelHERM(sigma, vOrder, size);
+        preCalc->uCoords = NULL;
+        preCalc->vCoords = NULL;
+        preCalc->poly    = NULL;
+        break;
+    case PM_SUBTRACTION_KERNEL_SIMPLE:
+      preCalc->xKernel = pmSubtractionKernelSIMPLE(sigma,uOrder,size);
+      preCalc->yKernel = pmSubtractionKernelSIMPLE(sigma,vOrder,size);
+      preCalc->uCoords = NULL;
+      preCalc->vCoords = NULL;
+      preCalc->poly    = NULL;
+      break;
+      case PM_SUBTRACTION_KERNEL_RINGS:
+        // the RINGS kernel uses the uCoords, vCoords, and poly elements of the structure
+        // we allocate these vectors here, but leave the kernel generation to the main function
+        preCalc->xKernel = NULL;
+        preCalc->yKernel = NULL;
+        preCalc->kernel  = NULL;
+        preCalc->uCoords = psVectorAllocEmpty(size, PS_TYPE_S32); // u coords
+        preCalc->vCoords = psVectorAllocEmpty(size, PS_TYPE_S32); // v coords
+        preCalc->poly    = psVectorAllocEmpty(size, PS_TYPE_F32); // Polynomial
+        return preCalc;
+      case PM_SUBTRACTION_KERNEL_ISIS_RADIAL:
+        preCalc->kernel  = pmSubtractionKernelHERM_RADIAL(sigma, uOrder, size);
+        preCalc->xKernel = NULL;
+        preCalc->yKernel = NULL;
+        preCalc->uCoords = NULL;
+        preCalc->vCoords = NULL;
+        preCalc->poly    = NULL;
+        return preCalc;
+      default:
+        psAbort("programming error: invalid type for PreCalc kernel");
+    }
+
+    preCalc->kernel = psKernelAlloc(-size, size, -size, size); // 2D Kernel
+
+    // generate 2D kernel from 1D realizations
+    for (int v = -size, y = 0; v <= size; v++, y++) {
+        for (int u = -size, x = 0; u <= size; u++, x++) {
+            preCalc->kernel->kernel[v][u] = preCalc->xKernel->data.F32[x] * preCalc->yKernel->data.F32[y]; // Value of kernel
+        }
+    }
+
+    return preCalc;
+}
+
+pmSubtractionKernels *pmSubtractionKernelsPOIS(int size, int spatialOrder, float penalty, psRegion bounds,
+                                               pmSubtractionMode mode)
+{
+    PS_ASSERT_INT_POSITIVE(size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(spatialOrder, NULL);
+
+    int num = PS_SQR(2 * size + 1) - 1; // Number of basis functions
+
+    pmSubtractionKernels *kernels = pmSubtractionKernelsAlloc(0, PM_SUBTRACTION_KERNEL_POIS, size, NULL, NULL, spatialOrder, penalty, bounds, mode); // Kernels
+    pmSubtractionKernelsMakeDescription(kernels);
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "kernel: %s --> %d elements", kernels->description, num);
+
+    if (!p_pmSubtractionKernelsAddGrid(kernels, 0, size)) {
+        psAbort("Should never get here.");
+    }
+
+    return kernels;
+}
+
+
+pmSubtractionKernels *pmSubtractionKernelsISIS(int size, int spatialOrder,
+                                               const psVector *fwhms, const psVector *orders,
+                                               float penalty, psRegion bounds, pmSubtractionMode mode)
+{
+    pmSubtractionKernels *kernels = p_pmSubtractionKernelsRawISIS(size, spatialOrder, fwhms, orders,
+                                                                  penalty, bounds, mode); // Kernels
+    if (!kernels) {
+        return NULL;
+    }
+
+    return kernels;
+}
+
+pmSubtractionKernels *pmSubtractionKernelsSPAM(int size, int spatialOrder, int inner, int binning,
+                                               float penalty, psRegion bounds, pmSubtractionMode mode)
+{
+    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) - 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, NULL, NULL, spatialOrder, penalty, bounds, mode); // Kernels
+    kernels->inner = inner;
+    kernels->binning = binning;
+    pmSubtractionKernelsMakeDescription(kernels);
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "kernel: %s --> %d elements", kernels->description, num);
+
+    kernels->uStop = psVectorAlloc(num, PS_TYPE_S32);
+    kernels->vStop = psVectorAlloc(num, PS_TYPE_S32);
+
+    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++) {
+            if (i == 0 && j == 0) {
+                // Skip normalisation component: added explicitly
+                index--;
+                continue;
+            }
+            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);
+        }
+    }
+
+    psFree(locations);
+    psFree(widths);
+
+    psWarning("Kernel penalty for dual-convolution is not configured for SPAM kernels.");
+    psVectorInit(kernels->penalties1, 0.0);
+    psVectorInit(kernels->penalties2, 0.0);
+
+    return kernels;
+}
+
+
+pmSubtractionKernels *pmSubtractionKernelsFRIES(int size, int spatialOrder, int inner, float penalty,
+                                                psRegion bounds, pmSubtractionMode mode)
+{
+    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) - 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, NULL, NULL, spatialOrder, penalty, bounds, mode); // Kernels
+    kernels->inner = inner;
+    pmSubtractionKernelsMakeDescription(kernels);
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "kernel: %s --> %d elements", kernels->description, num);
+
+    kernels->uStop = psVectorAlloc(num, PS_TYPE_S32);
+    kernels->vStop = psVectorAlloc(num, PS_TYPE_S32);
+
+    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++) {
+            if (i == 0 && j == 0) {
+                // Skip normalisation component: added explicitly
+                index--;
+                continue;
+            }
+            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);
+        }
+    }
+
+    psFree(start);
+    psFree(stop);
+
+    psWarning("Kernel penalty for dual-convolution is not configured for FRIES kernels.");
+    psVectorInit(kernels->penalties1, 0.0);
+    psVectorInit(kernels->penalties2, 0.0);
+
+    return kernels;
+}
+
+// Grid United with Normal Kernel [description: GUNK=ISIS(...)+POIS(...)]
+pmSubtractionKernels *pmSubtractionKernelsGUNK(int size, int spatialOrder, const psVector *fwhms,
+                                               const psVector *orders, int inner, float penalty,
+                                               psRegion bounds, pmSubtractionMode mode)
+{
+    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, penalty, bounds, mode); // Kernels
+    kernels->type = PM_SUBTRACTION_KERNEL_GUNK;
+    kernels->inner = inner;
+    pmSubtractionKernelsMakeDescription(kernels);
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "kernel: %s --> %d elements", kernels->description, (int) kernels->num);
+
+    int numISIS = kernels->num;         // Number of ISIS kernels
+
+    if (!p_pmSubtractionKernelsAddGrid(kernels, numISIS, inner)) {
+        psAbort("Should never get here.");
+    }
+
+    return kernels;
+}
+
+// RINGS --- just what it says
+pmSubtractionKernels *pmSubtractionKernelsRINGS(int size, int spatialOrder, int inner, int ringsOrder,
+                                                float penalty, psRegion bounds, pmSubtractionMode mode)
+{
+    PS_ASSERT_INT_POSITIVE(size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(spatialOrder, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(inner, NULL);
+    PS_ASSERT_INT_LESS_THAN_OR_EQUAL(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 + fibIndex < size) {
+            radius++;
+            int fibNew = fibIndex + fibIndexMinus1;
+            fibIndexMinus1 = fibIndex;
+            fibIndex = fibNew;
+            radius += fibIndex;
+            fibNum++;
+        }
+    }
+
+    int numInner = inner - 1;           // 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 = PM_SUBTRACTION_POLYTERMS(ringsOrder); // Number of polynomial variants of each ring
+
+    int num = numRings * numPoly; // Total number of basis functions
+
+    pmSubtractionKernels *kernels = pmSubtractionKernelsAlloc(num, PM_SUBTRACTION_KERNEL_RINGS, size, NULL, NULL, spatialOrder, penalty, bounds, mode); // Kernels
+    kernels->inner = inner;
+    kernels->ringsOrder = ringsOrder;
+    pmSubtractionKernelsMakeDescription(kernels);
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "kernel: %s --> %d elements", kernels->description, num);
+
+    // Set the Gaussian kernel parameters
+    int fibIndex = 1, fibIndexMinus1 = 0; // Fibonnacci parameters
+    int radiusLast = 1;                 // Last radius
+    for (int i = 1, index = 0; i < numRings + 1; i++) {
+        float lower2;                   // Lower limit of radius^2
+        float upper2;                   // Upper limit of radius^2
+        if (i <= inner) {
+            // 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++) {
+
+                pmSubtractionKernelPreCalc *preCalc = pmSubtractionKernelPreCalcAlloc (PM_SUBTRACTION_KERNEL_RINGS, 0, 0, RINGS_BUFFER, 0.0);
+                double moment = 0.0;    // Moment, for penalty
+
+                if (i == 0) {
+                    // Central pixel is easy
+                    preCalc->uCoords->data.S32[0] = 0;
+                    preCalc->vCoords->data.S32[0] = 0;
+                    preCalc->poly->data.F32[0] = 1.0;
+                    preCalc->uCoords->n = 1;
+                    preCalc->vCoords->n = 1;
+                    preCalc->poly->n = 1;
+                    radiusLast = 0;
+                    moment = 0.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 = powf(v/(float)size, 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 = powf(u/(float)size, uOrder); // Value of u^uOrder
+
+                                float polyVal = uPoly * vPoly; // Value of polynomial
+                                if (polyVal != 0) { // No point adding it otherwise
+                                    preCalc->uCoords->data.S32[j] = u;
+                                    preCalc->vCoords->data.S32[j] = v;
+                                    preCalc->poly->data.F32[j] = polyVal;
+                                    norm += polyVal;
+                                    moment += PS_SQR(polyVal) * PS_SQR(PS_SQR(u) + PS_SQR(v));
+
+                                    psVectorExtend(preCalc->uCoords, RINGS_BUFFER, 1);
+                                    psVectorExtend(preCalc->vCoords, RINGS_BUFFER, 1);
+                                    psVectorExtend(preCalc->poly, RINGS_BUFFER, 1);
+                                    psTrace("psModules.imcombine", 9, "u = %d, v = %d, poly = %f\n",
+                                            u, v, preCalc->poly->data.F32[j]);
+                                    j++;
+                                }
+                            }
+                        }
+                    }
+                    // Normalise kernel component to unit sum
+                    if (uOrder % 2 == 0 && vOrder % 2 == 0) {
+                        psBinaryOp(preCalc->poly, preCalc->poly, "*", psScalarAlloc(1.0 / norm, PS_TYPE_F32));
+                        // Add subtraction of 0,0 component to preserve photometric scaling
+                        preCalc->uCoords->data.S32[j] = 0;
+                        preCalc->vCoords->data.S32[j] = 0;
+                        preCalc->poly->data.F32[j] = -1.0;
+                        psVectorExtend(preCalc->uCoords, RINGS_BUFFER, 1);
+                        psVectorExtend(preCalc->vCoords, RINGS_BUFFER, 1);
+                        psVectorExtend(preCalc->poly, RINGS_BUFFER, 1);
+                    } else {
+                        norm = powf(size, uOrder) * powf(size, vOrder);
+                        psBinaryOp(preCalc->poly, preCalc->poly, "*", psScalarAlloc(1.0 / norm, PS_TYPE_F32));
+                    }
+                    moment /= PS_SQR(norm);
+                }
+
+                psTrace("psModules.imcombine", 8, "%ld pixels in kernel\n", preCalc->uCoords->n);
+
+                kernels->preCalc->data[index] = preCalc;
+                kernels->u->data.S32[index] = uOrder;
+                kernels->v->data.S32[index] = vOrder;
+
+		// XXX convert to use the convolved 2nd moment
+                kernels->penalties1->data.F32[index] = kernels->penalty * fabsf(moment);
+                if (!isfinite(kernels->penalties1->data.F32[index])) {
+                    psAbort ("invalid penalty");
+                }
+                kernels->penalties2->data.F32[index] = kernels->penalty * fabsf(moment);
+                if (!isfinite(kernels->penalties2->data.F32[index])) {
+                    psAbort ("invalid penalty");
+                }
+
+                psTrace("psModules.imcombine", 7, "Kernel %d: %d %d %d\n", index,
+                        i, uOrder, vOrder);
+            }
+        }
+    }
+
+    return kernels;
+}
+
+pmSubtractionKernels *pmSubtractionKernelsGenerate(pmSubtractionKernelsType type, int size, int spatialOrder,
+                                                   const psVector *fwhms, const psVector *orders, int inner,
+                                                   int binning, int ringsOrder, float penalty, psRegion bounds,
+                                                   pmSubtractionMode mode)
+{
+    switch (type) {
+      case PM_SUBTRACTION_KERNEL_POIS:
+        return pmSubtractionKernelsPOIS(size, spatialOrder, penalty, bounds, mode);
+      case PM_SUBTRACTION_KERNEL_ISIS:
+        return pmSubtractionKernelsISIS(size, spatialOrder, fwhms, orders, penalty, bounds, mode);
+      case PM_SUBTRACTION_KERNEL_ISIS_RADIAL:
+        return pmSubtractionKernelsISIS_RADIAL(size, spatialOrder, fwhms, orders, penalty, bounds, mode);
+      case PM_SUBTRACTION_KERNEL_HERM:
+        return pmSubtractionKernelsHERM(size, spatialOrder, fwhms, orders, penalty, bounds, mode);
+      case PM_SUBTRACTION_KERNEL_DECONV_HERM:
+        return pmSubtractionKernelsDECONV_HERM(size, spatialOrder, fwhms, orders, penalty, bounds, mode);
+      case PM_SUBTRACTION_KERNEL_SPAM:
+        return pmSubtractionKernelsSPAM(size, spatialOrder, inner, binning, penalty, bounds, mode);
+      case PM_SUBTRACTION_KERNEL_FRIES:
+        return pmSubtractionKernelsFRIES(size, spatialOrder, inner, penalty, bounds, mode);
+      case PM_SUBTRACTION_KERNEL_GUNK:
+        return pmSubtractionKernelsGUNK(size, spatialOrder, fwhms, orders, inner, penalty, bounds, mode);
+      case PM_SUBTRACTION_KERNEL_RINGS:
+        return pmSubtractionKernelsRINGS(size, spatialOrder, inner, ringsOrder, penalty, bounds, mode);
+    case PM_SUBTRACTION_KERNEL_SIMPLE:
+      return pmSubtractionKernelsISIS(size,spatialOrder,fwhms,orders,penalty,bounds,mode);
+      default:
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unknown kernel type: %x", type);
+        return NULL;
+    }
+}
+
+
+// Intermediate string parsing functions required because of different APIs for strtol and strtof
+static inline int parseStringInt(const char *string)
+{
+    return strtol(string, NULL, 10);
+}
+static inline float parseStringFloat(const char *string)
+{
+    return strtof(string, NULL);
+}
+
+
+// Parse a string of a number, up to some delimiter, and advance past the delimiter
+#define PARSE_STRING_NUMBER(TARGET, STRING, DELIM, PARSEFUNC) { \
+    char *start = STRING;               /* Start of string */ \
+    char *end = strchr(STRING, DELIM);  /* End of string */ \
+    if (!end) { \
+        psAbort("End of string encountered"); \
+    } \
+    int stringSize = end - STRING;      /* Size of string with value, NOT including \0 */ \
+    char value[stringSize + 1];         /* String to parse */ \
+    strncpy(value, start, stringSize); \
+    value[stringSize] = '\0'; \
+    TARGET = PARSEFUNC(value); \
+    STRING += stringSize + 1;           /* Advance past delimiter */ \
+}
+
+
+pmSubtractionKernels *pmSubtractionKernelsFromDescription(const char *description, int bgOrder,
+                                                          psRegion bounds, pmSubtractionMode mode)
+{
+    PS_ASSERT_STRING_NON_EMPTY(description, NULL);
+
+    if (bgOrder != 0) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Background order %d is not yet supported.", bgOrder);
+        return false;
+    }
+
+    pmSubtractionKernelsType type = PM_SUBTRACTION_KERNEL_NONE; // Type of kernel
+    int size = 0;                       // Half-size of kernel
+    int spatialOrder = 0;               // Order of spatial variations
+    const psVector *fwhms = NULL;       // FWHM of Gaussians
+    const psVector *orders = NULL;      // Polynomial order for each FWHM
+    int inner = 0;                      // Size of inner region
+    int binning = 0;                    // Binning to use
+    int ringsOrder = 0;                 // Polynomial order for rings
+    float penalty = 0.0;                // Penalty for wideness
+
+    // currently known descriptions:
+    // ISIS(...), ISIS_RADIAL(...), HERM(...), DECONV_HERM(...), POIS(...), SPAM(...),
+    // FRIES(...), GUNK=ISIS(...)+POIS(...), RINGS(...),
+    // the descriptive name is the set of characters before the (
+
+    type = pmSubtractionKernelsTypeFromString (description);
+    char *ptr = strchr(description, '(') + 1;
+    psAssert (ptr, "description is missing kernel parameters");
+
+    switch (type) {
+      case PM_SUBTRACTION_KERNEL_ISIS:
+      case PM_SUBTRACTION_KERNEL_ISIS_RADIAL:
+      case PM_SUBTRACTION_KERNEL_HERM:
+      case PM_SUBTRACTION_KERNEL_DECONV_HERM:
+    case PM_SUBTRACTION_KERNEL_SIMPLE:
+        PARSE_STRING_NUMBER(size, ptr, ',', parseStringInt);
+
+        // Count the number of Gaussians
+        int numGauss = 0;
+        for (char *string = ptr; string; string = strchr(string + 1, '(')) {
+            numGauss++;
+        }
+
+        fwhms = psVectorAlloc(numGauss, PS_TYPE_F32);
+        orders = psVectorAlloc(numGauss, PS_TYPE_S32);
+
+        for (int i = 0; i < numGauss; i++) {
+            ptr++;                                                               // Eat the '('
+            PARSE_STRING_NUMBER(fwhms->data.F32[i], ptr, ',', parseStringFloat); // Eat "1.234,"
+            PARSE_STRING_NUMBER(orders->data.S32[i], ptr, ')', parseStringInt);  // Eat "3)"
+        }
+
+        ptr++;                      // Eat ','
+        PARSE_STRING_NUMBER(spatialOrder, ptr, ',', parseStringInt);
+        penalty = parseStringFloat(ptr);
+        break;
+      case PM_SUBTRACTION_KERNEL_RINGS:
+        PARSE_STRING_NUMBER(size, ptr, ',', parseStringInt);
+        PARSE_STRING_NUMBER(inner, ptr, ',', parseStringInt);
+        PARSE_STRING_NUMBER(ringsOrder, ptr, ',', parseStringInt);
+        PARSE_STRING_NUMBER(spatialOrder, ptr, ',', parseStringInt);
+        PARSE_STRING_NUMBER(penalty, ptr, ')', parseStringInt);
+        break;
+      default:
+        psAbort("Deciphering kernels other than ISIS, HERM, DECONV_HERM or RINGS is not currently supported.");
+    }
+
+    pmSubtractionKernels *outKernel = pmSubtractionKernelsGenerate(type, size, spatialOrder,
+								  fwhms, orders, inner, binning,
+								  ringsOrder, penalty, bounds, mode);
+    psFree (fwhms);
+    psFree (orders);
+
+    return outKernel;
+}
+
+
+// the input string can either be just the name or the description string.  Currently known
+// descriptions: ISIS(...), ISIS_RADIAL(...), HERM(...), DECONV_HERM(...), POIS(...),
+// SPAM(...), FRIES(...), GUNK=ISIS(...)+POIS(...), RINGS(...),
+pmSubtractionKernelsType pmSubtractionKernelsTypeFromString(const char *type)
+{
+    // for a bare name (ISIS, HERM), use the full string length.
+    // otherwise, use the length up to the first '('
+    int nameLength = strlen(type);
+    char *ptr = strchr(type, '(');
+    if (ptr) {
+        nameLength = ptr - type;
+    }
+
+    if (strncasecmp(type, "POIS", nameLength) == 0) {
+        return PM_SUBTRACTION_KERNEL_POIS;
+    }
+    if (strncasecmp(type, "ISIS", nameLength) == 0) {
+        return PM_SUBTRACTION_KERNEL_ISIS;
+    }
+    if (strncasecmp(type, "ISIS_RADIAL", nameLength) == 0) {
+        return PM_SUBTRACTION_KERNEL_ISIS_RADIAL;
+    }
+    if (strncasecmp(type, "HERM", nameLength) == 0) {
+        return PM_SUBTRACTION_KERNEL_HERM;
+    }
+    if (strncasecmp(type, "DECONV_HERM", nameLength) == 0) {
+        return PM_SUBTRACTION_KERNEL_DECONV_HERM;
+    }
+    if (strncasecmp(type, "SPAM", nameLength) == 0) {
+        return PM_SUBTRACTION_KERNEL_SPAM;
+    }
+    if (strncasecmp(type, "FRIES", nameLength) == 0) {
+        return PM_SUBTRACTION_KERNEL_FRIES;
+    }
+    if (strncasecmp(type, "GUNK", nameLength) == 0) {
+        return PM_SUBTRACTION_KERNEL_GUNK;
+    }
+    // note that GUNK has a somewhat different description
+    if (strncasecmp(type, "GUNK=ISIS", nameLength) == 0) {
+        return PM_SUBTRACTION_KERNEL_GUNK;
+    }
+    if (strncasecmp(type, "RINGS", nameLength) == 0) {
+        return PM_SUBTRACTION_KERNEL_RINGS;
+    }
+    if (strncasecmp(type, "SIMPLE", nameLength) == 0) {
+      return PM_SUBTRACTION_KERNEL_SIMPLE;
+    }
+    psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unrecognised kernel type: %s", type);
+    return PM_SUBTRACTION_KERNEL_NONE;
+}
+
+bool pmSubtractionKernelsMakeDescription(pmSubtractionKernels *kernels) {
+
+    // free if it exists
+    psFree (kernels->description);
+
+    // generate the description parameter string
+    psString params = NULL;
+    if (kernels->fwhms) {
+	for (int i = 0; i < kernels->fwhms->n; i++) {
+	    psStringAppend(&params, "(%.1f,%d)", kernels->fwhms->data.F32[i], kernels->orders->data.S32[i]);
+	}
+    }
+
+    switch (kernels->type) {
+      case PM_SUBTRACTION_KERNEL_ISIS:
+	psStringAppend (&kernels->description, "ISIS(%d,%s,%d,%.2e)", kernels->size, params, kernels->spatialOrder, kernels->penalty);
+	break;
+
+      case PM_SUBTRACTION_KERNEL_ISIS_RADIAL:
+	psStringAppend(&kernels->description, "ISIS_RADIAL(%d,%s,%d,%.2e)", kernels->size, params, kernels->spatialOrder, kernels->penalty);
+	break;
+
+      case PM_SUBTRACTION_KERNEL_HERM:
+	psStringAppend(&kernels->description, "HERM(%d,%s,%d,%.2e)", kernels->size, params, kernels->spatialOrder, kernels->penalty);
+	break;
+
+      case PM_SUBTRACTION_KERNEL_DECONV_HERM:
+	psStringAppend(&kernels->description, "DECONV_HERM(%d,%s,%d,%.2e)", kernels->size, params, kernels->spatialOrder, kernels->penalty);
+	break;
+
+      case PM_SUBTRACTION_KERNEL_POIS:
+	psStringAppend(&kernels->description, "POIS(%d,%d,%.2e)", kernels->size, kernels->spatialOrder, kernels->penalty);
+	break;
+
+      case PM_SUBTRACTION_KERNEL_SPAM:
+	psStringAppend(&kernels->description, "SPAM(%d,%d,%d,%d,%.2e)", kernels->size, kernels->inner, kernels->binning, kernels->spatialOrder, kernels->penalty);
+	break;
+
+      case PM_SUBTRACTION_KERNEL_FRIES:
+	psStringAppend(&kernels->description, "FRIES(%d,%d,%d,%.2e)", kernels->size, kernels->inner, kernels->spatialOrder, kernels->penalty);
+	break;
+
+	// Grid United with Normal Kernel [description: GUNK=ISIS(...)+POIS(...)]
+      case PM_SUBTRACTION_KERNEL_GUNK:
+	psStringAppend(&kernels->description, "GUNK=ISIS(%d,%s,%d,%.2e)", kernels->size, params, kernels->spatialOrder, kernels->penalty);
+	psStringAppend(&kernels->description, "+POIS(%d,%d)", kernels->inner, kernels->spatialOrder);
+	break;
+	
+      case PM_SUBTRACTION_KERNEL_RINGS:
+	psStringAppend(&kernels->description, "RINGS(%d,%d,%d,%d,%.2e)", kernels->size, kernels->inner, kernels->ringsOrder, kernels->spatialOrder, kernels->penalty);
+	break;
+
+    case PM_SUBTRACTION_KERNEL_SIMPLE:
+      psStringAppend(&kernels->description, "SIMPLE(%d,%s)",kernels->size,params);
+      break;
+      default:
+        psAbort("unknown kernel");
+    }
+    psFree (params);
+    return true;
+}
+
+pmSubtractionKernels *pmSubtractionKernelsCopy(const pmSubtractionKernels *in)
+{
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(in, NULL);
+
+    pmSubtractionKernels *out = psAlloc(sizeof(pmSubtractionKernels)); // Kernels, to return
+    psMemSetDeallocator(out, (psFreeFunc)subtractionKernelsFree);
+
+    out->type = in->type;
+    out->description = psMemIncrRefCounter(in->description);
+    out->num = in->num;
+    out->u = psMemIncrRefCounter(in->u);
+    out->v = psMemIncrRefCounter(in->v);
+    out->widths = psMemIncrRefCounter(in->widths);
+    out->preCalc = psMemIncrRefCounter(in->preCalc);
+    out->penalty = in->penalty;
+    out->penalties1 = psMemIncrRefCounter(in->penalties1);
+    out->penalties2 = psMemIncrRefCounter(in->penalties2);
+    out->uStop = psMemIncrRefCounter(in->uStop);
+    out->vStop = psMemIncrRefCounter(in->vStop);
+    out->size = in->size;
+    out->inner = in->inner;
+    out->spatialOrder = in->spatialOrder;
+    out->bgOrder = in->bgOrder;
+    out->mode = in->mode;
+    out->xMin = in->xMin;
+    out->xMax = in->xMax;
+    out->yMin = in->yMin;
+    out->yMax = in->yMax;
+    out->solution1 = in->solution1 ? psVectorCopy(NULL, in->solution1, PS_TYPE_F64) : NULL;
+    out->solution2 = in->solution2 ? psVectorCopy(NULL, in->solution2, PS_TYPE_F64) : NULL;
+    out->solution1err = in->solution1err ? psVectorCopy(NULL, in->solution1err, PS_TYPE_F64) : NULL;
+    out->solution2err = in->solution2err ? psVectorCopy(NULL, in->solution2err, PS_TYPE_F64) : NULL;
+    out->sampleStamps = psMemIncrRefCounter(in->sampleStamps);
+
+    return out;
+}
+
+#define KERNEL_MOSAIC 2                 // Half-number of kernel instances in the mosaic image
+psImage *pmSubtractionKernelsImageMosaic(pmSubtractionKernels *kernels) {
+
+    psTrace("psModules.imcombine", 2, "Generating diagnostic image..\n");
+
+    // Generate image with convolution kernels
+    int size = kernels->size;       // Half-size of kernel
+    int fullSize = 2 * size + 1 + 1; // Full size of kernel
+    int imageSize = (2 * KERNEL_MOSAIC + 1) * fullSize;
+    psImage *convKernels = psImageAlloc((kernels->mode == PM_SUBTRACTION_MODE_DUAL ? 2 : 1) *
+					imageSize - 1 +
+					(kernels->mode == PM_SUBTRACTION_MODE_DUAL ? 4 : 0),
+					imageSize - 1, PS_TYPE_F32);
+    psImageInit(convKernels, NAN);
+    for (int j = -KERNEL_MOSAIC; j <= KERNEL_MOSAIC; j++) {
+	for (int i = -KERNEL_MOSAIC; i <= KERNEL_MOSAIC; i++) {
+	    psImage *kernel = pmSubtractionKernelImage(kernels, (float)i / (float)KERNEL_MOSAIC,
+						       (float)j / (float)KERNEL_MOSAIC,
+						       false); // Image of the kernel
+	    if (!kernel) {
+		psError(psErrorCodeLast(), false, "Unable to generate kernel image.");
+		psFree(convKernels);
+		return NULL;
+	    }
+
+	    if (psImageOverlaySection(convKernels, kernel, (i + KERNEL_MOSAIC) * fullSize,
+				      (j + KERNEL_MOSAIC) * fullSize, "=") == 0) {
+		psError(psErrorCodeLast(), false, "Unable to overlay kernel image.");
+		psFree(kernel);
+		psFree(convKernels);
+		return NULL;
+	    }
+	    psFree(kernel);
+
+	    if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+		kernel = pmSubtractionKernelImage(kernels, (float)i / (float)KERNEL_MOSAIC,
+						  (float)j / (float)KERNEL_MOSAIC,
+						  true); // Image of the kernel
+		if (!kernel) {
+		    psError(psErrorCodeLast(), false, "Unable to generate kernel image.");
+		    psFree(convKernels);
+		    return NULL;
+		}
+
+		if (psImageOverlaySection(convKernels, kernel,
+					  (2 * KERNEL_MOSAIC + 1 + i + KERNEL_MOSAIC) * fullSize + 4,
+					  (j + KERNEL_MOSAIC) * fullSize, "=") == 0) {
+		    psError(psErrorCodeLast(), false, "Unable to overlay kernel image.");
+		    psFree(kernel);
+		    psFree(convKernels);
+		    return NULL;
+		}
+		psFree(kernel);
+	    }
+	}
+    }
+    return convKernels;
+}
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionKernels.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionKernels.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionKernels.h	(revision 42651)
@@ -0,0 +1,264 @@
+#ifndef PM_SUBTRACTION_KERNELS_H
+#define PM_SUBTRACTION_KERNELS_H
+
+// #include <string.h>
+// #include <pslib.h>
+
+// Assertion to check pmSubtractionKernels
+#define PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(KERNELS, RETURNVALUE) { \
+    PS_ASSERT_PTR_NON_NULL(KERNELS, RETURNVALUE); \
+    PS_ASSERT_STRING_NON_EMPTY((KERNELS)->description, RETURNVALUE); \
+    PS_ASSERT_INT_NONNEGATIVE((KERNELS)->num, RETURNVALUE); \
+    PS_ASSERT_VECTOR_NON_NULL((KERNELS)->u, RETURNVALUE); \
+    PS_ASSERT_VECTOR_NON_NULL((KERNELS)->v, RETURNVALUE); \
+    PS_ASSERT_VECTOR_TYPE((KERNELS)->u, PS_TYPE_S32, RETURNVALUE); \
+    PS_ASSERT_VECTOR_TYPE((KERNELS)->v, PS_TYPE_S32, RETURNVALUE); \
+    PS_ASSERT_VECTOR_SIZE((KERNELS)->u, (KERNELS)->num, RETURNVALUE); \
+    PS_ASSERT_VECTOR_SIZE((KERNELS)->v, (KERNELS)->num, RETURNVALUE); \
+    if ((KERNELS)->type == PM_SUBTRACTION_KERNEL_ISIS) { \
+        PS_ASSERT_VECTOR_NON_NULL((KERNELS)->widths, RETURNVALUE); \
+        PS_ASSERT_VECTOR_TYPE((KERNELS)->widths, PS_TYPE_F32, RETURNVALUE); \
+        PS_ASSERT_VECTOR_SIZE((KERNELS)->widths, (KERNELS)->num, RETURNVALUE); \
+    } \
+    if ((KERNELS)->type == PM_SUBTRACTION_KERNEL_ISIS_RADIAL) { \
+        PS_ASSERT_VECTOR_NON_NULL((KERNELS)->widths, RETURNVALUE); \
+        PS_ASSERT_VECTOR_TYPE((KERNELS)->widths, PS_TYPE_F32, RETURNVALUE); \
+        PS_ASSERT_VECTOR_SIZE((KERNELS)->widths, (KERNELS)->num, RETURNVALUE); \
+    } \
+    if ((KERNELS)->type == PM_SUBTRACTION_KERNEL_SIMPLE) { \
+        PS_ASSERT_VECTOR_NON_NULL((KERNELS)->widths, RETURNVALUE); \
+        PS_ASSERT_VECTOR_TYPE((KERNELS)->widths, PS_TYPE_F32, RETURNVALUE); \
+        PS_ASSERT_VECTOR_SIZE((KERNELS)->widths, (KERNELS)->num, RETURNVALUE); \
+    } \
+    if ((KERNELS)->type == PM_SUBTRACTION_KERNEL_HERM) { \
+        PS_ASSERT_VECTOR_NON_NULL((KERNELS)->widths, RETURNVALUE); \
+        PS_ASSERT_VECTOR_TYPE((KERNELS)->widths, PS_TYPE_F32, RETURNVALUE); \
+        PS_ASSERT_VECTOR_SIZE((KERNELS)->widths, (KERNELS)->num, RETURNVALUE); \
+    } \
+    if ((KERNELS)->type == PM_SUBTRACTION_KERNEL_DECONV_HERM) { \
+        PS_ASSERT_VECTOR_NON_NULL((KERNELS)->widths, RETURNVALUE); \
+        PS_ASSERT_VECTOR_TYPE((KERNELS)->widths, PS_TYPE_F32, RETURNVALUE); \
+        PS_ASSERT_VECTOR_SIZE((KERNELS)->widths, (KERNELS)->num, RETURNVALUE); \
+    } \
+    if ((KERNELS)->uStop || (KERNELS)->vStop) { \
+        PS_ASSERT_VECTOR_NON_NULL((KERNELS)->uStop, RETURNVALUE); \
+        PS_ASSERT_VECTOR_NON_NULL((KERNELS)->vStop, RETURNVALUE); \
+        PS_ASSERT_VECTOR_TYPE((KERNELS)->uStop, PS_TYPE_S32, RETURNVALUE); \
+        PS_ASSERT_VECTOR_TYPE((KERNELS)->vStop, PS_TYPE_S32, RETURNVALUE); \
+        PS_ASSERT_VECTOR_SIZE((KERNELS)->uStop, (KERNELS)->num, RETURNVALUE); \
+        PS_ASSERT_VECTOR_SIZE((KERNELS)->vStop, (KERNELS)->num, RETURNVALUE); \
+    } \
+    if ((KERNELS)->preCalc) { \
+        PS_ASSERT_ARRAY_NON_NULL((KERNELS)->preCalc, RETURNVALUE); \
+        PS_ASSERT_ARRAY_SIZE((KERNELS)->preCalc, (KERNELS)->num, RETURNVALUE); \
+    } \
+    PS_ASSERT_INT_NONNEGATIVE((KERNELS)->size, RETURNVALUE); \
+    PS_ASSERT_INT_NONNEGATIVE((KERNELS)->inner, RETURNVALUE); \
+    PS_ASSERT_INT_NONNEGATIVE((KERNELS)->spatialOrder, RETURNVALUE); \
+    PS_ASSERT_INT_NONNEGATIVE((KERNELS)->bgOrder, RETURNVALUE); \
+}
+
+// Assertion to check that the solution is attached
+#define PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(KERNELS, RETURNVALUE) { \
+    PS_ASSERT_VECTOR_NON_NULL((KERNELS)->solution1, RETURNVALUE); \
+    PS_ASSERT_VECTOR_TYPE((KERNELS)->solution1, PS_TYPE_F64, RETURNVALUE); \
+    PS_ASSERT_VECTOR_SIZE((KERNELS)->solution1, \
+                          (KERNELS)->num * PM_SUBTRACTION_POLYTERMS((KERNELS)->spatialOrder) + 1 + \
+                              PM_SUBTRACTION_POLYTERMS((KERNELS)->bgOrder), \
+                          RETURNVALUE); \
+    if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) { \
+        PS_ASSERT_VECTOR_NON_NULL(kernels->solution2, RETURNVALUE); \
+        PS_ASSERT_VECTOR_TYPE((KERNELS)->solution2, PS_TYPE_F64, RETURNVALUE); \
+        PS_ASSERT_VECTOR_SIZE((KERNELS)->solution2, \
+                              (KERNELS)->num * PM_SUBTRACTION_POLYTERMS((KERNELS)->spatialOrder), \
+                               RETURNVALUE); \
+    } \
+}
+
+// Generate 1D convolution kernel for SIMPLE
+psVector *pmSubtractionKernelSIMPLE(float sigma, // Gaussian width
+				    int order,   // Unused polynomial order
+				    int size     // Kernel half-size
+				    );
+
+// Generate 1D convolution kernel for ISIS
+psVector *pmSubtractionKernelISIS(float sigma, // Gaussian width
+                                       int order, // Polynomial order
+                                       int size // Kernel half-size
+    );
+
+// Generate 1D convolution kernel for HERM (normalized for 2D)
+psVector *pmSubtractionKernelHERM(float sigma, // Gaussian width
+                                       int order, // Polynomial order
+                                       int size // Kernel half-size
+    );
+
+/// 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
+						psVector *fwhms, ///< requested kernel basis function
+						psVector *orders,
+                                                int spatialOrder, ///< Order of spatial variations
+                                                float penalty, ///< Penalty for wideness
+                                                psRegion bounds,       ///< Bounds for validity
+                                                pmSubtractionMode mode ///< Mode for subtraction
+    );
+
+/// Allocator for pre-calculated kernel data structure
+pmSubtractionKernelPreCalc *pmSubtractionKernelPreCalcAlloc(
+    pmSubtractionKernelsType type, ///< type of kernel to allocate (not all can be pre-calculated)
+    int uOrder,                    ///< order in x-direction
+    int vOrder,                    ///< order in x-direction
+    int size,                      ///< Half-size of the kernel
+    float sigma                    ///< sigma of gaussian kernel
+    );
+
+
+/// Generate POIS kernels
+pmSubtractionKernels *pmSubtractionKernelsPOIS(int size, ///< Half-size of the kernel (in both dims)
+                                               int spatialOrder, ///< Order of spatial variations
+                                               float penalty, ///< Penalty for wideness
+                                               psRegion bounds,       ///< Bounds for validity
+                                               pmSubtractionMode mode ///< Mode for subtraction
+    );
+
+/// 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
+                                                    float penalty, ///< Penalty for wideness
+                                                    psRegion bounds,       ///< Bounds for validity
+                                                    pmSubtractionMode mode ///< Mode for subtraction
+    );
+
+/// 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
+                                               float penalty, ///< Penalty for wideness
+                                               psRegion bounds,       ///< Bounds for validity
+                                               pmSubtractionMode mode ///< Mode for subtraction
+                                               );
+
+/// Generate ISIS + RADIAL_HERM kernels
+pmSubtractionKernels *pmSubtractionKernelsISIS_RADIAL(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
+                                                      float penalty, ///< Penalty for wideness
+                                                      psRegion bounds,       ///< Bounds for validity
+                                                      pmSubtractionMode mode ///< Mode for subtraction
+                                               );
+
+/// Generate HERM kernels
+pmSubtractionKernels *pmSubtractionKernelsHERM(int size, ///< Half-size of the kernel
+                                               int spatialOrder, ///< Order of spatial variations
+                                               const psVector *fwhms, ///< Gaussian FWHMs
+                                               const psVector *orders, ///< order of hermitian polynomials
+                                               float penalty, ///< Penalty for wideness
+                                               psRegion bounds,       ///< Bounds for validity
+                                               pmSubtractionMode mode ///< Mode for subtraction
+                                               );
+
+/// Generate DECONV_HERM kernels
+pmSubtractionKernels *pmSubtractionKernelsDECONV_HERM(int size, ///< Half-size of the kernel
+                                                      int spatialOrder, ///< Order of spatial variations
+                                                      const psVector *fwhms, ///< Gaussian FWHMs
+                                                      const psVector *orders, ///< order of hermitian polynomials
+                                                      float penalty, ///< Penalty for wideness
+                                                      psRegion bounds,       ///< Bounds for validity
+                                                      pmSubtractionMode mode ///< Mode for subtraction
+    );
+
+/// 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
+                                               float penalty, ///< Penalty for wideness
+                                               psRegion bounds,       ///< Bounds for validity
+                                               pmSubtractionMode mode ///< Mode for subtraction
+    );
+
+/// 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
+                                                float penalty, ///< Penalty for wideness
+                                                psRegion bounds,       ///< Bounds for validity
+                                                pmSubtractionMode mode ///< Mode for subtraction
+    );
+
+/// 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
+                                               float penalty, ///< Penalty for wideness
+                                               psRegion bounds,       ///< Bounds for validity
+                                               pmSubtractionMode mode ///< Mode for subtraction
+    );
+
+/// 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
+                                                float penalty, ///< Penalty for wideness
+                                                psRegion bounds,       ///< Bounds for validity
+                                                pmSubtractionMode mode ///< Mode for subtraction
+    );
+
+
+/// 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
+                                                   float penalty, ///< Penalty for wideness
+                                                   psRegion bounds,       ///< Bounds for validity
+                                                   pmSubtractionMode mode ///< Mode for subtraction
+    );
+
+/// Generate a kernel using the description
+pmSubtractionKernels *pmSubtractionKernelsFromDescription(
+    const char *description,            ///< Description of kernel
+    int bgOrder,                        ///< Polynomial order for background fitting
+    psRegion bounds,                    ///< Bounds for validity
+    pmSubtractionMode mode              ///< Mode for subtraction
+    );
+
+/// Return the appropriate type from a string
+pmSubtractionKernelsType pmSubtractionKernelsTypeFromString(const char *string // String name for kernel type
+    );
+
+bool pmSubtractionKernelsMakeDescription(pmSubtractionKernels *kernels);
+
+
+/// Copy kernels
+///
+/// A deep copy is performed on the solution only; the other components are merely pointers.
+pmSubtractionKernels *pmSubtractionKernelsCopy(
+    const pmSubtractionKernels *in      // Kernels to copy
+    );
+
+psImage *pmSubtractionKernelsImageMosaic(pmSubtractionKernels *kernels);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionMask.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionMask.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionMask.c	(revision 42651)
@@ -0,0 +1,295 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmErrorCodes.h"
+#include "pmFPA.h"
+#include "pmSubtractionTypes.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionKernels.h"
+
+#include "pmSubtractionMask.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Private (file-static) functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// 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
+                             psImageMaskType blank // Blank mask value
+    )
+{
+    image->data.F32[y][x] = NAN;
+    if (mask) {
+        mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= blank;
+    }
+    if (weight) {
+        weight->data.F32[y][x] = NAN;
+    }
+    return;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+psImage *pmSubtractionMask(psRegion *bounds, const pmReadout *ro1, const pmReadout *ro2,
+                           psImageMaskType maskVal, int size, int footprint, float badFrac,
+                           pmSubtractionMode mode)
+{
+    int numCols = 0, numRows = 0;       // Size of the images
+    if (ro1) {
+        PM_ASSERT_READOUT_NON_NULL(ro1, NULL);
+        PM_ASSERT_READOUT_IMAGE(ro1, NULL);
+        PM_ASSERT_READOUT_MASK(ro1, NULL);
+        numCols = ro1->image->numCols;
+        numRows = ro1->image->numRows;
+            }
+    if (ro2) {
+        PM_ASSERT_READOUT_NON_NULL(ro2, NULL);
+        PM_ASSERT_READOUT_IMAGE(ro2, NULL);
+        PM_ASSERT_READOUT_MASK(ro2, NULL);
+        numCols = ro2->image->numCols;
+        numRows = ro2->image->numRows;
+    }
+    if (ro1 && ro2) {
+        PS_ASSERT_IMAGES_SIZE_EQUAL(ro1->image, ro2->image, NULL);
+    }
+    if (!ro1 && !ro2) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "No image provided.");
+        return false;
+    }
+    psAssert(numCols > 0 && numRows > 0, "There should be an image provided");
+    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);
+    }
+
+    // Dereference inputs for convenience
+    psF32 **imageData1 = ro1 ? ro1->image->data.F32 : NULL;
+    psF32 **imageData2 = ro2 ? ro2->image->data.F32 : NULL;
+    psImageMaskType **maskData1 = ro1 ? ro1->mask->data.PS_TYPE_IMAGE_MASK_DATA : NULL;
+    psImageMaskType **maskData2 = ro2 ? ro2->mask->data.PS_TYPE_IMAGE_MASK_DATA : NULL;
+
+    // First, a pass through to determine the fraction of bad pixels
+    if (bounds || (isfinite(badFrac) && badFrac != 1.0)) {
+        int xMin = numCols, xMax = 0, yMin = numRows, yMax = 0; // Bounds of good pixels
+        int numBad = 0;                 // Number of bad pixels
+        for (int y = 0; y < numRows; y++) {
+            for (int x = 0; x < numCols; x++) {
+                if (ro1 && ((maskData1[y][x] & maskVal) || !isfinite(imageData1[y][x]))) {
+                    numBad++;
+                    continue;
+                }
+                if (ro2 && ((maskData2[y][x] & maskVal) || !isfinite(imageData2[y][x]))) {
+                    numBad++;
+                    continue;
+                }
+                xMin = PS_MIN(xMin, x);
+                xMax = PS_MAX(xMax, x);
+                yMin = PS_MIN(yMin, y);
+                yMax = PS_MAX(yMax, y);
+            }
+        }
+        if (bounds) {
+            bounds->x0 = xMin;
+            bounds->x1 = xMax;
+            bounds->y0 = yMin;
+            bounds->y1 = yMax;
+        }
+        if (isfinite(badFrac) && badFrac != 1.0 && numBad > badFrac * numCols * numRows) {
+            psError(PM_ERR_SMALL_AREA, 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_IMAGE_MASK); // The global mask
+    psImageInit(mask, 0);
+    psImageMaskType **maskData = mask->data.PS_TYPE_IMAGE_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;
+        }
+    }
+
+    for (int y = 0; y < numRows; y++) {
+        for (int x = 0; x < numCols; x++) {
+            if (ro1 && maskData1[y][x] & maskVal) {
+                maskData[y][x] |= PM_SUBTRACTION_MASK_BAD_1;
+            }
+            if (ro2 && maskData2[y][x] & maskVal) {
+                maskData[y][x] |= PM_SUBTRACTION_MASK_BAD_2;
+            }
+        }
+    }
+
+    // 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 REJ mask everything within a
+    // footprint's distance of those (within 'footprint').
+
+    bool oldThreads = psImageConvolveSetThreads(true); // Old value of threading for psImageConvolve
+
+    // Pixels that will be bad (or poor) if we convolve with a bad reference pixel
+    if (ro1 && !psImageConvolveMask(mask, mask, PM_SUBTRACTION_MASK_BAD_1, PM_SUBTRACTION_MASK_CONVOLVE_1,
+                                    -size, size, -size, size)) {
+        psError(psErrorCodeLast(), false, "Unable to convolve bad pixels from mask 1.");
+        psFree(mask);
+        return NULL;
+    }
+    if (ro2 && !psImageConvolveMask(mask, mask, PM_SUBTRACTION_MASK_BAD_2, PM_SUBTRACTION_MASK_CONVOLVE_2,
+                                    -size, size, -size, size)) {
+        psError(psErrorCodeLast(), false, "Unable to convolve bad pixels from mask 2.");
+        psFree(mask);
+        return NULL;
+    }
+
+    // Pixels that should not be chosen as stamps
+    psImageMaskType maskRej = PM_SUBTRACTION_MASK_BAD_1 | PM_SUBTRACTION_MASK_BAD_2 |
+        PM_SUBTRACTION_MASK_BORDER;     // Mask value for rejection
+    switch (mode) {
+      case PM_SUBTRACTION_MODE_1:
+        maskRej |= PM_SUBTRACTION_MASK_CONVOLVE_1;
+        break;
+      case PM_SUBTRACTION_MODE_2:
+        maskRej |= PM_SUBTRACTION_MASK_CONVOLVE_2;
+        break;
+      case PM_SUBTRACTION_MODE_UNSURE:
+      case PM_SUBTRACTION_MODE_SINGLE_AUTO:
+      case PM_SUBTRACTION_MODE_DUAL:
+        maskRej |= PM_SUBTRACTION_MASK_CONVOLVE_1 | PM_SUBTRACTION_MASK_CONVOLVE_2;
+        break;
+      default:
+        psAbort("Unsupported subtraction mode: %x", mode);
+    }
+    if (ro1 && ro2 && !psImageConvolveMask(mask, mask, maskRej, PM_SUBTRACTION_MASK_REJ,
+                                           -footprint, footprint, -footprint, footprint)) {
+        psError(psErrorCodeLast(), false, "Unable to convolve bad pixels.");
+        psFree(mask);
+        return NULL;
+    }
+
+    psImageConvolveSetThreads(oldThreads);
+
+    return mask;
+}
+
+
+bool pmSubtractionBorder(psImage *image, psImage *weight, psImage *mask,
+                         int size, psImageMaskType 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_IMAGE_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;
+}
+
+
+bool pmSubtractionMaskApply(psImage *image, psImage *weight, const psImage *mask, pmSubtractionMode mode)
+{
+    PS_ASSERT_IMAGE_NON_NULL(image, false);
+    PS_ASSERT_IMAGE_TYPE(image, PS_TYPE_F32, false);
+    if (weight) {
+        PS_ASSERT_IMAGE_NON_NULL(weight, false);
+        PS_ASSERT_IMAGE_TYPE(weight, PS_TYPE_F32, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(weight, image, false);
+    }
+    PS_ASSERT_IMAGE_NON_NULL(mask, false);
+    PS_ASSERT_IMAGE_TYPE(mask, PS_TYPE_IMAGE_MASK, false);
+    PS_ASSERT_IMAGES_SIZE_EQUAL(mask, image, false);
+
+    bool maskVal = PM_SUBTRACTION_MASK_BORDER; // Value to mask
+    switch (mode) {
+      case PM_SUBTRACTION_MODE_1:
+        maskVal |= PM_SUBTRACTION_MASK_CONVOLVE_1;
+        break;
+      case PM_SUBTRACTION_MODE_2:
+        maskVal |= PM_SUBTRACTION_MASK_CONVOLVE_2;
+        break;
+      case PM_SUBTRACTION_MODE_DUAL:
+        maskVal |= PM_SUBTRACTION_MASK_CONVOLVE_2 | PM_SUBTRACTION_MASK_CONVOLVE_2;
+        break;
+      case PM_SUBTRACTION_MODE_ERR:
+      case PM_SUBTRACTION_MODE_UNSURE:
+      case PM_SUBTRACTION_MODE_SINGLE_AUTO:
+      default:
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unsuppored subtraction mode: %x", mode);
+        return false;
+    }
+
+    int numCols = image->numCols, numRows = image->numRows; // Size of image
+    psImageMaskType **maskData = mask->data.PS_TYPE_IMAGE_MASK_DATA; // Dereference mask
+
+    for (int y = 0; y < numRows; y++) {
+        for (int x = 0; x < numCols; x++) {
+            if (maskData[y][x] & maskVal) {
+                image->data.F32[y][x] = NAN;
+                if (weight) {
+                    weight->data.F32[y][x] = NAN;
+                }
+            }
+        }
+    }
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionMask.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionMask.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionMask.h	(revision 42651)
@@ -0,0 +1,38 @@
+#ifndef PM_SUBTRACTION_MASK_H
+#define PM_SUBTRACTION_MASK_H
+
+#include <pslib.h>
+
+/// Generate a mask for use in the subtraction process
+psImage *pmSubtractionMask(
+    psRegion *bounds,                   ///< Bounds of valid pixels (or NULL), returned
+    const pmReadout *ro1,               ///< Readout 1
+    const pmReadout *ro2,               ///< Readout 2
+    psImageMaskType 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
+    pmSubtractionMode mode              ///< Subtraction mode
+    );
+
+/// 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
+                         psImageMaskType blank ///< Mask value for blank regions
+    );
+
+/// Apply the subtraction mask to an image and weight.
+///
+/// Unfortunately, image subtraction may result in a bi-modal image in masked areas, which can upset image
+/// statistics (very important for quantising images so that a product can be written out!).  This function
+/// sets masked areas to NAN in the image and weight.
+bool pmSubtractionMaskApply(psImage *image, ///< Image to which to apply mask
+                            psImage *weight, ///< Weight map to which to apply mask (or NULL)
+                            const psImage *mask, ///< Subtraction mask
+                            pmSubtractionMode mode ///< Subtraction mode
+    );
+
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionMatch.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionMatch.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionMatch.c	(revision 42651)
@@ -0,0 +1,1350 @@
+#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 "pmHDUUtils.h"
+#include "pmSubtractionTypes.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionParams.h"
+#include "pmSubtractionKernels.h"
+#include "pmSubtractionStamps.h"
+#include "pmSubtractionEquation.h"
+#include "pmSubtractionAnalysis.h"
+#include "pmSubtractionMask.h"
+#include "pmSubtractionThreads.h"
+#include "pmSubtractionVisual.h"
+#include "pmSubtractionSimple.h"
+#include "pmErrorCodes.h"
+
+#include "pmSubtractionMatch.h"
+
+#define BG_STAT PS_STAT_ROBUST_MEDIAN   // Statistic to use for background
+
+static bool useFFT = true;              // Do convolutions using FFT
+
+//#define TESTING
+//#define TESTING_MEMORY
+
+// Output memory usage information
+static void memCheck(const char *where)
+{
+#ifdef TESTING_MEMORY
+    psMemBlock **leaks = NULL;
+    int numLeaks = psMemCheckLeaks(0, &leaks, NULL, true);
+    size_t largestSize = 0;
+    psMemId largest = 0;
+    size_t totalSize = 0;
+    for (int i = 0; i < numLeaks; i++) {
+        psMemBlock *mb = leaks[i];
+        totalSize += mb->userMemorySize;
+        if (mb->userMemorySize > largestSize) {
+            largestSize = mb->userMemorySize;
+            largest = mb->id;
+        }
+    }
+    psFree(leaks);
+    fprintf(stderr, "%s:\n", where);
+    fprintf(stderr, "    Memory in use: %zd\n", totalSize);
+    fprintf(stderr, "    Largest block: %ld\n", largest);
+    //MEH -- osx may not like sbrk
+    fprintf(stderr, "    sbrk(): %zd\n", (size_t)sbrk(0));
+#endif
+    return;
+}
+
+// Check input arguments
+static bool subtractionMatchCheck(pmReadout *conv1, pmReadout *conv2, // Convolved images
+                                  const pmReadout *ro1, const pmReadout *ro2, // Input images
+                                  int stride, // Size for convolution patches
+                                  float normFrac,           // Fraction of window for normalisation window
+                                  float sysError,           // Systematic error in images
+                                  float skyError,           // Systematic error in images
+                                  float kernelError, // Systematic error in kernel
+                                  float covarFrac,   // Fraction for kernel truncation before covariance
+                                  psImageMaskType maskVal, // Value to mask for input
+                                  psImageMaskType maskBad, // Mask for output bad pixels
+                                  psImageMaskType maskPoor, // Mask for output poor pixels
+                                  float poorFrac, // Fraction for "poor"
+                                  float badFrac,   // Maximum fraction of bad input pixels to accept
+                                  pmSubtractionMode subMode // Mode of subtraction
+    )
+{
+    if (subMode != PM_SUBTRACTION_MODE_2) {
+        PM_ASSERT_READOUT_NON_NULL(conv1, false);
+        PM_ASSERT_READOUT_NON_NULL(ro1, false);
+        PM_ASSERT_READOUT_IMAGE(ro1, false);
+        if (conv1->image) {
+            psFree(conv1->image);
+            conv1->image = NULL;
+        }
+        if (conv1->mask) {
+            psFree(conv1->mask);
+            conv1->mask = NULL;
+        }
+        if (conv1->variance) {
+            psFree(conv1->variance);
+            conv1->variance = NULL;
+        }
+    }
+    if (subMode != PM_SUBTRACTION_MODE_1) {
+        PM_ASSERT_READOUT_NON_NULL(conv2, false);
+        PM_ASSERT_READOUT_NON_NULL(ro2, false);
+        PM_ASSERT_READOUT_IMAGE(ro2, false);
+        if (conv2->image) {
+            psFree(conv2->image);
+            conv2->image = NULL;
+        }
+        if (conv2->mask) {
+            psFree(conv2->mask);
+            conv2->mask = NULL;
+        }
+        if (conv2->variance) {
+            psFree(conv2->variance);
+            conv2->variance = NULL;
+        }
+    }
+
+    if (ro1 && ro2) {
+        PS_ASSERT_IMAGES_SIZE_EQUAL(ro1->image, ro2->image, false);
+    }
+    PS_ASSERT_INT_NONNEGATIVE(stride, false);
+    if (isfinite(normFrac)) {
+        PS_ASSERT_FLOAT_LARGER_THAN(normFrac, 0.0, false);
+        PS_ASSERT_FLOAT_LESS_THAN(normFrac, 1.0, false);
+    }
+    if (isfinite(sysError)) {
+        PS_ASSERT_FLOAT_LARGER_THAN_OR_EQUAL(sysError, 0.0, false);
+        PS_ASSERT_FLOAT_LESS_THAN(sysError, 1.0, false);
+    }
+    if (isfinite(skyError)) {
+        PS_ASSERT_FLOAT_LARGER_THAN_OR_EQUAL(skyError, 0.0, false);
+    }
+    if (isfinite(kernelError)) {
+        PS_ASSERT_FLOAT_LARGER_THAN_OR_EQUAL(kernelError, 0.0, false);
+        PS_ASSERT_FLOAT_LESS_THAN(kernelError, 1.0, false);
+    }
+    PS_ASSERT_FLOAT_LARGER_THAN_OR_EQUAL(covarFrac, 0.0, false);
+    PS_ASSERT_FLOAT_LESS_THAN(covarFrac, 1.0, false);
+    // Don't care about maskVal
+    // Don't care about maskBad
+    // Don't care about maskPoor
+    PS_ASSERT_FLOAT_LARGER_THAN(poorFrac, 0.0, false);
+    PS_ASSERT_FLOAT_LESS_THAN_OR_EQUAL(poorFrac, 1.0, false);
+    if (isfinite(badFrac)) {
+        PS_ASSERT_FLOAT_LARGER_THAN(badFrac, 0.0, false);
+        PS_ASSERT_FLOAT_LESS_THAN_OR_EQUAL(badFrac, 1.0, false);
+    }
+
+    return true;
+}
+
+
+/// Allocate images, as required
+static void subtractionMatchAlloc(pmReadout *conv1, pmReadout *conv2, // Output readouts
+                                  const pmReadout *ro1, const pmReadout *ro2, // Input readouts
+                                  const psImage *subMask,                     // Subtraction mask
+                                  psImageMaskType maskBad,                    // Mask value for bad pixels
+                                  pmSubtractionMode subMode,          // Subtraction mode
+                                  int numCols, int numRows            // Size of image
+    )
+{
+    if (subMode == PM_SUBTRACTION_MODE_1 || 
+	subMode == PM_SUBTRACTION_MODE_SINGLE_AUTO ||
+        subMode == PM_SUBTRACTION_MODE_DUAL) {
+        if (!conv1->image) {
+            conv1->image = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+        }
+        psImageInit(conv1->image, NAN);
+        if (ro1->variance) {
+            if (!conv1->variance) {
+                conv1->variance = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+            }
+            psImageInit(conv1->variance, NAN);
+        }
+        if (subMask) {
+            if (!conv1->mask) {
+                conv1->mask = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK);
+            }
+            psImageInit(conv1->mask, maskBad);
+        }
+    }
+    if (subMode == PM_SUBTRACTION_MODE_2 || 
+	subMode == PM_SUBTRACTION_MODE_SINGLE_AUTO ||
+        subMode == PM_SUBTRACTION_MODE_DUAL) {
+        if (!conv2->image) {
+            conv2->image = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+        }
+        psImageInit(conv2->image, NAN);
+        if (ro2->variance) {
+            if (!conv2->variance) {
+                conv2->variance = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+            }
+            psImageInit(conv2->variance, NAN);
+        }
+        if (subMask) {
+            if (!conv2->mask) {
+                conv2->mask = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK);
+            }
+            psImageInit(conv2->mask, maskBad);
+        }
+    }
+
+    return;
+}
+
+
+static void subtractionAnalysisUpdate(pmReadout *conv1, pmReadout *conv2, // Convolved images
+                                      const psMetadata *analysis, // Analysis metadata
+                                      const psMetadata *header // Header metadata
+    )
+{
+    if (conv1) {
+        conv1->analysis = psMetadataCopy(conv1->analysis, analysis);
+    }
+    if (conv2) {
+        conv2->analysis = psMetadataCopy(conv2->analysis, analysis);
+    }
+
+    if (conv1 && conv1->parent) {
+        pmHDU *hdu = pmHDUFromCell(conv1->parent);
+        if (hdu) {
+            hdu->header = psMetadataCopy(hdu->header, header);
+        }
+    }
+    if (conv2 && conv2->parent) {
+        pmHDU *hdu = pmHDUFromCell(conv2->parent);
+        if (hdu) {
+            hdu->header = psMetadataCopy(hdu->header, header);
+        }
+    }
+
+    return;
+}
+
+// bool pmSubtractionMaskInvalid (const pmReadout *readout, psImageMaskType maskVal) {
+// 
+//     if (!readout) return true;
+// 
+//     psImage *image = readout->image;
+//     psImage *mask  = readout->mask;
+//     psImage *variance = readout->variance;
+//     for (int y = 0; y < image->numRows; y++) {
+//         for (int x = 0; x < image->numCols; x++) {
+//             if (mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskVal) continue;
+//             bool valid = false;
+//             valid = isfinite(image->data.F32[y][x]);
+//             if (variance) {
+//                 valid &= isfinite(variance->data.F32[y][x]);
+//             }
+//             if (valid) continue;
+//             mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] = maskVal;
+//         }
+//     }
+// 
+//     return true;
+// }
+
+bool pmSubtractionMatchPrecalc(pmReadout *conv1, pmReadout *conv2, const pmReadout *ro1, const pmReadout *ro2,
+                               psMetadata *analysis, int stride, float kernelError, float covarFrac,
+                               psImageMaskType maskVal, psImageMaskType maskBad, psImageMaskType maskPoor,
+                               float poorFrac, float badFrac)
+{
+    PS_ASSERT_METADATA_NON_NULL(analysis, false);
+
+    // Extract the kernels
+    pmSubtractionMode mode = PM_SUBTRACTION_MODE_UNSURE; // Subtraction mode: which image to convolve
+    int size = 0;                       // Size of kernel
+    psList *kernelList = psListAlloc(NULL); // List of kernels
+    {
+        psMetadataIterator *iter = psMetadataIteratorAlloc(analysis, PS_LIST_HEAD,
+                                                           "^" PM_SUBTRACTION_ANALYSIS_KERNEL "$");
+        psMetadataItem *item;               // Item from iteration
+        while ((item = psMetadataGetAndIncrement(iter))) {
+            if (item->type != PS_DATA_UNKNOWN) {
+                psError(PM_ERR_PROG, true, "Unexpected type for kernel.");
+                psFree(iter);
+                psFree(kernelList);
+                return false;
+            }
+            pmSubtractionKernels *kernel = item->data.V; // Kernel
+            PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernel, false);
+            size = PS_MAX(size, kernel->size);
+            if (mode == PM_SUBTRACTION_MODE_UNSURE) {
+                mode = kernel->mode;
+            } else if (kernel->mode != mode) {
+                // There's some confusion, so let's set the mode to dual convolution.
+                // This is only used for the subtraction mask, so it's not a big deal.
+                mode = PM_SUBTRACTION_MODE_DUAL;
+            }
+            psListAdd(kernelList, PS_LIST_TAIL, kernel);
+        }
+        psFree(iter);
+    }
+    if (psListLength(kernelList) == 0) {
+        psError(PM_ERR_PROG, true, "Unable to find kernels");
+        psFree(kernelList);
+        return false;
+    }
+    psArray *kernels = psListToArray(kernelList); // Array of kernels
+    psFree(kernelList);
+
+    // Extract the regions
+    psArray *regions = psArrayAllocEmpty(kernels->n); // Array of regions
+    {
+        psMetadataIterator *iter = psMetadataIteratorAlloc(analysis, PS_LIST_HEAD,
+                                                           "^" PM_SUBTRACTION_ANALYSIS_REGION "$");
+        psMetadataItem *item;               // Item from iteration
+        while ((item = psMetadataGetAndIncrement(iter))) {
+            if (item->type != PS_DATA_REGION) {
+                psError(PM_ERR_PROG, true, "Unexpected type for region.");
+                psFree(iter);
+                psFree(kernels);
+                psFree(regions);
+                return false;
+            }
+            psRegion *region = item->data.V; // Region
+            psArrayAdd(regions, regions->n, region);
+        }
+        psFree(iter);
+    }
+    if (regions->n != kernels->n) {
+        psError(PM_ERR_PROG, true, "Differing number of kernels (%ld) and regions (%ld)",
+                kernels->n, regions->n);
+        psFree(regions);
+        psFree(kernels);
+        return false;
+    }
+
+    if (!subtractionMatchCheck(conv1, conv2, ro1, ro2, stride, NAN, NAN, NAN, kernelError, covarFrac,
+                               maskVal, maskBad, maskPoor, poorFrac, badFrac, mode)) {
+        psFree(kernels);
+        psFree(regions);
+        return false;
+    }
+
+    int numCols, numRows;       // Size of image
+    if (ro1) {
+        numCols = ro1->image->numCols;
+        numRows = ro1->image->numRows;
+    } else if (ro2) {
+        numCols = ro2->image->numCols;
+        numRows = ro2->image->numRows;
+    } else {
+        psAbort("No input image provided.");
+    }
+
+    // XXX this is done before calling this function
+    // pmSubtractionMaskInvalid(ro1, maskVal);
+    // pmSubtractionMaskInvalid(ro2, maskVal);
+
+    // General background subtraction, since this is done in pmSubtractionMatch
+    {
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS); // Random number generator
+        psStats *bg = psStatsAlloc(PS_STAT_ROBUST_MEDIAN); // Statistics for background
+        if (ro1) {
+            psStatsInit(bg);
+            if (!psImageBackground(bg, NULL, ro1->image, ro1->mask, maskVal, rng)) {
+                psError(PM_ERR_DATA, false, "Unable to measure background statistics.");
+                psFree(bg);
+                psFree(rng);
+                return false;
+            }
+            psBinaryOp(ro1->image, ro1->image, "-", psScalarAlloc((float)bg->robustMedian, PS_TYPE_F32));
+        }
+        if (ro2) {
+            psStatsInit(bg);
+            if (!psImageBackground(bg, NULL, ro2->image, ro2->mask, maskVal, rng)) {
+                psError(PM_ERR_DATA, false, "Unable to measure background statistics.");
+                psFree(bg);
+                psFree(rng);
+                return false;
+            }
+            psBinaryOp(ro2->image, ro2->image, "-", psScalarAlloc((float)bg->robustMedian, PS_TYPE_F32));
+        }
+        psFree(bg);
+        psFree(rng);
+    }
+
+    psRegion bounds = psRegionSet(NAN, NAN, NAN, NAN); // Bounds of valid pixels
+
+    psImage *subMask = pmSubtractionMask(&bounds, ro1, ro2, maskVal, size, 0,
+                                         badFrac, mode); // Subtraction mask
+    if (!subMask) {
+        psError(psErrorCodeLast(), false, "Unable to generate subtraction mask.");
+        psFree(kernels);
+        psFree(regions);
+        return false;
+    }
+
+    psMetadata *outAnalysis = psMetadataAlloc(); // Output analysis values
+    psMetadata *outHeader = psMetadataAlloc(); // Output header values
+
+    subtractionMatchAlloc(conv1, conv2, ro1, ro2, subMask, maskBad, mode, numCols, numRows);
+
+    psTrace("psModules.imcombine", 2, "Convolving...\n");
+    for (int i = 0; i < kernels->n; i++) {
+        pmSubtractionKernels *kernel = kernels->data[i]; // Kernel of interest
+        psRegion *region = regions->data[i]; // Region of interest
+
+        if (!pmSubtractionAnalysis(outAnalysis, outHeader, kernel, region, numCols, numRows)) {
+            psError(psErrorCodeLast(), false, "Unable to generate QA data");
+            psFree(outAnalysis);
+            psFree(outHeader);
+            psFree(subMask);
+            psFree(kernels);
+            psFree(regions);
+            return false;
+        }
+
+        if (!pmSubtractionConvolve(conv1, conv2, ro1, ro2, subMask, stride, maskBad, maskPoor, poorFrac,
+                                   kernelError, covarFrac, region, kernel, true, useFFT)) {
+            psError(psErrorCodeLast(), false, "Unable to convolve image.");
+            psFree(outAnalysis);
+            psFree(outHeader);
+            psFree(subMask);
+            psFree(kernels);
+            psFree(regions);
+            return false;
+        }
+    }
+
+    psFree(subMask);
+    psFree(kernels);
+    psFree(regions);
+
+    subtractionAnalysisUpdate(conv1, conv2, outAnalysis, outHeader);
+    psFree(outAnalysis);
+    psFree(outHeader);
+
+    return true;
+}
+
+bool pmSubtractionMatchAttempt(pmSubtractionQuality **bestMatch, pmSubtractionKernels *kernels, pmSubtractionStampList *stamps, pmSubtractionMode mode, int spatialOrder, bool final) {
+
+    pmSubtractionMode nativeMode = kernels->mode;
+    pmSubtractionMode nativeOrder = kernels->spatialOrder;
+
+    kernels->mode = mode;
+    kernels->spatialOrder = spatialOrder;
+
+    // we always need to recalculate the matrix equation elements...
+    pmSubtractionStampsResetStatus(stamps);
+
+    psTrace("psModules.imcombine", 3, "Convolving stamps as needed...\n");
+    if (!pmSubtractionConvolveStamps(stamps, kernels)) {
+	psError(psErrorCodeLast(), false, "Unable to convolve stamps.");
+	return false;
+    }
+
+    // step 1: generate the elements of the matrix equation Ax = B
+    psTrace("psModules.imcombine", 3, "Calculating kernel equations...\n");
+    if (!pmSubtractionCalculateEquation(stamps, kernels)) {
+	psError(psErrorCodeLast(), false, "Unable to calculate least-squares equation.");
+	return false;
+    }
+		    
+    // step 2: solve the matrix equation Ax = B
+    psTrace("psModules.imcombine", 3, "Solving kernel equations...\n");
+    if (!pmSubtractionSolveEquation(kernels, stamps)) {
+	psError(psErrorCodeLast(), false, "Unable to calculate least-squares equation.");
+	return false;
+    }
+    memCheck("  solve equation");
+
+    // calculate the score for this model fit attempt
+    // XXX store the chisq, flux and moments for stamp rejection
+    pmSubtractionCalculateChisqAndMoments(bestMatch, stamps, kernels); // Stamp deviations
+
+    // display the input and model stamps
+    pmSubtractionVisualShowFit(stamps, kernels);
+    pmSubtractionVisualPlotFit(kernels);
+    pmSubtractionVisualPlotConvKernels(kernels);
+
+    // reset the kernel if desired (on final pass, do not reset)
+    if (!final) {
+	kernels->mode = nativeMode;
+	kernels->spatialOrder = nativeOrder;
+    } else {
+      pmSubtractionKernelsMakeDescription(kernels);
+      psLogMsg("psModules.imcombine", PS_LOG_INFO, "final kernel: %s", kernels->description);
+    }
+    return true;
+}
+
+bool pmSubtractionMatch(pmReadout *conv1, pmReadout *conv2, const pmReadout *ro1, const pmReadout *ro2,
+                        int footprint, int stride, float regionSize, float stampSpacing, float threshold,
+                        const psArray *sources, const char *stampsName,
+                        pmSubtractionKernelsType type, int size, int spatialOrder,
+                        psVector *isisWidths, const psVector *isisOrders,
+                        int inner, int ringsOrder, int binning, float penalty,
+                        bool optimum, const psVector *optFWHMs, int optOrder, float optThreshold,
+                        int iter, float rej, float normFrac, float sysError, float skyError,
+                        float kernelError, float covarFrac, psImageMaskType maskVal, psImageMaskType maskBad,
+                        psImageMaskType maskPoor, float poorFrac, float badFrac, pmSubtractionMode subMode)
+{
+    if (!subtractionMatchCheck(conv1, conv2, ro1, ro2, stride, normFrac, sysError, skyError, kernelError,
+                               covarFrac, maskVal, maskBad, maskPoor, poorFrac, badFrac, subMode)) {
+        return false;
+    }
+
+    // We need both inputs
+    PM_ASSERT_READOUT_NON_NULL(ro1, false);
+    PM_ASSERT_READOUT_NON_NULL(ro2, 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
+    // 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_NONNEGATIVE(iter, false);
+    PS_ASSERT_FLOAT_LARGER_THAN(rej, 0.0, false);
+
+    // 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;
+    }
+
+    // Bail here if we're doing the simple matching
+    if (type == PM_SUBTRACTION_KERNEL_SIMPLE) {
+      if (!pmSubtractionSimpleMatch(conv1,conv2,ro1,ro2,sources,size,maskVal,maskBad,maskPoor,optThreshold)) {
+	return false;
+      }
+      return(true);
+    }
+
+    // Where does our variance map come from?
+    // Getting the variance exactly right is not necessary --- it's just used for weighting.
+    psImage *variance = NULL;             // Variance image to use
+    if (ro1->variance && ro2->variance) {
+        variance = (psImage*)psBinaryOp(NULL, ro1->variance, "+", ro2->variance);
+    } else if (ro1->variance) {
+        variance = psMemIncrRefCounter(ro1->variance);
+    } else if (ro2->variance) {
+        variance = psMemIncrRefCounter(ro2->variance);
+    } else {
+        variance = (psImage*)psBinaryOp(NULL, ro1->image, "+", ro2->image);
+    }
+
+    // Putting important variable declarations here, since they are freed after a "goto" if there is an error.
+    psImage *subMask = NULL;            // Mask for subtraction
+    psRegion *region = psRegionAlloc(NAN, NAN, NAN, NAN); // Iso-kernel region
+    psString regionString = NULL;       // String for region
+    pmSubtractionStampList *stamps = NULL; // Stamps for matching PSF
+    pmSubtractionKernels *kernels = NULL; // Kernel basis functions
+    psMetadata *analysis = psMetadataAlloc(); // QA data
+    psMetadata *header = psMetadataAlloc(); // QA data for header
+
+    int numCols = ro1->image->numCols, numRows = ro1->image->numRows; // Image dimensions
+
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS); // Random number generator
+
+    pmSubtractionQuality *bestMatch = NULL;
+
+    int N_TEST_MODES;
+    int N_TEST_ORDER = spatialOrder;
+
+    pmSubtractionMode TestModes[3];
+    switch (subMode) {
+      case PM_SUBTRACTION_MODE_1:
+	N_TEST_MODES = 1;
+	TestModes[0] = PM_SUBTRACTION_MODE_1;
+	break;
+      case PM_SUBTRACTION_MODE_2:
+	N_TEST_MODES = 1;
+	TestModes[0] = PM_SUBTRACTION_MODE_2;
+	break;
+      case PM_SUBTRACTION_MODE_SINGLE_AUTO:
+	N_TEST_MODES = 2;
+	TestModes[0] = PM_SUBTRACTION_MODE_1;
+	TestModes[1] = PM_SUBTRACTION_MODE_2;
+	break;
+      case PM_SUBTRACTION_MODE_DUAL:
+	N_TEST_MODES = 3;
+	TestModes[0] = PM_SUBTRACTION_MODE_1;
+	TestModes[1] = PM_SUBTRACTION_MODE_2;
+	TestModes[2] = PM_SUBTRACTION_MODE_DUAL;
+	break;
+      default:
+	psError(psErrorCodeLast(), false, "For now, only modes 1, 2, and DUAL are supported.");
+	goto MATCH_ERROR;
+    }
+
+    
+    memCheck("start");
+
+    // pmSubtractionMaskInvalid(ro1, maskVal);
+    // pmSubtractionMaskInvalid(ro2, maskVal);
+
+    psRegion bounds = psRegionSet(NAN, NAN, NAN, NAN); // Bounds of valid pixels
+
+    subMask = pmSubtractionMask(&bounds, ro1, ro2, maskVal, size, footprint, badFrac, subMode);
+    if (!subMask) {
+        psError(psErrorCodeLast(), false, "Unable to generate subtraction mask.");
+        goto MATCH_ERROR;
+    }
+
+    memCheck("mask");
+
+    // Get region of interest
+    int xRegions = 1, yRegions = 1;     // Number of iso-kernel regions
+    float xRegionSize = NAN, yRegionSize = NAN; // Size of iso-kernel regions
+    if (isfinite(regionSize) && regionSize != 0.0) {
+        xRegions = (bounds.x1 - bounds.x0) / regionSize + 1;
+        yRegions = (bounds.y1 - bounds.y0) / regionSize + 1;
+        xRegionSize = (float)(bounds.x1 - bounds.x0) / (float)xRegions;
+        yRegionSize = (float)(bounds.y1 - bounds.y0) / (float)yRegions;
+    } else {
+        xRegionSize = bounds.x1 - bounds.x0;
+        yRegionSize = bounds.y1 - bounds.y0;
+    }
+
+    // General background subtraction and measurement of stamp threshold
+    float stampThresh1 = NAN, stampThresh2 = NAN; // Stamp thresholds for images
+    {
+        psStats *bg = psStatsAlloc(PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV); // Statistics for background
+        if (ro1) {
+            psStatsInit(bg);
+            if (!psImageBackground(bg, NULL, ro1->image, ro1->mask, maskVal, rng)) {
+                psError(PM_ERR_DATA, false, "Unable to measure background statistics.");
+                psFree(bg);
+                goto MATCH_ERROR;
+            }
+            stampThresh1 = threshold * bg->robustStdev;
+            psBinaryOp(ro1->image, ro1->image, "-", psScalarAlloc((float)bg->robustMedian, PS_TYPE_F32));
+        }
+        if (ro2) {
+            psStatsInit(bg);
+            if (!psImageBackground(bg, NULL, ro2->image, ro2->mask, maskVal, rng)) {
+                psError(PM_ERR_DATA, false, "Unable to measure background statistics.");
+                psFree(bg);
+                goto MATCH_ERROR;
+            }
+            stampThresh2 = threshold * bg->robustStdev;
+            psBinaryOp(ro2->image, ro2->image, "-", psScalarAlloc((float)bg->robustMedian, PS_TYPE_F32));
+        }
+        psFree(bg);
+    }
+    
+    subtractionMatchAlloc(conv1, conv2, ro1, ro2, subMask, maskBad, subMode, numCols, numRows);
+    
+    // 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);
+            *region = psRegionSet(bounds.x0 + (int)(i * xRegionSize),
+                                  bounds.x0 + (int)((i + 1) * xRegionSize),
+                                  bounds.y0 + (int)(j * yRegionSize),
+                                  bounds.y0 + (int)((j + 1) * yRegionSize));
+            psFree(regionString);
+            regionString = psRegionToString(*region);
+            psLogMsg("psModules.imcombine", PS_LOG_DETAIL, "Iso-kernel region: %s out of %d,%d\n",
+		     regionString, numCols, numRows);
+
+            if (stampsName && strlen(stampsName) > 0) {
+                stamps = pmSubtractionStampsSetFromFile(stampsName, ro1->image, subMask, region, size,
+                                                        footprint, stampSpacing, normFrac,
+                                                        sysError, skyError, subMode);
+            } else if (sources) {
+                stamps = pmSubtractionStampsSetFromSources(sources, ro1->image, subMask, region, size,
+                                                           footprint, stampSpacing, normFrac,
+                                                           sysError, skyError, subMode);
+            }
+
+	    int nTries = 0;
+	    bool tryAgain = true;
+	    while (tryAgain) {
+		// We get the stamps here; we will also attempt to get stamps at the first iteration, but it
+		// doesn't matter.
+		if (!pmSubtractionStampsSelect(&stamps, ro1, ro2, subMask, variance, region, stampThresh1, stampThresh2,
+					       stampSpacing, normFrac, sysError, skyError, size, footprint, subMode)) {
+		    goto MATCH_ERROR;
+		}
+
+		// generate the window function from the set of stamps
+		// we attempt to set the window based on the measured kron radius, but the
+		// initial guess may be too small. allow the window to grow if the kron radius
+		// implies the need for a larger windon.  But only allow 2 additional tries
+		if (!pmSubtractionStampsGetWindow(&tryAgain, stamps, size)) {
+		    // if we failed, it might be due to the desired normWindow being larger than the current footprint.
+		    // in this case, just adjust the footprint and try again.
+		    if (tryAgain && (nTries >= 5)) {
+			// unrecoverable error
+			psError(PM_ERR_STAMPS, true, "Unable to get stamp window (failure to converge).");
+			goto MATCH_ERROR;
+		    }
+		    if (tryAgain) {
+			// keep the border constant
+			int border = footprint - size;
+			size = PS_MAX(stamps->normWindow1, stamps->normWindow2) + 2;
+			footprint = size + border;
+
+			// we need to reconstruct everything, so just free the stamps here and retry
+			psFree(stamps);
+			nTries ++;
+		    } else {
+			// unrecoverable error generated in pmSubtractionStampsGetWindow
+			psError(psErrorCodeLast(), false, "Unable to get stamp window.");
+			goto MATCH_ERROR;
+		    }
+		}
+	    }
+
+	    // check on the kernel scaling -- if the kron-based radial moments are very different, adjust to match them
+	    { 
+		// float fwhm1;
+		// float fwhm2;
+		// pmSubtractionGetFWHMs(&fwhm1, &fwhm2);
+		// psAssert(isfinite(fwhm1), "fwhm 1 not set");
+		// psAssert(isfinite(fwhm2), "fwhm 2 not set");
+
+		// XXX this is BAD: depends on the relationship below:
+		// stamps->normWindow1 = 2.75*R1;
+		// stamps->normWindow2 = 2.75*R2;
+		float radMoment1 = stamps->normWindow1 / 2.75;
+		float radMoment2 = stamps->normWindow2 / 2.75;
+		pmSubtractionParamsScale(NULL, NULL, isisWidths, radMoment1, radMoment2);
+
+		// float maxFWHM = PS_MAX(fwhm1, fwhm2);
+		// float maxRadial = PS_MAX(radMoment1, radMoment2);
+		
+		// if (fabs(2.0*(maxFWHM - maxRadial)/(maxFWHM + maxRadial)) > 0.25) {
+		// if (1) {
+		// 
+		//     float scale = maxRadial / maxFWHM;
+		//     psLogMsg ("psModules.imcombine", PS_LOG_INFO, "Kron and FWHM scales are quite different, re-scale by %f to use Kron", scale);
+		//     
+		//     for (int i = 0; i < isisWidths->n; i++) {
+		// 	isisWidths->data.F32[i] *= scale;
+		//     }
+		// }
+	    }
+
+	    // Define kernel basis functions
+	    if (optimum && (type == PM_SUBTRACTION_KERNEL_ISIS || type == PM_SUBTRACTION_KERNEL_GUNK)) {
+		kernels = pmSubtractionKernelsOptimumISIS(type, size, inner, spatialOrder,
+							  optFWHMs, optOrder, stamps, footprint,
+							  optThreshold, penalty, bounds, subMode);
+		if (!kernels) {
+		    psErrorClear();
+		    psWarning("Unable to derive optimum ISIS kernel --- switching to default.");
+		}
+	    }
+	    if (kernels == NULL) {
+		// Not an ISIS/GUNK kernel, or the optimum kernel search failed
+		kernels = pmSubtractionKernelsGenerate(type, size, spatialOrder, isisWidths, isisOrders,
+						       inner, binning, ringsOrder, penalty, bounds, subMode);
+	    }
+
+	    memCheck("kernels");
+
+// this section was an old version of auto-choosing the direction.  the test was not as reliable as
+// we would like; this is replaced by pmSubtractionMatchAttempt
+# if (0)
+	    if (subMode == PM_SUBTRACTION_MODE_UNSURE) {
+		pmSubtractionMode newMode = pmSubtractionBestMode(&stamps, &kernels, subMask, rej);
+		switch (newMode) {
+		  case PM_SUBTRACTION_MODE_1:
+		    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Convolving image 1 to match image 2.");
+		    break;
+		  case PM_SUBTRACTION_MODE_2:
+		    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Convolving image 2 to match image 1.");
+		    break;
+		  default:
+		    psError(psErrorCodeLast(), false, "Unable to determine subtraction order.");
+		    goto MATCH_ERROR;
+		}
+		subMode = newMode;
+	    }
+# endif
+
+	    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);
+
+		bool tryAgain = true;
+		while (tryAgain) {
+		    // We get the stamps here; we will also attempt to get stamps at the first iteration, but it
+		    // doesn't matter.
+		    if (!pmSubtractionStampsSelect(&stamps, ro1, ro2, subMask, variance, region, stampThresh1, stampThresh2,
+						   stampSpacing, normFrac, sysError, skyError, size, footprint, subMode)) {
+			goto MATCH_ERROR;
+		    }
+
+		    // generate the window function from the set of stamps
+		    if (!pmSubtractionStampsGetWindow(&tryAgain, stamps, size)) {
+			// if we failed, it might be due to the desired normWindow being larger than the current footprint.
+			// in this case, just adjust the footprint and try again.
+			if (tryAgain) {
+			    footprint = PS_MAX(stamps->normWindow1, stamps->normWindow2) + 2;
+
+			    // we need to reconstruct everything, so just free the stamps here and retry
+			    psFree(stamps);
+			} else {
+			    // unrecoverable error
+			    psError(psErrorCodeLast(), false, "Unable to get stamp window.");
+			    goto MATCH_ERROR;
+			}
+		    }
+		}
+
+		// step 0 : calculate the normalizations, pass along to the next steps via stamps->normValue
+		psTrace("psModules.imcombine", 3, "Calculating normalization...\n");
+		if (!pmSubtractionCalculateNormalization(stamps, kernels->mode)) {
+		    psError(psErrorCodeLast(), false, "Unable to calculate least-squares equation.");
+		    goto MATCH_ERROR;
+		}
+
+		// on each iteration, we start from scratch
+		psFree(bestMatch);
+
+		// choose the spatial order and subtraction direction (1, 2, dual)
+		// XXX need to make these respect recipe somewhat
+		for (int order = 0; order <= N_TEST_ORDER; order++) {
+		    for (int j = 0; j < N_TEST_MODES; j++) {
+			if (!pmSubtractionMatchAttempt(&bestMatch, kernels, stamps, TestModes[j], order, false)) {
+			    goto MATCH_ERROR;
+			}
+		    }
+		}
+		
+		// reject the deviant stamps based on the stats of the best match
+		psTrace("psModules.imcombine", 3, "Rejecting stamps...\n");
+		numRejected = pmSubtractionRejectStamps(kernels, stamps, bestMatch, subMask, rej);
+		if (numRejected < 0) {
+		    psError(psErrorCodeLast(), false, "Unable to reject stamps.");
+		    goto MATCH_ERROR;
+		}
+		memCheck("  reject stamps");
+	    }
+
+	    // apply the best fit so we are ready to roll
+	    psLogMsg("psModules.imcombine", PS_LOG_INFO, "applying order: %d, mode: %d\n", bestMatch->spatialOrder, bestMatch->mode);
+	    if (!pmSubtractionMatchAttempt(NULL, kernels, stamps, bestMatch->mode, bestMatch->spatialOrder, true)) {
+		goto MATCH_ERROR;
+	    }
+	    psFree(stamps);
+	    psFree(bestMatch);
+	    memCheck("solution");
+
+	    if (!pmSubtractionAnalysis(analysis, header, kernels, region, numCols, numRows)) {
+		psError(psErrorCodeLast(), false, "Unable to generate QA data");
+		goto MATCH_ERROR;
+	    }
+	    memCheck("diag outputs");
+
+	    psTrace("psModules.imcombine", 2, "Convolving...\n");
+	    if (!pmSubtractionConvolve(conv1, conv2, ro1, ro2, subMask, stride, maskBad, maskPoor, poorFrac,
+				       kernelError, covarFrac, region, kernels, true, useFFT)) {
+		psError(psErrorCodeLast(), false, "Unable to convolve image.");
+		goto MATCH_ERROR;
+	    }
+
+	    psFree(kernels);
+	    kernels = NULL;
+	}
+    }
+    psFree(rng);
+    rng = NULL;
+    psFree(region);
+    region = NULL;
+    psFree(regionString);
+    regionString = NULL;
+    psFree(subMask);
+    subMask = NULL;
+    psFree(variance);
+    variance = NULL;
+
+    if (conv1 && !pmSubtractionBorder(conv1->image, conv1->variance, conv1->mask, size, maskBad)) {
+	psError(psErrorCodeLast(), false, "Unable to set border of convolved image.");
+	goto MATCH_ERROR;
+    }
+    if (conv2 && !pmSubtractionBorder(conv2->image, conv2->variance, conv2->mask, size, maskBad)) {
+	psError(psErrorCodeLast(), false, "Unable to set border of convolved image.");
+	goto MATCH_ERROR;
+    }
+
+    memCheck("convolution");
+
+    subtractionAnalysisUpdate(conv1, conv2, analysis, header);
+    psFree(analysis);
+    psFree(header);
+
+#ifdef TESTING
+    {
+	if (subMode == PM_SUBTRACTION_MODE_1 || subMode == PM_SUBTRACTION_MODE_DUAL) {
+	    psFits *fits = psFitsOpen("convolved1.fits", "w");
+	    psFitsWriteImage(fits, NULL, conv1->image, 0, NULL);
+	    psFitsClose(fits);
+	}
+
+	if (subMode == PM_SUBTRACTION_MODE_2 || subMode == PM_SUBTRACTION_MODE_DUAL) {
+	    psFits *fits = psFitsOpen("convolved2.fits", "w");
+	    psFitsWriteImage(fits, NULL, conv2->image, 0, NULL);
+	    psFitsClose(fits);
+	}
+    }
+#endif
+
+    return true;
+
+MATCH_ERROR:
+    psFree(analysis);
+    psFree(header);
+    psFree(region);
+    psFree(regionString);
+    psFree(subMask);
+    psFree(kernels);
+    psFree(stamps);
+    psFree(variance);
+    psFree(rng);
+    psFree(bestMatch);
+    return false;
+}
+
+
+// Determine a rough width (integer value) of the star in the image
+// XXX Could improve this by using a user-provided list of floating-point widths (or an end point and
+// increment).
+static int subtractionOrderWidth(const psKernel *kernel, // Image
+				 float bg, // Background in image
+				 int size, // Maximum size
+				 const psArray *models, // Buffer of models
+				 const psVector *modelSums // Buffer of model sums
+    )
+{
+    assert(kernel);
+    assert(models);
+    assert(modelSums);
+
+    int xMin = -size, xMax = size; // Bounds in x
+    int yMin = -size, yMax = size; // Bounds in y
+
+    // Fit gaussians of varying widths to the image, record the chi^2
+    psVector *chi2 = psVectorAlloc(size, PS_TYPE_F32); // chi^2 as a function of radius
+    for (int sigma = 0; sigma < size; sigma++) {
+	double sumFG = 0.0; // Sum for calculating the normalisation of the Gaussian
+	psKernel *model = models->data[sigma]; // Model of interest
+	for (int y = yMin; y <= yMax; y++) {
+	    for (int x = xMin; x <= xMax; x++) {
+		sumFG += model->kernel[y][x] * (kernel->kernel[y][x] - bg);
+	    }
+	}
+	float norm = sumFG * modelSums->data.F64[sigma]; // Normalisation for Gaussian
+	double sumDev2 = 0.0;           // Sum of square deviations
+	for (int y = yMin; y <= yMax; y++) {
+	    for (int x = xMin; x <= xMax; x++) {
+		float dev = kernel->kernel[y][x] - bg - norm * model->kernel[y][x]; // Deviation
+		sumDev2 += PS_SQR(dev);
+	    }
+	}
+	chi2->data.F32[sigma] = sumDev2;
+    }
+
+    // Find the minimum chi^2
+    int bestIndex = -1;                 // Index of best chi^2
+    float bestChi2 = INFINITY;          // Best chi^2
+    for (int i = 0; i < size; i++) {
+	if (chi2->data.F32[i] < bestChi2) {
+	    bestChi2 = chi2->data.F32[i];
+	    bestIndex = i;
+	}
+    }
+    psFree(chi2);
+
+    return bestIndex + 1;
+}
+
+
+bool pmSubtractionOrderStamp(psVector *ratios, psVector *mask, const pmSubtractionStampList *stamps,
+			     const psArray *models, const psVector *modelSums,
+			     int index, float bg1, float bg2)
+{
+    PS_ASSERT_VECTOR_NON_NULL(ratios, false);
+    PS_ASSERT_VECTOR_NON_NULL(mask, false);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(ratios, mask, false);
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+    PS_ASSERT_INT_NONNEGATIVE(index, false);
+    PS_ASSERT_INT_LESS_THAN(index, stamps->num, false);
+    PS_ASSERT_ARRAY_NON_NULL(models, false);
+    PS_ASSERT_VECTOR_NON_NULL(modelSums, false);
+    PS_ASSERT_VECTOR_SIZE(modelSums, models->n, false);
+
+    pmSubtractionStamp *stamp = stamps->stamps->data[index]; // Stamp of interest
+    psAssert(stamp->status == PM_SUBTRACTION_STAMP_CALCULATE || stamp->status == PM_SUBTRACTION_STAMP_USED,
+	     "We checked this earlier.");
+
+    // Widths of stars
+    int width1 = subtractionOrderWidth(stamp->image1, bg1, stamps->footprint, models, modelSums);
+    int width2 = subtractionOrderWidth(stamp->image2, bg2, stamps->footprint, models, modelSums);
+
+    if (width1 == 0 || width2 == 0) {
+	ratios->data.F32[index] = NAN;
+	mask->data.PS_TYPE_VECTOR_MASK_DATA[index] = 0xff;
+    } else {
+	ratios->data.F32[index] = (float)width1 / (float)width2;
+	mask->data.PS_TYPE_VECTOR_MASK_DATA[index] = 0;
+	psTrace("psModules.imcombine", 3, "Stamp %d (%.1f,%.1f) widths: %d, %d --> %f\n",
+		index, stamp->x, stamp->y, width1, width2, ratios->data.F32[index]);
+    }
+
+    return true;
+}
+
+bool pmSubtractionOrderThread(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    psVector *ratios = job->args->data[0]; // Ratios of widths
+    psVector *mask = job->args->data[1]; // Mask for ratios
+    const pmSubtractionStampList *stamps = job->args->data[2]; // List of stamps
+    const psArray *models = job->args->data[3]; // Gaussian models
+    const psVector *modelSums = job->args->data[4]; // Gaussian model sums
+    int index = PS_SCALAR_VALUE(job->args->data[5], S32); // Stamp index
+    float bg1 = PS_SCALAR_VALUE(job->args->data[6], F32); // Background of image 1
+    float bg2 = PS_SCALAR_VALUE(job->args->data[7], F32); // Background of image 2
+
+    return pmSubtractionOrderStamp(ratios, mask, stamps, models, modelSums, index, bg1, bg2);
+}
+
+pmSubtractionMode pmSubtractionOrder(pmSubtractionStampList *stamps, float bg1, float bg2)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, PM_SUBTRACTION_MODE_ERR);
+
+    psVector *mask = psVectorAlloc(stamps->num, PS_TYPE_VECTOR_MASK); // Mask for stamps
+    psVector *ratios = psVectorAlloc(stamps->num, PS_TYPE_F32); // Ratios of widths
+
+    // Generate models
+    int size = stamps->footprint;       // Maximum size
+    psArray *models = psArrayAlloc(size); // Gaussian models
+    psVector *modelSums = psVectorAlloc(size, PS_TYPE_F64); // Gaussian model sums
+    for (int sigma = 0; sigma < size; sigma++) {
+	psKernel *model = psKernelAlloc(-size, size, -size, size); // Gaussian model
+	float invSigma2 = 1.0 / (float)PS_SQR(1 + sigma); // Inverse sigma squared
+	double sumGG = 0.0;         // Sum of square of Gaussian
+	for (int y = -size; y <= size; y++) {
+	    int y2 = PS_SQR(y);     // y squared
+	    for (int x = -size; x <= size; x++) {
+		float rad2 = PS_SQR(x) + y2; // Radius squared
+		float value = expf(-rad2 * invSigma2); // Model value
+		model->kernel[y][x] = value;
+		sumGG += PS_SQR(value);
+	    }
+	}
+	models->data[sigma] = model;
+	modelSums->data.F64[sigma] = 1.0 / sumGG;
+    }
+
+    // Fit models to stamps
+    for (int i = 0; i < stamps->num; i++) {
+	pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+	if (stamp->status != PM_SUBTRACTION_STAMP_CALCULATE && stamp->status != PM_SUBTRACTION_STAMP_USED) {
+	    mask->data.PS_TYPE_VECTOR_MASK_DATA[i] = 0xff;
+	    continue;
+	}
+
+	if (pmSubtractionThreaded()) {
+	    psThreadJob *job = psThreadJobAlloc("PSMODULES_SUBTRACTION_ORDER");
+	    psArrayAdd(job->args, 1, ratios);
+	    psArrayAdd(job->args, 1, mask);
+	    psArrayAdd(job->args, 1, stamps);
+	    psArrayAdd(job->args, 1, models);
+	    psArrayAdd(job->args, 1, modelSums);
+	    PS_ARRAY_ADD_SCALAR(job->args, i, PS_TYPE_S32);
+	    PS_ARRAY_ADD_SCALAR(job->args, bg1, PS_TYPE_F32);
+	    PS_ARRAY_ADD_SCALAR(job->args, bg2, PS_TYPE_F32);
+	    if (!psThreadJobAddPending(job)) {
+		return false;
+	    }
+	} else {
+	    if (!pmSubtractionOrderStamp(ratios, mask, stamps, models, modelSums, i, bg1, bg2)) {
+		psError(psErrorCodeLast(), false, "Unable to measure PSF width for stamp %d", i);
+		psFree(models);
+		psFree(modelSums);
+		psFree(ratios);
+		psFree(mask);
+		return false;
+	    }
+	}
+    }
+
+    if (!psThreadPoolWait(true, true)) {
+	psError(psErrorCodeLast(), false, "Error waiting for threads.");
+	psFree(models);
+	psFree(modelSums);
+	psFree(ratios);
+	psFree(mask);
+	return false;
+    }
+
+    psFree(models);
+    psFree(modelSums);
+
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN);
+    if (!psVectorStats(stats, ratios, NULL, mask, 0xff)) {
+	psError(psErrorCodeLast(), false, "Unable to calculate statistics for moments ratio.");
+	psFree(mask);
+	psFree(ratios);
+	psFree(stats);
+	return PM_SUBTRACTION_MODE_ERR;
+    }
+    psFree(ratios);
+    psFree(mask);
+
+    // XXX raise an error here or not?
+    if (isnan(stats->robustMedian)) {
+	psFree(stats);
+	return PM_SUBTRACTION_MODE_ERR;
+    }
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Median width ratio: %lf", stats->robustMedian);
+    pmSubtractionMode mode = (stats->robustMedian <= 1.0 ? PM_SUBTRACTION_MODE_1 : PM_SUBTRACTION_MODE_2);
+    psFree(stats);
+
+    return mode;
+}
+
+
+// Test a subtraction mode by performing a single iteration
+static bool subtractionModeTest(pmSubtractionStampList *stamps, // Stamps to use to find best mode
+				pmSubtractionKernels *kernels, // Kernel description
+				const char *description, // Description for trace
+				psImage *subMask,  // Subtraction mask
+				float rej               // Rejection threshold
+    )
+{
+    assert(stamps);
+    assert(kernels);
+
+    psAbort("this function is not working");
+# if (0)
+    psTrace("psModules.imcombine", 3, "Convolving stamps as needed...\n");
+    if (!pmSubtractionConvolveStamps(stamps, kernels)) {
+	psError(psErrorCodeLast(), false, "Unable to convolve stamps.");
+	return false;
+    }
+
+    psTrace("psModules.imcombine", 3, "Calculating %s normalization equation...\n", description);
+    if (!pmSubtractionCalculateEquation(stamps, kernels)) {
+	psError(psErrorCodeLast(), false, "Unable to calculate least-squares equation.");
+	return false;
+    }
+
+    psTrace("psModules.imcombine", 3, "Solving %s normalization equation...\n", description);
+    if (!pmSubtractionSolveEquation(kernels, stamps)) {
+	psError(psErrorCodeLast(), false, "Unable to calculate least-squares equation.");
+	return false;
+    }
+
+    psTrace("psModules.imcombine", 3, "Calculate %s deviations...\n", description);
+    psVector *deviations = pmSubtractionCalculateDeviations(stamps, kernels); // Stamp deviations
+    if (!deviations) {
+	psError(psErrorCodeLast(), false, "Unable to calculate deviations.");
+	return false;
+    }
+
+    // XXX this needs to be made consistent with the modified 'reject stamps' function
+    psTrace("psModules.imcombine", 3, "Rejecting %s stamps...\n", description);
+    long numRejected = pmSubtractionRejectStamps(kernels, stamps, deviations, subMask, rej);
+    if (numRejected < 0) {
+	psError(psErrorCodeLast(), false, "Unable to reject stamps.");
+	psFree(deviations);
+	return false;
+    }
+    psFree(deviations);
+
+    if (numRejected > 0) {
+	// Allow re-fit with reduced stamps set
+	psTrace("psModules.imcombine", 3, "Calculating %s normalization equation...\n", description);
+	if (!pmSubtractionCalculateEquation(stamps, kernels)) {
+	    psError(psErrorCodeLast(), false, "Unable to calculate least-squares equation.");
+	    return false;
+	}
+
+	psTrace("psModules.imcombine", 3, "Resolving %s equation...\n", description);
+	if (!pmSubtractionSolveEquation(kernels, stamps)) {
+	    psError(psErrorCodeLast(), false, "Unable to calculate least-squares equation.");
+	    return false;
+	}
+	psTrace("psModules.imcombine", 3, "Recalculate %s deviations...\n", description);
+
+	psVector *deviations = pmSubtractionCalculateDeviations(stamps, kernels); // Stamp deviations
+	if (!deviations) {
+	    psError(psErrorCodeLast(), false, "Unable to calculate deviations.");
+	    return false;
+	}
+	psTrace("psModules.imcombine", 3, "Measuring %s quality...\n", description);
+	long numRejected = pmSubtractionRejectStamps(kernels, stamps, deviations, subMask, NAN);
+	if (numRejected < 0) {
+	    psError(psErrorCodeLast(), false, "Unable to reject stamps.");
+	    psFree(deviations);
+	    return false;
+	}
+	psFree(deviations);
+    }
+# endif
+    return true;
+}
+
+
+pmSubtractionMode pmSubtractionBestMode(pmSubtractionStampList **stamps, pmSubtractionKernels **kernels,
+					const psImage *subMask, float rej)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(*stamps, PM_SUBTRACTION_MODE_ERR);
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(*kernels, PM_SUBTRACTION_MODE_ERR);
+
+    // Copies of the inputs
+    pmSubtractionStampList *stamps1 = pmSubtractionStampListCopy(*stamps);
+    pmSubtractionKernels *kernels1 = pmSubtractionKernelsCopy(*kernels);
+    psImage *subMask1 = psImageCopy(NULL, subMask, subMask->type.type);
+    kernels1->mode = PM_SUBTRACTION_MODE_1;
+
+    if (!subtractionModeTest(stamps1, kernels1, "convolve 1", subMask1, rej)) {
+	psError(psErrorCodeLast(), false, "Unable to test subtraction with convolution of image 1");
+	psFree(stamps1);
+	psFree(kernels1);
+	psFree(subMask1);
+	return PM_SUBTRACTION_MODE_ERR;
+    }
+    psFree(subMask1);
+
+    // Copies of the inputs
+    pmSubtractionStampList *stamps2 = pmSubtractionStampListCopy(*stamps);
+    pmSubtractionKernels *kernels2 = pmSubtractionKernelsCopy(*kernels);
+    psImage *subMask2 = psImageCopy(NULL, subMask, subMask->type.type);
+    kernels2->mode = PM_SUBTRACTION_MODE_2;
+
+    if (!subtractionModeTest(stamps2, kernels2, "convolve 2", subMask2, rej)) {
+	psError(psErrorCodeLast(), false, "Unable to test subtraction with convolution of image 2");
+	psFree(stamps2);
+	psFree(kernels2);
+	psFree(subMask2);
+	psFree(stamps1);
+	psFree(kernels1);
+	return PM_SUBTRACTION_MODE_ERR;
+    }
+    psFree(subMask2);
+
+
+    pmSubtractionStampList *bestStamps = NULL; // Best choice for stamps
+    pmSubtractionKernels *bestKernels = NULL; // Best choice for kernels
+    psLogMsg("psModules.imcombine", PS_LOG_INFO,
+	     "Image 1: %f +/- %f from %d stamps\nImage 2: %f +/- %f from %d stamps\n",
+	     kernels1->mean, kernels1->rms, kernels1->numStamps,
+	     kernels2->mean, kernels2->rms, kernels2->numStamps);
+
+    if (kernels1->mean < kernels2->mean) {
+	bestStamps = stamps1;
+	bestKernels = kernels1;
+    } else {
+	bestStamps = stamps2;
+	bestKernels = kernels2;
+    }
+
+    psFree(*stamps);
+    psFree(*kernels);
+    *stamps = psMemIncrRefCounter(bestStamps);
+    *kernels = psMemIncrRefCounter(bestKernels);
+
+    psFree(stamps1);
+    psFree(stamps2);
+    psFree(kernels1);
+    psFree(kernels2);
+
+    return bestKernels->mode;
+}
+
+static float scaleRefOption = NAN;
+static float scaleMinOption = NAN;
+static float scaleMaxOption = NAN;
+static bool  scaleOption = false;
+
+bool pmSubtractionParamScaleOptions(bool scale, float scaleRef, float scaleMin, float scaleMax) { 
+
+    if (scale) {
+	PS_ASSERT_FLOAT_LARGER_THAN(scaleRef, 0.0, false);
+	PS_ASSERT_FLOAT_LARGER_THAN(scaleMin, 0.0, false);
+	PS_ASSERT_FLOAT_LARGER_THAN(scaleMax, 0.0, false);
+	PS_ASSERT_FLOAT_LARGER_THAN(scaleMax, scaleMin, false);
+    }
+
+    scaleRefOption = scaleRef;
+    scaleMinOption = scaleMin;
+    scaleMaxOption = scaleMax;
+    scaleOption = scale;
+    
+    return true;
+}
+
+bool pmSubtractionParamsScale(int *kernelSize, int *stampSize, psVector *widths, float fwhm1, float fwhm2)
+{
+    // PS_ASSERT_PTR_NON_NULL(kernelSize, false);
+    // PS_ASSERT_PTR_NON_NULL(stampSize, false);
+    PS_ASSERT_VECTOR_NON_NULL(widths, false);
+    PS_ASSERT_VECTOR_TYPE(widths, PS_TYPE_F32, false);
+
+    if (!scaleOption) return true;
+
+    // pmSubtractionGetFWHMs(&fwhm1, &fwhm2);
+    // psAssert(isfinite(fwhm1), "fwhm 1 not set");
+    // psAssert(isfinite(fwhm2), "fwhm 2 not set");
+    
+    // float diff = sqrtf(PS_SQR(PS_MAX(fwhm1, fwhm2)) - PS_SQR(PS_MIN(fwhm1, fwhm2))); // Difference
+    float scale = PS_MAX(fwhm1, fwhm2) / scaleRefOption;      // Scaling factor
+
+    if (isfinite(scaleMinOption) && scale < scaleMinOption) {
+	scale = scaleMinOption;
+    }
+    if (isfinite(scaleMaxOption) && scale > scaleMaxOption) {
+	scale = scaleMaxOption;
+    }
+
+    for (int i = 0; i < widths->n; i++) {
+	widths->data.F32[i] *= scale;
+    }
+    if (kernelSize) {
+	*kernelSize = *kernelSize * scale + 0.5;
+    }
+    if (stampSize) {
+	*stampSize = *stampSize * scale + 0.5;
+    }
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Scaling kernel parameters by %f", scale);
+    if (kernelSize) psLogMsg("psModules.imcombine", PS_LOG_INFO, " modified kernel size %d", *kernelSize);
+    if (stampSize) psLogMsg("psModules.imcombine", PS_LOG_INFO, " modified stamp size %d", *stampSize);
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionMatch.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionMatch.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionMatch.h	(revision 42651)
@@ -0,0 +1,125 @@
+#ifndef PM_SUBTRACTION_MATCH_H
+#define PM_SUBTRACTION_MATCH_H
+
+#include <pslib.h>
+
+#include <pmHDU.h>
+#include <pmFPA.h>
+#include <pmSubtractionKernels.h>
+#include <pmSubtractionStamps.h>
+#include <pmSubtraction.h>
+
+/// Match two images
+bool pmSubtractionMatch(pmReadout *conv1, ///< Output convolved data for image 1
+                        pmReadout *conv2, ///< Output convolved data for image 2
+                        const pmReadout *ro1, ///< Image 1
+                        const pmReadout *ro2, ///< Image 2
+                        // Stamp parameters
+                        int footprint,  ///< Stamp half-size
+                        int stride,     ///< Size for convolution patches
+                        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
+                        // Kernel parameters
+                        pmSubtractionKernelsType type, ///< Kernel type
+                        int size,       ///< Kernel half-size
+                        int order,      ///< Spatial polynomial order
+                        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
+                        float penalty,  ///< Penalty for wideness
+                        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
+                        float normFrac, ///< Fraction of flux in window for normalisation window
+                        float sysError, ///< Relative systematic error in images
+                        float skyError, ///< Relative systematic error in images
+                        float kernelError, ///< Relative systematic error in kernel
+                        float covarFrac,   ///< Fraction for kernel truncation before covariance calculation
+                        psImageMaskType maskVal, ///< Value to mask for input
+                        psImageMaskType maskBad, ///< Mask for output bad pixels
+                        psImageMaskType maskPoor, ///< Mask for output poor pixels
+                        float poorFrac, ///< Fraction for "poor"
+                        float badFrac,   ///< Maximum fraction of bad input pixels to accept
+                        pmSubtractionMode mode ///< Mode of subtraction; may be modified
+    );
+
+/// Match two images using precalculated kernel
+bool pmSubtractionMatchPrecalc(pmReadout *conv1, ///< Output convolved data for image 1
+                               pmReadout *conv2, ///< Output convolved data for image 2
+                               const pmReadout *ro1, ///< Image 1
+                               const pmReadout *ro2, ///< Image 2
+                               psMetadata *analysis, ///< Analysis metadata with pre-calculated kernel, region
+                               int stride, ///< Size for convolution patches
+                               float kernelError, ///< Relative systematic error in kernel
+                               float covarFrac,   ///< Fraction for kernel truncation before covariance calc.
+                               psImageMaskType maskVal, ///< Value to mask for input
+                               psImageMaskType maskBad, ///< Mask for output bad pixels
+                               psImageMaskType maskPoor, ///< Mask for output poor pixels
+                               float poorFrac, ///< Fraction for "poor"
+                               float badFrac ///< Maximum fraction of bad input pixels to accept
+    );
+
+/// Execute a thread job to measure the PSF width ratios
+bool pmSubtractionOrderThread(psThreadJob *job ///< Job to execute
+    );
+
+/// Measure the PSF width ratio for a single stamp
+bool pmSubtractionOrderStamp(psVector *ratios, ///< PSF width ratios
+                             psVector *mask, ///< Mask for PSF width ratios
+                             const pmSubtractionStampList *stamps, ///< List of stamps
+                             const psArray *models, ///< Pre-calculated gaussian models
+                             const psVector *modelSums, ///< Pre-calculated gaussian model sums
+                             int index, ///< Index of stamp
+                             float bg1, ///< Background for image 1
+                             float bg2  ///< Background for image 2
+    );
+
+/// Determine which image to convolve
+pmSubtractionMode pmSubtractionOrder(pmSubtractionStampList *stamps, ///< Stamps that have been extracted
+                                     float bg1, float bg2 ///< Background for each image
+    );
+
+/// Determine best subtraction mode to use
+///
+/// Subtractions are attempted each way, and the mode with the lower residual is taken to be the best
+pmSubtractionMode pmSubtractionBestMode(
+    pmSubtractionStampList **stamps,    ///< Stamps to use for solution
+    pmSubtractionKernels **kernels,     ///< Kernels to use for solution
+    const psImage *subMask,             ///< Subtraction mask
+    float rej                           ///< Rejection threshold for stamps
+    );
+
+
+/// Scale subtraction parameters according to the FWHMs of the inputs
+// bool pmSubtractionParamsScale(
+//     int *kernelSize,                    ///< Half-size of the kernel
+//     int *stampSize,                     ///< Half-size of the stamp (footprint)
+//     psVector *widths,                   ///< ISIS widths
+//     float scaleRef,                     ///< Reference width for scaling
+//     float scaleMin,                     ///< Minimum scaling ratio, or NAN
+//     float scaleMax                      ///< Maximum scaling ratio, or NAN
+//     );
+
+bool pmSubtractionParamsScale(int *kernelSize, int *stampSize, psVector *widths, float fwhm1, float fwhm2);
+
+bool pmSubtractionParamScaleOptions(bool scale, float scaleRef, float scaleMin, float scaleMax);
+
+bool pmSubtractionMatchAttempt(
+    pmSubtractionQuality **bestMatch,
+    pmSubtractionKernels *kernels, 
+    pmSubtractionStampList *stamps, 
+    pmSubtractionMode mode, 
+    int spatialOrder, 
+    bool final
+    );
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionParams.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionParams.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionParams.c	(revision 42651)
@@ -0,0 +1,524 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <math.h>
+#include <pslib.h>
+
+#include "pmErrorCodes.h"
+#include "pmFPA.h"
+#include "pmSubtractionTypes.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionStamps.h"
+#include "pmSubtractionParams.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#if 0
+// XXX this was moved to pmSubtraction.c in r15443 -- delete 
+// 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
+
+/// Select the appropriate convolution, given the kernel basis function and subtraction mode
+static inline psKernel *selectConvolution(const pmSubtractionStamp *stamp, // Stamp
+                                          int kernelIndex, // Index for kernel component
+                                          pmSubtractionMode mode // Mode of subtraction
+    )
+{
+    switch (mode) {
+      case PM_SUBTRACTION_MODE_1:
+        return stamp->convolutions1->data[kernelIndex];
+      case PM_SUBTRACTION_MODE_2:
+        return stamp->convolutions2->data[kernelIndex];
+      default:
+        psAbort("Unsupported subtraction mode: %x", mode);
+    }
+    return NULL;                        // Unreached
+}
+
+// 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
+                            const psKernel *target, // Target stamp
+                            int kernelIndex, // Index for kernel component
+                            int footprint, // Size of region of interest
+                            pmSubtractionMode mode // Mode of subtraction
+    )
+{
+    psKernel *weight = stamp->weight;   // Weight image
+    psKernel *convolution = selectConvolution(stamp, kernelIndex, mode); // Convolution of interest
+
+    for (int y = -footprint; y <= footprint; y++) {
+        psF32 *in = &target->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
+                                   pmSubtractionMode mode // Mode of subtraction
+    )
+{
+    psKernel *weight = stamp->weight;   // Weight image
+    psKernel *convolution = selectConvolution(stamp, kernelIndex, mode); // 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(const psKernel *target, // Target 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
+                             pmSubtractionMode mode // Mode of subtraction
+    )
+{
+    double chi2 = 0.0;
+    psKernel *weight = stamp->weight;   // Weight image
+    psKernel *convolution = selectConvolution(stamp, kernelIndex, mode); // Convolution of interest
+
+    for (int y = -footprint; y <= footprint; y++) {
+        psF32 *in = &target->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(const psKernel *target, // Target stamp
+                          const pmSubtractionStamp *stamp, // Stamp
+                          int footprint, // Size of convolution
+                          pmSubtractionMode mode // Mode of subtraction
+    )
+{
+    psKernel *weight = stamp->weight;   // Weight image
+    psKernel *source;                   // Source stamp
+    switch (mode) {
+      case PM_SUBTRACTION_MODE_1:
+        source = stamp->image1;
+        break;
+      case PM_SUBTRACTION_MODE_2:
+        source = stamp->image2;
+        break;
+      default:
+        psAbort("Unsupported subtraction mode: %x", mode);
+    }
+
+    double chi2 = 0.0;                  // Chi^2
+    for (int y = -footprint; y <= footprint; y++) {
+        psF32 *in = &target->kernel[y][-footprint]; // Dereference input
+        psF32 *wt = &weight->kernel[y][-footprint]; // Dereference weight
+        psF32 *ref = &source->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 *target, // Target stamp
+                                const pmSubtractionStamp *stamp, // Stamp
+                                int kernelIndex, // Index for kernel component
+                                float coeff, // Coefficient of subtraction
+                                float bg, // Background term
+                                int footprint, // Size of region of interest
+                                pmSubtractionMode mode // Mode of subtraction
+    )
+{
+    psKernel *convolution = selectConvolution(stamp, kernelIndex, mode); // Convolution of interest
+    for (int y = -footprint; y <= footprint; y++) {
+        psF32 *in = &target->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, float penalty, psRegion bounds,
+                                                      pmSubtractionMode mode)
+{
+    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,
+                                                                  penalty, bounds, mode); // 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 *targets = psArrayAlloc(numStamps); // Deep copies of the targets
+    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 *target;               // Target image of interest
+        switch (mode) {
+          case PM_SUBTRACTION_MODE_1:
+            target = stamp->image2;
+            break;
+          case PM_SUBTRACTION_MODE_2:
+            target = stamp->image1;
+            break;
+          default:
+            psAbort("Unsupported subtraction mode: %x", mode);
+        }
+        psImage *copy = psImageCopy(NULL, target->image, PS_TYPE_F32); // Copy of the image
+        targets->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(psErrorCodeLast(), false, "Unable to convolve stamp %d.", i);
+            psFree(targets);
+            psFree(kernels);
+            psFree(badStamps);
+            return NULL;
+        }
+
+        // This sum is invariant to the kernel
+        psKernel *weight = stamp->weight; // Weight image
+        for (int v = -footprint; v <= footprint; v++) {
+            psF32 *wt = &weight->kernel[v][-footprint]; // Dereference weight
+            for (int u = -footprint; u <= footprint; u++, wt++) {
+                sum1 += 1.0 * *wt;
+            }
+        }
+        if (!isfinite(sum1)) {
+            psError(PM_ERR_DATA, true,
+                    "Sum of 1/sigma^2 is non-finite for stamp %d (%d,%d)\n",
+                    i, (int)stamp->x, (int)stamp->y);
+            psFree(targets);
+            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, mode);
+        }
+
+        lastChi2 += initialChi2(targets->data[i], stamp, footprint, mode);
+        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, targets->data[j], i, footprint, mode);
+            }
+
+            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(targets->data[j], stamp, i, coeff, bg, footprint, mode);
+            }
+
+            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(PM_ERR_DATA, true, "Unable to find best kernel component in round %d.", iter);
+            psFree(targets);
+            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(targets->data[j], stamp, bestIndex, bestCoeff, bestBG, footprint, mode);
+        }
+
+        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(targets);
+    psFree(sumC);
+    psFree(sumCC);
+
+    if (cutIndex < 0) {
+        psError(PM_ERR_DATA, 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(selectConvolution(stamp, i, mode));
+            }
+        }
+    }
+    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->convolutions1);
+        psFree(stamp->convolutions2);
+        switch (mode) {
+          case PM_SUBTRACTION_MODE_1:
+            stamp->convolutions1 = convNew->data[i];
+            stamp->convolutions2 = NULL;
+            break;
+          case PM_SUBTRACTION_MODE_2:
+            stamp->convolutions1 = NULL;
+            stamp->convolutions2 = convNew->data[i];
+            break;
+          default:
+            psAbort("Unsupported subtraction mode: %x", mode);
+        }
+    }
+
+    psFree(badStamps);
+    psFree(ranking);
+
+    // Maintain photometric scaling
+    if (type == PM_SUBTRACTION_KERNEL_ISIS) {
+
+        // XXX in r26035, this code was just wrong.  we had:
+
+        // psKernel *subtract = kernels->preCalc->data[0]
+
+        // but, kernels->preCalc was an array of psArray, not an array of kernels.  It is now
+        // an array of pmSubtractionKernelPreCalc.
+
+        pmSubtractionKernelPreCalc *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) {
+                pmSubtractionKernelPreCalc *preCalc = kernels->preCalc->data[i]; // Kernel of interest
+                psBinaryOp(preCalc->kernel->image, preCalc->kernel->image, "-", subtract->kernel->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) {
+                pmSubtractionKernelPreCalc *preCalc = kernels->preCalc->data[i]; // Kernel of interest
+                preCalc->kernel->kernel[0][0] -= 1.0;
+            }
+        }
+
+        if (!p_pmSubtractionKernelsAddGrid(kernels, numGaussians, inner)) {
+            psAbort("Should never get here.");
+        }
+
+        kernels->type = PM_SUBTRACTION_KERNEL_GUNK;
+    }
+
+    return kernels;
+}
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionParams.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionParams.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionParams.h	(revision 42651)
@@ -0,0 +1,23 @@
+#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
+                                                      float penalty, ///< Penalty for wideness
+                                                      psRegion bounds,       ///< Bounds of validity
+                                                      pmSubtractionMode mode ///< Mode for subtraction
+    );
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionSimple.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionSimple.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionSimple.c	(revision 42651)
@@ -0,0 +1,455 @@
+#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 "pmHDUUtils.h"
+#include "pmSubtractionTypes.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionParams.h"
+#include "pmSubtractionKernels.h"
+#include "pmSubtractionStamps.h"
+#include "pmSubtractionEquation.h"
+#include "pmSubtractionAnalysis.h"
+#include "pmSubtractionMask.h"
+#include "pmSubtractionThreads.h"
+#include "pmSubtractionVisual.h"
+#include "pmErrorCodes.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+
+#include "pmSubtractionSimple.h"
+
+
+bool simple_do_boxphot(int *nPix,
+		       float *flux,
+		       pmSource *source,
+		       psImage *image,
+		       psImage *mask,
+		       psImageMaskType maskVal,
+		       int size) {
+  *nPix = 0;
+  *flux = 0.0;
+  for (int y = source->peak->yf - size;y <= source->peak->yf + size;y++) {
+    for (int x = source->peak->xf - size; x <= source->peak->xf + size; x++) {
+      if ((y > 0)&&(y < image->numRows)&&
+	  (x > 0)&&(x < image->numCols)) {
+	if (!(mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskVal)) {
+	  *nPix += 1;
+	  *flux += image->data.F32[y][x];
+	}
+      }
+    }
+  }
+  *flux = log10(*flux);
+  return(true);
+}  
+
+bool simple_apply_mask(psImage *image, psImage *weight, psImage *mask,
+		       psImageMaskType maskVal) {
+  for (int y = 0; y < mask->numRows; y++) {
+    for (int x = 0; x < mask->numCols; x++) {
+      if (mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskVal) {
+	image->data.F32[y][x] = NAN;
+	if (weight) {
+	  weight->data.F32[y][x] = NAN;
+	}
+      }
+    }
+  }
+  return(true);
+}
+		       
+
+// Copied from pmSubtraction
+static void solvedKernelPreCalc(psKernel *kernel, // Kernel, updated
+				const pmSubtractionKernels *kernels, // Kernel basis functions
+				float value,                         // Normalisation value for basis function
+				int index                  // Index of basis function of interest
+				)
+{
+  int size = kernels->size;           // Kernel half-size
+  pmSubtractionKernelPreCalc *preCalc = kernels->preCalc->data[index]; // Precalculated values
+  for (int v = -size; v <= size; v++) {
+    for (int u = -size; u <= size; u++) {
+      kernel->kernel[v][u] +=  value * preCalc->kernel->kernel[v][u];
+    }
+  }
+
+  return;
+}
+//End copy
+
+bool pmSubtractionSimpleMatch(pmReadout *conv1,
+			      pmReadout *conv2,
+			      const pmReadout *ro1,
+			      const pmReadout *ro2,
+			      const psArray *sources,
+			      int size,
+			      psImageMaskType maskVal,
+			      psImageMaskType maskBad,
+			      psImageMaskType maskPoor,
+			      float deconvolveThreshold
+			      ) {
+  //
+  // We've already validated the input values at this level
+  float sig2fwhm = 2.0 * sqrt(2.0 * log(2.0));
+  float fwhm1 = 0,fwhm2 = 0;
+  float sigma1 = 0,sigma2 = 0,sigmaKern = 0;
+  float chisq = 1.0;
+  int convolution_direction = 0;
+  psImage *image1 = NULL;
+  psImage *mask1 = NULL;
+  psImage *var1 = NULL;
+
+  psImage *image2 = NULL;
+  psImage *mask2 = NULL;
+  psImage *var2 = NULL;
+
+  psImage *imageC1 = NULL;
+  psImage *maskC1 = NULL;
+  psImage *varC1 = NULL;
+
+  psImage *imageC2 = NULL;
+  psImage *maskC2 = NULL;
+  psImage *varC2 = NULL;
+
+  psImage *maskTemp = NULL;
+  
+  // Allocate images, as this is usually done by subtractionMatchAlloc after this function is called.  
+  int numCols = ro1->image->numCols;
+  int numRows = ro1->image->numRows;
+
+  if (conv1) {
+    //    conv1->covariance = psMemIncrRefCounter(ro1->covariance);
+    if (!conv1->image) {
+      conv1->image = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    }
+    psImageInit(conv1->image, NAN);
+    if (ro1->variance) {
+      if (!conv1->variance) {
+	conv1->variance = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+      }
+      psImageInit(conv1->variance, NAN);
+    }
+    if (!conv1->mask) {
+      conv1->mask = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK);
+    }
+    psImageInit(conv1->mask, maskBad);
+  }
+  if (conv2) {
+    //    
+    if (!conv2->image) {
+      conv2->image = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    }
+    psImageInit(conv2->image, NAN);
+    if (ro2->variance) {
+      if (!conv2->variance) {
+	conv2->variance = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+      }
+      psImageInit(conv2->variance, NAN);
+    }
+    if (!conv2->mask) {
+      conv2->mask = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK);
+    }
+    psImageInit(conv2->mask, maskBad);
+  }
+  
+  // Assign local aliases to images
+  image1 = ro1->image;
+  mask1  = ro1->mask;
+  var1   = ro1->variance;
+
+  image2 = ro2->image;
+  mask2  = ro2->mask;
+  var2   = ro2->variance;
+
+  if (conv1) {
+    imageC1 = conv1->image;
+    maskC1  = conv1->mask;
+    varC1   = conv1->variance;
+  }
+  
+  if (conv2) {
+    imageC2 = conv2->image;
+    maskC2  = conv2->mask;
+    varC2   = conv2->variance;
+  }
+ 
+  //
+  // Determine Gaussian widths
+  pmSubtractionGetFWHMs(&fwhm1,&fwhm2);
+  sigma1 = fwhm1 / sig2fwhm;
+  sigma2 = fwhm2 / sig2fwhm;
+  if (sigma1 > sigma2) {
+    convolution_direction = 2;
+    sigmaKern = sqrt(PS_SQR(sigma1) - PS_SQR(sigma2));
+  }
+  else if (sigma1 < sigma2) {
+    convolution_direction = 1;
+    sigmaKern = sqrt(PS_SQR(sigma2) - PS_SQR(sigma1));
+  }
+  if (!conv1) {
+    if (convolution_direction == 1) {
+      if (sigma1 - sigma2 > deconvolveThreshold) {
+	chisq = 100;
+      }
+    }
+    //    convolution_direction = 2;
+  }
+  if (!conv2) {
+    if (convolution_direction == 2) {
+      if (sigma2 - sigma1 > deconvolveThreshold) {
+	chisq = 100;
+      }
+    }
+    //    convolution_direction = 1;
+  }
+  
+  bool oldThreads = psImageConvolveSetThreads(true); // Old value of threading in psImageConvolve
+  
+  int maskBox = (int) ceil(sigmaKern * 1.1774); // diameter is 1/2 FWHM
+  int maskBlank = 8;  // I should be able to get this from a reference, right?
+  int maskPoorVal = 16384; // Another value that should be found elsewhere.
+  //
+  // Make a fake pmSubtractionKernels element so we can add it appropriately.
+  // Defining everything here is a bit clunky, but it's necessary to get the covariance
+  // correct.
+  psVector *fwhms = psVectorAlloc(1,PS_TYPE_F32);
+  fwhms->data.F32[0] = sigmaKern * sig2fwhm;
+  psVector *orders = psVectorAlloc(1,PS_TYPE_S32);
+  orders->data.S32[0] = 0;
+  psRegion bounds;
+  bounds.x0 = 0;
+  bounds.y0 = 0;
+  bounds.x1 = numCols;
+  bounds.y1 = numRows;
+  pmSubtractionKernels *kernels = pmSubtractionKernelsAlloc(1,PM_SUBTRACTION_KERNEL_SIMPLE,
+							    size,fwhms,orders,0,0.0,bounds,
+							    convolution_direction);
+  pmSubtractionKernelsMakeDescription(kernels); // Need this defined
+  pmSubtractionKernelPreCalc *preCalc = pmSubtractionKernelPreCalcAlloc(PM_SUBTRACTION_KERNEL_SIMPLE,0,0,size,sigmaKern);
+  kernels->widths->data.F32[0] = sigmaKern * sig2fwhm;
+  kernels->u->data.S32[0] = 0;
+  kernels->v->data.S32[0] = 0;
+  if (kernels->preCalc->data[0]) {
+    psFree(kernels->preCalc->data[0]);
+  }
+  kernels->preCalc->data[0] = psMemIncrRefCounter(preCalc);
+  kernels->solution1 = psVectorAlloc(3,PS_TYPE_F64);
+  kernels->solution1->data.F32[0] = 1.0;
+  kernels->solution1->data.F32[1] = 0.0;
+  kernels->solution1->data.F32[2] = 0.0;
+  kernels->solution1err = psVectorAlloc(3,PS_TYPE_F32);
+  kernels->solution1err->data.F32[0] = 0.0;
+  kernels->solution1err->data.F32[1] = 0.0;
+  kernels->solution1err->data.F32[2] = 0.0;
+  kernels->mean = 0.0;
+  kernels->rms = chisq; // This is the chi^2 value that's passed to ppStack
+  kernels->numStamps = sources->n;
+  
+  psKernel *kernel = psKernelAlloc(-size,size,-size,size);
+  solvedKernelPreCalc(kernel,kernels,1.0,0);
+  
+  //
+  // Do convolutions
+  if (convolution_direction == 1) {
+    if (conv1) {
+      psImageSmoothMask_Threaded(imageC1,image1,mask1,maskVal,sigmaKern,6,1e-6);
+      psImageSmoothMask_Threaded(varC1,var1,mask1,maskVal,sigmaKern * M_SQRT1_2,6,1e-6);
+
+      maskTemp = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK);
+      maskTemp = psImageConvolveMask(maskTemp,mask1,maskVal,maskBad,      // Mask bad values
+				   -maskBox,maskBox,-maskBox,maskBox);
+      maskC1 = psImageConvolveMask(maskC1,maskTemp,maskPoorVal,maskPoor,  // Mask poor values
+				   -maskBox,maskBox,-maskBox,maskBox);
+      psFree(maskTemp);
+
+      conv1->covariance = psImageCovarianceCalculate(kernel,ro1->covariance);
+      pmSubtractionBorder(imageC1,varC1,maskC1,maskBox,maskBlank);
+      simple_apply_mask(imageC1,varC1,maskC1,maskBad);
+    }
+    if (conv2) {
+      imageC2 = psImageCopy(imageC2,image2,PS_TYPE_F32);
+      varC2   = psImageCopy(varC2,var2,PS_TYPE_F32);
+      maskC2  = psImageCopy(maskC2,mask2,PS_TYPE_IMAGE_MASK);
+      conv2->covariance = psMemIncrRefCounter(ro2->covariance);
+    }
+  }
+  else if (convolution_direction == 2) {
+    if (conv2) {
+      psImageSmoothMask_Threaded(imageC2,image2,mask2,maskVal,sigmaKern,6,1e-6);
+      psImageSmoothMask_Threaded(varC2,var2,mask2,maskVal,sigmaKern * M_SQRT1_2,6,1e-6);
+
+      maskTemp = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK);
+      maskTemp = psImageConvolveMask(maskTemp,mask2,maskVal,maskBad,      // Mask bad values
+				   -maskBox,maskBox,-maskBox,maskBox);
+      maskC2 = psImageConvolveMask(maskC2,maskTemp,maskPoorVal,maskPoor,  // Mask poor values
+				   -maskBox,maskBox,-maskBox,maskBox);
+      psFree(maskTemp);
+
+      conv2->covariance = psImageCovarianceCalculate(kernel,ro2->covariance);
+      pmSubtractionBorder(imageC2,varC2,maskC2,maskBox,maskBlank);
+      simple_apply_mask(imageC2,varC2,maskC2,maskBad);
+    }
+    if (conv1) {
+      imageC1 = psImageCopy(imageC1,image1,PS_TYPE_F32);
+      varC1   = psImageCopy(varC1,var1,PS_TYPE_F32);
+      maskC1  = psImageCopy(maskC1,mask1,PS_TYPE_IMAGE_MASK);
+      conv1->covariance = psMemIncrRefCounter(ro1->covariance);
+    }
+  }    
+
+  psFree(kernel); // No longer needed after doing covariance calculation
+
+  //
+  // Do normalization
+  float normalization = 1.0;
+
+  // Scan source list, do box photometry on peaks, and then solve the linear relation.
+  int photRadius = (int) floor(PS_MAX(sigma1,sigma2) * 2.0 * sqrt(2.0 * log(2.0))); // Go out a FWHM diameter from the center.
+  psVector *logFluxDifferences = psVectorAlloc(sources->n,PS_TYPE_F32);
+  psVector *fitMask = psVectorAlloc(sources->n,PS_TYPE_VECTOR_MASK);
+  for (int i = 0; i < sources->n; i++) {
+    pmSource *source = sources->data[i];
+    int nPix1,nPix2;
+    float flux1,flux2;
+
+    if (conv1) {
+      simple_do_boxphot(&nPix1,&flux1,source,imageC1,maskC1,maskBad,photRadius);
+    }
+    else {
+      simple_do_boxphot(&nPix1,&flux1,source,image1,mask1,maskBad,photRadius);
+    }
+
+    if (conv2) {
+      simple_do_boxphot(&nPix2,&flux2,source,imageC2,maskC2,maskBad,photRadius);
+    }
+    else {
+      simple_do_boxphot(&nPix2,&flux2,source,image2,mask2,maskBad,photRadius);
+    }
+
+    logFluxDifferences->data.F32[i] = flux2 - flux1;
+    fitMask->data.PS_TYPE_VECTOR_MASK_DATA[i] = 0;
+    if ((PS_MIN(nPix1,nPix2) <= 0.75 * PS_MAX(nPix1,nPix2))||
+	(!isfinite(flux1))||(!isfinite(flux2))) {
+      fitMask->data.PS_TYPE_VECTOR_MASK_DATA[i] = 0xff;
+    }
+
+    //    fprintf(stderr,"SOURCES: %d %g %g %g -> %d %d %g %g %d %g\n",i,source->peak->xf,source->peak->yf,source->psfMag,
+    //	    nPix1,nPix2,flux1,flux2,fitMask->data.PS_TYPE_VECTOR_MASK_DATA[i],logFluxDifferences->data.F32[i]);
+  }
+
+  // Given the differences in log-flux space, the normalization factor is just the exponential of the median difference
+  psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+  if (!psVectorStats(stats,logFluxDifferences,NULL,fitMask,0xff)) {
+    // This should complain.
+    normalization = 1.0;
+  }
+
+  normalization = pow(10,stats->robustMedian);
+  // fprintf(stderr,"NORM: %g+/-%g\n",stats->robustMedian,stats->robustStdev);
+  
+  psFree(stats);
+  psFree(logFluxDifferences);
+  psFree(fitMask);
+
+  // Apply normalization
+  if (normalization != 1.0) {
+    if ((conv1)&&((convolution_direction == 1))) {
+      psBinaryOp(imageC1,imageC1,"*",psScalarAlloc((float) normalization, PS_TYPE_F32));
+      psBinaryOp(varC1,varC1,"*",psScalarAlloc((float) PS_SQR(normalization), PS_TYPE_F32));
+    }
+    else if ((conv2)&&(convolution_direction == 2)) {
+      normalization = 1.0 / normalization; // Because we fit one way, but are using it in the other.
+      psBinaryOp(imageC2,imageC2,"*",psScalarAlloc((float) normalization, PS_TYPE_F32));
+      psBinaryOp(varC2,varC2,"*",psScalarAlloc((float) PS_SQR(normalization), PS_TYPE_F32));
+    }
+  }
+  
+
+  //
+  
+  //
+  // Actually add it to the headers
+  psMetadata *outAnalysis = psMetadataAlloc();
+  psMetadata *outHeader   = psMetadataAlloc();
+  if (!pmSubtractionAnalysis(outAnalysis,outHeader,kernels,NULL,numCols,numRows)) {
+    psError(psErrorCodeLast(),false,"Unable to generate QA data");
+    psFree(fwhms);
+    psFree(orders);
+    psFree(preCalc);
+    psFree(kernels);
+  }
+  // This is a hack.  Yes, I know.  pmSAnalysis doesn't get the normalization correct, so I just do it here.
+  psMetadataRemoveKey(outAnalysis,PM_SUBTRACTION_ANALYSIS_NORM);
+  psMetadataRemoveKey(outHeader,PM_SUBTRACTION_ANALYSIS_NORM);
+  psMetadataAddF32(outAnalysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_NORM, PS_META_REPLACE, "Normalisation", normalization);
+  psMetadataAddF32(outHeader,   PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_NORM, PS_META_REPLACE, "Normalisation", normalization);
+
+  psFree(fwhms);
+  psFree(orders);
+  psFree(preCalc);
+  psFree(kernels);
+
+  if (conv1) {
+    conv1->analysis = psMetadataCopy(conv1->analysis, outAnalysis);
+    conv1->data_exists = true;
+    if (conv1->parent) {
+      conv1->parent->data_exists = true;
+      
+      if (conv1->parent->parent) {
+	conv1->parent->parent->data_exists = true;
+      }
+    }
+  }
+  if (conv2) {
+    conv2->analysis = psMetadataCopy(conv2->analysis, outAnalysis);
+    conv2->data_exists = true;
+    if (conv2->parent) {
+      conv2->parent->data_exists = true;
+      if (conv2->parent->parent) {
+	conv2->parent->parent->data_exists = true;
+      }
+    }
+  }
+
+  if (conv1 && conv1->parent) {
+    pmHDU *hdu = pmHDUFromCell(conv1->parent);
+    if (hdu) {
+      hdu->header = psMetadataCopy(hdu->header, outHeader);
+    }
+  }
+  if (conv2 && conv2->parent) {
+    pmHDU *hdu = pmHDUFromCell(conv2->parent);
+    if (hdu) {
+      hdu->header = psMetadataCopy(hdu->header, outHeader);
+    }
+  }
+  psFree(outAnalysis);
+  psFree(outHeader);
+
+  psImageConvolveSetThreads(oldThreads);
+  
+  //
+  // Return
+  return(true);
+}
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionSimple.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionSimple.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionSimple.h	(revision 42651)
@@ -0,0 +1,23 @@
+#ifndef PM_SUBTRACTION_SIMPLE_H
+#define PM_SUBTRACTION_SIMPLE_H
+
+#include <pslib.h>
+
+#include <pmSubtractionKernels.h>
+#include <pmSubtractionStamps.h>
+#include <pmSubtraction.h>
+
+bool pmSubtractionSimpleMatch(pmReadout *conv1,
+			      pmReadout *conv2,
+			      const pmReadout *ro1,
+			      const pmReadout *ro2,
+			      const psArray *sources,
+			      int size,
+			      psImageMaskType maskVal,
+			      psImageMaskType maskBad,
+			      psImageMaskType maskPoor,
+			      float deconvolveThreshold
+			      );
+
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionStamps.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionStamps.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionStamps.c	(revision 42651)
@@ -0,0 +1,1287 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmErrorCodes.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+// All these includes required to get stamps out of an array of pmSources
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+
+#include "pmSubtractionTypes.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionStamps.h"
+#include "pmSubtractionVisual.h"
+
+#define STAMP_LIST_BUFFER 20            // Number of stamps to add to list at a time
+
+#define SOURCE_MASK (PM_SOURCE_MODE_FAIL | PM_SOURCE_MODE_SATSTAR | PM_SOURCE_MODE_BLEND | \
+                     PM_SOURCE_MODE_BADPSF | PM_SOURCE_MODE_DEFECT | PM_SOURCE_MODE_SATURATED | \
+                     PM_SOURCE_MODE_CR_LIMIT) // Mask for bad sources
+#define SOURCE_FAINTEST 50.0            // Faintest magnitude to consider
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// 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);
+    psFree(list->window);
+    psFree(list->window1);
+    psFree(list->window2);
+}
+
+// Free function for pmSubtractionStamp
+static void subtractionStampFree(pmSubtractionStamp *stamp // Stamp to free
+                                 )
+{
+    psFree(stamp->image1);
+    psFree(stamp->image2);
+    psFree(stamp->weight);
+    psFree(stamp->convolutions1);
+    psFree(stamp->convolutions2);
+    psFree(stamp->matrix);
+    psFree(stamp->vector);
+}
+
+// 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;
+}
+
+
+// Search a region for a suitable stamp
+bool stampSearch(float *xStamp, float *yStamp, // Coordinates of stamp, to return
+                 float *fluxStamp, // Flux of stamp, to return
+                 const psImage *image1, const psImage *image2, // Images to search
+                 float thresh1, float thresh2, // Thresholds for images
+                 const psImage *subMask, // Subtraction mask
+                 int xMin, int xMax, int yMin, int yMax, // Bounds of search
+                 int numCols, int numRows, // Size of images
+                 int border             // Border around image
+    )
+{
+    bool found = false;                 // Found a suitable stamp?
+    *fluxStamp = -INFINITY;             // Flux of best stamp
+
+    // fprintf (stderr, "xMin, xMax: %d, %d -> ", xMin, xMax);
+    // float xRaw = xMin;
+    // float yRaw = yMin;
+
+    // Ensure we're not going to go outside the bounds of the image
+    xMin = PS_MAX(border, xMin);
+    xMax = PS_MIN(numCols - border - 1, xMax);
+    yMin = PS_MAX(border, yMin);
+    yMax = PS_MIN(numRows - border - 1, yMax);
+
+    if (xMax < xMin) {
+	// fprintf (stderr, "%f,%f : x-border\n", xRaw, yRaw);
+	return false;
+    }
+    if (yMax < yMin) {
+	// fprintf (stderr, "%f,%f : y-border\n", xRaw, yRaw);
+	return false;
+    }
+
+    psAssert (xMin <= xMax, "x mismatch?");
+    psAssert (yMin <= yMax, "y mismatch?");
+
+    for (int y = yMin; y <= yMax; y++) {
+        for (int x = xMin; x <= xMax; x++) {
+            if ((image1 && image1->data.F32[y][x] < thresh1) ||
+                (image2 && image2->data.F32[y][x] < thresh2)) {
+		// fprintf (stderr, "%f,%f : thresh\n", xRaw, yRaw);
+                continue;
+            }
+
+            if (subMask && subMask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] &
+                (PM_SUBTRACTION_MASK_BORDER | PM_SUBTRACTION_MASK_REJ)) {
+		// fprintf (stderr, "%f,%f : masked\n", xRaw, yRaw);
+                return false;
+            }
+
+            // We take the MIN to attempt to avoid transients in both images
+            float flux = (image1 && image2) ? PS_MIN(image1->data.F32[y][x], image2->data.F32[y][x]) :
+                ((image1) ? image1->data.F32[y][x] : image2->data.F32[y][x]); // Flux at pixel
+            if (flux > *fluxStamp) {
+                *xStamp = x + 0.5;
+                *yStamp = y + 0.5;
+                *fluxStamp = flux;
+                found = true;
+            }
+        }
+    }
+
+    // if (!found) {
+    // 	fprintf (stderr, "%f,%f : fails flux test\n", xRaw, yRaw);
+    // }
+
+    return found;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Functions for generating ds9 region files
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+static bool ds9regions = false;         // Save ds9 region files?
+
+void pmSubtractionRegions(bool state)
+{
+    ds9regions = state;
+}
+
+FILE *pmSubtractionStampsFile(const pmSubtractionStampList *stamps, const char *filename,
+                              const char *description)
+{
+    if (!ds9regions || !stamps || !filename) {
+        return NULL;
+    }
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Writing %s to ds9 region file: %s",
+             description, filename);
+
+    FILE *file = fopen(filename, "w");
+
+    // Outline the stamps
+    for (int i = 0; i < stamps->num; i++) {
+        psRegion *region = stamps->regions->data[i]; // Region of interest
+        float xCentre = 0.5 * (region->x0 + region->x1), yCentre = 0.5 * (region->y0 + region->y1);
+        fprintf(file, "image;box(%f,%f,%f,%f,0) # color=blue\nimage;text(%f,%f,{%d}) # color=blue\n",
+                xCentre, yCentre, region->x1 - region->x0, region->y1 - region->y0,
+                xCentre, yCentre, i);
+    }
+
+    return file;
+}
+
+void pmSubtractionStampPrint(FILE *ds9, float x, float y, float size, const char *color)
+{
+    if (!ds9regions || !ds9) {
+        return;
+    }
+    fprintf(ds9, "image;circle(%f,%f,%f)", x, y, size);
+    if (color && strlen(color) > 0) {
+        fprintf(ds9, " # color=%s", color);
+    }
+    fprintf(ds9, "\n");
+    return;
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+pmSubtractionStampList *pmSubtractionStampListAlloc(int numCols, int numRows, const psRegion *region,
+                                                    int footprint, float spacing, float normFrac,
+                                                    float sysErr, float skyErr)
+{
+    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 = (float)xSize / spacing + 1, yStamps = (float)ySize / spacing + 1; // Number of stamps
+
+    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 * ((float)ySize / (float)(yStamps)); // Subregion starts here
+        int yStop = yMin + (y + 1) * ((float)ySize / (float)(yStamps)) - 1; // Subregion stops here
+        assert(yStart >= yMin && yStop < yMax);
+
+        for (int x = 0; x < xStamps; x++, index++) {
+            int xStart = xMin + x * ((float)xSize / (float)(xStamps)); // Subregion starts here
+            int xStop = xMin + (x + 1) * ((float)xSize / (float)(xStamps)) - 1; // Subregion stops here
+            assert(xStart >= xMin && xStop < xMax);
+
+            list->stamps->data[index] = pmSubtractionStampAlloc();
+            psTrace("psModules.imcombine", 6, "Stamp region %d: [%d:%d,%d:%d]\n",
+                    index, xStart, xStop, yStart, yStop);
+            list->regions->data[index] = psRegionAlloc(xStart, xStop, yStart, yStop);
+        }
+    }
+
+    list->x = NULL;
+    list->y = NULL;
+    list->flux = NULL;
+    list->normFrac = normFrac;
+    list->normValue = NAN;
+    list->window = NULL;
+    list->window1 = NULL;
+    list->window2 = NULL;
+    list->normWindow1 = 0;
+    list->normWindow2 = 0;
+    list->footprint = footprint;
+    list->sysErr = sysErr;
+    list->skyErr = skyErr;
+
+    return list;
+}
+
+pmSubtractionStampList *pmSubtractionStampListCopy(const pmSubtractionStampList *in)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(in, NULL);
+
+    pmSubtractionStampList *out = psAlloc(sizeof(pmSubtractionStampList)); // Copied stamp list to return
+    psMemSetDeallocator(out, (psFreeFunc)subtractionStampListFree);
+
+    int num = out->num = in->num;       // Number of stamps
+    out->stamps = psArrayAlloc(num);
+    out->regions = psArrayAlloc(num);
+    out->x = NULL;
+    out->y = NULL;
+    out->flux = NULL;
+    out->window = psMemIncrRefCounter(in->window);
+    out->window1 = psMemIncrRefCounter(in->window1);
+    out->window2 = psMemIncrRefCounter(in->window2);
+    out->footprint = in->footprint;
+    out->normWindow1 = in->normWindow1;
+    out->normWindow2 = in->normWindow2;
+
+    for (int i = 0; i < num; i++) {
+        psRegion *inRegion = in->regions->data[i]; // Input region
+        out->regions->data[i] = psRegionAlloc(inRegion->x0, inRegion->x1, inRegion->y0, inRegion->y1);
+
+        pmSubtractionStamp *inStamp = in->stamps->data[i]; // Input stamp
+        pmSubtractionStamp *outStamp = psAlloc(sizeof(pmSubtractionStamp));
+        psMemSetDeallocator(outStamp, (psFreeFunc)subtractionStampFree);
+        outStamp->x = inStamp->x;
+        outStamp->y = inStamp->y;
+        outStamp->flux = inStamp->flux;
+        outStamp->xNorm = inStamp->xNorm;
+        outStamp->yNorm = inStamp->yNorm;
+        outStamp->status = inStamp->status;
+
+        outStamp->image1 = inStamp->image1 ? psKernelCopy(inStamp->image1) : NULL;
+        outStamp->image2 = inStamp->image2 ? psKernelCopy(inStamp->image2) : NULL;
+        outStamp->weight = inStamp->weight ? psKernelCopy(inStamp->weight) : NULL;
+
+        if (inStamp->convolutions1) {
+            int size = inStamp->convolutions1->n; // Size of array
+            outStamp->convolutions1 = psArrayAlloc(size);
+            for (int j = 0; j < size; j++) {
+                psKernel *conv = inStamp->convolutions1->data[j]; // Convolution
+                outStamp->convolutions1->data[j] = conv ? psKernelCopy(conv) : NULL;
+            }
+        } else {
+            outStamp->convolutions1 = NULL;
+        }
+        if (inStamp->convolutions2) {
+            int size = inStamp->convolutions2->n; // Size of array
+            outStamp->convolutions2 = psArrayAlloc(size);
+            for (int j = 0; j < size; j++) {
+                psKernel *conv = inStamp->convolutions2->data[j]; // Convolution
+                outStamp->convolutions2->data[j] = conv ? psKernelCopy(conv) : NULL;
+            }
+        } else {
+            outStamp->convolutions2 = NULL;
+        }
+
+        outStamp->matrix = inStamp->matrix ? psImageCopy(NULL, inStamp->matrix, PS_TYPE_F64) : NULL;
+        outStamp->vector = inStamp->vector ? psVectorCopy(NULL, inStamp->vector, PS_TYPE_F64) : NULL;
+
+        out->stamps->data[i] = outStamp;
+    }
+
+    return out;
+}
+
+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->status = PM_SUBTRACTION_STAMP_INIT;
+
+    stamp->image1 = NULL;
+    stamp->image2 = NULL;
+    stamp->weight = NULL;
+    stamp->convolutions1 = NULL;
+    stamp->convolutions2 = NULL;
+
+    stamp->matrix = NULL;
+    stamp->vector = NULL;
+    stamp->norm = NAN;
+    stamp->normI1 = NAN;
+    stamp->normI2 = NAN;
+    stamp->normSquare1 = NAN;
+    stamp->normSquare2 = NAN;
+
+    stamp->MxxI1 = NULL;
+    stamp->MyyI1 = NULL;
+    stamp->MxxI2 = NULL;
+    stamp->MyyI2 = NULL;
+
+    stamp->MxxI1raw = NAN;
+    stamp->MyyI1raw = NAN;
+    stamp->MxxI2raw = NAN;
+    stamp->MyyI2raw = NAN;
+
+    return stamp;
+}
+
+bool pmSubtractionStampsSelect(pmSubtractionStampList **stamps, // Stamps to read
+			       const pmReadout *ro1, // Readout 1
+			       const pmReadout *ro2, // Readout 2
+			       const psImage *subMask, // Mask for subtraction, or NULL
+			       psImage *variance,  // Variance map
+			       const psRegion *region, // Region of interest
+			       float thresh1,  // Threshold for stamp finding on readout 1
+			       float thresh2,  // Threshold for stamp finding on readout 2
+			       float stampSpacing, // Spacing between stamps
+			       float normFrac,     // Fraction of flux in window for normalisation window
+			       float sysError,     // Relative systematic error in images
+			       float skyError,     // Relative systematic error in images
+			       int size,         // Kernel half-size
+			       int footprint,     // Convolution footprint for stamps
+			       pmSubtractionMode mode // Mode for subtraction
+    )
+{
+    PS_ASSERT_PTR_NON_NULL(stamps, false);
+    PM_ASSERT_READOUT_NON_NULL(ro1, false);
+    PM_ASSERT_READOUT_NON_NULL(ro2, false);
+    PS_ASSERT_IMAGE_NON_NULL(subMask, false);
+    PS_ASSERT_IMAGE_NON_NULL(variance, false);
+    PS_ASSERT_PTR_NON_NULL(region, false);
+
+    psTrace("psModules.imcombine", 3, "Finding stamps...\n");
+
+    psImage *image1 = ro1 ? ro1->image : NULL, *image2 = ro2 ? ro2->image : NULL; // Images of interest
+
+    *stamps = pmSubtractionStampsFind(*stamps, image1, image2, subMask, region, thresh1, thresh2,
+                                      size, footprint, stampSpacing, normFrac, sysError, skyError, mode);
+    if (!*stamps) {
+        psError(psErrorCodeLast(), false, "Unable to find stamps.");
+        return false;
+    }
+
+    psTrace("psModules.imcombine", 3, "Extracting stamps...\n");
+    if (!pmSubtractionStampsExtract(*stamps, ro1->image, ro2->image, variance, size, *region)) {
+        psError(psErrorCodeLast(), false, "Unable to extract stamps.");
+        return false;
+    }
+
+    pmSubtractionVisualPlotStamps(*stamps, (pmReadout *) ro1);
+    return true;
+}
+
+pmSubtractionStampList *pmSubtractionStampsFind(pmSubtractionStampList *stamps, 
+						const psImage *image1,
+                                                const psImage *image2, 
+						const psImage *subMask,
+                                                const psRegion *region, 
+						float thresh1, 
+						float thresh2,
+                                                int size, 
+						int footprint, 
+						float spacing, 
+						float normFrac,
+                                                float sysErr, 
+						float skyErr, 
+						pmSubtractionMode mode)
+{
+    if (!image1 && !image2) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Must specify an image");
+        return NULL;
+    }
+    int numCols = 0, numRows = 0;       // Size of images
+    if (image1) {
+        PS_ASSERT_IMAGE_NON_NULL(image1, NULL);
+        PS_ASSERT_IMAGE_TYPE(image1, PS_TYPE_F32, NULL);
+        if (subMask) {
+            PS_ASSERT_IMAGES_SIZE_EQUAL(image1, subMask, NULL);
+        }
+        numCols = image1->numCols;
+        numRows = image1->numRows;
+    }
+    if (image2) {
+        PS_ASSERT_IMAGE_NON_NULL(image2, NULL);
+        PS_ASSERT_IMAGE_TYPE(image2, PS_TYPE_F32, NULL);
+        if (subMask) {
+            PS_ASSERT_IMAGES_SIZE_EQUAL(image2, subMask, NULL);
+        }
+        numCols = image2->numCols;
+        numRows = image2->numRows;
+    }
+    if (image1 && image2) {
+        PS_ASSERT_IMAGES_SIZE_EQUAL(image1, image2, NULL);
+    }
+    if (subMask) {
+        PS_ASSERT_IMAGE_NON_NULL(subMask, NULL);
+        PS_ASSERT_IMAGE_TYPE(subMask, PS_TYPE_IMAGE_MASK, NULL);
+        PS_ASSERT_IMAGE_SIZE(subMask, numCols, numRows, NULL);
+    }
+    PS_ASSERT_INT_POSITIVE(size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(footprint, NULL);
+    PS_ASSERT_FLOAT_LARGER_THAN(spacing, 0.0, NULL);
+    if (region) {
+        if (psRegionIsNaN(*region)) {
+            psString string = psRegionToString(*region);
+            psError(PM_ERR_PROG, true, "Input region (%s) contains NAN values", string);
+            psFree(string);
+            return false;
+        }
+        if (region->x0 < 0 || region->x1 > numCols ||
+            region->y0 < 0 || region->y1 > numRows) {
+            psString string = psRegionToString(*region);
+            psError(PM_ERR_PROG, true, "Input region (%s) does not fit in image (%dx%d)",
+                    string, numCols, numRows);
+            psFree(string);
+            return false;
+        }
+    }
+
+    int border = size + footprint;      // Border size
+
+    if (!stamps) {
+        stamps = pmSubtractionStampListAlloc(numCols, numRows, region, footprint, spacing,
+                                             normFrac, sysErr, skyErr);
+    }
+
+    // XXX TEST : dump all stars in the stamps here
+    if (0) {
+	FILE *f = fopen ("stamp.dat", "w");
+	for (int i = 0; i < stamps->num; i++) {
+	    psVector *xList = stamps->x->data[i];
+	    psVector *yList = stamps->y->data[i]; // Coordinate lists
+	    psVector *fluxList = stamps->flux->data[i]; // List of stamp fluxes
+
+	    for (int j = 0; j < xList->n; j++) {
+		fprintf (f, "%d %d  %f %f  %f\n", i, j, xList->data.F32[j], yList->data.F32[j], fluxList->data.F32[j]);
+	    }
+	}
+	fclose (f);
+    }
+
+    int numStamps = stamps->num;        // Number of stamp regions
+    int numFound = 0;                   // Number of stamps found
+    int numSearch = 0;                  // Number of regions searched for new stamp
+    int numGood = 0;                    // Number of good stamps in total
+    for (int i = 0; i < numStamps; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+
+        switch (stamp->status) {
+          case PM_SUBTRACTION_STAMP_NONE:
+            continue;
+          case PM_SUBTRACTION_STAMP_FOUND:
+          case PM_SUBTRACTION_STAMP_CALCULATE:
+          case PM_SUBTRACTION_STAMP_USED:
+            numGood++;
+            continue;
+          case PM_SUBTRACTION_STAMP_INIT:
+          case PM_SUBTRACTION_STAMP_REJECTED:
+            numSearch++;
+
+            float xStamp = 0.0, yStamp = 0.0; // Coordinates of stamp
+            float fluxStamp = -INFINITY; // 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 stamps off the top of the (sorted) list
+                for (int j = xList->n - 1; j >= 0 && !goodStamp; j--) {
+		    // fprintf (stderr, "%d : xList: %ld elements\n", i, xList->n);
+                    // Chop off the top of the list
+                    xList->n = j;
+                    yList->n = j;
+                    fluxList->n = j;
+
+#if 0
+                    // Fish around a bit to see if we can find a pixel that isn't masked
+                    // This is not a good idea if we're using the window feature
+                    psTrace("psModules.imcombine", 7, "Searching for stamp %d around %d,%d\n",
+                            i, xCentre, yCentre);
+
+                    // Search bounds
+                    int xCentre = xList->data.F32[j] - 0.5, yCentre = yList->data.F32[j] - 0.5;// Stamp centre
+                    int search = footprint - size; // Search radius
+                    int xMin = PS_MAX(border, xCentre - search);
+                    int xMax = PS_MIN(numCols - border -1, xCentre + search);
+                    int yMin = PS_MAX(border, yCentre - search);
+                    int yMax = PS_MIN(numRows - border - 1, yCentre + search);
+
+                    goodStamp = stampSearch(&xStamp, &yStamp, &fluxStamp, image1, image2, thresh1, thresh2,
+                                            subMask, xMin, xMax, yMin, yMax, numCols, numRows, border);
+                    // fprintf (stderr, "find: %d %d ==> %5.1f %5.1f (\n", xCentre, yCentre, xStamp, yStamp);
+#else
+                    // Only search the exact centre pixel
+                    goodStamp = stampSearch(&xStamp, &yStamp, &fluxStamp, image1, image2, thresh1, thresh2,
+                                            subMask, xList->data.F32[j], xList->data.F32[j],
+                                            yList->data.F32[j], yList->data.F32[j], numCols, numRows, border);
+#endif
+                }
+            } else {
+                // Use a simple method of automatically finding stamps --- take the highest pixel in the
+                // subregion
+                psRegion *subRegion = stamps->regions->data[i]; // Sub-region of interest
+
+                goodStamp = stampSearch(&xStamp, &yStamp, &fluxStamp, image1, image2, thresh1, thresh2,
+                                        subMask, subRegion->x0, subRegion->x1, subRegion->y0, subRegion->y1,
+                                        numCols, numRows, border);
+            }
+
+            if (goodStamp) {
+                stamp->x = xStamp;
+                stamp->y = yStamp;
+                stamp->flux = fluxStamp;
+
+                // Reset the postage stamps since we're making a new stamp
+                psFree(stamp->image1);
+                psFree(stamp->image2);
+                psFree(stamp->weight);
+                psFree(stamp->convolutions1);
+                psFree(stamp->convolutions2);
+                stamp->image1 = stamp->image2 = stamp->weight = NULL;
+                stamp->convolutions1 = stamp->convolutions2 = NULL;
+
+                stamp->status = PM_SUBTRACTION_STAMP_FOUND;
+                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 (numSearch > 0) {
+        psLogMsg("psModules.imcombine", PS_LOG_INFO, "Found %d stamps", numFound);
+    }
+
+    if (numGood == 0 && numFound == 0) {
+        // No good stamps
+        psError(PM_ERR_STAMPS, true, "Unable to find suitable stamps");
+        psFree(stamps);
+        return NULL;
+    }
+
+    return stamps;
+}
+
+
+
+pmSubtractionStampList *pmSubtractionStampsSet(const psVector *x, const psVector *y,
+                                               const psImage *image, const psImage *subMask,
+                                               const psRegion *region, int size, int footprint,
+                                               float spacing, float normFrac, float sysErr, float skyErr,
+                                               pmSubtractionMode mode)
+
+{
+    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);
+    PS_ASSERT_IMAGE_NON_NULL(image, NULL);
+    if (subMask) {
+        PS_ASSERT_IMAGE_NON_NULL(subMask, NULL);
+        PS_ASSERT_IMAGE_TYPE(subMask, PS_TYPE_IMAGE_MASK, NULL);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(image, subMask, NULL);
+    }
+    PS_ASSERT_INT_POSITIVE(size, NULL);
+    PS_ASSERT_INT_POSITIVE(footprint, NULL);
+    PS_ASSERT_FLOAT_LARGER_THAN(spacing, 0.0, NULL);
+
+    int numStars = x->n;                // Number of stars
+    pmSubtractionStampList *stamps = pmSubtractionStampListAlloc(subMask->numCols, subMask->numRows,
+                                                                 region, footprint, spacing,
+                                                                 normFrac, sysErr, skyErr); // Stamp list
+    int numStamps = stamps->num;        // Number of stamps
+
+    psString ds9name = NULL;            // Filename for ds9 region file
+    static int ds9num = 0;              // File number for ds9 region file
+    psStringAppend(&ds9name, "stamps_all_%d.ds9", ds9num);
+    FILE *ds9 = pmSubtractionStampsFile(stamps, ds9name, "all stamps"); // ds9 region file
+    psFree(ds9name);
+    ds9num++;
+
+    // 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++) {
+        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
+            psTrace("psModules.imcombine", 9, "Rejecting input stamp (%d,%d) because outside region",
+                    xPix, yPix);
+            pmSubtractionStampPrint(ds9, xPix, yPix, footprint, "red");
+            continue;
+        }
+
+        // fprintf (stderr, "stamp: %5.1f %5.1f == %d %d\n", xStamp, yStamp, xPix, yPix);
+
+        bool found = false;
+        for (int j = 0; j < numStamps && !found; 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
+
+                psVectorExtend(xList, STAMP_LIST_BUFFER, 1);
+                psVectorExtend(yList, STAMP_LIST_BUFFER, 1);
+                psVectorExtend(fluxList, STAMP_LIST_BUFFER, 1);
+
+                xList->data.F32[index] = xStamp;
+                yList->data.F32[index] = yStamp;
+                fluxList->data.F32[index] = image->data.F32[yPix][xPix];
+
+                found = true;
+                psTrace("psModules.imcombine", 9, "Putting input stamp (%d,%d) into subregion %d",
+                        xPix, yPix, j);
+                pmSubtractionStampPrint(ds9, xPix, yPix, footprint, "green");
+            }
+        }
+
+        if (!found) {
+            psTrace("psModules.imcombine", 9, "Unable to find subregion for stamp (%d,%d)",
+                    xPix, yPix);
+            pmSubtractionStampPrint(ds9, xPix, yPix, footprint, "yellow");
+        }
+    }
+
+    if (ds9) {
+        fclose(ds9);
+    }
+
+    int nTotal = 0;
+
+    // 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;
+	nTotal += num;
+    }
+    // fprintf (stderr, "nTotal %d\n", nTotal);
+    
+    return stamps;
+}
+
+// we are essentially using aperture photometry to determine the photometric match between the
+// images.  we need to choose an appropriate-sized aperture for this analysis.  If it is too
+// large, the measurement will be noisy (and possibly biased) due to the sky noise.  If it is
+// too small, or inconsistent, the measurement will be biased.  We use Kron-mag like aperture
+// scaled by the first radial moment.
+bool pmSubtractionStampsGetWindow(bool *tryAgain, pmSubtractionStampList *stamps, int kernelSize)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+    PS_ASSERT_INT_NONNEGATIVE(kernelSize, false);
+
+    // if we succeed, or fail with an unrecoverable error, do not try again
+    if (tryAgain) {
+	*tryAgain = false;
+    }
+
+    int size = stamps->footprint; // Size of postage stamps
+
+    // window for moments calculations downstream
+    psFree (stamps->window);
+    stamps->window = psKernelAlloc(-size, size, -size, size);
+    psImageInit(stamps->window->image, 0.0);
+
+    // window1 and window2 are mean stamp images used here to measure the 
+    // first radial moment, and thus the normalization window
+    psFree (stamps->window1);
+    stamps->window1 = psKernelAlloc(-size, size, -size, size);
+    psImageInit(stamps->window1->image, 0.0);
+
+    psFree (stamps->window2);
+    stamps->window2 = psKernelAlloc(-size, size, -size, size);
+    psImageInit(stamps->window2->image, 0.0);
+
+    // Generate an initial weighting window based on the fwhms (50% larger than the largest)
+    float fwhm1, fwhm2;
+
+    // XXX this is annoyingly hack-ish
+    pmSubtractionGetFWHMs(&fwhm1, &fwhm2);
+    
+    float sigma = 1.5 * PS_MAX(fwhm1, fwhm2) / 2.35;
+    
+    for (int y = -size; y <= size; y++) {
+	for (int x = -size; x <= size; x++) {
+	    stamps->window->kernel[y][x] = exp(-0.5*(x*x + y*y)/(sigma*sigma));
+	}
+    }
+
+    // generate normalizations for each stamp
+    psVector *norm1 = psVectorAlloc(stamps->num, PS_TYPE_F32);
+    psVector *norm2 = psVectorAlloc(stamps->num, PS_TYPE_F32);
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (!stamp) continue;
+        if (!stamp->image1) continue;
+        if (!stamp->image2) continue;
+
+        float sum1 = 0.0;
+        float sum2 = 0.0;
+        for (int y = -size; y <= size; y++) {
+            for (int x = -size; x <= size; x++) {
+                sum1 += stamp->image1->kernel[y][x];
+                sum2 += stamp->image2->kernel[y][x];
+            }
+        }
+        norm1->data.F32[i] = sum1;
+        norm2->data.F32[i] = sum2;
+    }
+
+    // storage vector for flux data
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN);
+    psVector *flux1 = psVectorAllocEmpty(2*stamps->num, PS_TYPE_F32);
+    psVector *flux2 = psVectorAllocEmpty(2*stamps->num, PS_TYPE_F32);
+
+    // generate the window pixels
+    double sum1 = 0.0;                   // Sum inside the window
+    double sum2 = 0.0;                   // Sum inside the window
+    float maxValue1 = 0.0;               // Maximum value, for normalisation
+    float maxValue2 = 0.0;               // Maximum value, for normalisation
+    for (int y = -size; y <= size; y++) {
+        for (int x = -size; x <= size; x++) {
+
+            flux1->n = 0;
+            flux2->n = 0;
+            for (int i = 0; i < stamps->num; i++) {
+                pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+                if (!stamp) continue;
+                if (!stamp->image1) continue;
+                if (!stamp->image2) continue;
+
+                psVectorAppend(flux1, stamp->image1->kernel[y][x] / norm1->data.F32[i]);
+                psVectorAppend(flux2, stamp->image2->kernel[y][x] / norm2->data.F32[i]);
+            }
+
+            float f1 = NAN;
+            if (flux1->n > 0) {
+                psStatsInit (stats);
+                if (!psVectorStats (stats, flux1, NULL, NULL, 0)) {
+                    psAbort ("failed to generate stats");
+                }
+                f1 = stats->sampleMedian;
+            }
+
+            float f2 = NAN;
+            if (flux2->n > 0) {
+                psStatsInit (stats);
+                if (!psVectorStats (stats, flux2, NULL, NULL, 0)) {
+                    psAbort ("failed to generate stats");
+                }
+                f2 = stats->sampleMedian;
+            }
+
+            stamps->window1->kernel[y][x] = f1;
+            if (PS_SQR(x) + PS_SQR(y) <= PS_SQR(size)) {
+                sum1 += stamps->window1->kernel[y][x];
+            }
+            maxValue1 = PS_MAX(maxValue1, stamps->window1->kernel[y][x]);
+
+            stamps->window2->kernel[y][x] = f2;
+            if (PS_SQR(x) + PS_SQR(y) <= PS_SQR(size)) {
+                sum2 += stamps->window2->kernel[y][x];
+            }
+            maxValue2 = PS_MAX(maxValue2, stamps->window2->kernel[y][x]);
+        }
+    }
+
+#if 0
+    {
+	psFits *fits = NULL;
+	fits = psFitsOpen ("window1.raw.fits", "w");
+        psFitsWriteImage (fits, NULL, stamps->window1->image, 0, NULL);
+        psFitsClose (fits);
+        fits = psFitsOpen ("window2.raw.fits", "w");
+        psFitsWriteImage (fits, NULL, stamps->window2->image, 0, NULL);
+        psFitsClose (fits);
+    }
+#endif
+
+    psTrace("psModules.imcombine", 3, "Window total (1): %f, threshold: %f\n", sum1, (1.0 - stamps->normFrac) * sum1);
+    psTrace("psModules.imcombine", 3, "Window total (2): %f, threshold: %f\n", sum2, (1.0 - stamps->normFrac) * sum2);
+
+    // attempt to calculate the normalization window based on the first radial moment
+    double Sr1 = 0.0;
+    double Sr2 = 0.0;
+    double Sf1 = 0.0;
+    double Sf2 = 0.0;
+    for (int y = -size; y <= size; y++) {
+	for (int x = -size; x <= size; x++) {
+	    float r = hypot(x, y);
+	    Sr1 += r * stamps->window1->kernel[y][x];
+	    Sr2 += r * stamps->window2->kernel[y][x];
+	    Sf1 += stamps->window1->kernel[y][x];
+	    Sf2 += stamps->window2->kernel[y][x];
+        }
+    }
+    
+    float R1 = Sr1 / Sf1;
+    float R2 = Sr2 / Sf2;
+
+    if (!isfinite(R1) || !isfinite(R2)) {
+        psError(PM_ERR_STAMPS, true, "Kron Radii are not finite (failure to converge).");
+	psFree (stats);
+	psFree (flux1);
+	psFree (flux2);
+	psFree (norm1);
+	psFree (norm2);
+        return false;
+    }
+
+    // Compare the Kron Radii (R1 & R2) to above to the FWHMs : if they are too discrepant, we will need to rescale
+    psLogMsg ("psModules.imcombine", PS_LOG_DETAIL, "Kron Radii vs FWHMs 1: fwhm: %f, kron %f\n", fwhm1, R1);
+    psLogMsg ("psModules.imcombine", PS_LOG_DETAIL, "Kron Radii vs FWHMs 2: fwhm: %f, kron %f\n", fwhm2, R2);
+
+    // XXX CAREFUL : in pmSubtractionMatch.c:703, we rely on this factor of 2.75..
+    stamps->normWindow1 = 2.75*R1;
+    stamps->normWindow2 = 2.75*R2;
+    psLogMsg ("psModules.imcombine", PS_LOG_DETAIL, "Windows from Kron Radii: %f for 1, %f for 2\n", stamps->normWindow1, stamps->normWindow2);
+
+
+    // if the calculated normWindows are too large, we will fall off the stamps.  In this case, we need to try again.
+    if ((stamps->normWindow1 > size) || (stamps->normWindow2 > size)) { 
+	if (tryAgain) {
+	    *tryAgain = true;
+	}
+	psFree (stats);
+	psFree (flux1);
+	psFree (flux2);
+	psFree (norm1);
+	psFree (norm2);
+	return false; 
+    }
+
+    // this is an unrecoverable error : something really bogus in the data
+    if (stamps->normWindow1 <= 0) {
+        psError(PM_ERR_STAMPS, true, "Unable to determine normalisation window size (1).");
+	psFree (stats);
+	psFree (flux1);
+	psFree (flux2);
+	psFree (norm1);
+	psFree (norm2);
+        return false;
+    }
+    if (stamps->normWindow2 <= 0) {
+        psError(PM_ERR_STAMPS, true, "Unable to determine normalisation window size (2).");
+	psFree (stats);
+	psFree (flux1);
+	psFree (flux2);
+	psFree (norm1);
+	psFree (norm2);
+        return false;
+    }
+
+    // Generate a weighting window based on the kron radii
+    float radius = 2.0 * PS_MAX(R1, R2);
+    psImageInit(stamps->window->image, 0.0);
+
+    // we use a top-hat window for the moments analysis
+    for (int y = -size; y <= size; y++) {
+	for (int x = -size; x <= size; x++) {
+	    if (hypot(x,y) > radius) continue;
+	    stamps->window->kernel[y][x] = 1.0;
+	}
+    }
+
+    // re-normalize so chisquare values are sensible
+    for (int y = -size; y <= size; y++) {
+        for (int x = -size; x <= size; x++) {
+            stamps->window1->kernel[y][x] /= maxValue1;
+        }
+    }
+    // re-normalize so chisquare values are sensible
+    for (int y = -size; y <= size; y++) {
+        for (int x = -size; x <= size; x++) {
+            stamps->window2->kernel[y][x] /= maxValue2;
+        }
+    }
+
+    psFree (stats);
+    psFree (flux1);
+    psFree (flux2);
+    psFree (norm1);
+    psFree (norm2);
+    return true;
+}
+
+static pthread_mutex_t getPenaltiesMutex = PTHREAD_MUTEX_INITIALIZER;
+
+// kernels->penalty is an overall scaling factor (user-supplied)
+bool pmSubtractionKernelPenaltiesStamp(pmSubtractionStamp *stamp, pmSubtractionKernels *kernels)
+{
+    // we only need the penalties if we are doing dual convolution
+    if (kernels->mode != PM_SUBTRACTION_MODE_DUAL) return true;
+
+    // we only calculate the penalties once.
+    if (kernels->havePenalties) return true;
+
+    // in a threaded context, only one thread can calculate the penalties.  attempt to grab a
+    // mutex before continuing
+    pthread_mutex_lock(&getPenaltiesMutex);
+
+    // did someone else already get the mutex and do this?
+    if (kernels->havePenalties) {
+	pthread_mutex_unlock(&getPenaltiesMutex);
+	return true;
+    }
+
+    for (int i = 0; i < kernels->num; i++) {
+	pmSubtractionKernelPenalties(stamp, kernels, i);
+    }
+
+    kernels->havePenalties = true;
+    pthread_mutex_unlock(&getPenaltiesMutex);
+    return true;
+}
+
+# define EMPIRICAL 0
+
+// kernels->penalty is an overall scaling factor (user-supplied)
+bool pmSubtractionKernelPenalties(pmSubtractionStamp *stamp, pmSubtractionKernels *kernels, int index)
+{
+    float penalty1, penalty2;
+    float fwhm1, fwhm2;
+
+    // XXX this is annoyingly hack-ish
+    pmSubtractionGetFWHMs(&fwhm1, &fwhm2);
+
+    bool zeroNull = false;
+    int uOrder = kernels->u->data.S32[index];
+    int vOrder = kernels->v->data.S32[index];
+    if (uOrder % 2 == 0 && vOrder % 2 == 0) zeroNull = true;
+
+    if (EMPIRICAL) {
+	psKernel *convolution1 = stamp->convolutions1->data[index];
+	penalty1 = pmSubtractionKernelPenaltySingle(convolution1, zeroNull);
+
+	psKernel *convolution2 = stamp->convolutions2->data[index];
+	penalty2 = pmSubtractionKernelPenaltySingle(convolution2, zeroNull);
+    } else {
+	pmSubtractionKernelPreCalc *kernel = kernels->preCalc->data[index];
+	float M2 = pmSubtractionKernelPenaltySingle(kernel->kernel, zeroNull);
+
+	if (1) {
+	    penalty1 = M2 * PS_SQR(fwhm1 / 2.35); // rescale the unconvolved second-moment by the image second moment 
+	    penalty2 = M2 * PS_SQR(fwhm2 / 2.35); // rescale the unconvolved second-moment by the image second moment 
+	    // penalty1 = M2 + PS_SQR(fwhm1 / 2.35); // rescale the unconvolved second-moment by the image second moment 
+	    // penalty2 = M2 + PS_SQR(fwhm2 / 2.35); // rescale the unconvolved second-moment by the image second moment 
+	} else {
+	    penalty1 = PS_SQR(fwhm1 / 2.35); // rescale the unconvolved second-moment by the image second moment 
+	    penalty2 = PS_SQR(fwhm2 / 2.35); // rescale the unconvolved second-moment by the image second moment 
+	}
+    }
+    kernels->penalties1->data.F32[index] = kernels->penalty * penalty1;
+    psAssert (isfinite(kernels->penalties1->data.F32[index]), "invalid penalty");
+
+    kernels->penalties2->data.F32[index] = kernels->penalty * penalty2;
+    psAssert (isfinite(kernels->penalties2->data.F32[index]), "invalid penalty");
+
+    // fprintf(stderr, "penalty1: %f, penalty2: %f\n", penalty1, penalty2);
+
+    return true;
+}
+
+float pmSubtractionKernelPenaltySingle(psKernel *kernel, bool zeroNull)
+{
+    // Calculate moments
+    double penalty = 0.0;                   // Moment, for penalty
+    double sum = 0.0, sum2 = 0.0;           // Sum of kernel component
+    float min = INFINITY, max = -INFINITY;  // Minimum and maximum kernel value
+    for (int v = kernel->yMin; v <= kernel->yMax; v++) {
+	for (int u = kernel->xMin; u <= kernel->xMax; u++) {
+            double value = kernel->kernel[v][u];
+	    if (false && zeroNull && (u == 0) && (v == 0)) {
+		value += 1.0;
+	    }
+            double value2 = PS_SQR(value);
+            sum += value;
+            sum2 += value2;
+            penalty += value2 * PS_SQR((PS_SQR(u) + PS_SQR(v)));
+            min = PS_MIN(value, min);
+            max = PS_MAX(value, max);
+        }
+    }
+    penalty *= 1.0 / sum2;
+
+    if (0) {
+	// fprintf(stderr, "min: %lf, max: %lf, moment: %lf, flux^2: %lf\n", min, max, penalty, sum2);
+	// psTrace("psModules.imcombine", 7, "Kernel %d: %f %d %d %f\n", index, fwhm, uOrder, vOrder, penalty);
+    }
+
+    return penalty;
+}
+
+bool pmSubtractionStampsExtract(pmSubtractionStampList *stamps, psImage *image1, psImage *image2,
+                                psImage *variance, int kernelSize, psRegion bounds)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+    PS_ASSERT_IMAGE_NON_NULL(image1, false);
+    PS_ASSERT_IMAGE_TYPE(image1, PS_TYPE_F32, false);
+    if (image2) {
+        PS_ASSERT_IMAGE_NON_NULL(image2, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(image2, image1, false);
+        PS_ASSERT_IMAGE_TYPE(image2, PS_TYPE_F32, false);
+    }
+    if (variance) {
+        PS_ASSERT_IMAGE_NON_NULL(variance, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(variance, image1, false);
+        PS_ASSERT_IMAGE_TYPE(variance, PS_TYPE_F32, false);
+        PS_ASSERT_INT_NONNEGATIVE(kernelSize, false);
+    }
+
+    int size = kernelSize + stamps->footprint; // Size of postage stamps
+
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (!stamp || stamp->status != PM_SUBTRACTION_STAMP_FOUND) {
+            continue;
+        }
+
+        p_pmSubtractionPolynomialNormCoords(&stamp->xNorm, &stamp->yNorm, stamp->x, stamp->y,
+                                            bounds.x0, bounds.x1, bounds.y0, bounds.y1);
+
+        int x = stamp->x - 0.5, y = stamp->y - 0.5; // Stamp coordinates
+        // fprintf (stderr, "stamp: %5.1f %5.1f == %d %d (size: %d)\n", stamp->x, stamp->y, x, y, size);
+
+        if (x < bounds.x0 + size || x > bounds.x1 - size || y < bounds.y0 + size || y > bounds.y1 - size) {
+	  psLogMsg("psModules.imcombine", 3, "Stamp %d (%d,%d) is within the region border.\n", i, x, y);
+          stamp->status = PM_SUBTRACTION_STAMP_NONE;
+	  continue;
+        }
+
+        // Catch memory leaks --- these should have been freed and NULLed before
+        assert(stamp->image1 == NULL);
+        assert(stamp->image2 == NULL);
+        assert(stamp->weight == NULL);
+
+        psRegion region = psRegionSet(x - size, x + size + 1, y - size, y + size + 1); // Region of interest
+
+        psImage *sub1 = psImageSubset(image1, region); // Subimage with stamp
+        stamp->image1 = psKernelAllocFromImage(sub1, size, size);
+        psFree(sub1);                   // Drop reference
+
+        if (image2) {
+            psImage *sub2 = psImageSubset(image2, region); // Subimage with stamp
+            stamp->image2 = psKernelAllocFromImage(sub2, size, size);
+            psFree(sub2);               // Drop reference
+        }
+
+        psKernel *weight = stamp->weight = psKernelAlloc(-size, size, -size, size); // Weight = 1/variance
+        if (variance) {
+            psImage *varSub = psImageSubset(variance, region); // Subimage with stamp
+            psKernel *var = psKernelAllocFromImage(varSub, size, size); // Variance postage stamp
+
+            if ((isfinite(stamps->skyErr) && (stamps->skyErr > 0)) ||
+                (isfinite(stamps->sysErr) && (stamps->sysErr > 0))) {
+                float sysErr = 0.25 * PS_SQR(stamps->sysErr); // Systematic error
+                float skyErr = stamps->skyErr;
+                psKernel *image1 = stamp->image1, *image2 = stamp->image2; // Input images
+                for (int y = -size; y <= size; y++) {
+                    for (int x = -size; x <= size; x++) {
+                        float additional = image1->kernel[y][x] + image2->kernel[y][x];
+                        weight->kernel[y][x] = 1.0 / (skyErr + var->kernel[y][x] + sysErr * PS_SQR(additional));
+                    }
+                }
+            } else {
+                for (int y = -size; y <= size; y++) {
+                    for (int x = -size; x <= size; x++) {
+                        weight->kernel[y][x] = 1.0 / var->kernel[y][x];
+                    }
+                }
+            }
+            psFree(var);
+            psFree(varSub);
+        } else {
+            psImageInit(weight->image, 1.0);
+        }
+
+        stamp->status = PM_SUBTRACTION_STAMP_CALCULATE;
+    }
+
+    return true;
+}
+
+pmSubtractionStampList *pmSubtractionStampsSetFromSources(const psArray *sources, const psImage *image,
+                                                          const psImage *subMask, const psRegion *region,
+                                                          int size, int footprint, float spacing,
+                                                          float normFrac, float sysErr, float skyErr,
+                                                          pmSubtractionMode mode)
+{
+    PS_ASSERT_ARRAY_NON_NULL(sources, NULL);
+    // Let pmSubtractionStampsSet take care of the rest of the assertions
+
+    int numIn = sources->n;             // Number of sources in input list
+
+    psVector *x = psVectorAllocEmpty(numIn, PS_TYPE_F32); // x coordinates
+    psVector *y = psVectorAllocEmpty(numIn, PS_TYPE_F32); // y coordinates
+
+    int numOut = 0;                     // Number of sources in output list
+    for (int i = 0; i < numIn; i++) {
+        pmSource *source = sources->data[i]; // Source of interest
+        if (!source || (source->mode & SOURCE_MASK) ||(source->psfMag > SOURCE_FAINTEST)) {
+            continue;
+        }
+       
+	// fprintf (stderr, "%f,%f : %f %f : %f %f\n", source->peak->xf, source->peak->yf,
+	// source->psfMag, source->apMag, source->psfMag - source->apMag, source->psfMagErr);
+
+	// XXX this is somewhat arbitrary...
+	if (source->psfMagErr > 0.05) continue;
+        if (isfinite(source->apMag)) {
+            if (fabs(source->psfMag - source->apMag) > 0.5) continue;
+        } else if (isfinite(source->apMagRaw)) {
+            if (fabs(source->psfMag - source->apMagRaw) > 0.5) continue;
+        } else {
+            // XXX: Should we carry on or drop this source?
+            // drop it for now
+            continue;
+        }
+
+        if (source->modelPSF) {
+            x->data.F32[numOut] = source->modelPSF->params->data.F32[PM_PAR_XPOS];
+            y->data.F32[numOut] = source->modelPSF->params->data.F32[PM_PAR_YPOS];
+        } else {
+            x->data.F32[numOut] = source->peak->xf;
+            y->data.F32[numOut] = source->peak->yf;
+        }
+        numOut++;
+    }
+    x->n = numOut;
+    y->n = numOut;
+
+    pmSubtractionStampList *stamps = pmSubtractionStampsSet(x, y, image, subMask, region, size,
+                                                            footprint, spacing, normFrac,
+                                                            sysErr, skyErr, mode); // Stamps
+    psFree(x);
+    psFree(y);
+
+    if (!stamps) {
+        psError(psErrorCodeLast(), false, "Unable to set stamps from sources.");
+    }
+
+    return stamps;
+}
+
+
+pmSubtractionStampList *pmSubtractionStampsSetFromFile(const char *filename, const psImage *image,
+                                                       const psImage *subMask, const psRegion *region,
+                                                       int size, int footprint, float spacing, float normFrac,
+                                                       float sysErr, float skyErr, pmSubtractionMode mode)
+{
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+    // Let pmSubtractionStampsSet take care of the rest of the assertions
+
+    psArray *data = psVectorsReadFromFile(filename, "%f %f");
+    if (!data) {
+        psError(psErrorCodeLast(), false, "Unable to read stamps file %s", filename);
+        return NULL;
+    }
+    psVector *x = data->data[0], *y = data->data[1]; // Stamp positions
+
+    // Correct for IRAF/FITS (unit-offset) positions to C (zero-offset) positions
+    psBinaryOp(x, x, "-", psScalarAlloc(1.0, PS_TYPE_F32));
+    psBinaryOp(y, y, "-", psScalarAlloc(1.0, PS_TYPE_F32));
+
+    pmSubtractionStampList *stamps = pmSubtractionStampsSet(x, y, image, subMask, region, size, footprint,
+                                                            spacing, normFrac, sysErr, skyErr, mode);
+    psFree(data);
+
+    return stamps;
+
+}
+
+
+bool pmSubtractionStampsResetStatus (pmSubtractionStampList *stamps) {
+
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (!stamp) continue;
+        if (stamp->status != PM_SUBTRACTION_STAMP_USED) continue;
+        stamp->status = PM_SUBTRACTION_STAMP_CALCULATE;
+    }
+    return true;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionStamps.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionStamps.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionStamps.h	(revision 42651)
@@ -0,0 +1,176 @@
+#ifndef PM_SUBTRACTION_STAMPS_H
+#define PM_SUBTRACTION_STAMPS_H
+
+/// 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
+    int footprint, // Half-size of stamps
+    float spacing, // Rough average spacing between stamps
+    float normFrac, // Fraction of flux in window for normalisation window
+    float sysErr,  // Relative systematic error or NAN
+    float skyErr  // Relative systematic error or NAN
+    );
+
+/// 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); \
+    PS_ASSERT_INT_NONNEGATIVE((LIST)->footprint, 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); \
+    } \
+}
+
+/// Copy a list of stamps
+///
+/// A deep copy is performed of the stamp list and the component stamps
+pmSubtractionStampList *pmSubtractionStampListCopy(
+    const pmSubtractionStampList *in    // Stamp list to copy
+    );
+
+/// Allocate a stamp
+pmSubtractionStamp *pmSubtractionStampAlloc(void);
+
+// find and extract the stamps
+bool pmSubtractionStampsSelect(pmSubtractionStampList **stamps, // Stamps to read
+			       const pmReadout *ro1, // Readout 1
+			       const pmReadout *ro2, // Readout 2
+			       const psImage *subMask, // Mask for subtraction, or NULL
+			       psImage *variance,  // Variance map
+			       const psRegion *region, // Region of interest
+			       float thresh1,  // Threshold for stamp finding on readout 1
+			       float thresh2,  // Threshold for stamp finding on readout 2
+			       float stampSpacing, // Spacing between stamps
+			       float normFrac,     // Fraction of flux in window for normalisation window
+			       float sysError,     // Relative systematic error in images
+			       float skyError,     // Relative systematic error in images
+			       int size,         // Kernel half-size
+			       int footprint,     // Convolution footprint for stamps
+			       pmSubtractionMode mode // Mode for subtraction
+    );
+
+
+/// Find stamps on an image
+pmSubtractionStampList *pmSubtractionStampsFind(
+    pmSubtractionStampList *stamps, ///< Output stamps, or NULL
+    const psImage *image1, ///< Image for which to find stamps
+    const psImage *image2, ///< Image for which to find stamps
+    const psImage *mask, ///< Mask, or NULL
+    const psRegion *region, ///< Region to search, or NULL
+    float thresh1, ///< Threshold for stamps in image 1
+    float thresh2, ///< Threshold for stamps in image 2
+    int size, ///< Kernel half-size
+    int footprint, ///< Half-size for stamps
+    float spacing, ///< Rough spacing for stamps
+    float normFrac, // Fraction of flux in window for normalisation window
+    float sysErr,  ///< Relative systematic error in images
+    float skyErr,  ///< Relative systematic error in images
+    pmSubtractionMode mode ///< Mode for subtraction
+    );
+
+/// Set stamps based on a list of x,y
+pmSubtractionStampList *pmSubtractionStampsSet(
+    const psVector *x, ///< x coordinates for each stamp
+    const psVector *y, ///< y coordinates for each stamp
+    const psImage *image, ///< Image for flux of stamp
+    const psImage *mask, ///< Mask, or NULL
+    const psRegion *region, ///< Region to search, or NULL
+    int size, ///< Kernel half-size
+    int footprint, ///< Half-size for stamps
+    float spacing, ///< Rough spacing for stamps
+    float normFrac, // Fraction of flux in window for normalisation window
+    float sysErr,  ///< Systematic error in images
+    float skyErr,  ///< Systematic error in images
+    pmSubtractionMode mode ///< Mode for subtraction
+    );
+
+/// Set stamps based on a list of sources
+pmSubtractionStampList *pmSubtractionStampsSetFromSources(
+    const psArray *sources,             ///< Sources for each stamp
+    const psImage *image,               ///< Image for flux of stamp
+    const psImage *subMask,             ///< Mask, or NULL
+    const psRegion *region,             ///< Region to search, or NULL
+    int size,                           ///< Kernel half-size
+    int footprint,                      ///< Half-size for stamps
+    float spacing,                      ///< Rough spacing for stamps
+    float normFrac, // Fraction of flux in window for normalisation window
+    float sysErr,                       ///< Systematic error in images
+    float skyErr,                       ///< Systematic error in images
+    pmSubtractionMode mode              ///< Mode for subtraction
+    );
+
+/// Set stamps based on values in a file
+pmSubtractionStampList *pmSubtractionStampsSetFromFile(
+    const char *filename,               ///< Filename of file containing x,y (or x,y,flux) on each line
+    const psImage *image,               ///< Image for flux of stamp
+    const psImage *subMask,             ///< Mask, or NULL
+    const psRegion *region,             ///< Region to search, or NULL
+    int size,                           ///< Kernel half-size
+    int footprint,                      ///< Half-size for stamps
+    float spacing,                      ///< Rough spacing for stamps
+    float normFrac, // Fraction of flux in window for normalisation window
+    float sysErr,                       ///< Systematic error in images
+    float skyErr,                       ///< Systematic error in images
+    pmSubtractionMode mode              ///< Mode for subtraction
+    );
+
+/// Calculate the window and normalisation window from the stamps
+bool pmSubtractionStampsGetWindow(
+    bool *tryAgain, 			///< re-try with new stamp size?
+    pmSubtractionStampList *stamps,     ///< List of stamps
+    int kernelSize                      ///< Half-size of kernel
+    );
+
+/// Extract stamps from the images
+bool pmSubtractionStampsExtract(pmSubtractionStampList *stamps, ///< Stamps
+                                psImage *image1, ///< Reference image
+                                psImage *image2, ///< Input image (or NULL)
+                                psImage *variance, ///< Variance map
+                                int kernelSize, ///< Kernel half-size
+                                psRegion bounds ///< Bounds of validity
+    );
+
+
+/// Turn on/off generation of ds9 region files
+///
+/// Intended for debugging
+void pmSubtractionRegions(bool state    ///< Generate ds9 region files?
+    );
+
+/// Open a file for ds9 regions
+///
+/// Intended for debugging
+FILE *pmSubtractionStampsFile(const pmSubtractionStampList *stamps, ///< List of stamps, for outlines
+                              const char *filename, ///< Filename to write
+                              const char *description ///< Description of file
+    );
+
+/// Print a stamp position to ds9 region file
+///
+/// Intended for debugging
+void pmSubtractionStampPrint(FILE *ds9, ///< ds9 region file
+                             float x, float y, ///< Position of stamp
+                             float size,///< Size of circle
+                             const char *color ///< Colour
+    );
+
+
+bool pmSubtractionStampsResetStatus (pmSubtractionStampList *stamps);
+
+
+bool pmSubtractionKernelPenaltiesStamp(pmSubtractionStamp *stamp, pmSubtractionKernels *kernels);
+bool pmSubtractionKernelPenalties(pmSubtractionStamp *stamp, pmSubtractionKernels *kernels, int index);
+float pmSubtractionKernelPenaltySingle(psKernel *kernel, bool zeroNull);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionThreads.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionThreads.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionThreads.c	(revision 42651)
@@ -0,0 +1,72 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmSubtractionTypes.h"
+#include "pmSubtractionMatch.h"
+#include "pmSubtractionEquation.h"
+#include "pmSubtraction.h"
+
+bool threaded = false;                  // Run with threads?
+
+bool pmSubtractionThreaded(void)
+{
+    return threaded;
+}
+
+void pmSubtractionThreadsInit(void)
+{
+    if (threaded) {
+        psAbort("Already running threaded.");
+    }
+
+    threaded = true;
+
+    {
+        psThreadTask *task = psThreadTaskAlloc("PSMODULES_SUBTRACTION_ORDER", 8);
+        task->function = &pmSubtractionOrderThread;
+        psThreadTaskAdd(task);
+        psFree(task);
+    }
+
+    {
+        psThreadTask *task = psThreadTaskAlloc("PSMODULES_SUBTRACTION_CALCULATE_EQUATION", 3);
+        task->function = &pmSubtractionCalculateEquationThread;
+        psThreadTaskAdd(task);
+        psFree(task);
+    }
+
+    {
+        psThreadTask *task = psThreadTaskAlloc("PSMODULES_SUBTRACTION_CONVOLVE_STAMP", 3);
+        task->function = &pmSubtractionConvolveStampThread;
+        psThreadTaskAdd(task);
+        psFree(task);
+    }
+
+    {
+        psThreadTask *task = psThreadTaskAlloc("PSMODULES_SUBTRACTION_CONVOLVE", 19);
+        task->function = &pmSubtractionConvolveThread;
+        psThreadTaskAdd(task);
+        psFree(task);
+    }
+
+    return;
+}
+
+
+void pmSubtractionThreadsFinalize(void)
+{
+    if (!threaded) {
+        return;
+    }
+
+    threaded = false;
+    psThreadTaskRemove("PSMODULES_SUBTRACTION_ORDER");
+    psThreadTaskRemove("PSMODULES_SUBTRACTION_CALCULATE_EQUATION");
+    psThreadTaskRemove("PSMODULES_SUBTRACTION_CONVOLVE");
+
+    return;
+}
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionThreads.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionThreads.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionThreads.h	(revision 42651)
@@ -0,0 +1,18 @@
+#ifndef PM_SUBTRACTION_THREADS_H
+#define PM_SUBTRACTION_THREADS_H
+
+/// Return whether theads have been activated
+bool pmSubtractionThreaded(void);
+
+/// Set up threading for image matching
+///
+/// Sets up thread tasks
+void pmSubtractionThreadsInit(void);
+
+
+/// Take down threading for image matching
+///
+/// Destroys thread tasks
+void pmSubtractionThreadsFinalize(void);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionTypes.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionTypes.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionTypes.h	(revision 42651)
@@ -0,0 +1,174 @@
+/* @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.36 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-06 02:31:25 $
+ * Copyright 2004-207 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_SUBTRACTION_TYPES_H
+#define PM_SUBTRACTION_TYPES_H
+
+/// @addtogroup imcombine Image Combinations
+/// @{
+
+/// Mask values for the subtraction mask
+typedef enum {
+    PM_SUBTRACTION_MASK_CLEAR          = 0x00, // No masking
+    PM_SUBTRACTION_MASK_BAD_1          = 0x01, // Image 1 is bad
+    PM_SUBTRACTION_MASK_BAD_2          = 0x02, // Image 2 is bad
+    PM_SUBTRACTION_MASK_CONVOLVE_1     = 0x04, // If image 1 is convolved, would be poor or bad
+    PM_SUBTRACTION_MASK_CONVOLVE_2     = 0x08, // If image 2 is convolved, would be poor or bad
+    PM_SUBTRACTION_MASK_CONVOLVE_BAD_1 = 0x10, // If image 1 is convolved, would be bad
+    PM_SUBTRACTION_MASK_CONVOLVE_BAD_2 = 0x20, // If image 2 is convolved, would be bad
+    PM_SUBTRACTION_MASK_BORDER         = 0x40, // Image border
+    PM_SUBTRACTION_MASK_REJ            = 0x80, // Previously tried as a stamp, and rejected
+} pmSubtractionMasks;
+
+/// 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_ISIS_RADIAL,  ///< ISIS + higher-order radial Hermitians
+    PM_SUBTRACTION_KERNEL_HERM,         ///< Hermitian polynomial kernels
+    PM_SUBTRACTION_KERNEL_DECONV_HERM,  ///< Deconvolved Hermitian polynomial kernels
+    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
+    PM_SUBTRACTION_KERNEL_SIMPLE,       ///< Simple Gaussian kernel to avoid complications
+} pmSubtractionKernelsType;
+
+/// Modes --- specifies which image to convolve
+typedef enum {
+    PM_SUBTRACTION_MODE_ERR,            // Error in the mode
+    PM_SUBTRACTION_MODE_1,              // Convolve image 1
+    PM_SUBTRACTION_MODE_2,              // Convolve image 2
+    PM_SUBTRACTION_MODE_UNSURE,         // deprecated way of choosing the direction
+    PM_SUBTRACTION_MODE_SINGLE_AUTO,    // choose between SINGLE1 and SINGLE2
+    PM_SUBTRACTION_MODE_DUAL,           // Dual convolution
+} pmSubtractionMode;
+
+/// Status of stamp
+typedef enum {
+    PM_SUBTRACTION_STAMP_INIT,          ///< Initial state
+    PM_SUBTRACTION_STAMP_FOUND,         ///< Found a suitable source for this stamp
+    PM_SUBTRACTION_STAMP_CALCULATE,     ///< Calculate matrix and vector values for this stamp
+    PM_SUBTRACTION_STAMP_USED,          ///< Use this stamp
+    PM_SUBTRACTION_STAMP_REJECTED,      ///< This stamp has been rejected
+    PM_SUBTRACTION_STAMP_NONE           ///< No stamp in this region
+} pmSubtractionStampStatus;
+
+typedef struct {
+    double score;
+    pmSubtractionMode mode;
+    int spatialOrder;
+    int nGood;
+    psVector *fluxes;
+    psVector *chisq;
+    psVector *moments;
+    psVector *stampMask;
+} pmSubtractionQuality;
+
+/// Kernels specification
+typedef struct {
+    pmSubtractionKernelsType type;      ///< Type of kernels --- allowing the use of multiple kernels
+    psString description;               ///< Description of the kernel parameters
+    int xMin, xMax, yMin, yMax;         ///< Bounds of image (for normalisation)
+    long num;                           ///< Number of kernel components (not including the spatial ones)
+    psVector *fwhms;			///< requested fwhms of the kernel Gaussians (ISIS, HERM or DECONV_HERM)
+    psVector *orders;                   ///< polynomial orders for each Gaussian (ISIS, HERM or DECONV_HERM)
+    psVector *u, *v;                    ///< Offset (for POIS) or polynomial order (for ISIS, HERM or DECONV_HERM)
+    psVector *widths;                   ///< measured Gaussian FWHMs of Gauss*poly (ISIS, HERM or DECONV_HERM)
+    psVector *uStop, *vStop;            ///< Width of kernel element (SPAM,FRIES only)
+    psArray *preCalc;                   ///< Array of images containing pre-calculated kernel (for ISIS, HERM or DECONV_HERM)
+    float penalty;                      ///< Penalty for wideness
+    psVector *penalties1;               ///< Penalty for each kernel component
+    psVector *penalties2;               ///< Penalty for each kernel component
+    bool havePenalties;			///< flag to test if we have already calculated the penalties or not.
+    int size;                           ///< The half-size of the kernel
+    int inner;                          ///< The size of an inner region
+    int binning;                        ///< Binning used for the SPAM kernels
+    int ringsOrder;			///< 
+    int spatialOrder;                   ///< The spatial order of the kernels
+    int bgOrder;                        ///< The order for the background fitting
+    pmSubtractionMode mode;             ///< Mode for subtraction
+    psVector *solution1, *solution2;    ///< Solution for the PSF matching
+    psVector *solution1err, *solution2err; ///< error in solution for the PSF matching
+    // Quality information
+    float mean, rms;                    ///< Mean and RMS of chi^2 from stamps
+    int numStamps;                      ///< Number of good stamps
+    float fResSigmaMean;		///< mean fractional stdev of residuals
+    float fResSigmaStdev;		///< stdev of fractional stdev of residuals
+    float fResOuterMean;		///< mean fractional positive swing in residuals
+    float fResOuterStdev;		///< stdev of fractional positive swing in residuals
+    float fResTotalMean;		///< mean fractional negative swing in residuals
+    float fResTotalStdev;		///< stdev of fractional negative swing in residuals
+    psArray *sampleStamps;              ///< array of brightest set of stamps for output visualizations
+} pmSubtractionKernels;
+
+// pmSubtractionKernels->preCalc is an array of pmSubtractionKernelPreCalc structures
+typedef struct {
+    psVector *uCoords;                  // used by RINGS
+    psVector *vCoords;                  // used by RINGS
+    psVector *poly;                     // used by RINGS
+
+    psVector *xKernel;                  // used by ISIS, HERM, DECONV_HERM
+    psVector *yKernel;                  // used by ISIS, HERM, DECONV_HERM
+    psKernel *kernel;                   // used by ISIS, HERM, DECONV_HERM
+} pmSubtractionKernelPreCalc;
+
+/// 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)
+    int footprint;                      ///< Half-size of stamps
+    float normFrac;                     ///< Fraction of flux in window for normalisation window
+    float normValue;			///< calculated normalization
+    float normValue2;			///< calculated normalization
+    psKernel *window1;                  ///< window function generated from ensemble of stamps (input 1)
+    psKernel *window2;                  ///< window function generated from ensemble of stamps (input 2)
+    psKernel *window;                   ///< weighting window function (sigma = 1.1 * MAX(fwhm))
+    float normWindow1;                  ///< Size of window for measuring normalisation
+    float normWindow2;                  ///< Size of window for measuring normalisation
+    float sysErr;                       ///< Systematic error
+    float skyErr;                       ///< increase effective readnoise
+} pmSubtractionStampList;
+
+/// A stamp for image subtraction
+typedef struct {
+    float x, y;                         ///< Position
+    float flux;                         ///< Flux
+    float xNorm, yNorm;                 ///< Normalised position
+    psKernel *image1;                   ///< Reference image postage stamp
+    psKernel *image2;                   ///< Input image postage stamp
+    psKernel *weight;                   ///< Weight image (1/variance) postage stamp, or NULL
+    psArray *convolutions1;             ///< Convolutions of image 1 for each kernel component, or NULL
+    psArray *convolutions2;             ///< Convolutions of image 2 for each kernel component, or NULL
+    psImage *matrix;                    ///< Least-squares matrix, or NULL
+    psVector *vector;                   ///< Least-squares vector, or NULL
+    double norm;                        ///< Normalisation difference
+    double normI1;                       ///< Sum(flux) for image 1
+    double normI2;                       ///< Sum(flux) for image 2
+    double normSquare1;                 ///< Sum(flux^2) for image 1 (used for penalty)
+    double normSquare2;                 ///< Sum(flux^2) for image 2 (used for penalty)
+    pmSubtractionStampStatus status;    ///< Status of stamp
+    psVector *MxxI1;			///< second moments of convolution images
+    psVector *MyyI1;			///< second moments of convolution images
+    psVector *MxxI2;			///< second moments of convolution images
+    psVector *MyyI2;			///< second moments of convolution images
+    double MxxI1raw;
+    double MyyI1raw;
+    double MxxI2raw;
+    double MyyI2raw;
+} pmSubtractionStamp;
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionVisual.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionVisual.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionVisual.c	(revision 42651)
@@ -0,0 +1,1056 @@
+/** Diagnostic plots for pmSubtraction
+ * @author Chris Beaumont, IfA
+ */
+
+/* Include Files   */
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <pslib.h>
+
+#include "pmKapaPlots.h"
+#include "pmFPA.h"
+#include "pmSubtractionTypes.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionStamps.h"
+#include "pmSubtractionEquation.h"
+#include "pmSubtractionKernels.h"
+#include "pmSubtractionVisual.h"
+
+#include "pmVisual.h"
+#include "pmVisualUtils.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmAstrometryObjects.h"
+
+# if (HAVE_KAPA)
+# include <kapa.h>
+
+//variables to determine when things are plotted
+static bool plotConvKernels      = true;
+static bool plotStamps           = true;
+static bool plotLeastSquares     = true;
+static bool plotImage            = true;
+
+// variables to store plotting window indices
+static int kapa1 = -1;
+static int kapa2 = -1;
+static int kapa3 = -1;
+static int kapa4 = -1;
+
+/** function prototypes*/
+static bool plotStampLocations(pmSubtractionStampList *stamps, pmReadout *ro);
+
+// Initialization Routines
+
+
+
+/** destroy windows at the end of a run*/
+bool pmSubtractionVisualClose(void)
+{
+    if(kapa1 != -1) KapaClose(kapa1);
+    if(kapa2 != -1) KapaClose(kapa2);
+    return true;
+}
+
+// Plotting Routines
+
+/** Display images of the convolution kernels
+ *  @param convKernels the kernels to plot
+ *    @return true for success */
+bool pmSubtractionVisualPlotConvKernels(pmSubtractionKernels *kernels) {
+
+    if (!pmVisualTestLevel("ppsub.kernels", 1)) return true;
+
+    if (!plotConvKernels) return true;
+
+    if (!pmVisualInitWindow(&kapa4, "kernels")) {
+        return false;
+    }
+
+    psImage *convKernels = pmSubtractionKernelsImageMosaic(kernels);
+    pmVisualScaleImage(kapa4, convKernels, "Convolution_Kernels", 0, true);
+    pmVisualAskUser(&plotConvKernels);
+    psFree(convKernels);
+    return true;
+}
+
+
+/** Display the postage stamps used to determine the convolution kernels
+    @param stamps to display
+    @return true for success */
+bool pmSubtractionVisualPlotStamps(pmSubtractionStampList *stamps, pmReadout *ro) {
+
+    if (!pmVisualTestLevel("ppsub.stamps", 1)) return true;
+
+    if (!plotStamps) return true;
+
+    if (!pmVisualInitWindow (&kapa1, "ppSub:Images")) {
+        return false;
+    }
+    if (!pmVisualInitWindow (&kapa2, "ppSub:StampMasterImage")) {
+        return false;
+    }
+
+    //Plot Location of stamps
+    plotStampLocations(stamps, ro);
+
+    //Find the stamp size
+    int imageMax = -1;
+    int numStamps = 0;
+    psElemType type = PS_TYPE_F32; // will be overwritten
+    for(int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i];
+        if (stamp == NULL) continue;
+
+        psKernel *k1 = stamp->image1;
+        if (k1 == NULL) continue;
+
+        psImage *i1 = k1->image;
+        if (i1 == NULL) continue;
+
+        imageMax = PS_MAX(imageMax, i1->numCols);
+        imageMax = PS_MAX(imageMax, i1->numRows);
+        numStamps++;
+        type = i1->type.type;
+    }
+    if (imageMax == -1) return false;
+
+    int border = 30;
+    int tileRowCount = (int) ceil(sqrt(numStamps));
+    int canvasX = tileRowCount * (imageMax * 2 + border);
+    int canvasY = tileRowCount * (imageMax + border);
+    psImage *canvas = psImageAlloc (canvasX, canvasY, type);
+    psImageInit (canvas, NAN);
+
+    //overlay the images
+    int stampNum = 0;
+    int stampListNum = 0;
+    while (stampNum < numStamps) {
+        int x0 = (2 * imageMax + border) * (stampNum % tileRowCount);
+        int y0 = (imageMax + border) * (stampNum / tileRowCount);
+
+        pmSubtractionStamp *stamp = stamps->stamps->data[stampListNum++];
+        if (stamp == NULL) continue;
+
+        psKernel *k1 = stamp->image1;
+        psKernel *k2 = stamp->image2;
+
+        if (k1 == NULL) continue;
+        psImage *i1 = k1->image;
+        psImage *i2 = k2->image;
+
+        if (i1 == NULL) continue;
+        psImageOverlaySection(canvas, i1, x0 + 0 * imageMax, y0, "=");
+        psImageOverlaySection(canvas, i2, x0 + 1 * imageMax + border / 4, y0, "=");
+        stampNum++;
+    }
+    pmVisualScaleImage(kapa1, canvas, "Subtraction_Stamps", 0, true);
+    psFree(canvas);
+
+    pmVisualAskUser(&plotStamps);
+    return true;
+}
+
+/** Plot the least-squares matrix of each stamp */
+bool pmSubtractionVisualPlotLeastSquares (pmSubtractionStampList *stamps) {
+
+    if (!pmVisualTestLevel("ppsub.chisq", 1)) return true;
+
+    if (!plotLeastSquares) return true;
+
+    if (!pmVisualInitWindow (&kapa1, "PPSub:Images")) {
+        return false;
+    }
+
+    //Find the stamp size
+    int imageMax = -1;
+    int numStamps = 0;
+    psElemType type = PS_TYPE_F64;
+
+    for(int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i];
+        if (stamp == NULL) continue;
+
+        psImage *im = stamp->matrix;
+        if (im == NULL) continue;
+
+        imageMax = PS_MAX(imageMax, im->numCols);
+        imageMax = PS_MAX(imageMax, im->numRows);
+        numStamps++;
+        type = im->type.type;
+    }
+    if (imageMax == -1) return false;
+
+    int border = 15;
+    imageMax += border;
+    int tileRowCount = (int) ceil(sqrt(numStamps));
+    int canvasX = tileRowCount * (imageMax);
+    int canvasY = tileRowCount * (imageMax);
+    psImage *canvas = psImageAlloc (canvasX, canvasY, type);
+    psImageInit (canvas, NAN);
+
+    //overlay the images
+    int stampNum = 0;
+    int stampListNum = 0;
+    while (stampNum < numStamps) {
+        int x0 = (imageMax) * (stampNum % tileRowCount);
+        int y0 = (imageMax) * (stampNum / tileRowCount);
+
+        pmSubtractionStamp *stamp = stamps->stamps->data[stampListNum++];
+        if (stamp == NULL) continue;
+
+        psImage *im = stamp->matrix;
+        if (im == NULL) continue;
+
+        psImageOverlaySection(canvas, im, x0, y0, "=");
+        stampNum++;
+
+	// renormalize the section
+	float maxValue = 0;
+	for (int iy = 0; iy < im->numRows; iy++) {
+	    for (int ix = 0; ix < im->numCols; ix++) {
+		maxValue = PS_MAX(maxValue, im->data.F64[iy][ix]);
+	    }
+	}
+	if (maxValue == 0.0) continue;
+	for (int iy = 0; iy < im->numRows; iy++) {
+	    for (int ix = 0; ix < im->numCols; ix++) {
+		canvas->data.F64[y0 + iy][x0 + ix] /= maxValue;
+	    }
+	}
+    }
+
+    psImage *canvas32 = pmVisualImageToFloat(canvas);
+    pmVisualScaleImage(kapa1, canvas32, "Least_Squares", 0, true);
+
+    if (0) {
+	static int count = 0;
+	char filename[64];
+	sprintf (filename, "chisq.%02d.fits", count);
+	count ++;
+	psFits *fits = psFitsOpen (filename, "w");
+	psFitsWriteImage (fits, NULL, canvas32, 0, NULL);
+	psFitsClose (fits);
+    }
+
+    pmVisualAskUser(&plotLeastSquares);
+    psFree(canvas);
+    psFree(canvas32);
+    return true;
+}
+
+/** Plot the least-squares matrix of each stamp */
+bool pmSubtractionVisualPlotLeastSquaresResid (const pmSubtractionStampList *stamps, psImage *matrixIn, int nUsed) {
+
+    if (!pmVisualTestLevel("ppsub.chisq", 1)) return true;
+
+    if (!plotLeastSquares) return true;
+
+    if (!pmVisualInitWindow (&kapa1, "PPSub:Images")) {
+        return false;
+    }
+
+    psImage *matrixNorm = psImageCopy(NULL, matrixIn, PS_TYPE_F64);
+    {
+	// renormalize the matrix
+	float maxValue = 0;
+	for (int iy = 0; iy < matrixNorm->numRows; iy++) {
+	    for (int ix = 0; ix < matrixNorm->numCols; ix++) {
+		maxValue = PS_MAX(maxValue, matrixNorm->data.F64[iy][ix]);
+	    }
+	}
+	for (int iy = 0; iy < matrixNorm->numRows; iy++) {
+	    for (int ix = 0; ix < matrixNorm->numCols; ix++) {
+		matrixNorm->data.F64[iy][ix] /= maxValue;
+	    }
+	}
+    }
+
+    // Find the stamp size
+    int imageMax = -1;
+    int numStamps = 0;
+    psElemType type = PS_TYPE_F64;
+
+    for(int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i];
+        if (stamp == NULL) continue;
+
+        psImage *im = stamp->matrix;
+        if (im == NULL) continue;
+
+        imageMax = PS_MAX(imageMax, im->numCols);
+        imageMax = PS_MAX(imageMax, im->numRows);
+        numStamps++;
+        type = im->type.type;
+    }
+    if (imageMax == -1) {
+	psFree (matrixNorm);
+	return false;
+    }
+
+    int border = 15;
+    imageMax += border;
+    int tileRowCount = (int) ceil(sqrt(numStamps));
+    int canvasX = tileRowCount * (imageMax);
+    int canvasY = tileRowCount * (imageMax);
+    psImage *canvas = psImageAlloc (canvasX, canvasY, type);
+    psImageInit (canvas, NAN);
+
+    // overlay the images
+    int stampNum = 0;
+    int stampListNum = 0;
+    while (stampNum < numStamps) {
+        int x0 = (imageMax) * (stampNum % tileRowCount);
+        int y0 = (imageMax) * (stampNum / tileRowCount);
+
+        pmSubtractionStamp *stamp = stamps->stamps->data[stampListNum++];
+        if (stamp == NULL) continue;
+
+        psImage *im = stamp->matrix;
+        if (im == NULL) continue;
+
+        stampNum++;
+
+	if (stamp->status != PM_SUBTRACTION_STAMP_USED) continue;
+
+	// renormalize the section
+	float maxValue = 0;
+	for (int iy = 0; iy < im->numRows; iy++) {
+	    for (int ix = 0; ix < im->numCols; ix++) {
+		maxValue = PS_MAX(maxValue, im->data.F64[iy][ix]);
+	    }
+	}
+	if (maxValue == 0.0) continue;
+	for (int iy = 0; iy < im->numRows; iy++) {
+	    for (int ix = 0; ix < im->numCols; ix++) {
+		canvas->data.F64[y0 + iy][x0 + ix] = (im->data.F64[iy][ix] / maxValue - matrixNorm->data.F64[iy][ix]) / matrixNorm->data.F64[iy][ix];
+	    }
+	}
+    }
+
+    psImage *canvas32 = pmVisualImageToFloat(canvas);
+    pmVisualRangeImage(kapa2, canvas32, "Least_Squares", 0, -100.0, 100.0);
+
+    if (0) {
+	static int count = 0;
+	char filename[64];
+	sprintf (filename, "chisq.%02d.fits", count);
+	count ++;
+	psFits *fits = psFitsOpen (filename, "w");
+	psFitsWriteImage (fits, NULL, canvas32, 0, NULL);
+	psFitsClose (fits);
+    }
+
+    pmVisualAskUser(&plotLeastSquares);
+    psFree(canvas);
+    psFree(canvas32);
+    psFree (matrixNorm);
+    return true;
+}
+
+bool pmSubtractionVisualShowSubtraction(psImage *image, psImage *ref, psImage *sub) {
+
+    if (!pmVisualTestLevel("ppsub.images.sub", 1)) return true;
+
+    if (!plotImage) return true;
+
+    if (!pmVisualInitWindow (&kapa1, "PPSub:Images")) {
+        return false;
+    }
+
+    pmVisualScaleImage(kapa1, image, "Image", 0, true);
+    pmVisualScaleImage(kapa1, ref, "Reference", 1, true);
+    pmVisualScaleImage(kapa1, sub, "Subtraction", 2, true);
+    pmVisualAskUser(&plotImage);
+    return true;
+}
+
+bool pmSubtractionVisualShowKernels(pmSubtractionKernels *kernels) {
+
+    if (!pmVisualTestLevel("ppsub.kernels.final", 1)) return true;
+
+    if (!pmVisualInitWindow (&kapa1, "PPSub:Images")) {
+        return false;
+    }
+
+    // get the kernel sizes
+    int footprint = kernels->size;
+
+    // output image is a grid of NXsub by NYsub sub-images
+    int NXsub = sqrt(kernels->num);
+    int NYsub = kernels->num / NXsub;
+    if (kernels->num % NXsub) NYsub++;
+
+    int NXpix = NXsub * (2*footprint + 1 + 3);
+    int NYpix = NYsub * (2*footprint + 1 + 3);
+
+    psImage *output = psImageAlloc(NXpix, NYpix, PS_TYPE_F32);
+    psImageInit (output, 0.0);
+
+    for (int i = 0; i < kernels->num; i++) {
+	pmSubtractionKernelPreCalc *preCalc = kernels->preCalc->data[i];
+	psKernel *kernel = preCalc->kernel;
+
+	int xSub = i % NXsub;
+	int ySub = i / NXsub;
+
+	int xPix = xSub * (2*footprint + 1 + 3) + footprint;
+	int yPix = ySub * (2*footprint + 1 + 3) + footprint;
+
+	double sum = 0.0;
+	for (int y = -footprint; y <= footprint; y++) {
+	    for (int x = -footprint; x <= footprint; x++) {
+		output->data.F32[y + yPix][x + xPix] = kernel->kernel[y][x];
+		sum += kernel->kernel[y][x];
+	    }
+	}
+	// fprintf (stderr, "kernel %d, sum %f\n", i, sum);
+    }							 
+	
+    pmVisualScaleImage(kapa1, output, "Image", 0, true);
+    psFree(output);
+    pmVisualAskUser(&plotImage);
+    return true;
+}
+
+bool pmSubtractionVisualShowBasis(pmSubtractionStampList *stamps) {
+
+    if (!pmVisualTestLevel("ppsub.basis", 1)) return true;
+
+    if (!pmVisualInitWindow (&kapa2, "ppSub:StampMasterImage")) {
+        return false;
+    }
+
+    // clear the overlay (red at least!)
+    KiiEraseOverlay (kapa2, "red");
+
+    // get the kernel sizes
+    int footprint = stamps->footprint;
+
+    // choose the brightest stamp
+    pmSubtractionStamp *maxStamp = NULL;
+    float maxFlux = NAN;
+    for (int i = 0; i < stamps->num; i++) {
+	pmSubtractionStamp *stamp = stamps->stamps->data[i];
+	if (!isfinite(stamp->flux)) continue;
+	if (!stamp->convolutions1 && !stamp->convolutions2) continue;
+	// fprintf (stderr, "flux: %f, maxFlux: %f  ", stamp->flux, maxFlux);
+	if (!maxStamp) {
+	    maxFlux = stamp->flux;
+	    maxStamp = stamp;
+	    // fprintf (stderr, "maxStamp %d\n", i);
+	    continue;
+	} else {
+	    // fprintf (stderr, "\n");
+	}
+	if (stamp->flux > maxFlux) {
+	    maxFlux = stamp->flux;
+	    maxStamp = stamp;
+	}
+    }
+
+    if (!isfinite(maxStamp->flux)) {
+	fprintf (stderr, "no valid stamps?\n");
+    }
+
+    int nKernels = 0;
+
+    // paste in the kernel images, scaled by sum2
+    if (maxStamp->convolutions1) {
+	// output image is a grid of NXsub by NYsub sub-images
+	nKernels = maxStamp->convolutions1->n;
+	int NXsub = sqrt(nKernels);
+	int NYsub = nKernels / NXsub;
+	if (nKernels % NXsub) NYsub++;
+
+	int NXpix = NXsub * (2*footprint + 1 + 3);
+	int NYpix = NYsub * (2*footprint + 1 + 3);
+
+	psImage *output = psImageAlloc(NXpix, NYpix, PS_TYPE_F32);
+	psImageInit (output, 0.0);
+
+	for (int i = 0; i < nKernels; i++) {
+            psKernel *kernel = maxStamp->convolutions1->data[i];
+	    
+	    int xSub = i % NXsub;
+	    int ySub = i / NXsub;
+	    
+	    int xPix = xSub * (2*footprint + 1 + 3) + footprint;
+	    int yPix = ySub * (2*footprint + 1 + 3) + footprint;
+	    
+	    double sum2 = 0.0;
+	    for (int y = -footprint; y <= footprint; y++) {
+		for (int x = -footprint; x <= footprint; x++) {
+		    sum2 += PS_SQR(kernel->kernel[y][x]);
+		}
+	    }
+	    float scale = sqrt(sum2) / PS_SQR(2*footprint + 1);
+	    for (int y = -footprint; y <= footprint; y++) {
+		for (int x = -footprint; x <= footprint; x++) {
+		    output->data.F32[y + yPix][x + xPix] = kernel->kernel[y][x] / scale;
+		}
+	    }
+	}		
+	pmVisualScaleImage(kapa2, output, "Image", 0, true);
+
+	if (0) {
+	    psFits *fits = psFitsOpen("basis.1.fits", "w");
+	    psFitsWriteImage(fits, NULL, output, 0, NULL);
+	    psFitsClose(fits);
+	}
+	psFree (output);
+    }
+	
+    if (maxStamp->convolutions2) {
+	// output image is a grid of NXsub by NYsub sub-images
+	nKernels = maxStamp->convolutions2->n;
+	int NXsub = sqrt(nKernels);
+	int NYsub = nKernels / NXsub;
+	if (nKernels % NXsub) NYsub++;
+
+	int NXpix = NXsub * (2*footprint + 1 + 3);
+	int NYpix = NYsub * (2*footprint + 1 + 3);
+
+	psImage *output = psImageAlloc(NXpix, NYpix, PS_TYPE_F32);
+	psImageInit (output, 0.0);
+
+	for (int i = 0; i < nKernels; i++) {
+            psKernel *kernel = maxStamp->convolutions2->data[i];
+	    
+	    int xSub = i % NXsub;
+	    int ySub = i / NXsub;
+	    
+	    int xPix = xSub * (2*footprint + 1 + 3) + footprint;
+	    int yPix = ySub * (2*footprint + 1 + 3) + footprint;
+	    
+	    double sum2 = 0.0;
+	    for (int y = -footprint; y <= footprint; y++) {
+		for (int x = -footprint; x <= footprint; x++) {
+		    sum2 += PS_SQR(kernel->kernel[y][x]);
+		}
+	    }
+	    float scale = sqrt(sum2) / PS_SQR(2*footprint + 1);
+	    for (int y = -footprint; y <= footprint; y++) {
+		for (int x = -footprint; x <= footprint; x++) {
+		    output->data.F32[y + yPix][x + xPix] = kernel->kernel[y][x] / scale;
+		}
+	    }
+	}		
+	pmVisualScaleImage(kapa2, output, "Image", 1, true);
+
+	if (0) {
+	    psFits *fits = psFitsOpen("basis.2.fits", "w");
+	    psFitsWriteImage(fits, NULL, output, 0, NULL);
+	    psFitsClose(fits);
+	}
+	psFree(output);
+    }					 
+	
+    pmVisualAskUser(&plotImage);
+    return true;
+}
+
+static bool plotStampLocations(pmSubtractionStampList *stamps, pmReadout *ro) {
+
+    if (!pmVisualScaleImage(kapa2, ro->image, "Stamp_master_image", 0, true)) {
+        fprintf(stderr, "Cannot display postage stamp master image. Skipping \n");
+        return false;
+    }
+
+    int Noverlay;
+    KiiOverlay *overlay;
+
+    // note: this uses the Ohana allocation tools:
+    // ALLOCATE (overlay, KiiOverlay, 3*peaks->n + 1);
+    ALLOCATE (overlay, KiiOverlay, stamps->num);
+
+    Noverlay = 0;
+    char boxID[PS_SMALLWORD];
+    for (int i = 0; i < stamps->num; i++) {
+
+        pmSubtractionStamp *stamp = stamps->stamps->data[i];
+        if (stamp == NULL) continue;
+
+        overlay[Noverlay].type = KII_OVERLAY_BOX;
+	if ((stamp->x < 1.0) && (stamp->y < 1.0)) {
+	    // fprintf (stderr, "stamp zero: %f %f\n", stamp->x, stamp->y);
+	    continue;
+	}
+	if (!isfinite(stamp->x) && !isfinite(stamp->y)) {
+	    // fprintf (stderr, "stamp nan: %f %f\n", stamp->x, stamp->y);
+	    continue;
+	}
+        overlay[Noverlay].x = stamp->x;
+        overlay[Noverlay].y = stamp->y;
+        overlay[Noverlay].dx = 40.0;
+        overlay[Noverlay].dy = 40.0;
+        overlay[Noverlay].angle = 0.0;
+        ps_snprintf_nowarn(boxID, PS_SMALLWORD, "%d", i);
+        overlay[Noverlay].text = boxID;
+        Noverlay ++;
+    }
+
+    KiiLoadOverlay (kapa2, overlay, Noverlay, "red");
+    FREE (overlay);
+    return true;
+}
+
+bool pmVisualShowImage(int kapaFD, psImage *inImage, const char *name, int channel, float min, float max) {
+
+    KiiImage image;
+    KapaImageData data;
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    
+    image.data2d = inImage->data.F32;
+    image.Nx = inImage->numCols;
+    image.Ny = inImage->numRows;
+    strcpy (data.name, name);
+    strcpy (data.file, name);
+    
+    data.zero  = min;
+    data.range = max;
+    data.logflux = 0;
+    
+    KiiSetChannel (kapaFD, channel);
+    KiiNewPicture2D (kapaFD, &image, &data, &coords);
+    return true;
+}
+
+static int footprint = 0;
+static int NX = 0;
+static int NY = 0;
+static psImage *sourceImage      = NULL;
+static psImage *targetImage      = NULL;
+static psImage *residualImage    = NULL;
+static psImage *fresidualImage   = NULL;
+static psImage *differenceImage  = NULL;
+static psImage *convolutionImage = NULL;
+
+bool pmSubtractionVisualShowFit(pmSubtractionStampList *stamps, pmSubtractionKernels *kernels) {
+
+    if (!pmVisualTestLevel("ppsub.fit", 1)) return true;
+
+    if (!pmVisualInitWindow(&kapa1, "ppSub:Images")) return false;
+    if (!pmVisualInitWindow(&kapa2, "ppSub:Misc")) return false;
+
+    // set up holding images for the visualization
+    pmSubtractionVisualShowFitInit (stamps);
+
+    int numKernels = kernels->num;      // Number of kernels
+
+    psImage *polyValues = NULL;         // Polynomial values
+    psKernel *residual = psKernelAlloc(-stamps->footprint, stamps->footprint, -stamps->footprint, stamps->footprint); // Residual image
+
+    double norm = p_pmSubtractionSolutionNorm(kernels); // Normalisation
+
+    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; }
+
+        // Calculate coefficients of the kernel basis functions
+        polyValues = p_pmSubtractionPolynomial(polyValues, kernels->spatialOrder, stamp->xNorm, stamp->yNorm);
+        double background = p_pmSubtractionSolutionBackground(kernels, polyValues); // Difference in background
+
+        psImageInit(residual->image, 0.0);
+
+        if (kernels->mode != PM_SUBTRACTION_MODE_DUAL) {
+	    psKernel *target;           // Target postage stamp
+	    psKernel *source;           // Source postage stamp
+	    psArray *convolutions;      // Convolution postage stamps for each kernel basis function
+            switch (kernels->mode) {
+              case PM_SUBTRACTION_MODE_1:
+		target = stamp->image2;
+		source = stamp->image1;
+		convolutions = stamp->convolutions1;
+		break;
+	      case PM_SUBTRACTION_MODE_2:
+		target = stamp->image1;
+		source = stamp->image2;
+		convolutions = stamp->convolutions2;
+		break;
+	      default:
+		psAbort("Unsupported subtraction mode: %x", kernels->mode);
+	    }
+
+	    for (int j = 0; j < numKernels; j++) {
+		psKernel *convolution = convolutions->data[j]; // Convolution
+		double coefficient = p_pmSubtractionSolutionCoeff(kernels, polyValues, j, false); // Coefficient
+		for (int y = - footprint; y <= footprint; y++) {
+		    for (int x = - footprint; x <= footprint; x++) {
+			residual->kernel[y][x] += convolution->kernel[y][x] * coefficient;
+		    }
+		}
+	    }
+	    // visualize the target, source, convolution and residual
+	    pmSubtractionVisualShowFitAddStamp (target, source, residual, background, norm, i);
+	} else {
+	    // Dual convolution
+	    psArray *convolutions1 = stamp->convolutions1; // Convolutions of the first image
+	    psArray *convolutions2 = stamp->convolutions2; // Convolutions of the second image
+	    psKernel *image1 = stamp->image1; // The first image
+	    psKernel *image2 = stamp->image2; // The second image
+
+	    for (int j = 0; j < numKernels; j++) {
+		psKernel *conv1 = convolutions1->data[j]; // Convolution of first image
+		psKernel *conv2 = convolutions2->data[j]; // Convolution of second image
+		double coeff1 = p_pmSubtractionSolutionCoeff(kernels, polyValues, j, false); // Coefficient 1
+		double coeff2 = p_pmSubtractionSolutionCoeff(kernels, polyValues, j, true); // Coefficient 2
+
+		for (int y = - footprint; y <= footprint; y++) {
+		    for (int x = - footprint; x <= footprint; x++) {
+			residual->kernel[y][x] += conv2->kernel[y][x] * coeff2 + conv1->kernel[y][x] * coeff1;
+		    }
+		}
+	    }
+	    // visualize the target, source, convolution and residual
+	    pmSubtractionVisualShowFitAddStamp (image2, image1, residual, background, norm, i);
+	}
+	psFree(polyValues);
+    }
+    pmSubtractionVisualShowFitImage(norm);
+
+    psFree (residual);
+    return true;
+}
+
+// generate 4 storage images large enough to hold the stamps:
+bool pmSubtractionVisualShowFitInit(pmSubtractionStampList *stamps) {
+
+    footprint = stamps->footprint;
+
+    float NXf = sqrt(stamps->num);
+    NX = (int) NXf == NXf ? NXf : NXf + 1.0;
+    
+    float NYf = stamps->num / NX;
+    NY = (int) NYf == NY ? NYf : NYf + 1.0;
+
+    int NXpix = (2*footprint + 1) * NX;
+    NXpix += (NX > 1) ? 3 * NX : 0;
+
+    int NYpix = (2*footprint + 1) * NY;
+    NYpix += (NY > 1) ? 3 * NY : 0;
+
+    sourceImage      = psImageAlloc (NXpix, NYpix, PS_TYPE_F32);
+    targetImage      = psImageAlloc (NXpix, NYpix, PS_TYPE_F32);
+    residualImage    = psImageAlloc (NXpix, NYpix, PS_TYPE_F32);
+    fresidualImage   = psImageAlloc (NXpix, NYpix, PS_TYPE_F32);
+    differenceImage  = psImageAlloc (NXpix, NYpix, PS_TYPE_F32);
+    convolutionImage = psImageAlloc (NXpix, NYpix, PS_TYPE_F32);
+    
+    psImageInit (sourceImage,      0.0);
+    psImageInit (targetImage,      0.0);
+    psImageInit (residualImage,    0.0);
+    psImageInit (fresidualImage,   0.0);
+    psImageInit (differenceImage,  0.0);
+    psImageInit (convolutionImage, 0.0);
+
+    return true;
+}
+
+bool pmSubtractionVisualShowFitAddStamp(psKernel *target, psKernel *source, psKernel *convolution, double background, double norm, int index) {
+
+    double sum;
+
+    int NXoff = index % NX;
+    int NYoff = index / NX;
+
+    int NXpix = NXoff * (2*footprint + 1 + 3) + footprint;
+    int NYpix = NYoff * (2*footprint + 1 + 3) + footprint;
+
+    // insert the (target) kernel into the (target) image:
+    sum = 0.0;
+    for (int y = -footprint; y <= footprint; y++) {
+	for (int x = -footprint; x <= footprint; x++) {
+	    targetImage->data.F32[y + NYpix][x + NXpix] = target->kernel[y][x];
+	    sum += targetImage->data.F32[y + NYpix][x + NXpix];
+	}
+    }
+    targetImage->data.F32[footprint + 1 + NYpix][NXpix] = sum;
+
+    // insert the (source) kernel into the (source) image:
+    sum = 0.0;
+    for (int y = -footprint; y <= footprint; y++) {
+	for (int x = -footprint; x <= footprint; x++) {
+	    sourceImage->data.F32[y + NYpix][x + NXpix] = source->kernel[y][x];
+	    sum += sourceImage->data.F32[y + NYpix][x + NXpix];
+	}
+    }
+    sourceImage->data.F32[footprint + 1 + NYpix][NXpix] = sum;
+
+    // insert the (convolution) kernel into the (convolution) image:
+    sum = 0.0;
+    for (int y = -footprint; y <= footprint; y++) {
+	for (int x = -footprint; x <= footprint; x++) {
+	    convolutionImage->data.F32[y + NYpix][x + NXpix] = convolution->kernel[y][x];
+	    sum += convolutionImage->data.F32[y + NYpix][x + NXpix];
+	}
+    }
+    convolutionImage->data.F32[footprint + 1 + NYpix][NXpix] = sum;
+    
+    // insert the (difference) kernel into the (difference) image:
+    sum = 0.0;
+    for (int y = -footprint; y <= footprint; y++) {
+	for (int x = -footprint; x <= footprint; x++) {
+	    differenceImage->data.F32[y + NYpix][x + NXpix] = target->kernel[y][x] - background - source->kernel[y][x] * norm;
+	    sum += differenceImage->data.F32[y + NYpix][x + NXpix];
+	}
+    }
+    differenceImage->data.F32[footprint + 1 + NYpix][NXpix] = sum;
+
+    // insert the (residual) kernel into the (residual) image:
+    sum = 0.0;
+    for (int y = -footprint; y <= footprint; y++) {
+	for (int x = -footprint; x <= footprint; x++) {
+	    residualImage->data.F32[y + NYpix][x + NXpix] = target->kernel[y][x] - background - source->kernel[y][x] * norm - convolution->kernel[y][x];
+	    sum += residualImage->data.F32[y + NYpix][x + NXpix];
+	}
+    }
+    residualImage->data.F32[footprint + 1 + NYpix][NXpix] = sum;
+
+    // insert the (fresidual) kernel into the (fresidual) image:
+    for (int y = -footprint; y <= footprint; y++) {
+	for (int x = -footprint; x <= footprint; x++) {
+	    fresidualImage->data.F32[y + NYpix][x + NXpix] = residualImage->data.F32[y + NYpix][x + NXpix] / sqrt(PS_MAX(target->kernel[y][x], 100.0));
+	}
+    }
+    return true;
+}
+
+bool pmSubtractionVisualShowFitImage(double norm) {
+
+    KiiEraseOverlay (kapa1, "red");
+    KiiEraseOverlay (kapa2, "red");
+
+    pmVisualShowImage(kapa1, targetImage, "Target", 0, -200.0, 400.0);
+    pmVisualShowImage(kapa1, sourceImage, "Source", 1, -200.0, 400.0);
+    pmVisualShowImage(kapa1, convolutionImage, "Convolution", 2, -200.0, 400.0);
+    KiiCenter (kapa1, 0.5*targetImage->numCols, 0.5*targetImage->numRows, 1);
+
+    pmVisualScaleImage(kapa2, fresidualImage, "Frac.Residual", 2, true);
+    pmVisualShowImage(kapa2, differenceImage, "Difference", 0, -200.0, 400.0);
+    pmVisualShowImage(kapa2, residualImage, "Residual", 1, -200.0, 400.0);
+    KiiCenter (kapa2, 0.5*residualImage->numCols, 0.5*residualImage->numRows, 1);
+
+    pmVisualAskUser(NULL);
+
+    psFree(targetImage);
+    psFree(sourceImage);
+    psFree(convolutionImage);
+    psFree(differenceImage);
+    psFree(residualImage);
+    psFree(fresidualImage);
+
+    targetImage = NULL;
+    sourceImage = NULL;
+    convolutionImage = NULL;
+    differenceImage = NULL;
+    residualImage = NULL;
+    fresidualImage = NULL;
+
+    return true;
+}
+
+bool pmSubtractionVisualPlotFit(const pmSubtractionKernels *kernels) {
+
+    Graphdata graphdata;
+
+    if (!pmVisualTestLevel("ppsub.fit", 1)) return true;
+
+    if (!pmVisualInitWindow(&kapa3, "ppSub:plots")) return false;
+
+    KapaClearSections (kapa3);
+    KapaInitGraph (&graphdata);
+
+    psVector *x = psVectorAllocEmpty (kernels->num, PS_TYPE_F32);
+    psVector *y = psVectorAllocEmpty (kernels->num, PS_TYPE_F32);
+    psVector *dy = psVectorAllocEmpty (kernels->num, PS_TYPE_F32);
+
+    graphdata.xmin = -1.0;
+    graphdata.xmax = kernels->num + 1.0;
+    graphdata.ymin = +32.0;
+    graphdata.ymax = -32.0;
+
+    psImage *polyValues = p_pmSubtractionPolynomial(NULL, kernels->spatialOrder, 0.0, 0.0);
+
+    // construct the plot vectors
+    for (int i = 0; i < kernels->num; i++) {
+        x->data.F32[i] = i;
+	y->data.F32[i] = p_pmSubtractionSolutionCoeff(kernels, polyValues, i, false);
+	dy->data.F32[i] = kernels->solution1err->data.F64[i];
+        graphdata.ymin = PS_MIN(graphdata.ymin, y->data.F32[i]);
+        graphdata.ymax = PS_MAX(graphdata.ymax, y->data.F32[i]);
+    }
+    x->n = y->n = dy->n = kernels->num;
+
+    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 (kapa3, &graphdata);
+
+    KapaSetFont (kapa3, "helvetica", 14);
+    KapaBox (kapa3, &graphdata);
+    KapaSendLabel (kapa3, "kernel number", KAPA_LABEL_XM);
+    KapaSendLabel (kapa3, "coeff", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = KAPA_POINT_CROSS;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+    graphdata.etype |= 0x01;
+
+    KapaPrepPlot   (kapa3, x->n, &graphdata);
+    KapaPlotVector (kapa3, x->n, x->data.F32, "x");
+    KapaPlotVector (kapa3, x->n, y->data.F32, "y");
+    KapaPlotVector (kapa3, x->n, dy->data.F32, "dym");
+    KapaPlotVector (kapa3, x->n, dy->data.F32, "dyp");
+
+    psFree (x);
+    psFree (y);
+    psFree (dy);
+    psFree (polyValues);
+
+    pmVisualAskUser(NULL);
+    return true;
+}
+
+// plot log(flux) vs log(chisq), log(flux) vs log(moments), log(chisq) vs log(moments)
+bool pmSubtractionVisualPlotChisqAndMoments(psVector *fluxes, psVector *chisq, psVector *moments) {
+
+    Graphdata graphdata;
+    KapaSection section;
+
+    if (!pmVisualTestLevel("ppsub.fit", 1)) return true;
+
+    if (!pmVisualInitWindow(&kapa3, "ppSub:plots")) return false;
+
+    KapaClearSections (kapa3);
+    KapaInitGraph (&graphdata);
+    KiiResize(kapa3, 1500, 500);
+
+    psVector *lchi = psVectorAlloc (fluxes->n, PS_TYPE_F32);
+    psVector *lflx = psVectorAlloc (fluxes->n, PS_TYPE_F32);
+    psVector *lMxx = psVectorAlloc (fluxes->n, PS_TYPE_F32);
+
+    // construct the plot vectors
+    for (int i = 0; i < fluxes->n; i++) {
+        lchi->data.F32[i] = log10(chisq->data.F32[i]);
+        lflx->data.F32[i] = log10(fluxes->data.F32[i]);
+        lMxx->data.F32[i] = log10(moments->data.F32[i]);
+    }
+
+    section.bg = KapaColorByName ("none"); // XXX probably should be 'none'
+
+    // section 1: lflux vs lchi
+    section.dx = 0.33;
+    section.dy = 1.00;
+    section.x  = 0.00;
+    section.y  = 0.00;
+    section.name = psStringCopy ("flux.v.chi");
+    KapaSetSection (kapa3, &section);
+    psFree (section.name);
+
+    graphdata.color = KapaColorByName ("black");
+    pmVisualScaleGraphdata(&graphdata, lflx, lchi, false);
+    KapaSetLimits (kapa3, &graphdata);
+
+    KapaSetFont (kapa3, "helvetica", 14);
+    KapaBox (kapa3, &graphdata);
+    KapaSendLabel (kapa3, "log(flux)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa3, "log(chisq)", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = KAPA_POINT_CROSS;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+
+    KapaPrepPlot   (kapa3, lflx->n, &graphdata);
+    KapaPlotVector (kapa3, lflx->n, lflx->data.F32, "x");
+    KapaPlotVector (kapa3, lflx->n, lchi->data.F32, "y");
+
+    // section 2: lflux vs lMxx
+    section.dx = 0.33;
+    section.dy = 1.00;
+    section.x  = 0.33;
+    section.y  = 0.00;
+    section.name = psStringCopy ("flux.v.mom");
+    KapaSetSection (kapa3, &section);
+    psFree (section.name);
+
+    graphdata.color = KapaColorByName ("black");
+    pmVisualScaleGraphdata(&graphdata, lflx, lMxx, false);
+    KapaSetLimits (kapa3, &graphdata);
+
+    KapaSetFont (kapa3, "helvetica", 14);
+    KapaBox (kapa3, &graphdata);
+    KapaSendLabel (kapa3, "log(flux)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa3, "log(moments)", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = KAPA_POINT_CROSS;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+
+    KapaPrepPlot   (kapa3, lflx->n, &graphdata);
+    KapaPlotVector (kapa3, lflx->n, lflx->data.F32, "x");
+    KapaPlotVector (kapa3, lflx->n, lMxx->data.F32, "y");
+
+    // section 1: lflux vs lchi
+    section.dx = 0.33;
+    section.dy = 1.00;
+    section.x  = 0.66;
+    section.y  = 0.00;
+    section.name = psStringCopy ("chi.v.mom");
+    KapaSetSection (kapa3, &section);
+    psFree (section.name);
+
+    graphdata.color = KapaColorByName ("black");
+    pmVisualScaleGraphdata(&graphdata, lchi, lMxx, false);
+    KapaSetLimits (kapa3, &graphdata);
+
+    KapaSetFont (kapa3, "helvetica", 14);
+    KapaBox (kapa3, &graphdata);
+    KapaSendLabel (kapa3, "log(chisq)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa3, "log(moments)", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = KAPA_POINT_CROSS;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+
+    KapaPrepPlot   (kapa3, lflx->n, &graphdata);
+    KapaPlotVector (kapa3, lflx->n, lchi->data.F32, "x");
+    KapaPlotVector (kapa3, lflx->n, lMxx->data.F32, "y");
+
+    psFree (lflx);
+    psFree (lchi);
+    psFree (lMxx);
+
+    pmVisualAskUser(NULL);
+    return true;
+}
+
+#else
+bool pmSubtractionVisualClose(void) {return true;}
+bool pmSubtractionVisualPlotConvKernels(psImage *convKernels) {return true;}
+bool pmSubtractionVisualPlotStamps(pmSubtractionStampList *stamps, pmReadout *ro) {return true;}
+bool pmSubtractionVisualPlotLeastSquares(pmSubtractionStampList *stamps) {return true;}
+bool pmSubtractionVisualShowSubtraction(psImage *image, psImage *ref, psImage *sub) {return true;}
+bool pmSubtractionVisualShowFitInit(pmSubtractionStampList *stamps) {return true;}
+bool pmSubtractionVisualShowFitAddStamp(psKernel *target, psKernel *source, psKernel *convolution, double background, double norm, int index) {return true;}
+bool pmSubtractionVisualShowFit() {return true;}
+bool pmSubtractionVisualPlotFit(const pmSubtractionKernels *kernels);
+#endif
Index: /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionVisual.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionVisual.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/imcombine/pmSubtractionVisual.h	(revision 42651)
@@ -0,0 +1,21 @@
+#ifndef PM_SUBTRACTION_VISUAL_H
+#define PM_SUBTRACTION_VISUAL_H
+
+bool pmSubtractionVisualClose(void);
+bool pmSubtractionVisualPlotConvKernels(pmSubtractionKernels *kernels);
+bool pmSubtractionVisualPlotStamps(pmSubtractionStampList *stamps, pmReadout *ro);
+bool pmSubtractionVisualPlotLeastSquares(pmSubtractionStampList *stamps);
+bool pmSubtractionVisualPlotLeastSquaresResid (const pmSubtractionStampList *stamps, psImage *matrixIn, int nUsed);
+bool pmSubtractionVisualShowSubtraction(psImage *image, psImage *ref, psImage *sub);
+
+bool pmSubtractionVisualShowFit(pmSubtractionStampList *stamps, pmSubtractionKernels *kernels);
+bool pmSubtractionVisualShowFitInit(pmSubtractionStampList *stamps);
+bool pmSubtractionVisualShowFitAddStamp(psKernel *target, psKernel *source, psKernel *convolution, double background, double norm, int index);
+bool pmSubtractionVisualShowFitImage(double norm);
+
+bool pmSubtractionVisualPlotFit(const pmSubtractionKernels *kernels);
+bool pmSubtractionVisualShowKernels(pmSubtractionKernels *kernels);
+bool pmSubtractionVisualShowBasis(pmSubtractionStampList *stamps);
+bool pmSubtractionVisualPlotChisqAndMoments(psVector *fluxes, psVector *chisq, psVector *moments);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/mainpage.dox
===================================================================
--- /branches/eam_branches/psModules.20240412/src/mainpage.dox	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/mainpage.dox	(revision 42651)
@@ -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_branches/psModules.20240412/src/objects/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/Makefile.am	(revision 42651)
@@ -0,0 +1,212 @@
+noinst_LTLIBRARIES = libpsmodulesobjects.la
+
+libpsmodulesobjects_la_CPPFLAGS = $(SRCINC) $(PSMODULES_CFLAGS) -I../pslib/
+libpsmodulesobjects_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+libpsmodulesobjects_la_SOURCES  = \
+	pmDetections.c \
+	pmSpan.c \
+	pmFootprint.c \
+	pmFootprintArrayGrow.c \
+	pmFootprintArraysMerge.c \
+	pmFootprintAssignPeaks.c \
+	pmFootprintCullPeaks.c \
+	pmFootprintFind.c \
+	pmFootprintSpans.c \
+	pmFootprintFindAtPoint.c \
+	pmFootprintIDs.c \
+	pmPeaks.c \
+	pmMoments.c \
+	pmModel.c \
+	pmModelClass.c \
+	pmModelUtils.c \
+	pmModel_CentralPixel.c \
+	pmSource.c \
+	pmPhotObj.c \
+	pmSourceMasks.c \
+	pmSourceMoments.c \
+	pmSourceDiffStats.c \
+	pmSourceExtendedPars.c \
+	pmSourceSatstar.c \
+	pmSourceUtils.c \
+	pmSourceSky.c \
+	pmSourceContour.c \
+	pmSourceFitModel.c \
+	pmSourceFitSet.c \
+	pmSourcePhotometry.c \
+	pmSourceOutputs.c \
+	pmSourceIO.c \
+	pmSourceIO_RAW.c \
+	pmSourceIO_OBJ.c \
+	pmSourceIO_SX.c \
+	pmSourceIO_CMP.c \
+	pmSourceIO_CFF.c \
+	pmSourceIO_SMPDATA.c \
+	pmSourceIO_PS1_DEV_0.c \
+	pmSourceIO_PS1_DEV_1.c \
+	pmSourceIO_PS1_CAL_0.c \
+	pmSourceIO_CMF_PS1_V1.c \
+	pmSourceIO_CMF_PS1_V2.c \
+	pmSourceIO_CMF_PS1_V3.c \
+	pmSourceIO_CMF_PS1_V4.c \
+	pmSourceIO_CMF_PS1_V5.c \
+	pmSourceIO_CMF_PS1_SV1.c \
+	pmSourceIO_CMF_PS1_SV2.c \
+	pmSourceIO_CMF_PS1_SV3.c \
+	pmSourceIO_CMF_PS1_SV4.c \
+	pmSourceIO_CMF_PS1_DV1.c \
+	pmSourceIO_CMF_PS1_DV2.c \
+	pmSourceIO_CMF_PS1_DV3.c \
+	pmSourceIO_CMF_PS1_DV4.c \
+	pmSourceIO_CMF_PS1_DV5.c \
+	pmSourceIO_MatchedRefs.c \
+	pmSourceIO_Glints.c \
+	pmSourceIO_Ghosts.c \
+	pmSourcePlots.c \
+	pmSourcePlotPSFModel.c \
+	pmSourcePlotMoments.c \
+	pmSourcePlotApResid.c \
+	pmSourceVisual.c \
+	pmResiduals.c \
+	pmPSF.c \
+	pmPSF_IO.c \
+	pmPSFtry.c \
+	pmPSFtryModel.c \
+	pmPSFtryFitEXT.c \
+	pmPSFtryMakePSF.c \
+	pmPSFtryFitPSF.c \
+	pmPSFtryMetric.c \
+	pmPCMdata.c \
+	pmPCM_MinimizeChisq.c \
+	pmSourceFitPCM.c \
+	pmTrend2D.c \
+	pmGrowthCurveGenerate.c \
+	pmGrowthCurve.c \
+	pmSourceMatch.c \
+	pmSourceLensing.c \
+	pmDetEff.c \
+	pmSourceGroups.c \
+	models/pmModel_GAUSS.c \
+	models/pmModel_PGAUSS.c \
+	models/pmModel_PS1_V1.c \
+	models/pmModel_QGAUSS.c \
+	models/pmModel_RGAUSS.c \
+	models/pmModel_SERSIC.c \
+	models/pmModel_EXP.c \
+	models/pmModel_DEV.c \
+	models/pmModel_TRAIL.c \
+	models/pmModel_HSC_V1.c
+
+pkginclude_HEADERS = \
+	pmDetections.h \
+	pmSpan.h \
+	pmFootprint.h \
+	pmFootprintSpans.h \
+	pmPeaks.h \
+	pmMoments.h \
+	pmModelFuncs.h \
+	pmModel.h \
+	pmModelClass.h \
+	pmModelUtils.h \
+	pmModel_CentralPixel.h \
+	pmSource.h \
+	pmPhotObj.h \
+	pmSourceMasks.h \
+	pmSourceDiffStats.h \
+	pmSourceExtendedPars.h \
+	pmSourceUtils.h \
+	pmSourceSky.h \
+	pmSourceContour.h \
+	pmSourceFitModel.h \
+	pmSourceFitSet.h \
+	pmSourcePhotometry.h \
+	pmSourceOutputs.h \
+	pmSourceIO.h \
+	pmSourceSatstar.h \
+	pmSourcePlots.h \
+	pmSourceVisual.h \
+	pmResiduals.h \
+	pmPSF.h \
+	pmPSF_IO.h \
+	pmPSFtry.h \
+	pmPCMdata.h \
+	pmTrend2D.h \
+	pmGrowthCurve.h \
+	pmGrowthCurveGenerate.h \
+	pmSourceMatch.h \
+	pmSourceLensing.h \
+	pmDetEff.h \
+	pmSourceGroups.h \
+	pmSourceInternal.h \
+	models/pmModel_GAUSS.h \
+	models/pmModel_PGAUSS.h \
+	models/pmModel_PS1_V1.h \
+	models/pmModel_QGAUSS.h \
+	models/pmModel_RGAUSS.h \
+	models/pmModel_SERSIC.h \
+	models/pmModel_EXP.h \
+	models/pmModel_DEV.h \
+	models/pmModel_TRAIL.h \
+	models/pmModel_HSC_V1.h
+
+CLEANFILES = *~
+
+# pmSourceID_CMF_* functions use a common framework
+BUILT_SOURCES = \
+pmSourceIO_CMF_PS1_V1.c \
+pmSourceIO_CMF_PS1_V2.c \
+pmSourceIO_CMF_PS1_V3.c \
+pmSourceIO_CMF_PS1_V4.c \
+pmSourceIO_CMF_PS1_V5.c \
+pmSourceIO_CMF_PS1_DV1.c \
+pmSourceIO_CMF_PS1_DV2.c \
+pmSourceIO_CMF_PS1_DV3.c \
+pmSourceIO_CMF_PS1_DV4.c \
+pmSourceIO_CMF_PS1_DV5.c \
+pmSourceIO_CMF_PS1_SV1.c \
+pmSourceIO_CMF_PS1_SV2.c \
+pmSourceIO_CMF_PS1_SV3.c \
+pmSourceIO_CMF_PS1_SV4.c
+
+pmSourceIO_CMF_PS1_V1.c : pmSourceIO_CMF.c.in mksource.pl
+	mksource.pl pmSourceIO_CMF.c.in PS1_V1 pmSourceIO_CMF_PS1_V1.c
+
+pmSourceIO_CMF_PS1_V2.c : pmSourceIO_CMF.c.in mksource.pl
+	mksource.pl pmSourceIO_CMF.c.in PS1_V2 pmSourceIO_CMF_PS1_V2.c
+
+pmSourceIO_CMF_PS1_V3.c : pmSourceIO_CMF.c.in mksource.pl
+	mksource.pl pmSourceIO_CMF.c.in PS1_V3 pmSourceIO_CMF_PS1_V3.c
+
+pmSourceIO_CMF_PS1_V4.c : pmSourceIO_CMF.c.in mksource.pl
+	mksource.pl pmSourceIO_CMF.c.in PS1_V4 pmSourceIO_CMF_PS1_V4.c
+
+pmSourceIO_CMF_PS1_V5.c : pmSourceIO_CMF.c.in mksource.pl
+	mksource.pl pmSourceIO_CMF.c.in PS1_V5 pmSourceIO_CMF_PS1_V5.c
+
+pmSourceIO_CMF_PS1_DV1.c : pmSourceIO_CMF.c.in mksource.pl
+	mksource.pl pmSourceIO_CMF.c.in PS1_DV1 pmSourceIO_CMF_PS1_DV1.c
+
+pmSourceIO_CMF_PS1_DV2.c : pmSourceIO_CMF.c.in mksource.pl
+	mksource.pl pmSourceIO_CMF.c.in PS1_DV2 pmSourceIO_CMF_PS1_DV2.c
+
+pmSourceIO_CMF_PS1_DV3.c : pmSourceIO_CMF.c.in mksource.pl
+	mksource.pl pmSourceIO_CMF.c.in PS1_DV3 pmSourceIO_CMF_PS1_DV3.c
+
+pmSourceIO_CMF_PS1_DV4.c : pmSourceIO_CMF.c.in mksource.pl
+	mksource.pl pmSourceIO_CMF.c.in PS1_DV4 pmSourceIO_CMF_PS1_DV4.c
+
+pmSourceIO_CMF_PS1_DV5.c : pmSourceIO_CMF.c.in mksource.pl
+	mksource.pl pmSourceIO_CMF.c.in PS1_DV5 pmSourceIO_CMF_PS1_DV5.c
+
+pmSourceIO_CMF_PS1_SV1.c : pmSourceIO_CMF.c.in mksource.pl
+	mksource.pl pmSourceIO_CMF.c.in PS1_SV1 pmSourceIO_CMF_PS1_SV1.c
+
+pmSourceIO_CMF_PS1_SV2.c : pmSourceIO_CMF.c.in mksource.pl
+	mksource.pl pmSourceIO_CMF.c.in PS1_SV2 pmSourceIO_CMF_PS1_SV2.c
+
+pmSourceIO_CMF_PS1_SV3.c : pmSourceIO_CMF.c.in mksource.pl
+	mksource.pl pmSourceIO_CMF.c.in PS1_SV3 pmSourceIO_CMF_PS1_SV3.c
+
+pmSourceIO_CMF_PS1_SV4.c : pmSourceIO_CMF.c.in mksource.pl
+	mksource.pl pmSourceIO_CMF.c.in PS1_SV4 pmSourceIO_CMF_PS1_SV4.c
+
+# EXTRA_DIST = pmErrorCodes.h.in pmErrorCodes.dat pmErrorCodes.c.in
Index: /branches/eam_branches/psModules.20240412/src/objects/mksource.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/mksource.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/mksource.pl	(revision 42651)
@@ -0,0 +1,209 @@
+#!/usr/bin/env perl
+$DEBUG = 0;
+
+# this program takes the pmSourceIO_CMF.in.c template file and generates the .c version based on the given I/O format made
+
+if (@ARGV != 3) { die "USAGE: mksource (template) (cmfmode) (output)\n"; }
+
+$template = $ARGV[0];
+$cmfmode = $ARGV[1];
+$output = $ARGV[2];
+
+if (! -f $template) { die "missing template file $template\n"; }
+
+# validate the cmfmode
+
+# keep the series (V1,SV1,DV1) separate)
+%cmfmodes_v = ("PS1_V1", 1,
+	       "PS1_V2", 2,
+	       "PS1_V3", 3,
+	       "PS1_V4", 4,
+	       "PS1_V5", 5,
+    );
+%cmfmodes_dv = ("PS1_DV1", 1,
+		"PS1_DV2", 2,
+		"PS1_DV3", 3,
+		"PS1_DV4", 4,
+		"PS1_DV5", 5,
+    );
+%cmfmodes_sv = ("PS1_SV1", 1,
+		"PS1_SV2", 2,
+		"PS1_SV3", 3,
+		"PS1_SV4", 4,
+    );
+
+open (FILE, "$template") || die "failed to open template $template\n";
+@list = <FILE>;
+close (FILE);
+
+open (FILE, ">$output");
+
+# operations we can perform:
+# @CMFMODE@ : replace with $cmfmode
+# @ALL@ : remove and keep the rest of the line
+# @=MODE@ : remove and keep if cmfmode == MODE
+# @>MODE@ : remove and keep if cmfmode > MODE
+# @<MODE@ : remove and keep if cmfmode > MODE
+
+# XXX need to add features: split @foo,bar,baz@ by commas
+# treat each chunk as a rule
+# add the following options:
+# !MODE -- exclude the given entry (defaults to all, or is ALL required?)
+# * and ? regexp 
+
+# some examples:
+# @ALL,!PS1_V1@
+# @PS1_DV?@
+# @PS1_V?,!PS1_V1@
+
+foreach $line (@list) {
+
+    # replace @CMFMODE@ wherever it appears
+    $line =~ s|\@CMFMODE\@|$cmfmode|g;
+    
+    # print and continue if we do not match @RULES@
+    unless ($line =~ m|\@.*\@|) { 
+	print "no rule\n" if $DEBUG;
+	print FILE $line;
+	next;
+    }
+
+    # grab the rules and the rest of the line
+    ($prefix,$rules,$content) = ($line =~ m|(.*)\@(.*)\@\s*(.*)|);
+    
+    # split the rules into separate items
+    @rules = split (",", $rules);
+
+    $keepLine = 0;
+    # does $cmfmode match any of the rules?
+    foreach $rule (@rules) {
+	print "rule: $rule\n" if $DEBUG;
+
+	# special rule "ALL"
+	if ($rule eq "ALL") { 
+	    print "ALL match\n" if $DEBUG;
+	    $keepLine = 1; 
+	    next; 
+	} # look for other rules (esp !foo)
+
+	# pure match
+	if ($cmfmode eq $rule) { 
+	    print "simple match\n" if $DEBUG;
+	    $keepLine = 1; 
+	    next; 
+	} # skip to end?
+
+	# NOT match
+	if ($rule =~ m|^!|) {
+	    print "NOT rule: $rule\n" if $DEBUG;
+	    ($realrule) = ($rule =~ m|^!(.*)|);
+	    if ($cmfmode eq $realrule) { $keepLine = 0; } # skip to end?
+	    next; 
+	}
+
+	# simple regexp: foo*
+	if ($rule =~ m|\*$|) {
+	    print "regexp * rule: $rule\n" if $DEBUG;
+	    ($realrule) = ($rule =~ m|(.*)\*$|);
+	    if ($cmfmode =~ m|$realrule|) { $keepLine = 1; } # skip to end?
+	    next; 
+	}
+
+	# simple regexp: foo?
+	if ($rule =~ m|\?$|) {
+	    print "regexp ? rule: $rule\n" if $DEBUG;
+	    ($realrule) = ($rule =~ m|(.*)\?$|);
+	    if (substr($cmfmode,0,-1) eq $realrule) { $keepLine = 1; } # skip to end?
+	    next; 
+	}
+
+	# rule: =FOO
+	if ($rule =~ m|^=|) {
+	    print "= rule: $rule\n" if $DEBUG;
+	    $realrule = substr($cmfmode,1);
+	    if ($cmfmode eq $realrule) { $keepLine = 1; } # skip to end?
+	    next; 
+	}
+
+	# only apply the < > <= >= rules if cmfmode is one of cmfmodes
+	# rule: >=FOO
+	if ($rule =~ m|^>=|) {
+	    print ">= rule: $rule\n" if $DEBUG;
+	    # find the cmfmode series (v, dv, sv)
+	    $realrule = substr($rule,2);
+	    %series = &cmf_series ($realrule);
+	    if (! %series) { next; }
+	    if ($series{$cmfmode} == 0) { next; }
+	    $thisLevel = $series{$realrule};
+	    $myLevel = $series{$cmfmode};
+	    print "levels: $cmfmode, $realrule, $myLevel, $thisLevel\n" if $DEBUG;
+	    if ($myLevel >= $thisLevel) { $keepLine = 1; }
+	    next; 
+	}
+
+	# rule: >FOO
+	if ($rule =~ m|^>|) {
+	    print "> rule: $rule\n" if $DEBUG;
+	    # find the cmfmode series (v, dv, sv)
+	    $realrule = substr($rule,1);
+	    %series = &cmf_series ($realrule);
+	    if (! %series) { next; }
+	    if ($series{$cmfmode} == 0) { next; }
+	    $thisLevel = $series{$realrule};
+	    $myLevel = $series{$cmfmode};
+	    print "levels: $cmfmode, $realrule, $myLevel, $thisLevel\n" if $DEBUG;
+	    if ($myLevel > $thisLevel) { $keepLine = 1; }
+	    next; 
+	}
+
+	# rule: <=FOO
+	if ($rule =~ m|^<=|) {
+	    print "<= rule: $rule\n" if $DEBUG;
+	    # find the cmfmode series (v, dv, sv)
+	    $realrule = substr($rule,2);
+	    %series = &cmf_series ($realrule);
+	    if (! %series) { next; }
+	    if ($series{$cmfmode} == 0) { next; }
+	    $thisLevel = $series{$realrule};
+	    $myLevel = $series{$cmfmode};
+	    print "levels: $cmfmode, $realrule, $myLevel, $thisLevel\n" if $DEBUG;
+	    if ($myLevel <= $thisLevel) { $keepLine = 1; }
+	    next; 
+	}
+
+	# rule: <FOO
+	if ($rule =~ m|^<|) {
+	    print "< rule: $rule\n" if $DEBUG;
+	    # find the cmfmode series (v, dv, sv)
+	    $realrule = substr($rule,1);
+	    %series = &cmf_series ($realrule);
+	    if (! %series) { next; }
+	    if ($series{$cmfmode} == 0) { next; }
+	    $thisLevel = $series{$realrule};
+	    $myLevel = $series{$cmfmode};
+	    print "levels: $cmfmode, $realrule, $myLevel, $thisLevel\n" if $DEBUG;
+	    if ($myLevel < $thisLevel) { $keepLine = 1; }
+	    next; 
+	}
+
+    }
+    print "line: $line\n" if $DEBUG;
+
+    if ($keepLine) {
+	print FILE "$prefix $content\n";
+    }
+}
+
+close (FILE);
+
+exit 0;
+
+sub cmf_series {
+
+    my ($rule) = $_[0];
+
+    if ($cmfmodes_v{$rule})  { return %cmfmodes_v;  }
+    if ($cmfmodes_sv{$rule}) { return %cmfmodes_sv; }
+    if ($cmfmodes_dv{$rule}) { return %cmfmodes_dv; }
+    return 0;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/models/fwhm.sh
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/fwhm.sh	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/fwhm.sh	(revision 42651)
@@ -0,0 +1,317 @@
+
+macro find.fwhm.pgauss
+  if ($0 != 1)
+    echo "USAGE: find.fwhm"
+    break
+  end
+  
+  $dz = 0.01
+  create z 0 50 $dz
+
+  # Gaussian taylor expansion
+  # f = 1 / (1 + z + z^2/2 + z^3/6)
+  # 1 + z + z^2/2 + z^3/6 = 2
+  # f = z + z^2/2 + z^3/6 - 2, find where f == 0.0
+  
+  set f = z + 0.5*z^2 + (1/6.0)*z^3 - 2.0
+  set dfdz = 1 + z + 0.5*z^2
+
+  lim -n 0 z f; clear; box; plot z f
+  lim -n 1 z dfdz; clear; box; plot z dfdz  
+
+  $nZ0 = 0
+  $nZ1 = 5 / $dz
+
+  $Zg = 0.5*(z[$nZ0] + z[$nZ1])  
+  $nZg = int($Zg / $dz)
+  $dZ = 1.0
+
+  for i 0 10
+    $Fg = f[$nZg]
+    $dFdz_g = dfdz[$nZg]
+
+    $dZ = $Fg / $dFdz_g
+
+    echo $Zg $Fg $dZ $dFdz_g
+
+    $Zg -= $dZ
+    $Zg = max ($Zg , 0)
+    $nZg = int($Zg / $dz)
+  end 
+  echo $Zg $Fg $dZ $dFdz_g
+  $Zhm = $Zg
+  $FWHM = 2*sqrt(2*$Zg)
+
+  echo $Zhm : $FWHM
+end
+
+macro find.fwhm.rgauss
+  if ($0 != 2)
+    echo "USAGE: find.fwhm (K)"
+    break
+  end
+  
+  $K = $1
+
+  if ($K == 0.0)
+    $Zhm = (sqrt(5) - 1.0) / 2.0
+    $FWHM = 2*sqrt(2*$Zhm)
+    echo $K : $Zhm : $FWHM
+    return
+  end
+
+  $dz = 0.01
+  create z 0 50 $dz
+
+  # set f = 1.0 / (1.0 + $K*z + z^1.667)
+  # f = $K*z + z^1.667 - 1.0, find where f == 0.0
+  
+  set f = z + z^$K - 1.0
+  set dfdz = ln(z) * z^$K + 1.0
+
+  lim -n 0 z f; clear; box; plot z f
+  lim -n 1 z dfdz; clear; box; plot z dfdz  
+
+  $nZ0 = 0
+  $nZ1 = 5 / $dz
+
+  $Zg = 0.5*(z[$nZ0] + z[$nZ1])  
+  $nZg = int($Zg / $dz)
+  $dZ = 1.0
+
+  for i 0 10
+    $Fg = f[$nZg]
+    $dFdz_g = dfdz[$nZg]
+
+    $dZ = $Fg / $dFdz_g
+
+    echo $nZg $Zg $Fg $dZ $dFdz_g
+
+    $Zg -= $dZ
+    $Zg = max ($Zg , 0)
+    $nZg = int($Zg / $dz)
+  end 
+  # echo $Zg $Fg $dZ $dFdz_g
+  $Zhm = $Zg
+  $FWHM = 2*sqrt(2*$Zg)
+
+  echo $K : $Zhm : $FWHM
+end
+
+macro find.fwhm.ps1v1
+  if ($0 != 2)
+    echo "USAGE: find.fwhm (K)"
+    break
+  end
+  
+  $K = $1
+
+  if ($K == 0.0)
+    $Zhm = 1.0
+    $FWHM = 2*sqrt(2)
+    fprintf "%4.2f, // %4.1f, %4.2f" $FWHM $K $Zhm 
+    return
+  end
+
+  $dz = 0.01
+  create z 0 50 $dz
+
+  # set f = 1.0 / (1.0 + $K*z + z^1.667)
+  # f = $K*z + z^1.667 - 1.0, find where f == 0.0
+  
+  set f = $K*z + z^1.667 - 1.0
+  set dfdz = $K + 1.677*z^0.667
+
+  #lim -n 0 z f; clear; box; plot z f
+  #lim -n 1 z dfdz; clear; box; plot z dfdz  
+
+  # these just need to bound the solution
+  $nZ0 = 0.5 / $dz
+  $nZ1 = 5.0 / $dz
+
+  $Zg = 0.5*(z[$nZ0] + z[$nZ1])  
+  $nZg = int($Zg / $dz)
+  $dZ = 1.0
+
+  for i 0 10
+    # interpolate between $nZg and $nZg + 1
+    $Fg = f[$nZg] + ($Zg - $nZg*$dz)*(f[$nZg+1] - f[$nZg])/$dz
+    $dFdz_g = dfdz[$nZg] + ($Zg - $nZg*$dz)*(dfdz[$nZg+1] - dfdz[$nZg])/$dz
+
+    # $Fg = f[$nZg]
+    # $dFdz_g = dfdz[$nZg]
+
+    $dZ = $Fg / $dFdz_g
+
+    # echo -no-return $Zg $Fg $dZ $dFdz_g
+
+    $Zg -= $dZ
+
+    $Zg = max ($Zg , 0)
+    $Zg = min ($Zg , z[-2])
+    $nZg = int($Zg / $dz)
+    
+    # echo ": $Zg $nZg"
+  end 
+  # echo $Zg $Fg $dZ $dFdz_g
+  $Zhm = $Zg
+  $FWHM = 2*sqrt(2*$Zg)
+
+  fprintf "%4.2f, // %4.1f, %4.2f" $FWHM $K $Zhm 
+end
+
+# hsc
+macro find.fwhm.hscv1
+  if ($0 != 2)
+    echo "USAGE: find.fwhm (K)"
+    break
+  end
+  
+  $K = $1
+
+  if ($K == 0.0)
+    $Zhm = 1.0
+    $FWHM = 2*sqrt(2)
+    fprintf "%4.2f, // %4.1f, %4.2f" $FWHM $K $Zhm 
+    return
+  end
+
+  $dz = 0.01
+  create z 0 50 $dz
+
+  # set f = 1.0 / (1.0 + $K*z + z^1.667)
+  # f = $K*z + z^1.667 - 1.0, find where f == 0.0
+  
+  set f = $K*z + z^1.8 - 1.0
+  set dfdz = $K + 1.8*z^0.8
+
+  #lim -n 0 z f; clear; box; plot z f
+  #lim -n 1 z dfdz; clear; box; plot z dfdz  
+
+  # these just need to bound the solution
+  $nZ0 = 0.5 / $dz
+  $nZ1 = 5.0 / $dz
+
+  $Zg = 0.5*(z[$nZ0] + z[$nZ1])  
+  $nZg = int($Zg / $dz)
+  $dZ = 1.0
+
+  for i 0 10
+    # interpolate between $nZg and $nZg + 1
+    $Fg = f[$nZg] + ($Zg - $nZg*$dz)*(f[$nZg+1] - f[$nZg])/$dz
+    $dFdz_g = dfdz[$nZg] + ($Zg - $nZg*$dz)*(dfdz[$nZg+1] - dfdz[$nZg])/$dz
+
+    # $Fg = f[$nZg]
+    # $dFdz_g = dfdz[$nZg]
+
+    $dZ = $Fg / $dFdz_g
+
+    # echo -no-return $Zg $Fg $dZ $dFdz_g
+
+    $Zg -= $dZ
+
+    $Zg = max ($Zg , 0)
+    $Zg = min ($Zg , z[-2])
+    $nZg = int($Zg / $dz)
+    
+    # echo ": $Zg $nZg"
+  end 
+  # echo $Zg $Fg $dZ $dFdz_g
+  $Zhm = $Zg
+  $FWHM = 2*sqrt(2*$Zg)
+
+  fprintf "%4.2f, // %4.1f, %4.2f" $FWHM $K $Zhm 
+end
+
+# qgauss is like ps1_v1 with z^2.25
+macro find.fwhm.qgauss
+  if ($0 != 2)
+    echo "USAGE: find.qgauss (K)"
+    break
+  end
+  
+  $K = $1
+
+  if ($K == 0.0)
+    $Zhm = 1.0
+    $FWHM = 2*sqrt(2)
+    fprintf "%4.2f, // %4.1f, %4.2f" $FWHM $K $Zhm 
+    return
+  end
+
+  $dz = 0.01
+  create z 0 50 $dz
+
+  # set f = 1.0 / (1.0 + $K*z + z^2.25)
+  # f = $K*z + z^2.25 - 1.0, find where f == 0.0
+  
+  set f = $K*z + z^2.25 - 1.0
+  set dfdz = $K + 2.25*z^1.25
+
+  #lim -n 0 z f; clear; box; plot z f
+  #lim -n 1 z dfdz; clear; box; plot z dfdz  
+
+  $nZ0 = 0
+  $nZ1 = 5 / $dz
+
+  $Zg = 0.5*(z[$nZ0] + z[$nZ1])  
+  $nZg = int($Zg / $dz)
+  $dZ = 1.0
+
+  for i 0 10
+    # interpolate between $nZg and $nZg + 1
+    $Fg = f[$nZg] + ($Zg - $nZg*$dz)*(f[$nZg+1] - f[$nZg])/$dz
+    $dFdz_g = dfdz[$nZg] + ($Zg - $nZg*$dz)*(dfdz[$nZg+1] - dfdz[$nZg])/$dz
+
+    # $Fg = f[$nZg]
+    # $dFdz_g = dfdz[$nZg]
+
+    $dZ = $Fg / $dFdz_g
+
+    # echo $Zg $Fg $dZ $dFdz_g
+
+    $Zg -= $dZ
+    $Zg = max ($Zg , 0)
+    $Zg = min ($Zg , z[-2])
+    $nZg = int($Zg / $dz)
+  end 
+  # echo $Zg $Fg $dZ $dFdz_g
+  $Zhm = $Zg
+  $FWHM = 2*sqrt(2*$Zg)
+
+  fprintf "%4.2f, // %4.1f, %4.2f" $FWHM $K $Zhm 
+end
+
+macro fwhm.trend
+  if ($0 != 4)
+    echo "USAGE: fwhm.trend (model) (struct) (output)"
+    echo "  model: pgauss, rgauss, ps1v1, qgauss, hscv1"
+    break
+  end
+
+  delete fwhm_v k_v
+
+  $SAVE = 1
+  if ($SAVE) exec rm -f $3
+  if ($SAVE) output $3
+
+  $minK = -1
+  $maxK = 20
+  $delK = 0.2
+
+  if ($SAVE) echo "# define FWHM_BIN $delK"
+  if ($SAVE) echo "# define MIN_FWHM_BIN $minK"
+  if ($SAVE) echo "# define N_FWHM_BIN {int(($maxK - $minK) / $delK) + 1}"
+#  if ($SAVE) echo "static float $2[] = \{"
+
+  for k $minK $maxK $delK -incl
+    find.fwhm.$1 $k
+    concat $k k_v
+    concat $FWHM fwhm_v
+  end
+#  if ($SAVE) echo "\}@"
+  if ($SAVE) output stdout
+
+#  lim k_v fwhm_v; clear; box; plot k_v fwhm_v -pt 10 -sz 1.0
+end
+
Index: /branches/eam_branches/psModules.20240412/src/objects/models/hscv1.fwhm.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/hscv1.fwhm.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/hscv1.fwhm.h	(revision 42651)
@@ -0,0 +1,111 @@
+# define FWHM_BIN 0.2
+# define MIN_FWHM_BIN -1
+# define N_FWHM_BIN 106
+static float HSC_V1_Scale[] = {
+3.75, // -1.0, 1.76
+3.55, // -0.8, 1.57
+3.35, // -0.6, 1.40
+3.17, // -0.4, 1.25
+2.99, // -0.2, 1.12
+2.83, //  0.0, 1.00
+2.68, //  0.2, 0.90
+2.54, //  0.4, 0.81
+2.41, //  0.6, 0.73
+2.30, //  0.8, 0.66
+2.19, //  1.0, 0.60
+2.10, //  1.2, 0.55
+2.01, //  1.4, 0.51
+1.93, //  1.6, 0.47
+1.86, //  1.8, 0.43
+1.79, //  2.0, 0.40
+1.74, //  2.2, 0.38
+1.68, //  2.4, 0.35
+1.63, //  2.6, 0.33
+1.58, //  2.8, 0.31
+1.54, //  3.0, 0.30
+1.50, //  3.2, 0.28
+1.46, //  3.4, 0.27
+1.43, //  3.6, 0.25
+1.39, //  3.8, 0.24
+1.36, //  4.0, 0.23
+1.33, //  4.2, 0.22
+1.31, //  4.4, 0.21
+1.28, //  4.6, 0.20
+1.26, //  4.8, 0.20
+1.23, //  5.0, 0.19
+1.21, //  5.2, 0.18
+1.19, //  5.4, 0.18
+1.17, //  5.6, 0.17
+1.15, //  5.8, 0.17
+1.13, //  6.0, 0.16
+1.12, //  6.2, 0.16
+1.10, //  6.4, 0.15
+1.08, //  6.6, 0.15
+1.07, //  6.8, 0.14
+1.05, //  7.0, 0.14
+1.04, //  7.2, 0.14
+1.03, //  7.4, 0.13
+1.01, //  7.6, 0.13
+1.00, //  7.8, 0.13
+0.99, //  8.0, 0.12
+0.98, //  8.2, 0.12
+0.97, //  8.4, 0.12
+0.95, //  8.6, 0.11
+0.94, //  8.8, 0.11
+0.93, //  9.0, 0.11
+0.92, //  9.2, 0.11
+0.91, //  9.4, 0.10
+0.91, //  9.6, 0.10
+0.90, //  9.8, 0.10
+0.89, // 10.0, 0.10
+0.88, // 10.2, 0.10
+0.87, // 10.4, 0.09
+0.86, // 10.6, 0.09
+0.85, // 10.8, 0.09
+0.85, // 11.0, 0.09
+0.84, // 11.2, 0.09
+0.83, // 11.4, 0.09
+0.83, // 11.6, 0.09
+0.82, // 11.8, 0.08
+0.81, // 12.0, 0.08
+0.81, // 12.2, 0.08
+0.80, // 12.4, 0.08
+0.79, // 12.6, 0.08
+0.79, // 12.8, 0.08
+0.78, // 13.0, 0.08
+0.77, // 13.2, 0.08
+0.77, // 13.4, 0.07
+0.76, // 13.6, 0.07
+0.76, // 13.8, 0.07
+0.75, // 14.0, 0.07
+0.75, // 14.2, 0.07
+0.74, // 14.4, 0.07
+0.74, // 14.6, 0.07
+0.73, // 14.8, 0.07
+0.73, // 15.0, 0.07
+0.72, // 15.2, 0.07
+0.72, // 15.4, 0.06
+0.71, // 15.6, 0.06
+0.71, // 15.8, 0.06
+0.70, // 16.0, 0.06
+0.70, // 16.2, 0.06
+0.70, // 16.4, 0.06
+0.69, // 16.6, 0.06
+0.69, // 16.8, 0.06
+0.68, // 17.0, 0.06
+0.68, // 17.2, 0.06
+0.68, // 17.4, 0.06
+0.67, // 17.6, 0.06
+0.67, // 17.8, 0.06
+0.66, // 18.0, 0.06
+0.66, // 18.2, 0.05
+0.66, // 18.4, 0.05
+0.65, // 18.6, 0.05
+0.65, // 18.8, 0.05
+0.65, // 19.0, 0.05
+0.64, // 19.2, 0.05
+0.64, // 19.4, 0.05
+0.64, // 19.6, 0.05
+0.63, // 19.8, 0.05
+0.63, // 20.0, 0.05
+};
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_DEV.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_DEV.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_DEV.c	(revision 42651)
@@ -0,0 +1,489 @@
+/******************************************************************************
+ * this file defines the DEV source shape model.  Note that these model functions are loaded
+ * by pmModelClass.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 as 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
+
+   note that a standard dev model uses exp(-K*(z^(1/2n) - 1).  the additional elements (K,
+   the -1 offset) are absorbed in this model by the normalization, the exponent, and the
+   radial scale.  We fit the elements in this form, then re-normalize them on output.
+   *****************************************************************************/
+
+#include <stdio.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+#include "pmModel_CentralPixel.h"
+
+#include "pmModel_DEV.h"
+
+# define PM_MODEL_NPARAM          7
+# define PM_MODEL_FUNC            pmModelFunc_DEV
+# define PM_MODEL_FLUX            pmModelFlux_DEV
+# define PM_MODEL_GUESS           pmModelGuess_DEV
+# define PM_MODEL_LIMITS          pmModelLimits_DEV
+# define PM_MODEL_RADIUS          pmModelRadius_DEV
+# define PM_MODEL_SET_FWHM        pmModelSetFWHM_DEV
+# define PM_MODEL_FROM_PSF        pmModelFromPSF_DEV
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_DEV
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_DEV
+# define PM_MODEL_SET_LIMITS      pmModelSetLimits_DEV
+
+// f = exp(-kappa*r^(1/index)) 
+// f = exp(-kappa*z^(0.5/index)) 
+// index = 4, 0.5/index = 0.125
+# define ALPHA 0.125 
+
+// the model is a function of the pixel coordinate (pixcoord[0,1] = x,y)
+// 0.5 PIX: the parameters are defined in terms of pixel coords, so the incoming pixcoords
+// values need to be pixel coords
+
+// Lax parameter limits
+static float paramsMinLax[] = { -1.0e3, 1.0e-2, -100, -100, 0.001, 0.001, -1.0 };
+static float paramsMaxLax[] = { 1.0e5, 1.0e9, 1.0e5, 1.0e5, 100, 100, 1.0 };
+
+// Moderate parameter limits
+static float *paramsMinModerate = paramsMinLax;
+static float *paramsMaxModerate = paramsMaxLax;
+
+// Strict parameter limits
+static float *paramsMinStrict = paramsMinLax;
+static float *paramsMaxStrict = paramsMaxLax;
+
+// Parameter limits to use
+static float *paramsMinUse = paramsMinLax;
+static float *paramsMaxUse = paramsMaxLax;
+static float betaUse[] = { 2, 3e6, 5, 5, 10.0, 10.0, 0.5 };
+
+static bool limitsApply = true;         // Apply limits?
+
+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);
+
+    // If the elliptical contour is defined in a valid way, we should never trigger this
+    // assert.  Other models (like PGAUSS) don't use fractional powers, and thus do not have
+    // NaN values for negative values of z
+    psAssert (z >= 0, "do not allow negative z values in model");
+
+    // for DEV, we can hard-wire kappa(4):
+    // float index = 4.0;
+    float kappa = 7.670628;
+
+    // r = sqrt(z)
+    float q = kappa*pow(z,ALPHA);
+    float f0 = exp(-q);
+
+    assert (isfinite(q));
+
+    // only worry about the central pixels at most
+    float radius = hypot(X, Y);
+    if (radius <= 1.5) {
+	// Nsub ~ 10*index^2 + 1
+	psEllipseAxes axes = pmPSF_ModelToAxes(PAR, true); // DEV uses Reff
+	int Nsub = 2 * ((int)(25 / axes.minor)) + 1;
+	Nsub = PS_MIN (Nsub, 121);
+	Nsub = PS_MAX (Nsub, 11);
+	f0 = pmModelCP_SersicSubpix (X, Y, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], 4.0, Nsub);
+    }   
+    assert (isfinite(f0));
+
+    float f1 = PAR[PM_PAR_I0]*f0;
+    float f = PAR[PM_PAR_SKY] + f1;
+
+    assert (isfinite(f1));
+    assert (isfinite(f));
+
+    if (deriv != NULL) {
+        psF32 *dPAR = deriv->data.F32;
+
+        dPAR[PM_PAR_SKY]  = +1.0;
+        dPAR[PM_PAR_I0]   = +f0;
+
+	if (z > 0.01) {
+	  float z1 = f1*kappa*ALPHA*pow(z,ALPHA-1.0);
+	  dPAR[PM_PAR_XPOS] = +1.0*z1*(2.0*px + Y*PAR[PM_PAR_SXY]);
+	  dPAR[PM_PAR_YPOS] = +1.0*z1*(2.0*py + 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;
+	} else {
+	  // gradient -> 0 for z -> 0, but has undef form
+	  float z1 = f1*kappa*ALPHA*pow(z,ALPHA);
+	  dPAR[PM_PAR_XPOS] = +1.0*z1*(2.0/PAR[PM_PAR_SXX] + PAR[PM_PAR_SXY]);
+	  dPAR[PM_PAR_YPOS] = +1.0*z1*(2.0/PAR[PM_PAR_SYY] + PAR[PM_PAR_SXY]);
+	  dPAR[PM_PAR_SXX]  = +2.0*z1*px/PAR[PM_PAR_SXX]/PAR[PM_PAR_SXX];
+	  dPAR[PM_PAR_SYY]  = +2.0*z1*py/PAR[PM_PAR_SYY]/PAR[PM_PAR_SYY];
+	  dPAR[PM_PAR_SXY]  = -1.0*z1;
+	}
+    }
+    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)
+{
+    if (!limitsApply) {
+        return true;
+    }
+    psAssert(nParam >= 0 && nParam < PM_MODEL_NPARAM, "Parameter index is out of bounds");
+
+    // we need to calculate the limits for SXY specially
+    float q2 = NAN;
+    if (nParam == PM_PAR_SXY) {
+        float f1 = 1.0 / PS_SQR(params[PM_PAR_SYY]) + 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        float f2 = 1.0 / PS_SQR(params[PM_PAR_SYY]) - 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        float 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
+	// NOTE: the factor of 2 is needed to convert par[SXX,SYY] to shape.sx,sy
+        q2 = 2.0*0.5*sqrtf(q1);
+    }
+
+    switch (mode) {
+      case PS_MINIMIZE_BETA_LIMIT: {
+          psAssert(beta, "Require beta to limit beta");
+          float limit = betaUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (fabs(beta[nParam]) > fabs(limit)) {
+              beta[nParam] = (beta[nParam] > 0) ? fabs(limit) : -fabs(limit);
+              psTrace("psModules.objects", 5, "|beta[nParam==%d]| > |beta_lim|; %g v. %g",
+                      nParam, beta[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MIN: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMinUse, "Require parameter limits to limit parameters");
+          float limit = paramsMinUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] < limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] < params_min; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MAX: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMaxUse, "Require parameter limits to limit parameters");
+          float limit = paramsMaxUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] > limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] > params_max; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      default:
+        psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+// make an initial guess for parameters
+// 0.5 PIX: moments and peaks are in pixel coords, thus so are model parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal)
+{
+    // for the moment, we are going to require moments and KronFlux
+    if (!source->moments) return false;
+    pmMoments *moments = source->moments;
+
+    if (!isfinite(moments->KronFlux)) return false;
+    if (!isfinite(moments->Mrf)) return false;
+    if (moments->Mrf < 0.0) return false;
+
+    psF32 *PAR  = model->params->data.F32;
+
+    // sky is set to 0.0
+    PAR[PM_PAR_SKY]  = 0.0;
+
+    psEllipseMoments emoments;
+    emoments.x2 = moments->Mxx;
+    emoments.xy = moments->Mxy;
+    emoments.y2 = moments->Myy;
+
+    // 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;
+
+    // Mxx, Mxy, Myy define the elliptical shape, but Mrf defines the width 
+    // the factor of 2.3 comes from Table 1 of Graham and Driver (2005)
+    float scale = moments->Mrf / axes.major / 2.3;
+    axes.major *= scale;
+    axes.minor *= scale;
+
+    pmModelAxesToParams (&PAR[PM_PAR_SXX], &PAR[PM_PAR_SXY], &PAR[PM_PAR_SYY], axes, true);
+
+    // psEllipseAxes axes;
+    // use the code in SetShape here to avoid doing this 2x
+    // pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], true);
+
+    // float norm = pmSersicNorm (4);  // hardwire
+    float norm = 0.00168012;
+    float normFlux = 2.0 * M_PI * axes.major * axes.minor * norm;
+    PAR[PM_PAR_I0] = moments->KronFlux / normFlux;
+
+    // set the model position
+    if (!pmModelSetPosition(&PAR[PM_PAR_XPOS], &PAR[PM_PAR_YPOS], source)) {
+      return false;
+    }
+
+    return(true);
+}
+
+// A DeVaucouleur model is equivalent to a Sersic with index = 4.0
+psF64 PM_MODEL_FLUX (const psVector *params)
+{
+    psF32 *PAR = params->data.F32;
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], true);
+
+    float norm = 0.00168012;
+    float flux = PAR[PM_PAR_I0] * 2.0 * M_PI * axes.major * axes.minor * 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)
+{
+    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);
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], true);
+
+    // static value for DEV:
+    float kappa = 7.670628;
+
+    // f = Io exp(-kappa*z^n) -> z^n = ln(Io/f) / kappa
+    psF64 zn = log(PAR[PM_PAR_I0] / flux) / kappa;
+    psF64 radius = axes.major * pow(zn, 0.5 / ALPHA);
+
+    psAssert (isfinite(radius), "fix this code: radius should not be nan for Io = %f, flux = %f, major = %f (%f, %f, %f)", 
+	      PAR[PM_PAR_I0], flux, axes.major, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY]);
+    return (radius);
+}
+
+psF64 PM_MODEL_SET_FWHM (const psVector *params, psF64 sigma) {
+  return (NAN);
+}
+
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, const 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)
+    bool useReff = modelPSF->class->useReff;
+    if (!pmPSF_FitToModel (out, 0.1, useReff)) {
+        psTrace("psModules.objects", 5, "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, const 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?
+    bool useReff = model->class->useReff;
+    if (!pmPSF_FitToModel (PAR, 0.1, useReff)) {
+        psTrace ("psModules.objects", 3, "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)
+{
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    status = true;
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+
+void PM_MODEL_SET_LIMITS(pmModelLimitsType type)
+{
+    switch (type) {
+      case PM_MODEL_LIMITS_NONE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_IGNORE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = false;
+        break;
+      case PM_MODEL_LIMITS_LAX:
+        paramsMinUse = paramsMinLax;
+        paramsMaxUse = paramsMaxLax;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_MODERATE:
+        paramsMinUse = paramsMinModerate;
+        paramsMaxUse = paramsMaxModerate;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_STRICT:
+        paramsMinUse = paramsMinStrict;
+        paramsMaxUse = paramsMaxStrict;
+        limitsApply = true;
+        break;
+      default:
+        psAbort("Unrecognised model limits type: %x", type);
+    }
+    return;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_DEV.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_DEV.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_DEV.h	(revision 42651)
@@ -0,0 +1,16 @@
+#ifndef PM_MODEL_DEV_H
+
+#include "pmModel.h"
+
+psF32 pmModelFunc_DEV(psVector *deriv, const psVector *params, const psVector *pixcoord);
+bool pmModelLimits_DEV(psMinConstraintMode mode, int nParam, float *params, float *beta);
+bool pmModelGuess_DEV(pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal);
+psF64 pmModelFlux_DEV(const psVector *params);
+psF64 pmModelRadius_DEV(const psVector *params, psF64 flux);
+psF64 pmModelSetFWHM_DEV(const psVector *params, psF64 flux);
+bool pmModelFromPSF_DEV(pmModel *modelPSF, pmModel *modelFLT, const pmPSF *psf);
+bool  pmModelParamsFromPSF_DEV(pmModel *model, const pmPSF *psf, float Xo, float Yo, float Io);
+bool pmModelFitStatus_DEV(pmModel *model);
+void pmModelSetLimits_DEV(pmModelLimitsType type);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_EXP.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_EXP.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_EXP.c	(revision 42651)
@@ -0,0 +1,560 @@
+/******************************************************************************
+ * this file defines the Exponential (EXP) source shape model.  Note that these model functions
+ * are loaded by pmModelClass.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 as a PSF representations
+ * share a few parameters, for which # define names are listed in pmModel.h:
+
+   f = exp(-sqrt(z)) (since z is r^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
+
+   *****************************************************************************/
+
+#include <stdio.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+#include "pmModel_CentralPixel.h"
+
+#include "pmModel_EXP.h"
+
+# define PM_MODEL_NPARAM          7
+# define PM_MODEL_FUNC            pmModelFunc_EXP
+# define PM_MODEL_FLUX            pmModelFlux_EXP
+# define PM_MODEL_GUESS           pmModelGuess_EXP
+# define PM_MODEL_LIMITS          pmModelLimits_EXP
+# define PM_MODEL_RADIUS          pmModelRadius_EXP
+# define PM_MODEL_SET_FWHM        pmModelSetFWHM_EXP
+# define PM_MODEL_FROM_PSF        pmModelFromPSF_EXP
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_EXP
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_EXP
+# define PM_MODEL_SET_LIMITS      pmModelSetLimits_EXP
+
+// the model is a function of the pixel coordinate (pixcoord[0,1] = x,y)
+// 0.5 PIX: the parameters are defined in terms of pixel coords, so the incoming pixcoords
+// values need to be pixel coords
+//
+
+// Notes on changing kappa value from 1.70056 to 1.678
+// I'm using a functional form f(x,y) = Io exp(-kappa (r / r_e)).  
+// The article by Graham & Driver (2005) uses a form Ie exp(-bn [(r / r_e) -1]) 
+// which is equal to Ie exp(-bn (r / r_e)) exp(bn).  
+// Thus, my Io = Ie exp(bn) and my kappa is their bn.
+// My value of kappa is 1.700, their value for bn is 1.678., so I am off by a small amount there (1.5%).  
+
+
+#define KAPPA_EXP 1.678
+#define OLD_KAPP_EXP 1.70056
+
+
+// Lax parameter limits
+static float paramsMinLax[] = { -1.0e3, 1.0e-2, -100, -100, 0.05, 0.05, -1.0 };
+static float paramsMaxLax[] = { 1.0e5, 1.0e9, 1.0e5, 1.0e5, 100, 100, 1.0 };
+
+// Moderate parameter limits
+static float *paramsMinModerate = paramsMinLax;
+static float *paramsMaxModerate = paramsMaxLax;
+
+// Strict parameter limits
+static float *paramsMinStrict = paramsMinLax;
+static float *paramsMaxStrict = paramsMaxLax;
+
+// Parameter limits to use
+static float *paramsMinUse = paramsMinLax;
+static float *paramsMaxUse = paramsMaxLax;
+static float betaUse[] = { 2, 3e6, 5, 5, 10.0, 10.0, 0.5};
+
+static bool limitsApply = true;         // Apply limits?
+
+// # include "pmModel_SERSIC.CP.h"
+
+// the problems I'm having with the SERSIC-like functions are:
+// 1) making sure I have the right functional form so that PAR[SXX,etc] represent R_eff (half-light radius)
+// 2) getting the central pixel right
+// 3) getting the derivaties right.
+
+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;
+
+    // If the elliptical contour is defined in a valid way, we should never trigger this
+    // assert.  Other models (like PGAUSS) don't use fractional powers, and thus do not have
+    // NaN values for negative values of z
+    psAssert (z >= 0, "do not allow negative z values in model");
+
+    // for EXP, we can hard-wire kappa(1):
+    // float index = 1.0;
+    float kappa = KAPPA_EXP;
+
+    // sqrt(z) is r
+    float q = kappa*sqrt(z);
+    psF32 f0 = exp(-q);
+
+    assert (isfinite(q));
+
+    // only worry about the central 4 pixels at most
+    psF32 radius = hypot(X, Y);
+    if (radius <= 1.5) {
+	f0 = pmModelCP_SersicSubpix (X, Y, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], 1.0, 51);
+    }
+    assert (isfinite(f0));
+
+    psF32 f1 = PAR[PM_PAR_I0]*f0;
+    psF32 f = PAR[PM_PAR_SKY] + f1;
+
+    assert (isfinite(f1));
+    assert (isfinite(f));
+
+    if (deriv != NULL) {
+        psF32 *dPAR = deriv->data.F32;
+
+        dPAR[PM_PAR_SKY]  = +1.0;
+        dPAR[PM_PAR_I0]   = +f0;
+
+	if (z > 0.01) {
+	  float z1 = 0.5*f1*kappa/sqrt(z);
+	  dPAR[PM_PAR_XPOS] = +1.0*z1*(2.0*px + Y*PAR[PM_PAR_SXY]);
+	  dPAR[PM_PAR_YPOS] = +1.0*z1*(2.0*py + 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;
+	} else {
+	  // gradient -> 0 for z -> 0, but has undef form
+	  float z1 = 0.5*f1*kappa;
+	  dPAR[PM_PAR_XPOS] = +1.0*z1*(2.0/PAR[PM_PAR_SXX] + PAR[PM_PAR_SXY]);
+	  dPAR[PM_PAR_YPOS] = +1.0*z1*(2.0/PAR[PM_PAR_SYY] + PAR[PM_PAR_SXY]);
+	  dPAR[PM_PAR_SXX]  = +2.0*z1*px/PAR[PM_PAR_SXX]/PAR[PM_PAR_SXX];
+	  dPAR[PM_PAR_SYY]  = +2.0*z1*py/PAR[PM_PAR_SYY]/PAR[PM_PAR_SYY];
+	  dPAR[PM_PAR_SXY]  = -1.0*z1;
+	}
+    }
+    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)
+{
+    if (!limitsApply) {
+        return true;
+    }
+    psAssert(nParam >= 0 && nParam < PM_MODEL_NPARAM, "Parameter index is out of bounds");
+
+    // we need to calculate the limits for SXY specially
+    float q2 = NAN;
+    if (nParam == PM_PAR_SXY) {
+        float f1 = 1.0 / PS_SQR(params[PM_PAR_SYY]) + 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        float f2 = 1.0 / PS_SQR(params[PM_PAR_SYY]) - 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        float 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
+	// NOTE: the factor of 2 is needed to convert par[SXX,SYY] to shape.sx,sy
+        q2 = 2.0*0.5*sqrtf(q1);
+    }
+
+    switch (mode) {
+      case PS_MINIMIZE_BETA_LIMIT: {
+          psAssert(beta, "Require beta to limit beta");
+          float limit = betaUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (fabs(beta[nParam]) > fabs(limit)) {
+              beta[nParam] = (beta[nParam] > 0) ? fabs(limit) : -fabs(limit);
+              psTrace("psModules.objects", 5, "|beta[nParam==%d]| > |beta_lim|; %g v. %g",
+                      nParam, beta[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MIN: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMinUse, "Require parameter limits to limit parameters");
+          float limit = paramsMinUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] < limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] < params_min; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MAX: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMaxUse, "Require parameter limits to limit parameters");
+          float limit = paramsMaxUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] > limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] > params_max; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      default:
+        psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+// make an initial guess for parameters
+// 0.5 PIX: moments and peaks are in pixel coords, thus so are model parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal)
+{
+    // for the moment, we are going to require moments and KronFlux
+    if (!source->moments) return false;
+    pmMoments *moments = source->moments;
+
+    if (!isfinite(moments->KronFlux)) return false;
+    if (!isfinite(moments->Mrf)) return false;
+    if (moments->Mrf < 0.0) return false;
+
+    psF32 *PAR  = model->params->data.F32;
+
+    // sky is set to 0.0
+    PAR[PM_PAR_SKY]  = 0.0;
+
+    psEllipseMoments emoments;
+    emoments.x2 = moments->Mxx;
+    emoments.xy = moments->Mxy;
+    emoments.y2 = moments->Myy;
+
+    // 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;
+
+    // Mxx, Mxy, Myy define the elliptical shape, but Mrf defines the width 
+    float scale = moments->Mrf / axes.major;
+    axes.major *= scale;
+    axes.minor *= scale;
+
+    pmModelAxesToParams (&PAR[PM_PAR_SXX], &PAR[PM_PAR_SXY], &PAR[PM_PAR_SYY], axes, true);
+
+    // psEllipseAxes axes;
+    // use the code in SetShape here to avoid doing this 2x
+    // pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], true);
+
+    // float norm = pmSersicNorm (4);  // hardwire
+    float norm = 0.34578;
+    float normFlux = 2.0 * M_PI * axes.major * axes.minor * norm;
+    PAR[PM_PAR_I0] = moments->KronFlux / normFlux;
+
+    // set the model position
+    if (!pmModelSetPosition(&PAR[PM_PAR_XPOS], &PAR[PM_PAR_YPOS], source)) {
+      return false;
+    }
+
+    return(true);
+}
+// An exponential model is equivalent to a Sersic with index = 1.0
+psF64 PM_MODEL_FLUX (const psVector *params)
+{
+    psF32 *PAR = params->data.F32;
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], true);
+
+    // static value for EXP:
+    float norm = 0.34578; // \int exp(-kappa*sqrt(z)) r dr
+
+    float flux = PAR[PM_PAR_I0] * 2.0 * M_PI * axes.major * axes.minor * 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)
+{
+    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);
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], true);
+
+    // static value for EXP:
+    float kappa = KAPPA_EXP;
+
+    // f = Io exp(-kappa*sqrt(z)) -> sqrt(z) = ln(Io/f) / kappa
+    psF64 zn = log(PAR[PM_PAR_I0] / flux) / kappa;
+    psF64 radius = axes.major * zn;
+
+    psAssert (isfinite(radius), "fix this code: radius should not be nan for Io = %f, flux = %f, major = %f (%f, %f, %f)", 
+	      PAR[PM_PAR_I0], flux, axes.major, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY]);
+    return (radius);
+}
+
+psF64 PM_MODEL_SET_FWHM (const psVector *params, psF64 sigma) {
+  return (NAN);
+}
+
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, const 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)
+    bool useReff = modelPSF->class->useReff;
+    if (!pmPSF_FitToModel (out, 0.1, useReff)) {
+        psTrace("psModules.objects", 5, "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, const 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?
+    bool useReff = model->class->useReff;
+    if (!pmPSF_FitToModel (PAR, 0.1, useReff)) {
+        psTrace ("psModules.objects", 3, "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)
+{
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    status = true;
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+
+void PM_MODEL_SET_LIMITS(pmModelLimitsType type)
+{
+    switch (type) {
+      case PM_MODEL_LIMITS_NONE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_IGNORE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = false;
+        break;
+      case PM_MODEL_LIMITS_LAX:
+        paramsMinUse = paramsMinLax;
+        paramsMaxUse = paramsMaxLax;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_MODERATE:
+        paramsMinUse = paramsMinModerate;
+        paramsMaxUse = paramsMaxModerate;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_STRICT:
+        paramsMinUse = paramsMinStrict;
+        paramsMaxUse = paramsMaxStrict;
+        limitsApply = true;
+        break;
+      default:
+        psAbort("Unrecognised model limits type: %x", type);
+    }
+    return;
+}
+
+# if (0)
+void bilin_inter_function () {
+	// first, use Rmajor and index to find the central pixel flux (fraction of total flux)
+	psEllipseAxes axes;
+	pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], true);
+
+	// get the central pixel flux from the lookup table
+	float xPix = (axes.major - centralPixelXo) / centralPixeldX;
+	xPix = PS_MIN (PS_MAX(xPix, 0), centralPixelNX - 1);
+	float yPix = (index - centralPixelYo) / centralPixeldY;
+	yPix = PS_MIN (PS_MAX(yPix, 0), centralPixelNY - 1);
+
+	// the integral of a Sersic has an analytical form as follows:
+	float logGamma = lgamma(2.0*index);
+	float bnFactor = pow(bn, 2.0*index);
+	float norm = 2.0 * M_PI * PS_SQR(axes.major) * index * exp(bn) * exp(logGamma) / bnFactor;
+
+	// XXX interpolate to get the value
+	// XXX for the moment, just integerize
+	// XXX I need to multiply by the integrated flux to get the flux in the central pixel
+	float Vcenter = centralPixel[(int)yPix][(int)xPix] * norm;
+	
+	float px1 = 1.0 / PAR[PM_PAR_SXX];
+	float py1 = 1.0 / PAR[PM_PAR_SYY];
+	float z10 = PS_SQR(px1);
+	float z01 = PS_SQR(py1);
+
+	// which pixels do we need for this interpolation?
+	// (I do not keep state information, so I don't know anything about other evaluations of nearby pixels...)
+	if ((X >= 0) && (Y >= 0)) {
+	    float z11 = z10 + z01 + PAR[PM_PAR_SXY]; // X * Y positive
+	    float V00 = Vcenter;
+	    float V10 = Io*exp(-bn*pow(z10,par7));
+	    float V01 = Io*exp(-bn*pow(z01,par7));
+	    float V11 = Io*exp(-bn*pow(z11,par7));
+	    f1 = interpolatePixels(V00, V10, V01, V11, X, Y);
+	}
+	if ((X < 0) && (Y >= 0)) {
+	    float z11 = z10 + z01 - PAR[PM_PAR_SXY]; // X * Y negative
+	    float V00 = Io*exp(-bn*pow(z10,par7));
+	    float V10 = Vcenter;
+	    float V01 = Io*exp(-bn*pow(z11,par7));
+	    float V11 = Io*exp(-bn*pow(z01,par7));
+	    f1 = interpolatePixels(V00, V10, V01, V11, (1.0 + X), Y);
+	}
+	if ((X >= 0) && (Y < 0)) {
+	    float z11 = z10 + z01 - PAR[PM_PAR_SXY]; // X * Y negative
+	    float V00 = Io*exp(-bn*pow(z01,par7));
+	    float V10 = Io*exp(-bn*pow(z11,par7));
+	    float V01 = Vcenter;
+	    float V11 = Io*exp(-bn*pow(z10,par7));
+	    f1 = interpolatePixels(V00, V10, V01, V11, X, (1.0 + Y));
+	}
+	if ((X < 0) && (Y < 0)) {
+	    float z11 = z10 + z01 + PAR[PM_PAR_SXY]; // X * Y positive
+	    float V00 = Io*exp(-bn*pow(z11,par7));
+	    float V10 = Io*exp(-bn*pow(z10,par7));
+	    float V01 = Io*exp(-bn*pow(z01,par7));
+	    float V11 = Vcenter;
+	    f1 = interpolatePixels(V00, V10, V01, V11, (1.0 + X), (1.0 + Y));
+	}
+}
+# endif
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_EXP.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_EXP.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_EXP.h	(revision 42651)
@@ -0,0 +1,16 @@
+#ifndef PM_MODEL_EXP_H
+
+#include "pmModel.h"
+
+psF32 pmModelFunc_EXP(psVector *deriv, const psVector *params, const psVector *pixcoord);
+bool pmModelLimits_EXP(psMinConstraintMode mode, int nParam, float *params, float *beta);
+bool pmModelGuess_EXP(pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal);
+psF64 pmModelFlux_EXP(const psVector *params);
+psF64 pmModelRadius_EXP(const psVector *params, psF64 flux);
+psF64 pmModelSetFWHM_EXP(const psVector *params, psF64 flux);
+bool pmModelFromPSF_EXP(pmModel *modelPSF, pmModel *modelFLT, const pmPSF *psf);
+bool  pmModelParamsFromPSF_EXP(pmModel *model, const pmPSF *psf, float Xo, float Yo, float Io);
+bool pmModelFitStatus_EXP(pmModel *model);
+void pmModelSetLimits_EXP(pmModelLimitsType type);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_GAUSS.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_GAUSS.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_GAUSS.c	(revision 42651)
@@ -0,0 +1,413 @@
+/******************************************************************************
+ * this file defines the GAUSS source shape model.  Note that these model functions are loaded
+ * by pmModelClass.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 as 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
+ *****************************************************************************/
+
+#include <stdio.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmModel_GAUSS.h"
+
+# define PM_MODEL_NPARAM          7
+# 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_SET_FWHM        pmModelSetFWHM_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
+# define PM_MODEL_SET_LIMITS      pmModelSetLimits_GAUSS
+
+// Lax parameter limits
+static float paramsMinLax[] = { -1.0e3, 1.0e-2, -100, -100, 0.5, 0.5, -1.0 };
+static float paramsMaxLax[] = { 1.0e5, 1.0e9, 1.0e5, 1.0e5, 100, 100, 1.0 };
+
+// Moderate parameter limits
+static float *paramsMinModerate = paramsMinLax;
+static float *paramsMaxModerate = paramsMaxLax;
+
+// Strict parameter limits
+static float *paramsMinStrict = paramsMinLax;
+static float *paramsMaxStrict = paramsMaxLax;
+
+// Parameter limits to use
+static float *paramsMinUse = paramsMinLax;
+static float *paramsMaxUse = paramsMaxLax;
+static float betaUse[] = { 1000, 3e6, 5, 5, 2.0, 2.0, 0.5 };
+
+static bool limitsApply = true;         // Apply limits?
+
+// the model is a function of the pixel coordinate (pixcoord[0,1] = x,y)
+// 0.5 PIX: the parameters are defined in terms of pixel coords, so the incoming pixcoords
+// values need to be pixel coords
+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)
+{
+    if (!limitsApply) {
+        return true;
+    }
+    psAssert(nParam >= 0 && nParam < PM_MODEL_NPARAM, "Parameter index is out of bounds");
+
+    // we need to calculate the limits for SXY specially
+    float q2 = NAN;
+    if (nParam == PM_PAR_SXY) {
+	// NOTE: the factor of 2 is needed to convert par[SXX,SYY] to shape.sx,sy
+        float f1 = 2.0 / PS_SQR(params[PM_PAR_SYY]) + 2.0 / PS_SQR(params[PM_PAR_SXX]);
+        float f2 = 2.0 / PS_SQR(params[PM_PAR_SYY]) - 2.0 / PS_SQR(params[PM_PAR_SXX]);
+        float 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*sqrtf(q1);
+    }
+
+    switch (mode) {
+      case PS_MINIMIZE_BETA_LIMIT: {
+          psAssert(beta, "Require beta to limit beta");
+          float limit = betaUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (fabs(beta[nParam]) > fabs(limit)) {
+              beta[nParam] = (beta[nParam] > 0) ? fabs(limit) : -fabs(limit);
+              psTrace("psModules.objects", 5, "|beta[nParam==%d]| > |beta_lim|; %g v. %g",
+                      nParam, beta[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MIN: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMinUse, "Require parameter limits to limit parameters");
+          float limit = paramsMinUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] < limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] < params_min; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MAX: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMaxUse, "Require parameter limits to limit parameters");
+          float limit = paramsMaxUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] > limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] > params_max; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+    default:
+        psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+// make an initial guess for parameters
+// 0.5 PIX: moments and peaks are in pixel coords, thus so are model parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal)
+{
+    psF32 *PAR  = model->params->data.F32;
+
+    // sky is set to 0.0
+    PAR[PM_PAR_SKY]  = 0.0;
+
+    // set the shape parameters
+    // the last parameter is the scaling factor for Moments to shape parameter radius guess
+    if (!pmModelSetShape(&PAR[PM_PAR_SXX], &PAR[PM_PAR_SXY], &PAR[PM_PAR_SYY], source->moments, false, 1.0)) {
+      return false;
+    }
+
+    // set the model normalization
+    if (!pmModelSetNorm(&PAR[PM_PAR_I0], source)) {
+      return false;
+    }
+
+    // set the model position
+    if (!pmModelSetPosition(&PAR[PM_PAR_XPOS], &PAR[PM_PAR_YPOS], source)) {
+      return false;
+    }
+
+    return(true);
+}
+
+psF64 PM_MODEL_FLUX (const psVector *params)
+{
+    psF32 *PAR = params->data.F32;
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], false);
+
+    // Area is equivalent to 2 pi sigma^2
+    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)
+{
+    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);
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], false);
+
+    psF64 radius = axes.major * sqrt (2.0 * log(PAR[PM_PAR_I0] / flux));
+    psAssert (isfinite(radius), "fix this code: radius should not be nan for Io = %f, flux = %f, major = %f (%f, %f, %f)", 
+	      PAR[PM_PAR_I0], flux, axes.major, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY]);
+
+    return (radius);
+}
+
+psF64 PM_MODEL_SET_FWHM (const psVector *params, psF64 sigma) {
+    return (2.35482004503*sigma);
+}
+
+// construct the PSF model from the FLT model and the psf
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, const 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 2D PSF model fits polarization terms (E0,E1,E2)
+    // convert to shape terms (SXX,SYY,SXY)
+    bool useReff = modelPSF->class->useReff;
+    if (!pmPSF_FitToModel (out, 0.1, useReff)) {
+        psTrace ("psModules.objects", 3, "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, const 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)
+    bool useReff = model->class->useReff;
+    if (!pmPSF_FitToModel (PAR, 0.1, useReff)) {
+        psTrace ("psModules.objects", 3, "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
+// XXX how is this used?  it prevents forced photometry from ever being 'successful'
+bool PM_MODEL_FIT_STATUS (pmModel *model)
+{
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    status = true;
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+void PM_MODEL_SET_LIMITS(pmModelLimitsType type)
+{
+    switch (type) {
+      case PM_MODEL_LIMITS_NONE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_IGNORE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = false;
+        break;
+      case PM_MODEL_LIMITS_LAX:
+        paramsMinUse = paramsMinLax;
+        paramsMaxUse = paramsMaxLax;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_MODERATE:
+        paramsMinUse = paramsMinModerate;
+        paramsMaxUse = paramsMaxModerate;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_STRICT:
+        paramsMinUse = paramsMinStrict;
+        paramsMaxUse = paramsMaxStrict;
+        limitsApply = true;
+        break;
+      default:
+        psAbort("Unrecognised model limits type: %x", type);
+    }
+    return;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_GAUSS.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_GAUSS.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_GAUSS.h	(revision 42651)
@@ -0,0 +1,16 @@
+#ifndef PM_MODEL_GAUSS_H
+
+#include "pmModel.h"
+
+psF32 pmModelFunc_GAUSS(psVector *deriv, const psVector *params, const psVector *pixcoord);
+bool pmModelLimits_GAUSS(psMinConstraintMode mode, int nParam, float *params, float *beta);
+bool pmModelGuess_GAUSS(pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal);
+psF64 pmModelFlux_GAUSS(const psVector *params);
+psF64 pmModelRadius_GAUSS(const psVector *params, psF64 flux);
+psF64 pmModelSetFWHM_GAUSS(const psVector *params, psF64 flux);
+bool pmModelFromPSF_GAUSS(pmModel *modelPSF, pmModel *modelFLT, const pmPSF *psf);
+bool  pmModelParamsFromPSF_GAUSS(pmModel *model, const pmPSF *psf, float Xo, float Yo, float Io);
+bool pmModelFitStatus_GAUSS(pmModel *model);
+void pmModelSetLimits_GAUSS(pmModelLimitsType type);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_HSC_V1.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_HSC_V1.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_HSC_V1.c	(revision 42651)
@@ -0,0 +1,506 @@
+/******************************************************************************
+ * this file defines the HSC_V1 source shape model.  Note that these model functions are
+ * loaded by pmModelClass.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 as 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^(3.33/2))  (z = r^2, so r^3.33)
+
+   * 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 (k)
+   *****************************************************************************/
+
+#include <stdio.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmModel_HSC_V1.h"
+
+# define PM_MODEL_NPARAM          8
+# define PM_MODEL_FUNC            pmModelFunc_HSC_V1
+# define PM_MODEL_FLUX            pmModelFlux_HSC_V1
+# define PM_MODEL_GUESS           pmModelGuess_HSC_V1
+# define PM_MODEL_LIMITS          pmModelLimits_HSC_V1
+# define PM_MODEL_RADIUS          pmModelRadius_HSC_V1
+# define PM_MODEL_SET_FWHM        pmModelSetFWHM_HSC_V1
+# define PM_MODEL_FROM_PSF        pmModelFromPSF_HSC_V1
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_HSC_V1
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_HSC_V1
+# define PM_MODEL_SET_LIMITS      pmModelSetLimits_HSC_V1
+
+# define ALPHA   1.8
+# define ALPHA_M 0.8
+
+// the model is a function of the pixel coordinate (pixcoord[0,1] = x,y)
+// 0.5 PIX: the parameters are defined in terms of pixel coords, so the incoming pixcoords
+// values need to be pixel coords
+
+// Lax parameter limits
+static float paramsMinLax[] = { -1.0e3, 1.0e-2, -100, -100, 0.5, 0.5, -1.0, -1.0 };
+static float paramsMaxLax[] = { 1.0e5, 1.0e9, 1.0e5, 1.0e5, 100, 100, 1.0, 20.0 };
+
+// Moderate parameter limits
+// Tolerate a small divot (k < 0)
+static float paramsMinModerate[] = { -1.0e3, 1.0e-2, -100, -100, 0.5, 0.5, -1.0, -0.05 };
+static float paramsMaxModerate[] = { 1.0e5, 1.0e9, 1.0e5, 1.0e5, 100, 100, 1.0, 20.0 };
+
+// Strict parameter limits
+// k = PAR_7 < 0 is very undesirable (big divot in the middle)
+static float paramsMinStrict[] = { -1.0e3, 1.0e-2, -100, -100, 0.5, 0.5, -1.0, 0.0 };
+static float paramsMaxStrict[] = { 1.0e5, 1.0e9, 1.0e5, 1.0e5, 100, 100, 1.0, 20.0 };
+
+// Parameter limits to use
+static float *paramsMinUse = paramsMinLax;
+static float *paramsMaxUse = paramsMaxLax;
+static float betaUse[] = { 1000, 3e6, 5, 5, 1.0, 1.0, 0.5, 2.0 };
+
+static bool limitsApply = true;         // Apply limits?
+
+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,ALPHA_M);
+    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] + ALPHA*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)
+{
+    if (!limitsApply) {
+        return true;
+    }
+    psAssert(nParam >= 0 && nParam < PM_MODEL_NPARAM, "Parameter index is out of bounds");
+
+    // we need to calculate the limits for SXY specially
+    float q2 = NAN;
+    if (nParam == PM_PAR_SXY) {
+        float f1 = 2.0 / PS_SQR(params[PM_PAR_SYY]) + 2.0 / PS_SQR(params[PM_PAR_SXX]);
+        float f2 = 2.0 / PS_SQR(params[PM_PAR_SYY]) - 2.0 / PS_SQR(params[PM_PAR_SXX]);
+        float 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*sqrtf(q1);
+    }
+
+    switch (mode) {
+      case PS_MINIMIZE_BETA_LIMIT: {
+          psAssert(beta, "Require beta to limit beta");
+          float limit = betaUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (fabs(beta[nParam]) > fabs(limit)) {
+              beta[nParam] = (beta[nParam] > 0) ? fabs(limit) : -fabs(limit);
+              psTrace("psModules.objects", 5, "|beta[nParam==%d]| > |beta_lim|; %g v. %g",
+                      nParam, beta[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MIN: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMinUse, "Require parameter limits to limit parameters");
+          float limit = paramsMinUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] < limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] < params_min; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MAX: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMaxUse, "Require parameter limits to limit parameters");
+          float limit = paramsMaxUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] > limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] > params_max; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      default:
+        psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+
+// make an initial guess for parameters
+// 0.5 PIX: moments and peaks are in pixel coords, thus so are model parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal)
+{
+    psF32 *PAR  = model->params->data.F32;
+
+    // sky is set to 0.0
+    PAR[PM_PAR_SKY]  = 0.0;
+
+    // set the shape parameters
+    // the last parameter is the scaling factor for Moments to shape parameter radius guess
+    if (!pmModelSetShape(&PAR[PM_PAR_SXX], &PAR[PM_PAR_SXY], &PAR[PM_PAR_SYY], source->moments, false, 1.0)) {
+      return false;
+    }
+
+    // set the model normalization
+    if (!pmModelSetNorm(&PAR[PM_PAR_I0], source)) {
+      return false;
+    }
+
+    // set the model position
+    if (!pmModelSetPosition(&PAR[PM_PAR_XPOS], &PAR[PM_PAR_YPOS], source)) {
+      return false;
+    }
+
+    // extra parameter
+    PAR[PM_PAR_7]    = 0.5;
+
+    return(true);
+}
+
+// integrate the model to get the full flux
+psF64 PM_MODEL_FLUX (const psVector *params)
+{
+    float z, norm;
+
+    psF32 *PAR = params->data.F32;
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], false);
+    float AspectRatio = axes.minor / axes.major;
+
+    // flux = 2 \pi \int f(r) r dr
+    norm = 0.0;
+
+    # define DR 0.25
+
+    // f = f(r) * r
+    float f0 = 0.0;
+    float f1, f2;
+    for (float r = DR; r < 150; r += DR) {
+	z = 0.5 * PS_SQR(r / axes.major);
+        f1 = r / (1 + PAR[PM_PAR_7]*z + pow(z, ALPHA));
+        r += DR;
+	z = 0.5 * PS_SQR(r / axes.major);
+        f2 = r / (1 + PAR[PM_PAR_7]*z + pow(z, ALPHA));
+        norm += f0 + 4*f1 + f2;
+        f0 = f2;
+    }
+    norm *= DR / 3.0;
+
+    psF64 Flux = PAR[PM_PAR_I0] * norm * 2.0 * M_PI * AspectRatio;
+
+    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;
+
+    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;
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], false);
+    psF64 sigma = axes.major;
+
+    // we can solve directly if PAR_7 goes to 0.0
+    if (PAR[PM_PAR_7] == 0.0) { 
+        psF32 z =  powf(PAR[PM_PAR_I0] / flux - 1.0, 1.0 / ALPHA);
+        return ( sigma * sqrt (2.0 * z) );
+    }
+    psF64 limit = flux / PAR[PM_PAR_I0];
+
+    // use the fact that f is monotonically decreasing
+    z = 0;
+
+    // choose a z value guaranteed to be beyond our limit
+    float z0 = 0.0;
+    float z1 = pow((1.0 / limit), (1.0 / ALPHA));
+    psAssert (isfinite(z1), "fix this code: z1 should not be nan for Io = %f, flux = %f, major = %f (%f, %f, %f)", 
+	      PAR[PM_PAR_I0], flux, axes.major, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY]);
+    if (PAR[PM_PAR_7] < 0.0) z1 *= 2.0;
+
+    // starting guess:
+    z = 0.5*(z0 + z1);
+    float dz = 1.0;
+
+    // use Newton-Raphson to minimize f(z) - limit = 0
+    for (int i = 0; (i < 10) && (fabs(dz) > 0.0001); i++) {
+	float q = (1.0 + PAR[PM_PAR_7]*z + pow(z, ALPHA));
+	float dqdz = (PAR[PM_PAR_7] + ALPHA*pow(z, ALPHA - 1.0));
+
+	float f = 1.0 / q;
+	float dfdz = -dqdz * f / q;
+
+	dz = (f - limit) / dfdz;
+
+	// fprintf (stderr, "%f %f %f : %f %f\n", f, z, dz, dfdz, q);
+	z -= dz;
+	z = PS_MAX(z, 0.0);
+    }
+    psF64 radius = sigma * sqrt (2.0 * z);
+
+    if (isnan(radius))
+        psAbort("error in code: radius is NaN");
+
+    return (radius);
+}
+
+// I used the script in models/fwhm.sh to generate the trend of FWHM scaling vs the K value:
+# include "hscv1.fwhm.h"
+
+psF64 PM_MODEL_SET_FWHM (const psVector *params, psF64 sigma) {
+
+    psF32 *PAR = params->data.F32;
+
+    float core = PAR[PM_PAR_7];
+
+    if (!isfinite(core)) return (2.0*M_SQRT2*sigma);
+
+    int binCore = MAX(0, MIN (N_FWHM_BIN - 2, (int)((core - MIN_FWHM_BIN)/FWHM_BIN)));
+    float coreBin = binCore * FWHM_BIN + MIN_FWHM_BIN;
+
+    float scale = (core - coreBin) * (HSC_V1_Scale[binCore + 1] - HSC_V1_Scale[binCore]) / FWHM_BIN + HSC_V1_Scale[binCore];
+
+    // fprintf (stderr, "core: %f, binCore: %d, scale: %f\n", core, binCore, scale);
+
+    return (scale * sigma);
+}
+
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, const 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)
+    bool useReff = modelPSF->class->useReff;
+    if (!pmPSF_FitToModel (out, 0.1, useReff)) {
+        psTrace("psModules.objects", 5, "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, const 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?
+    bool useReff = model->class->useReff;
+    if (!pmPSF_FitToModel (PAR, 0.1, useReff)) {
+        psTrace ("psModules.objects", 3, "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)
+{
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    status = true;
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+
+void PM_MODEL_SET_LIMITS(pmModelLimitsType type)
+{
+    switch (type) {
+      case PM_MODEL_LIMITS_NONE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_IGNORE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = false;
+        break;
+      case PM_MODEL_LIMITS_LAX:
+        paramsMinUse = paramsMinLax;
+        paramsMaxUse = paramsMaxLax;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_MODERATE:
+        paramsMinUse = paramsMinModerate;
+        paramsMaxUse = paramsMaxModerate;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_STRICT:
+        paramsMinUse = paramsMinStrict;
+        paramsMaxUse = paramsMaxStrict;
+        limitsApply = true;
+        break;
+      default:
+        psAbort("Unrecognised model limits type: %x", type);
+    }
+    return;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_HSC_V1.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_HSC_V1.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_HSC_V1.h	(revision 42651)
@@ -0,0 +1,16 @@
+#ifndef PM_MODEL_HSC_V1_H
+
+#include "pmModel.h"
+
+psF32 pmModelFunc_HSC_V1(psVector *deriv, const psVector *params, const psVector *pixcoord);
+bool pmModelLimits_HSC_V1(psMinConstraintMode mode, int nParam, float *params, float *beta);
+bool pmModelGuess_HSC_V1(pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal);
+psF64 pmModelFlux_HSC_V1(const psVector *params);
+psF64 pmModelRadius_HSC_V1(const psVector *params, psF64 flux);
+psF64 pmModelSetFWHM_HSC_V1(const psVector *params, psF64 flux);
+bool pmModelFromPSF_HSC_V1(pmModel *modelPSF, pmModel *modelFLT, const pmPSF *psf);
+bool  pmModelParamsFromPSF_HSC_V1(pmModel *model, const pmPSF *psf, float Xo, float Yo, float Io);
+bool pmModelFitStatus_HSC_V1(pmModel *model);
+void pmModelSetLimits_HSC_V1(pmModelLimitsType type);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_PGAUSS.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_PGAUSS.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_PGAUSS.c	(revision 42651)
@@ -0,0 +1,476 @@
+/******************************************************************************
+ * this file defines the PGAUSS source shape model.  Note that these model functions are loaded
+ * by pmModelClass.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 as 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
+ *****************************************************************************/
+
+#include <stdio.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmModel_PGAUSS.h"
+
+# define PM_MODEL_NPARAM          7
+# 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_SET_FWHM        pmModelSetFWHM_PGAUSS
+# define PM_MODEL_FROM_PSF        pmModelFromPSF_PGAUSS
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_PGAUSS
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_PGAUSS
+# define PM_MODEL_SET_LIMITS      pmModelSetLimits_PGAUSS
+
+// Lax parameter limits
+static float paramsMinLax[] = { -1.0e3, 1.0e-2, -100, -100, 0.5, 0.5, -1.0 };
+static float paramsMaxLax[] = { 1.0e5, 1.0e9, 1.0e5, 1.0e5, 100, 100, 1.0 };
+
+// Moderate parameter limits
+static float *paramsMinModerate = paramsMinLax;
+static float *paramsMaxModerate = paramsMaxLax;
+
+// Strict parameter limits
+static float *paramsMinStrict = paramsMinLax;
+static float *paramsMaxStrict = paramsMaxLax;
+
+// Parameter limits to use
+static float *paramsMinUse = paramsMinLax;
+static float *paramsMaxUse = paramsMaxLax;
+static float betaUse[] = { 1000, 3e6, 5, 5, 2.0, 2.0, 0.5 };
+
+static bool limitsApply = true;         // Apply limits?
+
+// the model is a function of the pixel coordinate (pixcoord[0,1] = x,y)
+// 0.5 PIX: the parameters are defined in terms of pixel coords, so the incoming pixcoords
+// values need to be pixel coords
+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)
+{
+    if (!limitsApply) {
+        return true;
+    }
+    psAssert(nParam >= 0 && nParam < PM_MODEL_NPARAM, "Parameter index is out of bounds");
+
+    // we need to calculate the limits for SXY specially
+    float q2 = NAN;
+    if (nParam == PM_PAR_SXY) {
+	// NOTE: the factor of 2 is needed to convert par[SXX,SYY] to shape.sx,sy
+        float f1 = 2.0 / PS_SQR(params[PM_PAR_SYY]) + 2.0 / PS_SQR(params[PM_PAR_SXX]);
+        float f2 = 2.0 / PS_SQR(params[PM_PAR_SYY]) - 2.0 / PS_SQR(params[PM_PAR_SXX]);
+        float 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*sqrtf(q1);
+    }
+
+    switch (mode) {
+      case PS_MINIMIZE_BETA_LIMIT: {
+          psAssert(beta, "Require beta to limit beta");
+          float limit = betaUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (fabs(beta[nParam]) > fabs(limit)) {
+              beta[nParam] = (beta[nParam] > 0) ? fabs(limit) : -fabs(limit);
+              psTrace("psModules.objects", 5, "|beta[nParam==%d]| > |beta_lim|; %g v. %g",
+                      nParam, beta[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MIN: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMinUse, "Require parameter limits to limit parameters");
+          float limit = paramsMinUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] < limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] < params_min; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MAX: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMaxUse, "Require parameter limits to limit parameters");
+          float limit = paramsMaxUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] > limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] > params_max; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      default:
+        psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+
+// make an initial guess for parameters
+// 0.5 PIX: moments and peaks are in pixel coords, thus so are model parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal)
+{
+    psF32 *PAR  = model->params->data.F32;
+
+    // sky is set to 0.0
+    PAR[PM_PAR_SKY]  = 0.0;
+
+    // set the shape parameters
+    // the last parameter is the scaling factor for Moments to shape parameter radius guess
+    if (!pmModelSetShape(&PAR[PM_PAR_SXX], &PAR[PM_PAR_SXY], &PAR[PM_PAR_SYY], source->moments, false, 1.0)) {
+      return false;
+    }
+
+    // set the model normalization
+    if (!pmModelSetNorm(&PAR[PM_PAR_I0], source)) {
+      return false;
+    }
+
+    // set the model position
+    if (!pmModelSetPosition(&PAR[PM_PAR_XPOS], &PAR[PM_PAR_YPOS], source)) {
+      return false;
+    }
+
+    return(true);
+}
+
+// integrate the model to get the full flux
+psF64 PM_MODEL_FLUX (const psVector *params)
+{
+    float z, norm;
+
+    psF32 *PAR = params->data.F32;
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], false);
+
+    float AspectRatio = axes.minor / axes.major;
+
+    // flux = 2 \pi \int f(r) r dr
+    norm = 0.0;
+
+    # define DR 0.25
+
+    // f = f(r) * r
+    float f0 = 0.0;
+    float f1, f2;
+    for (float r = DR; r < 150; r += DR) {
+	z = 0.5 * PS_SQR(r / axes.major);
+        f1 = r / (1 + z + z*z/2.0 + z*z*z/6.0);
+        r += DR;
+	z = 0.5 * PS_SQR(r / axes.major);
+        f2 = r / (1 + z + z*z/2.0 + z*z*z/6.0);
+        norm += f0 + 4*f1 + f2;
+        f0 = f2;
+    }
+    norm *= DR / 3.0;
+
+    psF64 Flux = PAR[PM_PAR_I0] * norm * 2.0 * M_PI * AspectRatio;
+
+    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;
+
+    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);
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], false);
+
+    psF64 sigma = axes.major;
+
+    psF64 limit = flux / PAR[PM_PAR_I0];
+
+    // use the fact that f is monotonically decreasing
+    z = 0;
+
+    // choose a z value guaranteed to be beyond our limit
+    float z0 = pow((1.0 / limit), (1.0 / 3.0));
+    psAssert (isfinite(z0), "fix this code: z0 should not be nan for Io = %f, flux = %f, major = %f (%f, %f, %f)", 
+	      PAR[PM_PAR_I0], flux, axes.major, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY]);
+
+    float z1 = (1.0 / limit);
+    psAssert (isfinite(z1), "fix this code: z1 should not be nan for Io = %f, flux = %f, major = %f (%f, %f, %f)", 
+	      PAR[PM_PAR_I0], flux, axes.major, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY]);
+
+    z1 = PS_MAX (z0, z1);
+    z0 = 0.0;
+
+    // starting guess:
+    z = 0.5*(z0 + z1);
+    float dz = 1.0;
+
+    for (int i = 0; (i < 10) && (fabs(dz) > 0.0001); i++) {
+	// use Newton-Raphson to minimize f(z) - limit = 0
+	float dqdz = (1.0 + z + z*z/2.0);
+	float q = (dqdz + z*z*z/6.0);
+
+	float f = 1.0 / q;
+	float dfdz = -dqdz * f / q;
+
+	dz = (f - limit) / dfdz;
+
+	// fprintf (stderr, "%f %f %f : %f %f\n", f, z, dz, dfdz, q);
+	z -= dz;
+	z = PS_MAX(z, 0.0);
+    }
+
+    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);
+}
+
+// scale factor is constant for PGAUSS, I found it with the fwhm.sh script
+psF64 PM_MODEL_SET_FWHM (const psVector *params, psF64 sigma) {
+    return (3.0063103*sigma);
+}
+
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, const 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)
+    bool useReff = modelPSF->class->useReff;
+    if (!pmPSF_FitToModel (out, 0.1, useReff)) {
+        psTrace("psModules.objects", 5, "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, const 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)
+    bool useReff = model->class->useReff;
+    if (!pmPSF_FitToModel (PAR, 0.1, useReff)) {
+        psTrace ("psModules.objects", 3, "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)
+{
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    status = true;
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+
+void PM_MODEL_SET_LIMITS(pmModelLimitsType type)
+{
+    switch (type) {
+      case PM_MODEL_LIMITS_NONE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_IGNORE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = false;
+        break;
+      case PM_MODEL_LIMITS_LAX:
+        paramsMinUse = paramsMinLax;
+        paramsMaxUse = paramsMaxLax;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_MODERATE:
+        paramsMinUse = paramsMinModerate;
+        paramsMaxUse = paramsMaxModerate;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_STRICT:
+        paramsMinUse = paramsMinStrict;
+        paramsMaxUse = paramsMaxStrict;
+        limitsApply = true;
+        break;
+      default:
+        psAbort("Unrecognised model limits type: %x", type);
+    }
+    return;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_PGAUSS.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_PGAUSS.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_PGAUSS.h	(revision 42651)
@@ -0,0 +1,16 @@
+#ifndef PM_MODEL_PGAUSS_H
+
+#include "pmModel.h"
+
+psF32 pmModelFunc_PGAUSS(psVector *deriv, const psVector *params, const psVector *pixcoord);
+bool pmModelLimits_PGAUSS(psMinConstraintMode mode, int nParam, float *params, float *beta);
+bool pmModelGuess_PGAUSS(pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal);
+psF64 pmModelFlux_PGAUSS(const psVector *params);
+psF64 pmModelRadius_PGAUSS(const psVector *params, psF64 flux);
+psF64 pmModelSetFWHM_PGAUSS(const psVector *params, psF64 flux);
+bool pmModelFromPSF_PGAUSS(pmModel *modelPSF, pmModel *modelFLT, const pmPSF *psf);
+bool  pmModelParamsFromPSF_PGAUSS(pmModel *model, const pmPSF *psf, float Xo, float Yo, float Io);
+bool pmModelFitStatus_PGAUSS(pmModel *model);
+void pmModelSetLimits_PGAUSS(pmModelLimitsType type);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_PS1_V1.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_PS1_V1.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_PS1_V1.c	(revision 42651)
@@ -0,0 +1,506 @@
+/******************************************************************************
+ * this file defines the PS1_V1 source shape model.  Note that these model functions are
+ * loaded by pmModelClass.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 as 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^(3.33/2))  (z = r^2, so r^3.33)
+
+   * 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 (k)
+   *****************************************************************************/
+
+#include <stdio.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmModel_PS1_V1.h"
+
+# define PM_MODEL_NPARAM          8
+# define PM_MODEL_FUNC            pmModelFunc_PS1_V1
+# define PM_MODEL_FLUX            pmModelFlux_PS1_V1
+# define PM_MODEL_GUESS           pmModelGuess_PS1_V1
+# define PM_MODEL_LIMITS          pmModelLimits_PS1_V1
+# define PM_MODEL_RADIUS          pmModelRadius_PS1_V1
+# define PM_MODEL_SET_FWHM        pmModelSetFWHM_PS1_V1
+# define PM_MODEL_FROM_PSF        pmModelFromPSF_PS1_V1
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_PS1_V1
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_PS1_V1
+# define PM_MODEL_SET_LIMITS      pmModelSetLimits_PS1_V1
+
+# define ALPHA   1.666
+# define ALPHA_M 0.666
+
+// the model is a function of the pixel coordinate (pixcoord[0,1] = x,y)
+// 0.5 PIX: the parameters are defined in terms of pixel coords, so the incoming pixcoords
+// values need to be pixel coords
+
+// Lax parameter limits
+static float paramsMinLax[] = { -1.0e3, 1.0e-2, -100, -100, 0.5, 0.5, -1.0, -1.0 };
+static float paramsMaxLax[] = { 1.0e5, 1.0e9, 1.0e5, 1.0e5, 100, 100, 1.0, 20.0 };
+
+// Moderate parameter limits
+// Tolerate a small divot (k < 0)
+static float paramsMinModerate[] = { -1.0e3, 1.0e-2, -100, -100, 0.5, 0.5, -1.0, -0.05 };
+static float paramsMaxModerate[] = { 1.0e5, 1.0e9, 1.0e5, 1.0e5, 100, 100, 1.0, 20.0 };
+
+// Strict parameter limits
+// k = PAR_7 < 0 is very undesirable (big divot in the middle)
+static float paramsMinStrict[] = { -1.0e3, 1.0e-2, -100, -100, 0.5, 0.5, -1.0, 0.0 };
+static float paramsMaxStrict[] = { 1.0e5, 1.0e9, 1.0e5, 1.0e5, 100, 100, 1.0, 20.0 };
+
+// Parameter limits to use
+static float *paramsMinUse = paramsMinLax;
+static float *paramsMaxUse = paramsMaxLax;
+static float betaUse[] = { 1000, 3e6, 5, 5, 1.0, 1.0, 0.5, 2.0 };
+
+static bool limitsApply = true;         // Apply limits?
+
+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,ALPHA_M);
+    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] + ALPHA*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)
+{
+    if (!limitsApply) {
+        return true;
+    }
+    psAssert(nParam >= 0 && nParam < PM_MODEL_NPARAM, "Parameter index is out of bounds");
+
+    // we need to calculate the limits for SXY specially
+    float q2 = NAN;
+    if (nParam == PM_PAR_SXY) {
+        float f1 = 2.0 / PS_SQR(params[PM_PAR_SYY]) + 2.0 / PS_SQR(params[PM_PAR_SXX]);
+        float f2 = 2.0 / PS_SQR(params[PM_PAR_SYY]) - 2.0 / PS_SQR(params[PM_PAR_SXX]);
+        float 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*sqrtf(q1);
+    }
+
+    switch (mode) {
+      case PS_MINIMIZE_BETA_LIMIT: {
+          psAssert(beta, "Require beta to limit beta");
+          float limit = betaUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (fabs(beta[nParam]) > fabs(limit)) {
+              beta[nParam] = (beta[nParam] > 0) ? fabs(limit) : -fabs(limit);
+              psTrace("psModules.objects", 5, "|beta[nParam==%d]| > |beta_lim|; %g v. %g",
+                      nParam, beta[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MIN: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMinUse, "Require parameter limits to limit parameters");
+          float limit = paramsMinUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] < limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] < params_min; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MAX: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMaxUse, "Require parameter limits to limit parameters");
+          float limit = paramsMaxUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] > limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] > params_max; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      default:
+        psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+
+// make an initial guess for parameters
+// 0.5 PIX: moments and peaks are in pixel coords, thus so are model parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal)
+{
+    psF32 *PAR  = model->params->data.F32;
+
+    // sky is set to 0.0
+    PAR[PM_PAR_SKY]  = 0.0;
+
+    // set the shape parameters
+    // the last parameter is the scaling factor for Moments to shape parameter radius guess
+    if (!pmModelSetShape(&PAR[PM_PAR_SXX], &PAR[PM_PAR_SXY], &PAR[PM_PAR_SYY], source->moments, false, 1.0)) {
+      return false;
+    }
+
+    // set the model normalization
+    if (!pmModelSetNorm(&PAR[PM_PAR_I0], source)) {
+      return false;
+    }
+
+    // set the model position
+    if (!pmModelSetPosition(&PAR[PM_PAR_XPOS], &PAR[PM_PAR_YPOS], source)) {
+      return false;
+    }
+
+    // extra parameter
+    PAR[PM_PAR_7]    = 0.5;
+
+    return(true);
+}
+
+// integrate the model to get the full flux
+psF64 PM_MODEL_FLUX (const psVector *params)
+{
+    float z, norm;
+
+    psF32 *PAR = params->data.F32;
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], false);
+    float AspectRatio = axes.minor / axes.major;
+
+    // flux = 2 \pi \int f(r) r dr
+    norm = 0.0;
+
+    # define DR 0.25
+
+    // f = f(r) * r
+    float f0 = 0.0;
+    float f1, f2;
+    for (float r = DR; r < 150; r += DR) {
+	z = 0.5 * PS_SQR(r / axes.major);
+        f1 = r / (1 + PAR[PM_PAR_7]*z + pow(z, ALPHA));
+        r += DR;
+	z = 0.5 * PS_SQR(r / axes.major);
+        f2 = r / (1 + PAR[PM_PAR_7]*z + pow(z, ALPHA));
+        norm += f0 + 4*f1 + f2;
+        f0 = f2;
+    }
+    norm *= DR / 3.0;
+
+    psF64 Flux = PAR[PM_PAR_I0] * norm * 2.0 * M_PI * AspectRatio;
+
+    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;
+
+    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;
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], false);
+    psF64 sigma = axes.major;
+
+    // we can solve directly if PAR_7 goes to 0.0
+    if (PAR[PM_PAR_7] == 0.0) { 
+        psF32 z =  powf(PAR[PM_PAR_I0] / flux - 1.0, 1.0 / ALPHA);
+        return ( sigma * sqrt (2.0 * z) );
+    }
+    psF64 limit = flux / PAR[PM_PAR_I0];
+
+    // use the fact that f is monotonically decreasing
+    z = 0;
+
+    // choose a z value guaranteed to be beyond our limit
+    float z0 = 0.0;
+    float z1 = pow((1.0 / limit), (1.0 / ALPHA));
+    psAssert (isfinite(z1), "fix this code: z1 should not be nan for Io = %f, flux = %f, major = %f (%f, %f, %f)", 
+	      PAR[PM_PAR_I0], flux, axes.major, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY]);
+    if (PAR[PM_PAR_7] < 0.0) z1 *= 2.0;
+
+    // starting guess:
+    z = 0.5*(z0 + z1);
+    float dz = 1.0;
+
+    // use Newton-Raphson to minimize f(z) - limit = 0
+    for (int i = 0; (i < 10) && (fabs(dz) > 0.0001); i++) {
+	float q = (1.0 + PAR[PM_PAR_7]*z + pow(z, ALPHA));
+	float dqdz = (PAR[PM_PAR_7] + ALPHA*pow(z, ALPHA - 1.0));
+
+	float f = 1.0 / q;
+	float dfdz = -dqdz * f / q;
+
+	dz = (f - limit) / dfdz;
+
+	// fprintf (stderr, "%f %f %f : %f %f\n", f, z, dz, dfdz, q);
+	z -= dz;
+	z = PS_MAX(z, 0.0);
+    }
+    psF64 radius = sigma * sqrt (2.0 * z);
+
+    if (isnan(radius))
+        psAbort("error in code: radius is NaN");
+
+    return (radius);
+}
+
+// I used the script in models/fwhm.sh to generate the trend of FWHM scaling vs the K value:
+# include "ps1v1.fwhm.h"
+
+psF64 PM_MODEL_SET_FWHM (const psVector *params, psF64 sigma) {
+
+    psF32 *PAR = params->data.F32;
+
+    float core = PAR[PM_PAR_7];
+
+    if (!isfinite(core)) return (2.0*M_SQRT2*sigma);
+
+    int binCore = MAX(0, MIN (N_FWHM_BIN - 2, (int)((core - MIN_FWHM_BIN)/FWHM_BIN)));
+    float coreBin = binCore * FWHM_BIN + MIN_FWHM_BIN;
+
+    float scale = (core - coreBin) * (PS1_V1_Scale[binCore + 1] - PS1_V1_Scale[binCore]) / FWHM_BIN + PS1_V1_Scale[binCore];
+
+    // fprintf (stderr, "core: %f, binCore: %d, scale: %f\n", core, binCore, scale);
+
+    return (scale * sigma);
+}
+
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, const 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)
+    bool useReff = modelPSF->class->useReff;
+    if (!pmPSF_FitToModel (out, 0.1, useReff)) {
+        psTrace("psModules.objects", 5, "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, const 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?
+    bool useReff = model->class->useReff;
+    if (!pmPSF_FitToModel (PAR, 0.1, useReff)) {
+        psTrace ("psModules.objects", 3, "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)
+{
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    status = true;
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+
+void PM_MODEL_SET_LIMITS(pmModelLimitsType type)
+{
+    switch (type) {
+      case PM_MODEL_LIMITS_NONE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_IGNORE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = false;
+        break;
+      case PM_MODEL_LIMITS_LAX:
+        paramsMinUse = paramsMinLax;
+        paramsMaxUse = paramsMaxLax;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_MODERATE:
+        paramsMinUse = paramsMinModerate;
+        paramsMaxUse = paramsMaxModerate;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_STRICT:
+        paramsMinUse = paramsMinStrict;
+        paramsMaxUse = paramsMaxStrict;
+        limitsApply = true;
+        break;
+      default:
+        psAbort("Unrecognised model limits type: %x", type);
+    }
+    return;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_PS1_V1.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_PS1_V1.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_PS1_V1.h	(revision 42651)
@@ -0,0 +1,16 @@
+#ifndef PM_MODEL_PS1_V1_H
+
+#include "pmModel.h"
+
+psF32 pmModelFunc_PS1_V1(psVector *deriv, const psVector *params, const psVector *pixcoord);
+bool pmModelLimits_PS1_V1(psMinConstraintMode mode, int nParam, float *params, float *beta);
+bool pmModelGuess_PS1_V1(pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal);
+psF64 pmModelFlux_PS1_V1(const psVector *params);
+psF64 pmModelRadius_PS1_V1(const psVector *params, psF64 flux);
+psF64 pmModelSetFWHM_PS1_V1(const psVector *params, psF64 flux);
+bool pmModelFromPSF_PS1_V1(pmModel *modelPSF, pmModel *modelFLT, const pmPSF *psf);
+bool  pmModelParamsFromPSF_PS1_V1(pmModel *model, const pmPSF *psf, float Xo, float Yo, float Io);
+bool pmModelFitStatus_PS1_V1(pmModel *model);
+void pmModelSetLimits_PS1_V1(pmModelLimitsType type);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_QGAUSS.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_QGAUSS.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_QGAUSS.c	(revision 42651)
@@ -0,0 +1,510 @@
+/******************************************************************************
+ * this file defines the QGAUSS source shape model.  Note that these model functions are
+ * loaded by pmModelClass.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 as 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 (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 (k)
+   *****************************************************************************/
+
+#include <stdio.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmModel_QGAUSS.h"
+
+# define PM_MODEL_NPARAM          8
+# 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_SET_FWHM        pmModelSetFWHM_QGAUSS
+# define PM_MODEL_FROM_PSF        pmModelFromPSF_QGAUSS
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_QGAUSS
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_QGAUSS
+# define PM_MODEL_SET_LIMITS      pmModelSetLimits_QGAUSS
+
+# define ALPHA   2.250
+# define ALPHA_M 1.250
+
+// the model is a function of the pixel coordinate (pixcoord[0,1] = x,y)
+// 0.5 PIX: the parameters are defined in terms of pixel coords, so the incoming pixcoords
+// values need to be pixel coords
+
+// Lax parameter limits
+static float paramsMinLax[] = { -1.0e3, 1.0e-2, -100, -100, 0.5, 0.5, -1.0, -1.0 };
+static float paramsMaxLax[] = { 1.0e5, 1.0e9, 1.0e5, 1.0e5, 100, 100, 1.0, 20.0 };
+
+// Moderate parameter limits
+// Tolerate a small divot (k < 0)
+static float paramsMinModerate[] = { -1.0e3, 1.0e-2, -100, -100, 0.5, 0.5, -1.0, -0.05 };
+static float paramsMaxModerate[] = { 1.0e5, 1.0e9, 1.0e5, 1.0e5, 100, 100, 1.0, 20.0 };
+
+// Strict parameter limits
+// k = PAR_7 < 0 is very undesirable (big divot in the middle)
+static float paramsMinStrict[] = { -1.0e3, 1.0e-2, -100, -100, 0.5, 0.5, -1.0, 0.0 };
+static float paramsMaxStrict[] = { 1.0e5, 1.0e9, 1.0e5, 1.0e5, 100, 100, 1.0, 20.0 };
+
+// Parameter limits to use
+static float *paramsMinUse = paramsMinLax;
+static float *paramsMaxUse = paramsMaxLax;
+static float betaUse[] = { 1000, 3e6, 5, 5, 1.0, 1.0, 0.5, 2.0 };
+
+static bool limitsApply = true;         // Apply limits?
+
+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,ALPHA_M);
+    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] + ALPHA*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)
+{
+    if (!limitsApply) {
+        return true;
+    }
+    psAssert(nParam >= 0 && nParam < PM_MODEL_NPARAM, "Parameter index is out of bounds");
+
+    // we need to calculate the limits for SXY specially
+    float q2 = NAN;
+    if (nParam == PM_PAR_SXY) {
+	// NOTE: the factor of 2 is needed to convert par[SXX,SYY] to shape.sx,sy
+        float f1 = 2.0 / PS_SQR(params[PM_PAR_SYY]) + 2.0 / PS_SQR(params[PM_PAR_SXX]);
+        float f2 = 2.0 / PS_SQR(params[PM_PAR_SYY]) - 2.0 / PS_SQR(params[PM_PAR_SXX]);
+        float 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*sqrtf(q1);
+    }
+
+    switch (mode) {
+      case PS_MINIMIZE_BETA_LIMIT: {
+          psAssert(beta, "Require beta to limit beta");
+          float limit = betaUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (fabs(beta[nParam]) > fabs(limit)) {
+              beta[nParam] = (beta[nParam] > 0) ? fabs(limit) : -fabs(limit);
+              psTrace("psModules.objects", 5, "|beta[nParam==%d]| > |beta_lim|; %g v. %g",
+                      nParam, beta[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MIN: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMinUse, "Require parameter limits to limit parameters");
+          float limit = paramsMinUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] < limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] < params_min; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MAX: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMaxUse, "Require parameter limits to limit parameters");
+          float limit = paramsMaxUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] > limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] > params_max; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      default:
+        psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+
+// make an initial guess for parameters
+// 0.5 PIX: moments and peaks are in pixel coords, thus so are model parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal)
+{
+    psF32 *PAR  = model->params->data.F32;
+
+    // sky is set to 0.0
+    PAR[PM_PAR_SKY]  = 0.0;
+
+    // set the shape parameters
+    // the last parameter is the scaling factor for Moments to shape parameter radius guess
+    if (!pmModelSetShape(&PAR[PM_PAR_SXX], &PAR[PM_PAR_SXY], &PAR[PM_PAR_SYY], source->moments, false, 1.5)) {
+      return false;
+    }
+
+    // set the model normalization
+    if (!pmModelSetNorm(&PAR[PM_PAR_I0], source)) {
+      return false;
+    }
+
+    // set the model position
+    if (!pmModelSetPosition(&PAR[PM_PAR_XPOS], &PAR[PM_PAR_YPOS], source)) {
+      return false;
+    }
+
+    // extra parameter
+    PAR[PM_PAR_7]    = 1.0;
+
+    return(true);
+}
+
+// integrate the model to get the full flux
+psF64 PM_MODEL_FLUX (const psVector *params)
+{
+    float z, norm;
+
+    psF32 *PAR = params->data.F32;
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], false);
+    float AspectRatio = axes.minor / axes.major;
+
+    // flux = 2 \pi \int f(r) r dr
+    norm = 0.0;
+
+    # define DR 0.25
+
+    // f = f(r) * r
+    float f0 = 0.0;
+    float f1, f2;
+    for (float r = DR; r < 150; r += DR) {
+	z = 0.5 * PS_SQR(r / axes.major);
+        f1 = r / (1 + PAR[PM_PAR_7]*z + pow(z, ALPHA));
+        r += DR;
+	z = 0.5 * PS_SQR(r / axes.major);
+        f2 = r / (1 + PAR[PM_PAR_7]*z + pow(z, ALPHA));
+        norm += f0 + 4*f1 + f2;
+        f0 = f2;
+    }
+    norm *= DR / 3.0;
+
+    psF64 Flux = PAR[PM_PAR_I0] * norm * 2.0 * M_PI * AspectRatio;
+
+    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;
+
+    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;
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], false);
+    psF64 sigma = axes.major;
+
+    // we can solve directly if PAR_7 goes to 0.0
+    if (PAR[PM_PAR_7] == 0.0) { 
+        psF32 z =  powf(PAR[PM_PAR_I0] / flux - 1.0, 1.0 / ALPHA);
+        return ( sigma * sqrt (2.0 * z) );
+    }
+    psF64 limit = flux / PAR[PM_PAR_I0];
+
+    // use the fact that f is monotonically decreasing
+    z = 0;
+
+    // choose a z value guaranteed to be beyond our limit
+    float z0 = 0.0;
+    float z1 = pow((1.0 / limit), (1.0 / ALPHA));
+    psAssert (isfinite(z1), "fix this code: z1 should not be nan for Io = %f, flux = %f, major = %f (%f, %f, %f)", 
+	      PAR[PM_PAR_I0], flux, axes.major, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY]);
+    if (PAR[PM_PAR_7] < 0.0) z1 *= 2.0;
+
+    // starting guess:
+    z = 0.5*(z0 + z1);
+    float dz = 1.0;
+
+    // use Newton-Raphson to minimize f(z) - limit = 0
+    for (int i = 0; (i < 10) && (fabs(dz) > 0.0001); i++) {
+	float q = (1.0 + PAR[PM_PAR_7]*z + pow(z, ALPHA));
+	float dqdz = (PAR[PM_PAR_7] + ALPHA*pow(z, ALPHA - 1.0));
+
+	float f = 1.0 / q;
+	float dfdz = -dqdz * f / q;
+
+	dz = (f - limit) / dfdz;
+
+	// fprintf (stderr, "%f %f %f : %f %f\n", f, z, dz, dfdz, q);
+	z -= dz;
+	z = PS_MAX(z, 0.0);
+    }
+    psF64 radius = sigma * sqrt (2.0 * z);
+
+    if (isnan(radius))
+        psAbort("error in code: radius is NaN");
+
+    return (radius);
+}
+
+// I used the script in models/fwhm.sh to generate the trend of FWHM scaling vs the K value
+# include "qgauss.fwhm.h"
+
+psF64 PM_MODEL_SET_FWHM (const psVector *params, psF64 sigma) {
+
+    psF32 *PAR = params->data.F32;
+
+    float core = PAR[PM_PAR_7];
+
+    if (!isfinite(core)) return (2.0*M_SQRT2*sigma);
+
+    int binCore = MAX(0, MIN (N_FWHM_BIN - 2, (int)((core - MIN_FWHM_BIN)/FWHM_BIN)));
+    float coreBin = binCore * FWHM_BIN + MIN_FWHM_BIN;
+
+    float scale = (core - coreBin) * (QGAUSS_Scale[binCore + 1] - QGAUSS_Scale[binCore]) / FWHM_BIN + QGAUSS_Scale[binCore];
+
+    return (scale * sigma);
+}
+
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, const 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)
+    bool useReff = modelPSF->class->useReff;
+    if (!pmPSF_FitToModel (out, 0.1, useReff)) {
+        psTrace("psModules.objects", 5, "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, const 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);
+
+    if (! isfinite(Io)) {
+        fprintf(stderr, "non-finite Io passed to PM_MODEL_PARAMS_FROM_PSF\n");
+        return false;
+    }
+
+    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)
+    bool useReff = model->class->useReff;
+    if (!pmPSF_FitToModel (PAR, 0.1, useReff)) {
+        psTrace ("psModules.objects", 3, "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)
+{
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    status = true;
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+
+void PM_MODEL_SET_LIMITS(pmModelLimitsType type)
+{
+    switch (type) {
+      case PM_MODEL_LIMITS_NONE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_IGNORE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = false;
+        break;
+      case PM_MODEL_LIMITS_LAX:
+        paramsMinUse = paramsMinLax;
+        paramsMaxUse = paramsMaxLax;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_MODERATE:
+        paramsMinUse = paramsMinModerate;
+        paramsMaxUse = paramsMaxModerate;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_STRICT:
+        paramsMinUse = paramsMinStrict;
+        paramsMaxUse = paramsMaxStrict;
+        limitsApply = true;
+        break;
+      default:
+        psAbort("Unrecognised model limits type: %x", type);
+    }
+    return;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_QGAUSS.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_QGAUSS.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_QGAUSS.h	(revision 42651)
@@ -0,0 +1,16 @@
+#ifndef PM_MODEL_QGAUSS_H
+
+#include "pmModel.h"
+
+psF32 pmModelFunc_QGAUSS(psVector *deriv, const psVector *params, const psVector *pixcoord);
+bool pmModelLimits_QGAUSS(psMinConstraintMode mode, int nParam, float *params, float *beta);
+bool pmModelGuess_QGAUSS(pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal);
+psF64 pmModelFlux_QGAUSS(const psVector *params);
+psF64 pmModelRadius_QGAUSS(const psVector *params, psF64 flux);
+psF64 pmModelSetFWHM_QGAUSS(const psVector *params, psF64 flux);
+bool pmModelFromPSF_QGAUSS(pmModel *modelPSF, pmModel *modelFLT, const pmPSF *psf);
+bool  pmModelParamsFromPSF_QGAUSS(pmModel *model, const pmPSF *psf, float Xo, float Yo, float Io);
+bool pmModelFitStatus_QGAUSS(pmModel *model);
+void pmModelSetLimits_QGAUSS(pmModelLimitsType type);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_RGAUSS.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_RGAUSS.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_RGAUSS.c	(revision 42651)
@@ -0,0 +1,483 @@
+/******************************************************************************
+ * this file defines the RGAUSS source shape model (XXX need a better name!).  Note that these
+ * model functions are loaded by pmModelClass.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 as 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)
+ *****************************************************************************/
+
+#include <stdio.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmModel_RGAUSS.h"
+
+# define PM_MODEL_NPARAM          8
+# 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_SET_FWHM        pmModelSetFWHM_RGAUSS
+# define PM_MODEL_FROM_PSF        pmModelFromPSF_RGAUSS
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_RGAUSS
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_RGAUSS
+# define PM_MODEL_SET_LIMITS      pmModelSetLimits_RGAUSS
+
+// the model is a function of the pixel coordinate (pixcoord[0,1] = x,y)
+// 0.5 PIX: the parameters are defined in terms of pixel coords, so the incoming pixcoords
+// values need to be pixel coords
+
+// Lax parameter limits
+static float paramsMinLax[] = { -1.0e3, 1.0e-2, -100, -100, 0.5, 0.5, -1.0, 1.25 };
+static float paramsMaxLax[] = { 1.0e5, 1.0e9, 1.0e5, 1.0e5, 100, 100, 1.0, 4.0 };
+
+// Moderate parameter limits
+static float *paramsMinModerate = paramsMinLax;
+static float *paramsMaxModerate = paramsMaxLax;
+
+// Strict parameter limits
+static float *paramsMinStrict = paramsMinLax;
+static float *paramsMaxStrict = paramsMaxLax;
+
+// Parameter limits to use
+static float *paramsMinUse = paramsMinLax;
+static float *paramsMaxUse = paramsMaxLax;
+static float betaUse[] = { 1000, 3e6, 5, 5, 0.5, 0.5, 0.5, 0.5 };
+
+static bool limitsApply = true;         // Apply limits?
+
+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 the limit is zero as z -> 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)
+{
+    if (!limitsApply) {
+        return true;
+    }
+    psAssert(nParam >= 0 && nParam < PM_MODEL_NPARAM, "Parameter index is out of bounds");
+
+    // we need to calculate the limits for SXY specially
+    float q2 = NAN;
+    if (nParam == PM_PAR_SXY) {
+	// NOTE: the factor of 2 is needed to convert par[SXX,SYY] to shape.sx,sy
+        float f1 = 2.0 / PS_SQR(params[PM_PAR_SYY]) + 2.0 / PS_SQR(params[PM_PAR_SXX]);
+        float f2 = 2.0 / PS_SQR(params[PM_PAR_SYY]) - 2.0 / PS_SQR(params[PM_PAR_SXX]);
+        float 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*sqrtf(q1);
+    }
+
+    switch (mode) {
+      case PS_MINIMIZE_BETA_LIMIT: {
+          psAssert(beta, "Require beta to limit beta");
+          float limit = betaUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (fabs(beta[nParam]) > fabs(limit)) {
+              beta[nParam] = (beta[nParam] > 0) ? fabs(limit) : -fabs(limit);
+              psTrace("psModules.objects", 5, "|beta[nParam==%d]| > |beta_lim|; %g v. %g",
+                      nParam, beta[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MIN: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMinUse, "Require parameter limits to limit parameters");
+          float limit = paramsMinUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] < limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] < params_min; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MAX: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMaxUse, "Require parameter limits to limit parameters");
+          float limit = paramsMaxUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] > limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] > params_max; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      default:
+        psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+
+// make an initial guess for parameters
+// 0.5 PIX: moments and peaks are in pixel coords, thus so are model parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal)
+{
+    psF32 *PAR  = model->params->data.F32;
+
+    // sky is set to 0.0
+    PAR[PM_PAR_SKY]  = 0.0;
+
+    // set the shape parameters
+    // the last parameter is the scaling factor for Moments to shape parameter radius guess
+    if (!pmModelSetShape(&PAR[PM_PAR_SXX], &PAR[PM_PAR_SXY], &PAR[PM_PAR_SYY], source->moments, false, 2.0)) {
+      return false;
+    }
+
+    // set the model normalization
+    if (!pmModelSetNorm(&PAR[PM_PAR_I0], source)) {
+      return false;
+    }
+
+    // set the model position
+    if (!pmModelSetPosition(&PAR[PM_PAR_XPOS], &PAR[PM_PAR_YPOS], source)) {
+      return false;
+    }
+
+    // extra parameter
+    PAR[PM_PAR_7]    = 1.5;
+
+    return(true);
+}
+
+// integrate the model to get the full flux
+psF64 PM_MODEL_FLUX (const psVector *params)
+{
+    float z, norm;
+
+    psF32 *PAR = params->data.F32;
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], false);
+    float AspectRatio = axes.minor / axes.major;
+
+    // flux = 2 \pi \int f(r) r dr
+    norm = 0.0;
+
+    # define DR 0.25
+
+    // f = f(r) * r
+    float f0 = 0.0;
+    float f1, f2;
+    for (float r = DR; r < 150; r += DR) {
+	z = 0.5 * PS_SQR(r / axes.major);
+        f1 = r / (1 + z + pow(z, PAR[PM_PAR_7]));
+        r += DR;
+	z = 0.5 * PS_SQR(r / axes.major);
+        f2 = r / (1 + z + pow(z, PAR[PM_PAR_7]));
+        norm += f0 + 4*f1 + f2;
+        f0 = f2;
+    }
+    norm *= DR / 3.0;
+
+    psF64 Flux = PAR[PM_PAR_I0] * norm * 2.0 * M_PI * AspectRatio;
+
+    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;
+
+    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);
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], false);
+    psF64 sigma = axes.major;
+
+    psF64 limit = flux / PAR[PM_PAR_I0];
+
+    // use the fact that f is monotonically decreasing
+    z = 0;
+
+    // choose a z value guaranteed to be beyond our limit
+    float z0 = pow((1.0 / limit), (1.0 / PAR[PM_PAR_7]));
+    psAssert (isfinite(z0), "fix this code: z0 should not be nan for Io = %f, flux = %f, major = %f (%f, %f, %f), par 7 = %f", 
+	      PAR[PM_PAR_I0], flux, axes.major, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], PAR[PM_PAR_7]);
+
+    float z1 = (1.0 / limit);
+    psAssert (isfinite(z1), "fix this code: z1 should not be nan for Io = %f, flux = %f, major = %f (%f, %f, %f), par 7 = %f", 
+	      PAR[PM_PAR_I0], flux, axes.major, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], PAR[PM_PAR_7]);
+
+    z1 = PS_MAX (z0, z1);
+    z0 = 0.0;
+
+    // starting guess:
+    z = 0.5*(z0 + z1);
+    float dz = 1.0;
+
+    for (int i = 0; (i < 10) && (fabs(dz) > 0.0001); i++) {
+	// use Newton-Raphson to minimize f(z) - limit = 0
+	float q = (1 + z + pow(z,PAR[PM_PAR_7]));
+	float dqdz = (1.0 + PAR[PM_PAR_7]*pow(z,PAR[PM_PAR_7] - 1.0));
+
+	float f = 1.0 / q;
+	float dfdz = -dqdz * f / q;
+
+	dz = (f - limit) / dfdz;
+
+	// fprintf (stderr, "%f %f %f : %f %f\n", f, z, dz, dfdz, q);
+	z -= dz;
+	z = PS_MAX(z, 0.0);
+    }
+
+    psF64 radius = sigma * sqrt (2.0 * z);
+
+    if (isnan(radius))
+        psAbort("error in code: radius is NaN");
+
+    return (radius);
+}
+
+psF64 PM_MODEL_SET_FWHM (const psVector *params, psF64 sigma) {
+  return (NAN);
+}
+
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, const 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)
+    bool useReff = modelPSF->class->useReff;
+    if (!pmPSF_FitToModel (out, 0.1, useReff)) {
+        psTrace("psModules.objects", 5, "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, const 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)
+    bool useReff = model->class->useReff;
+    if (!pmPSF_FitToModel (PAR, 0.1, useReff)) {
+        psTrace ("psModules.objects", 3, "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)
+{
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    status = true;
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+
+void PM_MODEL_SET_LIMITS(pmModelLimitsType type)
+{
+    switch (type) {
+      case PM_MODEL_LIMITS_NONE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_IGNORE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = false;
+        break;
+      case PM_MODEL_LIMITS_LAX:
+        paramsMinUse = paramsMinLax;
+        paramsMaxUse = paramsMaxLax;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_MODERATE:
+        paramsMinUse = paramsMinModerate;
+        paramsMaxUse = paramsMaxModerate;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_STRICT:
+        paramsMinUse = paramsMinStrict;
+        paramsMaxUse = paramsMaxStrict;
+        limitsApply = true;
+        break;
+      default:
+        psAbort("Unrecognised model limits type: %x", type);
+    }
+    return;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_RGAUSS.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_RGAUSS.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_RGAUSS.h	(revision 42651)
@@ -0,0 +1,16 @@
+#ifndef PM_MODEL_RGAUSS_H
+
+#include "pmModel.h"
+
+psF32 pmModelFunc_RGAUSS(psVector *deriv, const psVector *params, const psVector *pixcoord);
+bool pmModelLimits_RGAUSS(psMinConstraintMode mode, int nParam, float *params, float *beta);
+bool pmModelGuess_RGAUSS(pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal);
+psF64 pmModelFlux_RGAUSS(const psVector *params);
+psF64 pmModelRadius_RGAUSS(const psVector *params, psF64 flux);
+psF64 pmModelSetFWHM_RGAUSS(const psVector *params, psF64 flux);
+bool pmModelFromPSF_RGAUSS(pmModel *modelPSF, pmModel *modelFLT, const pmPSF *psf);
+bool  pmModelParamsFromPSF_RGAUSS(pmModel *model, const pmPSF *psf, float Xo, float Yo, float Io);
+bool pmModelFitStatus_RGAUSS(pmModel *model);
+void pmModelSetLimits_RGAUSS(pmModelLimitsType type);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_SERSIC.CP.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_SERSIC.CP.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_SERSIC.CP.h	(revision 42651)
@@ -0,0 +1,30 @@
+static int centralPixelNX = 29;
+static int centralPixelNY = 12;
+static float centralPixelXo = 2.0;
+static float centralPixelYo = 0.5;
+static float centralPixeldX = 0.5;
+static float centralPixeldY = 0.5;
+
+static float centralPixel[12][29] = {
+{  0.0521,  0.0336,  0.0235,  0.0173,  0.0133,  0.0105,  0.0085,  0.0070,  0.0059,  0.0051,  0.0044,  0.0038,  0.0033,  0.0030,  0.0026,  0.0024,  0.0021,  0.0019,  0.0018,  0.0016,  0.0015,  0.0014,  0.0013,  0.0012,  0.0011,  0.0010,  0.0010,  0.0009,  0.0008, }, 
+{  0.0817,  0.0556,  0.0402,  0.0304,  0.0238,  0.0191,  0.0157,  0.0131,  0.0111,  0.0096,  0.0083,  0.0073,  0.0064,  0.0057,  0.0051,  0.0046,  0.0042,  0.0038,  0.0035,  0.0032,  0.0029,  0.0027,  0.0025,  0.0023,  0.0022,  0.0020,  0.0019,  0.0018,  0.0017, }, 
+{  0.1096,  0.0785,  0.0591,  0.0462,  0.0371,  0.0305,  0.0255,  0.0217,  0.0187,  0.0162,  0.0143,  0.0126,  0.0113,  0.0101,  0.0091,  0.0083,  0.0076,  0.0069,  0.0064,  0.0059,  0.0054,  0.0050,  0.0047,  0.0044,  0.0041,  0.0038,  0.0036,  0.0034,  0.0032, }, 
+{  0.1339,  0.0998,  0.0777,  0.0624,  0.0513,  0.0431,  0.0367,  0.0317,  0.0277,  0.0244,  0.0217,  0.0194,  0.0175,  0.0158,  0.0144,  0.0132,  0.0121,  0.0112,  0.0103,  0.0096,  0.0089,  0.0083,  0.0078,  0.0073,  0.0069,  0.0065,  0.0061,  0.0058,  0.0055, }, 
+{  0.1547,  0.1190,  0.0951,  0.0781,  0.0655,  0.0559,  0.0484,  0.0423,  0.0374,  0.0334,  0.0300,  0.0271,  0.0246,  0.0225,  0.0206,  0.0190,  0.0176,  0.0163,  0.0152,  0.0142,  0.0133,  0.0125,  0.0117,  0.0110,  0.0104,  0.0099,  0.0093,  0.0089,  0.0084, }, 
+{  0.1726,  0.1361,  0.1110,  0.0928,  0.0791,  0.0685,  0.0600,  0.0531,  0.0475,  0.0427,  0.0387,  0.0353,  0.0323,  0.0297,  0.0275,  0.0255,  0.0237,  0.0221,  0.0207,  0.0195,  0.0183,  0.0172,  0.0163,  0.0154,  0.0146,  0.0139,  0.0132,  0.0126,  0.0120, }, 
+{  0.1881,  0.1513,  0.1255,  0.1065,  0.0919,  0.0805,  0.0713,  0.0637,  0.0574,  0.0521,  0.0476,  0.0437,  0.0403,  0.0373,  0.0347,  0.0323,  0.0303,  0.0284,  0.0267,  0.0252,  0.0238,  0.0225,  0.0214,  0.0203,  0.0193,  0.0184,  0.0176,  0.0168,  0.0161, }, 
+{  0.2017,  0.1649,  0.1387,  0.1191,  0.1040,  0.0919,  0.0821,  0.0740,  0.0672,  0.0614,  0.0564,  0.0521,  0.0483,  0.0450,  0.0420,  0.0394,  0.0370,  0.0349,  0.0329,  0.0312,  0.0296,  0.0281,  0.0268,  0.0255,  0.0244,  0.0233,  0.0223,  0.0214,  0.0205, }, 
+{  0.2138,  0.1771,  0.1507,  0.1308,  0.1152,  0.1027,  0.0924,  0.0839,  0.0767,  0.0705,  0.0651,  0.0604,  0.0563,  0.0527,  0.0494,  0.0465,  0.0439,  0.0415,  0.0393,  0.0374,  0.0356,  0.0339,  0.0324,  0.0310,  0.0296,  0.0284,  0.0273,  0.0262,  0.0252, }, 
+{  0.2246,  0.1883,  0.1618,  0.1416,  0.1257,  0.1128,  0.1022,  0.0933,  0.0857,  0.0792,  0.0735,  0.0686,  0.0642,  0.0603,  0.0568,  0.0536,  0.0508,  0.0482,  0.0458,  0.0436,  0.0416,  0.0398,  0.0381,  0.0365,  0.0351,  0.0337,  0.0324,  0.0313,  0.0301, }, 
+{  0.2347,  0.1986,  0.1721,  0.1517,  0.1355,  0.1224,  0.1115,  0.1023,  0.0945,  0.0877,  0.0817,  0.0765,  0.0719,  0.0677,  0.0640,  0.0606,  0.0576,  0.0548,  0.0522,  0.0499,  0.0477,  0.0458,  0.0439,  0.0422,  0.0406,  0.0391,  0.0377,  0.0364,  0.0352, }, 
+{  0.2448,  0.2086,  0.1819,  0.1614,  0.1450,  0.1316,  0.1204,  0.1110,  0.1029,  0.0958,  0.0897,  0.0842,  0.0793,  0.0750,  0.0711,  0.0675,  0.0643,  0.0613,  0.0586,  0.0561,  0.0538,  0.0517,  0.0497,  0.0479,  0.0462,  0.0445,  0.0430,  0.0416,  0.0403, }, 
+}; 
+
+static float interpolatePixels (float V00, float V10, float V01, float V11, float dx, float dy) {
+    // bilinear interpolation
+    float rx = 1.0 - dx;
+    float ry = 1.0 - dy;
+    float value = V00*rx*ry + V10*dx*ry + V01*rx*dy + V11*dx*dy;
+    return value;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_SERSIC.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_SERSIC.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_SERSIC.c	(revision 42651)
@@ -0,0 +1,510 @@
+/******************************************************************************
+ * this file defines the SERSIC source shape model.  Note that these model functions are loaded
+ * by pmModelClass.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 as 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 (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   - normalized sersic parameter
+
+   * note that a Sersic model is usually defined in terms of R_e, the half-light radius.  This
+     construction does not include a factor of 2 in the X^2 term, etc, like for a Gaussian.
+     Conversion from SXX, SYY, SXY to R_major, R_minor, theta can be done by using:
+     shape.sx = SXX / sqrt(2), shape.sy = SYY / sqrt(2), shape.sxy = SXY, then calling
+     psEllipseShapeToAxes, and multiplying the values of axes.major, axes.minor by sqrt(2)
+
+   note that a standard sersic model uses exp(-K*(z^(1/2n) - 1).  the additional elements (K,
+   the -1 offset) are absorbed in this model by the normalization, the exponent, and the
+   radial scale.  We fit the elements in this form, then re-normalize them on output.
+   *****************************************************************************/
+
+#include <stdio.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+#include "pmModel_CentralPixel.h"
+
+#include "pmModel_SERSIC.h"
+
+# define PM_MODEL_NPARAM          8
+# 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_SET_FWHM        pmModelSetFWHM_SERSIC
+# define PM_MODEL_FROM_PSF        pmModelFromPSF_SERSIC
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_SERSIC
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_SERSIC
+# define PM_MODEL_SET_LIMITS      pmModelSetLimits_SERSIC
+
+// the model is a function of the pixel coordinate (pixcoord[0,1] = x,y)
+// 0.5 PIX: the parameters are defined in terms of pixel coords, so the incoming pixcoords
+// values need to be pixel coords
+
+// Lax parameter limits
+static float paramsMinLax[] = { -1.0e3, 1.0e-2, -100, -100, 0.001, 0.001, -1.0, 0.0625 };
+static float paramsMaxLax[] = { 1.0e5, 1.0e9, 1.0e5, 1.0e5, 100, 100, 1.0, 1.0 };
+
+// Moderate parameter limits
+static float *paramsMinModerate = paramsMinLax;
+static float *paramsMaxModerate = paramsMaxLax;
+
+// Strict parameter limits
+static float *paramsMinStrict = paramsMinLax;
+static float *paramsMaxStrict = paramsMaxLax;
+
+// Parameter limits to use
+static float *paramsMinUse = paramsMinLax;
+static float *paramsMaxUse = paramsMaxLax;
+static float betaUse[] = { 2, 3e6, 5, 5, 10.0, 10.0, 0.5, 1.0};
+
+static bool limitsApply = true;         // Apply limits?
+
+// # include "pmModel_SERSIC.CP.h"
+
+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;
+
+    // If the elliptical contour is defined in a valid way, we should never trigger this
+    // assert.  Other models (like PGAUSS) don't use fractional powers, and thus do not have
+    // NaN values for negative values of z
+    psAssert (z >= 0, "do not allow negative z values in model");
+
+    float Sindex = 0.5 / PAR[PM_PAR_7];
+    float kappa = pmSersicKappa (Sindex);
+
+    float q = kappa*pow(z,PAR[PM_PAR_7]);
+    psF32 f0 = exp(-q);
+
+    assert (isfinite(q));
+
+    psF32 radius = hypot(X, Y);
+    if (radius <= 1.5) {
+	// Nsub ~ 10*index^2 + 1
+	psEllipseAxes axes = pmPSF_ModelToAxes(PAR, true); // SERSIC model uses Reff
+	int Nsub = 2 * ((int)(6.0*Sindex / axes.minor)) + 1;
+	Nsub = PS_MIN (Nsub, 121);
+	Nsub = PS_MAX (Nsub, 11);
+	f0 = pmModelCP_SersicSubpix (X, Y, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], Sindex, Nsub);
+    }
+    if (!isfinite(f0)) {
+        fprintf(stderr, "f0 is not finite for %f %f %f %f %f.  Parameters: \n", X, Y, radius, z, q);
+        fprintf(stderr, "%f %f %f %f %f %f %f %f\n", PAR[0], PAR[1], PAR[2], PAR[3], PAR[4],
+            PAR[5], PAR[6], PAR[7]);
+    }
+    assert (isfinite(f0));
+
+    psF32 f1 = PAR[PM_PAR_I0]*f0;
+    psF32 f = PAR[PM_PAR_SKY] + f1;
+
+    assert (isfinite(f1));
+    assert (isfinite(f));
+
+    if (deriv != NULL) {
+        psF32 *dPAR = deriv->data.F32;
+
+        dPAR[PM_PAR_SKY]  = +1.0;
+        dPAR[PM_PAR_I0]   = +f0;
+
+	if (z > 0.01) {
+	  float z1 = f1*kappa*PAR[PM_PAR_7]*pow(z,PAR[PM_PAR_7]-1.0);
+	  dPAR[PM_PAR_XPOS] = +1.0*z1*(2.0*px + Y*PAR[PM_PAR_SXY]);
+	  dPAR[PM_PAR_YPOS] = +1.0*z1*(2.0*py + 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_7]    = -1.0*f1*q*log(z);
+	} else {
+	  // gradient -> 0 for z -> 0, but has undef form
+	  float z1 = f1*kappa*PAR[PM_PAR_7]*pow(z,PAR[PM_PAR_7]);
+	  dPAR[PM_PAR_XPOS] = +1.0*z1*(2.0/PAR[PM_PAR_SXX] + PAR[PM_PAR_SXY]);
+	  dPAR[PM_PAR_YPOS] = +1.0*z1*(2.0/PAR[PM_PAR_SYY] + PAR[PM_PAR_SXY]);
+	  dPAR[PM_PAR_SXX]  = +2.0*z1*px/PAR[PM_PAR_SXX]/PAR[PM_PAR_SXX];
+	  dPAR[PM_PAR_SYY]  = +2.0*z1*py/PAR[PM_PAR_SYY]/PAR[PM_PAR_SYY];
+	  dPAR[PM_PAR_SXY]  = -1.0*z1;
+	  // dPAR[PM_PAR_7]    = -1.0*f1*q*log(z + 0.0001);
+	  dPAR[PM_PAR_7]    = -1.0*f1*q*log(z + 0.0001); // factor of 16 to reduce the gain
+	}
+        assert (isfinite(dPAR[PM_PAR_7]));
+    }
+    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)
+{
+    if (!limitsApply) {
+        return true;
+    }
+    psAssert(nParam >= 0 && nParam < PM_MODEL_NPARAM, "Parameter index is out of bounds");
+
+    // we need to calculate the limits for SXY specially
+    float q2 = NAN;
+    if (nParam == PM_PAR_SXY) {
+        float f1 = 1.0 / PS_SQR(params[PM_PAR_SYY]) + 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        float f2 = 1.0 / PS_SQR(params[PM_PAR_SYY]) - 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        float 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
+	// NOTE: the factor of 2 is needed to convert par[SXX,SYY] to shape.sx,sy
+        q2 = 2.0*0.5*sqrtf(q1);
+    }
+
+    switch (mode) {
+      case PS_MINIMIZE_BETA_LIMIT: {
+          psAssert(beta, "Require beta to limit beta");
+          float limit = betaUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (fabs(beta[nParam]) > fabs(limit)) {
+              beta[nParam] = (beta[nParam] > 0) ? fabs(limit) : -fabs(limit);
+              psTrace("psModules.objects", 5, "|beta[nParam==%d]| > |beta_lim|; %g v. %g",
+                      nParam, beta[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MIN: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMinUse, "Require parameter limits to limit parameters");
+          float limit = paramsMinUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] < limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] < params_min; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MAX: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMaxUse, "Require parameter limits to limit parameters");
+          float limit = paramsMaxUse[nParam];
+          if (nParam == PM_PAR_SXY) {
+              limit *= q2;
+          }
+          if (params[nParam] > limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] > params_max; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      default:
+        psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+// make an initial guess for parameters
+// 0.5 PIX: moments and peaks are in pixel coords, thus so are model parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal)
+{
+    pmMoments *moments = source->moments;
+    psF32     *PAR  = model->params->data.F32;
+
+    // sky is set to 0.0
+    PAR[PM_PAR_SKY]  = 0.0;
+
+    // the other parameters depend on the guess for PAR_7
+    if (!isfinite(PAR[PM_PAR_7])) {
+	PAR[PM_PAR_7]    = 0.25;
+    }    
+    float index = 0.5 / PAR[PM_PAR_7];
+
+    // the scale-length is a function of the moments and the index:
+    // Rmajor_guess = Rmajor_moments * Scale * Zero
+    float Scale = 0.70 + 0.053 * PAR[PM_PAR_7];
+    float Zero  = 1.16 - 0.615 * PAR[PM_PAR_7];
+
+    // Sersic shape is a bit special
+    psEllipseMoments emoments;
+    emoments.x2 = moments->Mxx;
+    emoments.xy = moments->Mxy;
+    emoments.y2 = moments->Myy;
+
+    // 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;
+
+    // set a lower limit to avoid absurd solutions..
+    float Rmajor = PS_MAX(1.0, Scale * axes.major + Zero);
+    float Rminor = Rmajor * (axes.minor / axes.major);
+
+    // fprintf (stderr, "guess index: %f : %f, %f -> %f, %f\n", index, axes.major, axes.minor, Rmajor, Rminor);
+
+    axes.major = Rmajor;
+    axes.minor = Rminor;
+
+    pmModelAxesToParams (&PAR[PM_PAR_SXX], &PAR[PM_PAR_SXY], &PAR[PM_PAR_SYY], axes, true);
+
+    float bn = 1.9992*index - 0.3271;
+    // float fR = 1.0 / (sqrt(2.0) * pow (bn, index));
+    float Io = exp(0.5*bn);
+
+    // set the model normalization (adjust for Sersic best guess)
+    if (!pmModelSetNorm(&PAR[PM_PAR_I0], source)) {
+      return false;
+    }
+    PAR[PM_PAR_I0] /= Io;
+
+    // set the model position
+    if (!pmModelSetPosition(&PAR[PM_PAR_XPOS], &PAR[PM_PAR_YPOS], source)) {
+      return false;
+    }
+
+    return(true);
+}
+
+// A sersic model has an integral flux which can be analytically represented
+psF64 PM_MODEL_FLUX (const psVector *params)
+{
+    psF32 *PAR = params->data.F32;
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], true);
+
+    float Sindex = 0.5 / PAR[PM_PAR_7];
+    float norm = pmSersicNorm (Sindex);
+
+    float flux = PAR[PM_PAR_I0] * 2.0 * M_PI * axes.major * axes.minor * 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)
+{
+    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);
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], true);
+
+    float Sindex = 0.5 / PAR[PM_PAR_7];
+    float kappa = pmSersicKappa (Sindex);
+
+    // f = Io exp(-z^n) -> z^n = ln(Io/f)
+    psF64 zn = log(PAR[PM_PAR_I0] / flux) / kappa;
+    psF64 radius = axes.major * pow(zn, Sindex);
+
+    psAssert (isfinite(radius), "fix this code: radius should not be nan for Io = %f, flux = %f, major = %f (%f, %f, %f), par 7 = %f", 
+	      PAR[PM_PAR_I0], flux, axes.major, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], PAR[PM_PAR_7]);
+    return (radius);
+}
+
+psF64 PM_MODEL_SET_FWHM (const psVector *params, psF64 sigma) {
+  return (NAN);
+}
+
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, const 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)
+    bool useReff = modelPSF->class->useReff;
+    if (!pmPSF_FitToModel (out, 0.1, useReff)) {
+        psTrace("psModules.objects", 5, "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, const 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?
+    bool useReff = model->class->useReff;
+    if (!pmPSF_FitToModel (PAR, 0.1, useReff)) {
+        psTrace ("psModules.objects", 3, "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)
+{
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    status = true;
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+
+void PM_MODEL_SET_LIMITS(pmModelLimitsType type)
+{
+    switch (type) {
+      case PM_MODEL_LIMITS_NONE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_IGNORE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = false;
+        break;
+      case PM_MODEL_LIMITS_LAX:
+        paramsMinUse = paramsMinLax;
+        paramsMaxUse = paramsMaxLax;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_MODERATE:
+        paramsMinUse = paramsMinModerate;
+        paramsMaxUse = paramsMaxModerate;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_STRICT:
+        paramsMinUse = paramsMinStrict;
+        paramsMaxUse = paramsMaxStrict;
+        limitsApply = true;
+        break;
+      default:
+        psAbort("Unrecognised model limits type: %x", type);
+    }
+    return;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_SERSIC.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_SERSIC.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_SERSIC.h	(revision 42651)
@@ -0,0 +1,16 @@
+#ifndef PM_MODEL_SERSIC_H
+
+#include "pmModel.h"
+
+psF32 pmModelFunc_SERSIC(psVector *deriv, const psVector *params, const psVector *pixcoord);
+bool pmModelLimits_SERSIC(psMinConstraintMode mode, int nParam, float *params, float *beta);
+bool pmModelGuess_SERSIC(pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal);
+psF64 pmModelFlux_SERSIC(const psVector *params);
+psF64 pmModelRadius_SERSIC(const psVector *params, psF64 flux);
+psF64 pmModelSetFWHM_SERSIC(const psVector *params, psF64 flux);
+bool pmModelFromPSF_SERSIC(pmModel *modelPSF, pmModel *modelFLT, const pmPSF *psf);
+bool  pmModelParamsFromPSF_SERSIC(pmModel *model, const pmPSF *psf, float Xo, float Yo, float Io);
+bool pmModelFitStatus_SERSIC(pmModel *model);
+void pmModelSetLimits_SERSIC(pmModelLimitsType type);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_TRAIL.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_TRAIL.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_TRAIL.c	(revision 42651)
@@ -0,0 +1,487 @@
+/******************************************************************************
+ * this file defines the TRAIL source shape model.  This represents an infinitely thin
+ * line of length convolved with a Gaussian PSF.  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 as a PSF representations share a few parameters, for which #
+#include "pmModelClass.h"
+ * 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     - flux normalization
+ * PM_PAR_XPOS 2   - X center of object
+ * PM_PAR_YPOS 3   - Y center of object
+ * PM_PAR_LENGTH 4 - trail length
+ * PM_PAR_THETA 5  - position angle
+ * PM_PAR_SIGMA 6  - PSF Gaussian sigma (not fitted?)
+ *****************************************************************************/
+
+#include <stdio.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmModel_TRAIL.h"
+
+# define PM_MODEL_NPARAM          7
+# define PM_MODEL_FUNC            pmModelFunc_TRAIL
+# define PM_MODEL_FLUX            pmModelFlux_TRAIL
+# define PM_MODEL_GUESS           pmModelGuess_TRAIL
+# define PM_MODEL_LIMITS          pmModelLimits_TRAIL
+# define PM_MODEL_RADIUS          pmModelRadius_TRAIL
+# define PM_MODEL_SET_FWHM        pmModelSetFWHM_TRAIL
+# define PM_MODEL_FROM_PSF        pmModelFromPSF_TRAIL
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_TRAIL
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_TRAIL
+# define PM_MODEL_SET_LIMITS      pmModelSetLimits_TRAIL
+
+// Lax parameter limits 
+static float paramsMinLax[] = { -1.0e3, 1.0e-2, -1.0e2, -1.0e2,   0.5, -3.3, -0.5 };
+static float paramsMaxLax[] = {  1.0e5, 1.00+9, +1.0e5, +1.0e5, 150.0, +3.3 , 5.0 };
+
+// Moderate parameter limits
+static float *paramsMinModerate = paramsMinLax;
+static float *paramsMaxModerate = paramsMaxLax;
+
+// Strict parameter limits
+static float *paramsMinStrict = paramsMinLax;
+static float *paramsMaxStrict = paramsMaxLax;
+
+// Parameter limits to use
+static float *paramsMinUse = paramsMinLax;
+static float *paramsMaxUse = paramsMaxLax;
+static float betaUse[] = { 1000, 3e6, 5, 5, 2.0, 0.1, 0.1 };
+
+static bool limitsApply = true;         // Apply limits?
+
+// the model is a function of the pixel coordinate (pixcoord[0,1] = x,y)
+// 0.5 PIX: the parameters are defined in terms of pixel coords, so the incoming pixcoords
+// values need to be pixel coords
+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 ST = sin(PAR[PM_PAR_THETA]);
+    psF32 CT = cos(PAR[PM_PAR_THETA]);
+
+    psF32 S2 = 2.0 * PS_SQR(PAR[PM_PAR_SIGMA]);
+
+    psF32 Zp = (X*CT + Y*ST + 0.5*PAR[PM_PAR_LENGTH]) / sqrt(S2);
+    psF32 Zm = (X*CT + Y*ST - 0.5*PAR[PM_PAR_LENGTH]) / sqrt(S2);
+
+    // psF32 Zp = (X*CT + Y*ST + 0.5*PAR[PM_PAR_LENGTH]) / sqrt(S2);
+    // psF32 Zm = (X*CT + Y*ST - 0.5*PAR[PM_PAR_LENGTH]) / sqrt(S2);
+
+    psF32 Ep = erf(Zp);
+    psF32 Em = erf(Zm);
+
+    psF32 Rxy = Y*CT - X*ST;
+    psF32 Gxy = exp(-Rxy*Rxy/S2);
+
+    psF32 Pxy = Gxy * (Ep - Em);
+    psF32 f = Pxy * PAR[PM_PAR_I0] + PAR[PM_PAR_SKY];
+
+    if (deriv != NULL) {
+        psF32 *dPAR = deriv->data.F32;
+
+        dPAR[PM_PAR_SKY]    = 1.0;
+        dPAR[PM_PAR_I0]     = Pxy;
+
+	float dGdR = -2.0 * Rxy * Gxy / S2; // -R Gxy / (2 Sigma^2)
+
+	// are these signs correct? I think so: (dR/dXo = -dR/dX); dRdX below is actually dR/dXo
+	float dRdX = +ST;
+	float dRdY = -CT;
+	float dRdT = -Y*ST - X*CT;
+
+	float dGdX = dGdR * dRdX;
+	float dGdY = dGdR * dRdY;
+	float dGdT = dGdR * dRdT;
+
+	// are these signs correct? I think so: (dR/dXo = -dR/dX); dRdX below is actually dR/dXo
+	float dZpdX = -CT / sqrt(S2);
+	float dZmdX = dZpdX; // float dZmdX = -CT / sqrt(S2); dZmdX = dZpdX
+
+	float dZpdY = -ST / sqrt(S2); // float dZmdY = -ST / sqrt(S2); dZmdY = dZpdY
+	float dZmdY = dZpdY;
+
+	float dZpdL = +0.5 / sqrt(S2);
+	float dZmdL = -0.5 / sqrt(S2);
+
+	float dZpdT = (-X*ST + Y*CT) / sqrt(S2);
+	float dZmdT = dZpdT; // dZpdT = dZmdT
+
+	float dEdZp = exp (-Zp*Zp) * M_2_SQRTPI;
+	float dEdZm = exp (-Zm*Zm) * M_2_SQRTPI;
+
+	float dEpdX = dEdZp * dZpdX;
+	float dEmdX = dEdZm * dZmdX;
+
+	float dEpdY = dEdZp * dZpdY;
+	float dEmdY = dEdZm * dZmdY;
+
+	float dEpdL = dEdZp * dZpdL;
+	float dEmdL = dEdZm * dZmdL;
+
+	float dEpdT = dEdZp * dZpdT;
+	float dEmdT = dEdZm * dZmdT;
+
+	float dPdX = dGdX * (Ep - Em) + Gxy * (dEpdX - dEmdX);
+	float dPdY = dGdY * (Ep - Em) + Gxy * (dEpdY - dEmdY);
+
+	// dGdL is 0.0 because dRdL is 0.0
+	float dPdL = Gxy * (dEpdL - dEmdL);
+
+	float dPdT = dGdT * (Ep - Em) + Gxy * (dEpdT - dEmdT);
+
+        dPAR[PM_PAR_XPOS]   = PAR[PM_PAR_I0] * dPdX;
+        dPAR[PM_PAR_YPOS]   = PAR[PM_PAR_I0] * dPdY;
+
+        dPAR[PM_PAR_LENGTH] = PAR[PM_PAR_I0] * dPdL;
+        dPAR[PM_PAR_THETA]  = PAR[PM_PAR_I0] * dPdT;
+        dPAR[PM_PAR_SIGMA]  = 0;	// we don't actually allow this to vary, so we do not need to calculate it
+
+	//	for (int i = 0; i < 7; i++) {
+	//	  if (isnan(dPAR[i])) {
+	//	    fprintf (stderr, "*");
+	//	  }
+	//	}
+    }
+    //    if (isnan(f)) {
+    //      fprintf (stderr, "!");
+    //    }
+    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)
+{
+    if (!limitsApply) {
+        return true;
+    }
+    psAssert(nParam >= 0 && nParam < PM_MODEL_NPARAM, "Parameter index is out of bounds");
+
+    switch (mode) {
+      case PS_MINIMIZE_BETA_LIMIT: {
+          psAssert(beta, "Require beta to limit beta");
+          float limit = betaUse[nParam];
+          if (fabs(beta[nParam]) > fabs(limit)) {
+              beta[nParam] = (beta[nParam] > 0) ? fabs(limit) : -fabs(limit);
+              psTrace("psModules.objects", 5, "|beta[nParam==%d]| > |beta_lim|; %g v. %g",
+                      nParam, beta[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MIN: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMinUse, "Require parameter limits to limit parameters");
+          float limit = paramsMinUse[nParam];
+          if (params[nParam] < limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] < params_min; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+      case PS_MINIMIZE_PARAM_MAX: {
+          psAssert(params, "Require parameters to limit parameters");
+          psAssert(paramsMaxUse, "Require parameter limits to limit parameters");
+          float limit = paramsMaxUse[nParam];
+          if (params[nParam] > limit) {
+              params[nParam] = limit;
+              psTrace("psModules.objects", 5, "params[nParam==%d] > params_max; %g v. %g",
+                      nParam, params[nParam], limit);
+              return false;
+          }
+          return true;
+      }
+    default:
+        psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+# define NA 21
+# define NR 21
+static float flux[NA][NR];
+static float npix[NA][NR];
+
+bool pmTrailGetAngle (float *To, float *Io, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal, float sigma) {
+
+  float Xo, Yo;
+  if (!pmModelSetPosition(&Xo, &Yo, source)) return false;
+
+  psImage *image = source->pixels;
+  psImage *mask = source->maskObj;
+  psF32 **imData = image->data.F32;
+  psImageMaskType **mkData = mask->data.PS_TYPE_IMAGE_MASK_DATA;
+
+  // do a loop over the pixels, generating (dX,dY) dot (cos(theta),sin(theta))
+  int dP;
+  int dX = Xo - mask->col0;
+  dP = mask->numCols - dX;
+  int DX = PS_MAX(dP, dX);
+  int NX = mask->numCols;
+
+  int dY = Yo - mask->row0;
+  dP = mask->numRows - dY;
+  int DY = PS_MAX(dP, dY);
+  int NY = mask->numRows;
+
+  // just hard wire this for now...
+  float radius = 10.0;
+  float radius2 = PS_SQR(radius);
+
+  // we have an array of Angles x Radii
+  float dT = M_PI / NA;
+  for (int na = 0; na < NA; na++) {
+    memset (flux[na], 0, NR*sizeof(float));
+    memset (npix[na], 0, NR*sizeof(float));
+  }
+
+  // we skip any pixels [real or virtual] outside of the specified radius (nominally the aperture radius)
+  // ix and iy track pixels relative to the centroid
+  for (int ix = -DX; ix < DX + 1; ix++) {
+    if (ix > radius) continue;
+    int mx = ix + dX;
+    for (int iy = -DY; iy < DY + 1; iy++) {
+      if (iy > radius) continue;
+      if (ix*ix + iy*iy > radius2) continue;
+      int my = iy + dY;
+      
+      // 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;
+      
+      // count pixels which are masked only with bad pixels
+      if (mkData[my][mx] & maskVal)continue;
+
+      // we have defined NA to be 21
+      int na = 0;
+      for (float angle = 0.0; na < NA; angle += dT, na ++) {
+
+	// XXX optimization : pre-compute the angle sines and cosines
+	float Rad = (ix * cos(angle)) + (iy * sin(angle));
+	int nr = PS_MAX (PS_MIN (NR, Rad + 0.5*NR), 0);
+
+	flux[na][nr] += imData[my][mx];
+	npix[na][nr] ++;
+      }
+    }
+  }
+
+  // generate a psf model (with integral of 1.0)
+  float Ao = 1.0 / sqrt(2*M_PI) / sigma;
+  float psf[NR];
+  for (int nr = 0; nr < NR; nr++) {
+    psf[nr] = Ao*exp(-0.5*PS_SQR((nr - 0.5*NR)/sigma));
+  }
+
+  float fangle[NA];
+  for (int na = 0; na < NA; na++) {
+    float fpsf = 0.0;
+    for (int nr = 0; nr < NR; nr++) {
+      fpsf += psf[nr]*flux[na][nr];
+    }
+    fangle[na] = fpsf;
+    // fprintf (stderr, "fpsf: %f, theta = %f\n", fpsf, PS_DEG_RAD*dT*na);
+  }
+
+  float peak = fangle[0];
+  int pbin = 0;
+  for (int na = 0; na < NA; na++) {
+    if (fangle[na] > peak) {
+      peak = fangle[na];
+      pbin = na;
+    }
+  }
+
+  // result is peak, pbin -> angle
+  *To = dT * pbin;
+  *Io = peak * Ao;
+  
+  return true;
+}
+
+// make an initial guess for parameters
+// 0.5 PIX: moments and peaks are in pixel coords, thus so are model parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal)
+{
+    psF32 *PAR  = model->params->data.F32;
+
+    // sky is set to 0.0
+    PAR[PM_PAR_SKY]  = 0.0;
+
+    psF32 *psfPAR  = source->modelPSF->params->data.F32;
+    bool useReff = source->modelPSF->class->useReff;
+
+    psEllipseAxes psfAxes;
+    pmModelParamsToAxes (&psfAxes, psfPAR[PM_PAR_SXX], psfPAR[PM_PAR_SXY], psfPAR[PM_PAR_SYY], useReff);
+
+    psEllipseMoments emoments;
+    emoments.x2 = source->moments->Mxx;
+    emoments.xy = source->moments->Mxy;
+    emoments.y2 = source->moments->Myy;
+
+    // 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;
+
+    float size = NAN;
+    if (!isfinite(source->moments->Mrf)) {
+      size = axes.major;
+    } else {
+      size = (axes.major > sqrt(source->moments->Mrf)) ? axes.major : sqrt(source->moments->Mrf);
+    }
+
+    float theta, peak;
+    pmTrailGetAngle (&theta, &peak, source, maskVal, markVal, psfAxes.major);
+
+    // axes.major is a sigma in the major direction; scale to 
+    PAR[PM_PAR_LENGTH] = 1.5*2.35*size; // a tophat of length L has L = 1.5 * 2.35 * sigma
+    PAR[PM_PAR_THETA] = axes.theta; // theta in radians
+    PAR[PM_PAR_SIGMA] = psfAxes.major; // psf major axes (sigma of the psf)
+
+    // set the model normalization
+    // if (!pmModelSetNorm(&PAR[PM_PAR_I0], source)) {
+    //   return false;
+    // }
+    PAR[PM_PAR_I0] = peak;
+
+    // set the model position
+    if (!pmModelSetPosition(&PAR[PM_PAR_XPOS], &PAR[PM_PAR_YPOS], source)) {
+       return false;
+    }
+
+    return(true);
+}
+
+psF64 PM_MODEL_FLUX (const psVector *params)
+{
+    psF32 *PAR = params->data.F32;
+    psF64 Flux = PAR[PM_PAR_I0] * PAR[PM_PAR_LENGTH] * PAR[PM_PAR_SIGMA] * 2.0 * sqrt(2.0 * M_PI);
+    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)
+{
+    psF32 *PAR = params->data.F32;
+
+    // PAR_LENGTH is the unconvolved length.  add a bit for safety
+    return (0.5*PAR[PM_PAR_LENGTH] + 2);
+}
+
+psF64 PM_MODEL_SET_FWHM (const psVector *params, psF64 sigma) {
+  return (NAN);
+}
+
+// construct the PSF model from the FLT model and the psf
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, const pmPSF *psf)
+{
+    psWarning ("do you really want to use a trail as a PSF model??");
+    return false;
+}
+
+// generate a model based on a the psf model
+bool PM_MODEL_PARAMS_FROM_PSF (pmModel *model, const pmPSF *psf, float Xo, float Yo, float Io)
+{
+    psWarning ("do you really want to use a trail as a PSF model??");
+    return false;
+}
+
+// check the status of the fitted model
+// this test is invalid if the parameters are derived
+// from the PSF model
+// XXX how is this used?  it prevents forced photometry from ever being 'successful'
+bool PM_MODEL_FIT_STATUS (pmModel *model)
+{
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    status = true;
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+void PM_MODEL_SET_LIMITS(pmModelLimitsType type)
+{
+    switch (type) {
+      case PM_MODEL_LIMITS_NONE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_IGNORE:
+        paramsMinUse = NULL;
+        paramsMaxUse = NULL;
+        limitsApply = false;
+        break;
+      case PM_MODEL_LIMITS_LAX:
+        paramsMinUse = paramsMinLax;
+        paramsMaxUse = paramsMaxLax;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_MODERATE:
+        paramsMinUse = paramsMinModerate;
+        paramsMaxUse = paramsMaxModerate;
+        limitsApply = true;
+        break;
+      case PM_MODEL_LIMITS_STRICT:
+        paramsMinUse = paramsMinStrict;
+        paramsMaxUse = paramsMaxStrict;
+        limitsApply = true;
+        break;
+      default:
+        psAbort("Unrecognised model limits type: %x", type);
+    }
+    return;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_TRAIL.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_TRAIL.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/pmModel_TRAIL.h	(revision 42651)
@@ -0,0 +1,16 @@
+#ifndef PM_MODEL_TRAIL_H
+
+#include "pmModel.h"
+
+psF32 pmModelFunc_TRAIL(psVector *deriv, const psVector *params, const psVector *pixcoord);
+bool pmModelLimits_TRAIL(psMinConstraintMode mode, int nParam, float *params, float *beta);
+bool pmModelGuess_TRAIL(pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal);
+psF64 pmModelFlux_TRAIL(const psVector *params);
+psF64 pmModelRadius_TRAIL(const psVector *params, psF64 flux);
+psF64 pmModelSetFWHM_TRAIL(const psVector *params, psF64 flux);
+bool pmModelFromPSF_TRAIL(pmModel *modelPSF, pmModel *modelFLT, const pmPSF *psf);
+bool  pmModelParamsFromPSF_TRAIL(pmModel *model, const pmPSF *psf, float Xo, float Yo, float Io);
+bool pmModelFitStatus_TRAIL(pmModel *model);
+void pmModelSetLimits_TRAIL(pmModelLimitsType type);
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/models/ps1v1.fwhm.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/ps1v1.fwhm.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/ps1v1.fwhm.h	(revision 42651)
@@ -0,0 +1,111 @@
+# define FWHM_BIN 0.2
+# define MIN_FWHM_BIN -1
+# define N_FWHM_BIN 106
+static float PS1_V1_Scale[] = {
+3.89, // -1.0, 1.89
+3.64, // -0.8, 1.66
+3.42, // -0.6, 1.46
+3.20, // -0.4, 1.28
+3.01, // -0.2, 1.13
+2.83, //  0.0, 1.00
+2.67, //  0.2, 0.89
+2.52, //  0.4, 0.79
+2.39, //  0.6, 0.71
+2.27, //  0.8, 0.65
+2.17, //  1.0, 0.59
+2.07, //  1.2, 0.54
+1.99, //  1.4, 0.49
+1.91, //  1.6, 0.46
+1.84, //  1.8, 0.42
+1.78, //  2.0, 0.39
+1.72, //  2.2, 0.37
+1.66, //  2.4, 0.35
+1.61, //  2.6, 0.33
+1.57, //  2.8, 0.31
+1.53, //  3.0, 0.29
+1.49, //  3.2, 0.28
+1.45, //  3.4, 0.26
+1.41, //  3.6, 0.25
+1.38, //  3.8, 0.24
+1.35, //  4.0, 0.23
+1.32, //  4.2, 0.22
+1.30, //  4.4, 0.21
+1.27, //  4.6, 0.20
+1.25, //  4.8, 0.19
+1.23, //  5.0, 0.19
+1.20, //  5.2, 0.18
+1.18, //  5.4, 0.18
+1.16, //  5.6, 0.17
+1.15, //  5.8, 0.16
+1.13, //  6.0, 0.16
+1.11, //  6.2, 0.15
+1.09, //  6.4, 0.15
+1.08, //  6.6, 0.15
+1.06, //  6.8, 0.14
+1.05, //  7.0, 0.14
+1.04, //  7.2, 0.13
+1.02, //  7.4, 0.13
+1.01, //  7.6, 0.13
+1.00, //  7.8, 0.12
+0.99, //  8.0, 0.12
+0.97, //  8.2, 0.12
+0.96, //  8.4, 0.12
+0.95, //  8.6, 0.11
+0.94, //  8.8, 0.11
+0.93, //  9.0, 0.11
+0.92, //  9.2, 0.11
+0.91, //  9.4, 0.10
+0.90, //  9.6, 0.10
+0.89, //  9.8, 0.10
+0.89, // 10.0, 0.10
+0.88, // 10.2, 0.10
+0.87, // 10.4, 0.09
+0.86, // 10.6, 0.09
+0.85, // 10.8, 0.09
+0.85, // 11.0, 0.09
+0.84, // 11.2, 0.09
+0.83, // 11.4, 0.09
+0.82, // 11.6, 0.08
+0.82, // 11.8, 0.08
+0.81, // 12.0, 0.08
+0.80, // 12.2, 0.08
+0.80, // 12.4, 0.08
+0.79, // 12.6, 0.08
+0.79, // 12.8, 0.08
+0.78, // 13.0, 0.08
+0.77, // 13.2, 0.07
+0.77, // 13.4, 0.07
+0.76, // 13.6, 0.07
+0.76, // 13.8, 0.07
+0.75, // 14.0, 0.07
+0.75, // 14.2, 0.07
+0.74, // 14.4, 0.07
+0.74, // 14.6, 0.07
+0.73, // 14.8, 0.07
+0.73, // 15.0, 0.07
+0.72, // 15.2, 0.07
+0.72, // 15.4, 0.06
+0.71, // 15.6, 0.06
+0.71, // 15.8, 0.06
+0.70, // 16.0, 0.06
+0.70, // 16.2, 0.06
+0.70, // 16.4, 0.06
+0.69, // 16.6, 0.06
+0.69, // 16.8, 0.06
+0.68, // 17.0, 0.06
+0.68, // 17.2, 0.06
+0.68, // 17.4, 0.06
+0.67, // 17.6, 0.06
+0.67, // 17.8, 0.06
+0.66, // 18.0, 0.06
+0.66, // 18.2, 0.05
+0.66, // 18.4, 0.05
+0.65, // 18.6, 0.05
+0.65, // 18.8, 0.05
+0.65, // 19.0, 0.05
+0.64, // 19.2, 0.05
+0.64, // 19.4, 0.05
+0.64, // 19.6, 0.05
+0.63, // 19.8, 0.05
+0.63, // 20.0, 0.05
+};
Index: /branches/eam_branches/psModules.20240412/src/objects/models/qgauss.fwhm.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/models/qgauss.fwhm.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/models/qgauss.fwhm.h	(revision 42651)
@@ -0,0 +1,111 @@
+# define FWHM_BIN 0.2
+# define MIN_FWHM_BIN -1
+# define N_FWHM_BIN 106
+static float QGAUSS_Scale[] = {
+3.47, // -1.0, 1.50
+3.34, // -0.8, 1.40
+3.21, // -0.6, 1.29
+3.08, // -0.4, 1.19
+2.96, // -0.2, 1.09
+2.83, //  0.0, 1.00
+2.70, //  0.2, 0.91
+2.58, //  0.4, 0.83
+2.47, //  0.6, 0.76
+2.36, //  0.8, 0.70
+2.26, //  1.0, 0.64
+2.16, //  1.2, 0.58
+2.07, //  1.4, 0.54
+1.99, //  1.6, 0.50
+1.92, //  1.8, 0.46
+1.85, //  2.0, 0.43
+1.78, //  2.2, 0.40
+1.72, //  2.4, 0.37
+1.67, //  2.6, 0.35
+1.62, //  2.8, 0.33
+1.57, //  3.0, 0.31
+1.53, //  3.2, 0.29
+1.49, //  3.4, 0.28
+1.45, //  3.6, 0.26
+1.42, //  3.8, 0.25
+1.39, //  4.0, 0.24
+1.35, //  4.2, 0.23
+1.33, //  4.4, 0.22
+1.30, //  4.6, 0.21
+1.27, //  4.8, 0.20
+1.25, //  5.0, 0.19
+1.23, //  5.2, 0.19
+1.20, //  5.4, 0.18
+1.18, //  5.6, 0.18
+1.16, //  5.8, 0.17
+1.14, //  6.0, 0.16
+1.13, //  6.2, 0.16
+1.11, //  6.4, 0.15
+1.09, //  6.6, 0.15
+1.08, //  6.8, 0.15
+1.06, //  7.0, 0.14
+1.05, //  7.2, 0.14
+1.03, //  7.4, 0.13
+1.02, //  7.6, 0.13
+1.01, //  7.8, 0.13
+1.00, //  8.0, 0.12
+0.98, //  8.2, 0.12
+0.97, //  8.4, 0.12
+0.96, //  8.6, 0.12
+0.95, //  8.8, 0.11
+0.94, //  9.0, 0.11
+0.93, //  9.2, 0.11
+0.92, //  9.4, 0.11
+0.91, //  9.6, 0.10
+0.90, //  9.8, 0.10
+0.89, // 10.0, 0.10
+0.88, // 10.2, 0.10
+0.87, // 10.4, 0.10
+0.87, // 10.6, 0.09
+0.86, // 10.8, 0.09
+0.85, // 11.0, 0.09
+0.84, // 11.2, 0.09
+0.84, // 11.4, 0.09
+0.83, // 11.6, 0.09
+0.82, // 11.8, 0.08
+0.81, // 12.0, 0.08
+0.81, // 12.2, 0.08
+0.80, // 12.4, 0.08
+0.80, // 12.6, 0.08
+0.79, // 12.8, 0.08
+0.78, // 13.0, 0.08
+0.78, // 13.2, 0.08
+0.77, // 13.4, 0.07
+0.77, // 13.6, 0.07
+0.76, // 13.8, 0.07
+0.75, // 14.0, 0.07
+0.75, // 14.2, 0.07
+0.74, // 14.4, 0.07
+0.74, // 14.6, 0.07
+0.73, // 14.8, 0.07
+0.73, // 15.0, 0.07
+0.72, // 15.2, 0.07
+0.72, // 15.4, 0.06
+0.72, // 15.6, 0.06
+0.71, // 15.8, 0.06
+0.71, // 16.0, 0.06
+0.70, // 16.2, 0.06
+0.70, // 16.4, 0.06
+0.69, // 16.6, 0.06
+0.69, // 16.8, 0.06
+0.69, // 17.0, 0.06
+0.68, // 17.2, 0.06
+0.68, // 17.4, 0.06
+0.67, // 17.6, 0.06
+0.67, // 17.8, 0.06
+0.67, // 18.0, 0.06
+0.66, // 18.2, 0.05
+0.66, // 18.4, 0.05
+0.66, // 18.6, 0.05
+0.65, // 18.8, 0.05
+0.65, // 19.0, 0.05
+0.65, // 19.2, 0.05
+0.64, // 19.4, 0.05
+0.64, // 19.6, 0.05
+0.64, // 19.8, 0.05
+0.63, // 20.0, 0.05
+};
Index: /branches/eam_branches/psModules.20240412/src/objects/pmDetEff.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmDetEff.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmDetEff.c	(revision 42651)
@@ -0,0 +1,193 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <string.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+#include "pmDetEff.h"
+
+static void detEffFree(pmDetEff *de)
+{
+    psFree(de->magOffsets);
+    psFree(de->counts);
+    psFree(de->magDiffMean);
+    psFree(de->magDiffStdev);
+    psFree(de->magErrMean);
+}
+
+
+pmDetEff *pmDetEffAlloc(float magRef, int numSources, int numBins)
+{
+    pmDetEff *de = psAlloc(sizeof(pmDetEff)); // Detection efficiency, to return
+    psMemSetDeallocator(de, (psFreeFunc)detEffFree);
+
+    de->magRef = magRef;
+    de->numSources = numSources;
+    de->numBins = numBins;
+
+    de->magOffsets = NULL;
+    de->counts = NULL;
+    de->magDiffMean = NULL;
+    de->magDiffStdev = NULL;
+    de->magErrMean = NULL;
+
+    return de;
+}
+
+
+bool pmDetEffWrite(psFits *fits, pmDetEff *de, const psMetadata *header, const char *extname)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    PS_ASSERT_FITS_WRITABLE(fits, false);
+    PM_ASSERT_DETEFF_RESULTS(de, false);
+
+    psArray *table = psArrayAlloc(de->numBins); // Table to write
+    for (int i = 0; i < de->numBins; i++) {
+        psMetadata *row = psMetadataAlloc(); // Table row
+        psMetadataAddF32(row, PS_LIST_TAIL, "OFFSET", 0, "Magnitude offset from reference",
+                         de->magOffsets->data.F32[i]);
+        psMetadataAddS32(row, PS_LIST_TAIL, "COUNTS", 0, "Number of sources recovered",
+                         de->counts->data.S32[i]);
+        psMetadataAddF32(row, PS_LIST_TAIL, "DIFF.MEAN", 0, "Mean magnitude difference",
+                         de->magDiffMean->data.F32[i]);
+        psMetadataAddF32(row, PS_LIST_TAIL, "DIFF.STDEV", 0, "Stdev magnitude difference",
+                         de->magDiffStdev->data.F32[i]);
+        psMetadataAddF32(row, PS_LIST_TAIL, "ERR.MEAN", 0, "Mean magnitude error",
+                         de->magErrMean->data.F32[i]);
+        table->data[i] = row;
+    }
+
+    psMetadata *deHeader = psMetadataCopy(NULL, header); // Header for detection efficiency
+    psMetadataAddF32(deHeader, PS_LIST_TAIL, "DETEFF.MAGREF", PS_META_REPLACE, "Magnitude reference",
+                     de->magRef);
+    psMetadataAddS32(deHeader, PS_LIST_TAIL, "DETEFF.NUM", PS_META_REPLACE, "Number of fake sources injected",
+                     de->numSources);
+
+    if (!psFitsWriteTable(fits, deHeader, table, extname)) {
+        psError(PS_ERR_IO, false, "Unable to write detection efficiency table.");
+        psFree(table);
+        psFree(deHeader);
+        return false;
+    }
+
+    psFree(table);
+    psFree(deHeader);
+
+    return true;
+}
+
+bool pmReadoutWriteDetEff(psFits *fits, const pmReadout *readout,
+                          const psMetadata *header, const char *extname)
+{
+    PM_ASSERT_READOUT_NON_NULL(readout, false);
+
+    bool mdok;                          // Status of MD lookup
+    pmDetEff *de = psMetadataLookupPtr(&mdok, readout->analysis, PM_DETEFF_ANALYSIS); // Detection efficiency
+    if (!mdok || !de) {
+        // Wrote everything there was to write
+        return true;
+    }
+    return pmDetEffWrite(fits, de, header, extname);
+}
+
+
+pmDetEff *pmDetEffRead(psFits *fits, const char *extname)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    PS_ASSERT_STRING_NON_EMPTY(extname, false);
+
+    if (!psFitsMoveExtNameClean(fits, extname)) {
+        // Nothing to read
+        return NULL;
+    }
+
+    psMetadata *header = psFitsReadHeader(NULL, fits); // Header for table
+    if (!header) {
+        psError(PS_ERR_IO, false, "Unable to read FITS header");
+        return NULL;
+    }
+
+    int numBins = psFitsTableSize(fits);// Size of table
+    bool mdok;                          // Status of MD lookup
+    int numSources = psMetadataLookupS32(&mdok, header, "DETEFF.NUM"); // Number of fake sources injected
+    if (!mdok) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to find number of sources");
+        psFree(header);
+        return NULL;
+    }
+    float magRef = psMetadataLookupF32(&mdok, header, "DETEFF.MAGREF"); // Magnitude reference
+    if (!mdok) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to find magnitude reference");
+        psFree(header);
+        return NULL;
+    }
+    psFree(header);
+
+    pmDetEff *de = pmDetEffAlloc(magRef, numSources, numBins); // Detection efficiency
+    de->magOffsets = psVectorAlloc(numBins, PS_TYPE_F32);
+    de->counts = psVectorAlloc(numBins, PS_TYPE_S32);
+    de->magDiffMean = psVectorAlloc(numBins, PS_TYPE_F32);
+    de->magDiffStdev = psVectorAlloc(numBins, PS_TYPE_F32);
+    de->magErrMean = psVectorAlloc(numBins, PS_TYPE_F32);
+
+    psArray *table = psFitsReadTable(fits); // FITS table
+    if (!table) {
+        psError(PS_ERR_IO, false, "Unable to read detection efficiency table.");
+        psFree(de);
+        return false;
+    }
+
+    for (int i = 0; i < numBins; i++) {
+        psMetadata *row = table->data[i]; // Table row
+        de->magOffsets->data.F32[i] = psMetadataLookupF32(NULL, row, "OFFSET");
+        de->counts->data.S32[i] = psMetadataLookupS32(NULL, row, "COUNTS");
+        de->magDiffMean->data.F32[i] = psMetadataLookupF32(NULL, row, "DIFF.MEAN");
+        de->magDiffStdev->data.F32[i] = psMetadataLookupF32(NULL, row, "DIFF.STDEV");
+        de->magErrMean->data.F32[i] = psMetadataLookupF32(NULL, row, "ERR.MEAN");
+    }
+
+    psFree(table);
+    return de;
+}
+
+bool pmReadoutReadDetEff(psFits *fits, const pmReadout *readout, const char *extname)
+{
+    PM_ASSERT_READOUT_NON_NULL(readout, false);
+
+    pmDetEff *de = pmDetEffRead(fits, extname);
+    if (!de) {
+        if (psErrorCodeLast() != PS_ERR_NONE) {
+            return false;
+        }
+        return true;
+    }
+
+    bool status = psMetadataAddPtr(readout->analysis, PS_LIST_TAIL, PM_DETEFF_ANALYSIS, PS_META_REPLACE | PS_DATA_UNKNOWN, "Detection efficiency", de);
+    psFree (de);
+    return status;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmDetEff.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmDetEff.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmDetEff.h	(revision 42651)
@@ -0,0 +1,76 @@
+#ifndef PM_DET_EFF_H
+#define PM_DET_EFF_H
+
+#define PM_DETEFF_ANALYSIS "DETEFF"     // Location of detection efficiency on pmReadout.analysis
+
+// Detection efficiency characterisation
+typedef struct {
+    float magRef;                       // Reference magnitude
+    int numSources;                     // Number of sources
+    int numBins;                        // Number of bins
+    psVector *magOffsets;               // Magnitude offsets for each bin
+    psVector *counts;                   // Counts of sources recovered for each bin
+    psVector *magDiffMean;              // Mean magnitude difference for each bin
+    psVector *magDiffStdev;             // Stdev of magnitude difference for each bin
+    psVector *magErrMean;               // Mean magnitude error for each bin
+} pmDetEff;
+
+
+/// Allocator
+pmDetEff *pmDetEffAlloc(float magRef,   // Reference magnitude
+                        int numSources, // Number of sources
+                        int numBins     // Number of bins
+                        );
+
+/// Write detection efficiency to FITS file
+bool pmDetEffWrite(psFits *fits,        // FITS file to which to write
+                   pmDetEff *deteff,    // Detection efficiency to write
+                   const psMetadata *header, // Header to write
+                   const char *extname  // Extension name
+                   );
+
+/// Write detection efficiency from a readout to a FITS file
+bool pmReadoutWriteDetEff(psFits *fits,// FITS file to which to write
+                          const pmReadout *readout, // Readout with detection efficiency
+                          const psMetadata *header, // Header to write
+                          const char *extname // Extension name
+    );
+
+/// Read detection efficiency
+pmDetEff *pmDetEffRead(psFits *fits,    // FITS file from which to read
+                       const char *extname // Extension name
+                       );
+
+/// Read detection efficiency into a readout
+bool pmReadoutReadDetEff(psFits *fits,// FITS file to which to write
+                         const pmReadout *readout, // Readout with detection efficiency
+                         const char *extname // Extension name
+    );
+
+#define PM_ASSERT_DETEFF_NON_NULL(DE, RETURN) { \
+    if (!(DE)) { \
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Detection efficiency %s is NULL", #DE); \
+        return RETURN; \
+    } \
+}
+
+#define PM_ASSERT_DETEFF_RESULTS(DE, RETURN) { \
+    PM_ASSERT_DETEFF_NON_NULL(DE, RETURN); \
+    PS_ASSERT_VECTOR_NON_NULL((DE)->magOffsets, RETURN); \
+    PS_ASSERT_VECTOR_SIZE((DE)->magOffsets, (long)(DE)->numBins, RETURN); \
+    PS_ASSERT_VECTOR_TYPE((DE)->magOffsets, PS_TYPE_F32, RETURN); \
+    PS_ASSERT_VECTOR_NON_NULL((DE)->counts, RETURN); \
+    PS_ASSERT_VECTOR_SIZE((DE)->counts, (long)(DE)->numBins, RETURN); \
+    PS_ASSERT_VECTOR_TYPE((DE)->counts, PS_TYPE_S32, RETURN); \
+    PS_ASSERT_VECTOR_NON_NULL((DE)->magDiffMean, RETURN); \
+    PS_ASSERT_VECTOR_SIZE((DE)->magDiffMean, (long)(DE)->numBins, RETURN); \
+    PS_ASSERT_VECTOR_TYPE((DE)->magDiffMean, PS_TYPE_F32, RETURN); \
+    PS_ASSERT_VECTOR_NON_NULL((DE)->magDiffStdev, RETURN); \
+    PS_ASSERT_VECTOR_SIZE((DE)->magDiffStdev, (long)(DE)->numBins, RETURN); \
+    PS_ASSERT_VECTOR_TYPE((DE)->magDiffStdev, PS_TYPE_F32, RETURN); \
+    PS_ASSERT_VECTOR_NON_NULL((DE)->magErrMean, RETURN); \
+    PS_ASSERT_VECTOR_SIZE((DE)->magErrMean, (long)(DE)->numBins, RETURN); \
+    PS_ASSERT_VECTOR_TYPE((DE)->magErrMean, PS_TYPE_F32, RETURN); \
+}
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/pmDetections.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmDetections.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmDetections.c	(revision 42651)
@@ -0,0 +1,51 @@
+/* @file  pmDetections.c
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-08-01 00:00:17 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+#include "pmDetections.h"
+
+void pmDetectionsFree (pmDetections *detections) {
+
+  if (!detections) return;
+
+  psFree (detections->footprints);
+  psFree (detections->peaks);
+  psFree (detections->oldPeaks);
+  psFree (detections->oldFootprints);
+
+  psFree (detections->newSources);
+  psFree (detections->allSources);
+  return;
+}
+
+// generate a pmDetections container with empty (allocated) footprints and peaks containers
+pmDetections *pmDetectionsAlloc(void) {
+
+    pmDetections *detections = (pmDetections *)psAlloc(sizeof(pmDetections));
+    psMemSetDeallocator(detections, (psFreeFunc) pmDetectionsFree);
+
+    detections->footprints    = NULL;
+    detections->peaks         = NULL;
+    detections->oldPeaks      = NULL;
+    detections->oldFootprints = NULL;
+    detections->newSources    = NULL;
+    detections->allSources    = NULL;
+    detections->last          = 0;
+
+    return (detections);
+}
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmDetections.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmDetections.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmDetections.h	(revision 42651)
@@ -0,0 +1,34 @@
+/* @file  pmDetections.h
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-08-01 00:00:17 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+# ifndef PM_DETECTIONS_H
+# define PM_DETECTIONS_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+/** pmDetections structure
+ *
+ * A strcture to carry the combined footprint and peak information
+ *
+ */
+typedef struct {
+  psArray *footprints;        // collection of footprints in the image
+  psArray *oldFootprints;     // collection of footprints previously found
+  psArray *peaks;             // collection of all peaks contained by the footprints
+  psArray *oldPeaks;          // collection of all peaks previously found
+  psArray *newSources;        // collection of sources
+  psArray *allSources;        // collection of sources
+  int last;
+} pmDetections;
+
+pmDetections *pmDetectionsAlloc (void);
+
+/// @}
+# endif /* PM_DETECTIONS_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmFootprint.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmFootprint.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmFootprint.c	(revision 42651)
@@ -0,0 +1,273 @@
+/* @file  pmFootprint.c
+ * low-level pmFootprint functions
+ *
+ * @author RHL, Princeton & IfA; EAM, IfA
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-12-08 02:51:14 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+
+static void footprintFree(pmFootprint *tmp)
+{
+   if (!tmp) {
+        return;
+   }
+
+   psTrace("psModules.objects", 10, "---- begin ----\n");
+
+   psFree(tmp->spans);
+   psFree(tmp->peaks);
+
+   psTrace("psModules.objects", 10, "---- 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", 10, "---- 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->nspans = 0; // we may allocate more spans than we set -- this is the number of active spans
+    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", 10, "---- end ----\n");
+    return(footprint);
+}
+
+bool pmFootprintAllocEmptySpans (pmFootprint *footprint, int nSpans) {
+
+    psArrayRealloc (footprint->spans, nSpans);
+    for (int i = 0; i < nSpans; i++) {
+	footprint->spans->data[i] = pmSpanAlloc(-1, -1, -1);
+    }
+    footprint->spans->n = nSpans;
+    return true;
+}
+
+// reset the footprint containers
+bool pmFootprintInit(pmFootprint *footprint) {
+
+    footprint->bbox.x0 = footprint->bbox.y0 = 0;
+    footprint->bbox.x1 = footprint->bbox.y1 = -1;
+    footprint->nspans = 0;
+    return true;
+}
+
+bool pmFootprintTest(const psPtr ptr) {
+    return (psMemGetDeallocator(ptr) == (psFreeFunc)footprintFree);
+}
+
+// XXX not actually used anywhere
+pmFootprint *pmFootprintNormalize(pmFootprint *fp) {
+    if (fp != NULL && !fp->normalized) {
+	if (PM_PEAKS_CULL_WITH_SMOOTHED_IMAGE) {
+	    fp->peaks = psArraySort(fp->peaks, pmPeaksSortBySmoothFluxDescend);
+	} else {
+	    fp->peaks = psArraySort(fp->peaks, pmPeaksSortByRawFluxDescend);
+	}
+	fp->normalized = true;
+    }
+
+    return fp;
+}
+
+//
+// Add a span to a footprint, returning the new span
+//
+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->nspans ++;
+
+    fp->npix += x1 - x0 + 1;
+
+    if (fp->nspans == 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;
+}
+
+
+// Set the next available elements of the nSpan entry in footprint->spans
+pmSpan *pmFootprintSetSpan(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;
+    }
+
+    int N = fp->nspans;
+    if (N == fp->spans->n) {
+	// if we need more space, extend fp->spans as needed
+	int Nalloc = fp->spans->n + 100;
+	psArrayRealloc(fp->spans, Nalloc);
+	fp->spans->n = Nalloc;
+	for (int i = N; i < fp->spans->n; i++) {
+	    fp->spans->data[i] = pmSpanAlloc(-1, -1, -1);
+	}
+    }
+
+    pmSpan *span = fp->spans->data[N];
+    span->y = y;
+    span->x0 = x0;
+    span->x1 = x1;
+
+    fp->nspans ++;
+
+    fp->npix += x1 - x0 + 1;
+
+    if (fp->nspans == 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 span;
+}
+
+void pmFootprintSetBBox(pmFootprint *fp) {
+    assert (fp != NULL);
+    if (fp->nspans == 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->nspans; 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->nspans; i++) {
+       pmSpan *span = fp->spans->data[i];
+       npix += span->x1 - span->x0 + 1;
+   }
+   fp->npix = npix;
+
+   return npix;
+}
+
+/*
+ * 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;
+}
+
+// create a new footprint with the same span set as the input footprint
+pmFootprint *pmFootprintCopyData(pmFootprint *inFoot, psImage *image) {
+
+    pmFootprint *outFoot = pmFootprintAlloc(inFoot->nspans, image);
+    for (int i = 0; i < inFoot->nspans; i++) {
+	pmSpan *span = inFoot->spans->data[i];
+	pmFootprintAddSpan(outFoot, span->y, span->x0, span->x1);
+    }
+    return outFoot;
+}
+
+/************************************************************************************************************/
Index: /branches/eam_branches/psModules.20240412/src/objects/pmFootprint.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmFootprint.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmFootprint.h	(revision 42651)
@@ -0,0 +1,100 @@
+/* @file  pmFootprint.h
+ *
+ * @author RHL, Princeton & IfA; EAM, IfA
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-12-09 21:16:09 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FOOTPRINT_H
+#define PM_FOOTPRINT_H
+
+// We need to choose up front if the culling algorithm uses the raw or smoothed image.
+// depending on which we choose, we should produce sorted peaks based on peak->rawFlux or
+// peak->smoothFlux
+
+# define PM_PEAKS_CULL_WITH_SMOOTHED_IMAGE 1
+
+typedef struct {
+    const int id;                       //!< unique ID
+    int npix;                           //!< number of pixels in this pmFootprint
+    int nspans;
+    psArray *spans;                     //!< the allocated 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 pmFootprintInit(pmFootprint *footprint);
+bool pmFootprintTest(const psPtr ptr);
+
+bool pmFootprintAllocEmptySpans (pmFootprint *footprint, int nSpans);
+
+pmFootprint *pmFootprintCopyData(pmFootprint *inFoot, psImage *image);
+
+pmFootprint *pmFootprintNormalize(pmFootprint *fp);
+int pmFootprintSetNpix(pmFootprint *fp);
+void pmFootprintSetBBox(pmFootprint *fp);
+
+pmSpan *pmFootprintAddSpan(pmFootprint *fp,     // the footprint to add to
+                           const int y, // row to add
+                           int x0,      // range of
+                           int x1);    //          columns
+
+pmSpan *pmFootprintSetSpan(pmFootprint *fp,	// the footprint to add to
+			   const int y,		// row to add
+			   int x0,		// range of
+			   int x1); 		// columns
+
+psArray *pmFootprintsFind(const psImage *img, const float threshold, const int npixMin);
+
+bool pmFootprintsFindAtPoint(pmFootprint *fp,
+			     pmFootprintSpans *fpSpans,
+			     const psImage *img,     // image to search
+			     psImage *mask,
+			     const float threshold,   // Threshold
+			     const psArray *peaks, // array of peaks; finding one terminates search for footprint
+			     int row, int col);
+
+// pmFootprint *pmFootprintsFindAtPoint(const psImage *img,
+//                                     const float threshold,
+//                                     const psArray *peaks,
+//                                     int row, int col);
+
+bool pmFootprintSpansBuild(pmFootprint *fp, // the footprint that we're building
+			   pmFootprintSpans *fpSpans,
+			   const psImage *img, // the psImage we're working on
+			   psImage *mask, // the associated masks
+			   const float threshold // Threshold
+    );
+
+psArray *pmFootprintArrayGrow(const psArray *footprints, int r);
+psArray *pmFootprintArraysMerge(const psArray *footprints1, const psArray *footprints2,
+                                const int includePeaks);
+
+psImage *pmSetFootprintArrayIDs(const psArray *footprints, const bool relativeIDs);
+psImage *pmSetFootprintID(psImage *idImage, const pmFootprint *fp, const int id);
+void pmSetFootprintArrayIDsForImage(psImage *idImage,
+                                    const psArray *footprints, // the footprints to insert
+                                    const bool relativeIDs // show IDs starting at 0, not pmFootprint->id
+    );
+
+psErrorCode pmFootprintsAssignPeaks(psArray *footprints, const psArray *peaks);
+
+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 fPadding, // fractional padding added to stdev since bright peaks have unreasonably high significance
+				 const float min_threshold, // minimum permitted coll height
+				 const float max_threshold,// maximum permitted coll height
+				 const bool isWeightVar // the input weight may be variance (sigma^2) or S/N (1/sigma)
+    );
+
+psArray *pmFootprintArrayToPeaks(const psArray *footprints);
+
+/// @}
+# endif /* PM_FOOTPRINT_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmFootprintArrayGrow.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmFootprintArrayGrow.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmFootprintArrayGrow.c	(revision 42651)
@@ -0,0 +1,112 @@
+/* @file  pmFootprintArrayGrow.c
+ *
+ * @author RHL, Princeton & IfA; EAM, IfA
+ *
+ * @version $Revision: 1.12 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-01-27 06:39:38 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+
+# define USE_FFTS_TO_CONVOLVE 1
+
+/*
+ * Grow a psArray of pmFootprints isotropically by r pixels, returning a new psArray of new pmFootprints
+ */
+psArray *pmFootprintArrayGrow(const psArray *footprints, // footprints to grow
+                              int r) {  // how much to grow each footprint
+    assert (footprints->n == 0 || pmFootprintTest(footprints->data[0]));
+
+    psTimerStart ("grow");
+
+    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);
+    psLogMsg ("psphot", PS_LOG_MINUTIA, "set footprint array IDs: %f sec\n", psTimerMark ("grow"));
+
+#if 1
+    // Use a separable convolution: should be faster
+    idImage = (psImage*)psBinaryOp(idImage, idImage, "MIN", psScalarAlloc(1, PS_TYPE_S32));
+    psImage *idImageMask = psImageCopy(NULL, idImage, PS_TYPE_IMAGE_MASK); // Image with 1 = object
+    psImage *grownIdImage = psImageConvolveMask(NULL, idImageMask, 0x01, 0x01, -r, r, -r, r); // Grown mask
+    if (!grownIdImage) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to grow mask.");
+        psFree(grownIdImage);
+        psFree(idImage);
+        return NULL;
+    }
+    psFree(idImageMask);
+#else
+    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;
+            }
+        }
+    }
+
+# if (USE_FFTS_TO_CONVOLVE)
+    psImage *f32ImageIn = psImageCopy (NULL, idImage, PS_TYPE_F32);
+    psImage *f32ImageOut = psImageConvolveFFT(NULL, f32ImageIn, NULL, 0, circle);
+    psImage *grownIdImage = psImageCopy (NULL, f32ImageOut, PS_TYPE_S32);
+    psFree (f32ImageIn);
+    psFree (f32ImageOut);
+#else
+    psImage *grownIdImage = psImageConvolveDirect(NULL, idImage, circle); // Here's the actual grow step
+#endif // USE_FFTS_TO_CONVOLVE
+    psFree(circle);
+#endif // Don't bother at all
+
+    psFree(idImage);
+
+    psLogMsg ("psphot", PS_LOG_MINUTIA, "convolved with grow disc: %f sec\n", psTimerMark ("grow"));
+
+    psArray *grown = pmFootprintsFind(grownIdImage, 0.5, 1); // and here we rebuild the grown footprints
+    psLogMsg ("psphot", PS_LOG_MINUTIA, "found grown footprints: %f sec\n", psTimerMark ("grow"));
+
+    assert (grown != NULL);
+    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
+     */
+    psArray *peaks = pmFootprintArrayToPeaks(footprints);
+    pmFootprintsAssignPeaks(grown, peaks);
+    psFree(peaks);
+
+
+
+
+    psLogMsg ("psphot", PS_LOG_MINUTIA, "finished grow: %f sec\n", psTimerMark ("grow"));
+
+    return grown;
+
+}
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmFootprintArraysMerge.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmFootprintArraysMerge.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmFootprintArraysMerge.c	(revision 42651)
@@ -0,0 +1,97 @@
+/* @file  pmFootprintArraysMerge.c
+ *
+ * @author RHL, Princeton & IfA; EAM, IfA
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-12-08 02:51:14 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+
+/*
+ * 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 *pmFootprintArraysMerge(const psArray *footprints1, // one set of footprints
+                                const psArray *footprints2, // the other set
+                                const int includePeaks // which peaks to set? 0x1 => footprints1, 0x2 => 2
+    )
+{
+    if (!footprints1 && !footprints2) {
+        // No footprints in merged array
+        return psArrayAllocEmpty(0);
+    }
+
+    assert(!footprints1 || footprints1->n == 0 || pmFootprintTest(footprints1->data[0]));
+    assert(!footprints2 || footprints2->n == 0 || pmFootprintTest(footprints2->data[0]));
+
+    if (!footprints1 || footprints1->n == 0 || !footprints2 || footprints2->n == 0) {
+        // nothing to do but put copies on merged
+        const psArray *old = (!footprints1 || 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);
+    pmSetFootprintArrayIDsForImage(idImage, footprints2, true);
+
+    psArray *merged = pmFootprintsFind(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) {
+        psArray *peaks = pmFootprintArrayToPeaks(footprints1);
+        pmFootprintsAssignPeaks(merged, peaks);
+        psFree(peaks);
+    }
+
+    if (includePeaks & 0x2) {
+        psArray *peaks = pmFootprintArrayToPeaks(footprints2);
+        pmFootprintsAssignPeaks(merged, peaks);
+        psFree(peaks);
+    }
+
+    return merged;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmFootprintAssignPeaks.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmFootprintAssignPeaks.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmFootprintAssignPeaks.c	(revision 42651)
@@ -0,0 +1,152 @@
+/* @file  pmFootprintAssignPeaks.c
+ *
+ * @author RHL, Princeton & IfA; EAM, IfA
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-12-08 02:51:14 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+
+/*
+ * Given a psArray of pmFootprints and another of pmPeaks, assign the peaks to the
+ * footprints in which they fall; if they _don't_ fall in a footprint, add a suitable
+ * one to the list.
+ */
+psErrorCode
+pmFootprintsAssignPeaks(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 || psMemCheckPeak(peaks->data[0]));
+    
+    if ((footprints->n == 0) && (peaks->n == 0)) {
+	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);
+    if (ids) { assert (ids->type.type == PS_TYPE_S32); }
+    
+    const int row0 = ids ? ids->row0 : 0;
+    const int col0 = ids ? ids->col0 : 0;
+    const int numRows = ids ? ids->numRows : -1;
+    const int numCols = ids ? ids->numCols : -1;
+
+    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;
+	
+	if (ids) { psAssert (x >= 0 && x < numCols && y >= 0 && y < numRows, "out of range");}
+	int id = ids ? ids->data.S32[y][x] : 0;
+
+	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);
+	peak->footprint = fp; // reference to containing footprint
+    }
+    
+    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];
+
+	// XXX are we allowed to have peak-less footprints??
+	if (fp->peaks->n == 0) continue;
+	if (fp->peaks->n == 1) continue;
+
+	// make sure the peaks are sorted in a way consistent with our cull process
+	if (PM_PEAKS_CULL_WITH_SMOOTHED_IMAGE) {
+	    fp->peaks = psArraySort(fp->peaks, pmPeaksSortBySmoothFluxDescend);
+	} else {
+	    fp->peaks = psArraySort(fp->peaks, pmPeaksSortByRawFluxDescend);
+	}
+
+	// XXX check for an assert on duplicates (I don't think they can happen, but
+	// let's double check for now)
+
+	for (int j = 1; j < fp->peaks->n; j++) {
+	    psAssert (fp->peaks->data[j] != fp->peaks->data[j-1], "duplicate peak!");
+	}
+	continue;
+
+	// XXX WHY am I culling duplicates?  how can there be duplicates?
+	// XXX EAM : the algorithm below should be much faster than using psArrayRemove if
+	// the number of peaks in the footprint is large, or if there are no duplicates.
+	// if we have a lot of small-number peak arrays with duplicates, this may be
+	// slower.
+
+	// track the number of good peaks in the footprint
+	int lastGood = 0;
+
+	// check for duplicates
+	// on first pass, we set the index to NULL if peak is a duplicate
+	// XXX EAM : this can leave behind duplicates of the same S/N
+	// (if sorted list has A, B, A, B ...)
+	for (int j = 1; j < fp->peaks->n; j++) { 
+	    if (fp->peaks->data[j] == fp->peaks->data[lastGood]) {
+		// everything on the array has its own mem reference; free and drop this one
+		psFree (fp->peaks->data[j]);
+		fp->peaks->data[j] = NULL;
+	    } else {
+		lastGood ++;
+	    }
+	}
+	int nGood = lastGood + 1;
+
+	// no deleted peaks, go to next footprint
+	if (nGood == fp->peaks->n) continue;
+
+	int nKeep = 0;
+
+	psArray *goodPeaks = psArrayAlloc (nGood);
+	// on second pass, save the good peaks
+	for (int j = 0; j < fp->peaks->n; j++) { // check for duplicates
+	    if (fp->peaks->data[j] == NULL) continue;
+	    // transfer the data (NULL to avoid double free)
+	    // this is only slightly sleazy
+	    goodPeaks->data[nKeep] = fp->peaks->data[j];
+	    fp->peaks->data[j] = NULL;
+	    nKeep ++;
+	}
+	psAssert (nGood == nKeep, "mis-counted nKeep or nGood");
+
+	// free the old (now NULL-filled) array
+	psFree (fp->peaks);
+	fp->peaks = goodPeaks;
+    }
+
+    // (void)psArrayRemoveIndex(fp->peaks, j);
+
+
+    return PS_ERR_NONE;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmFootprintCullPeaks.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmFootprintCullPeaks.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmFootprintCullPeaks.c	(revision 42651)
@@ -0,0 +1,372 @@
+/* @file  pmFootprint.c
+ * low-level pmFootprint functions
+ *
+ * @author RHL, Princeton & IfA; EAM, IfA
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-12-08 02:51:14 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+
+bool dumpfootprints (pmFootprint *fp, pmFootprintSpans *fpSp);
+
+/*
+ * 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 (less?) than nsigma DN below your
+ * starting point, discard the peak.
+ */
+
+# define IN_PEAK 1
+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 fPadding, // fractional padding added to stdev since bright peaks have unreasonably high significance
+                                 const float min_threshold, // minimum permitted coll height
+                                 const float max_threshold,
+				 const bool useSmoothedImage
+    ) { // maximum 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 < 2) { // 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;
+
+    psImage *subImg = psImageSubset((psImage *)img, subRegion);
+    psAssert (subImg, "trouble making local subimage");
+
+    psImage *idImg = psImageAlloc(subImg->numCols, subImg->numRows, PS_TYPE_S32);
+
+    // We need a psArray of peaks brighter than the current peak.
+    // We reject peaks which either:
+    // 1) are below the local threshold
+    // 2) have a brighter peak within their threshold
+
+    // allocate the full-sized array.  if the final array is much smaller, we can realloc
+    // at that point.
+    psArray *brightPeaks = psArrayAllocEmpty(fp->peaks->n);
+    psArrayAdd (brightPeaks, 128, fp->peaks->data[0]);
+
+    // XXX test point
+    // pmPeak *myPeak = fp->peaks->data[0];
+    // if ((fabs(myPeak->x - 205) < 100) && (fabs(myPeak->y - 806) < 100)) {
+    // 	fprintf (stderr, "test peak\n");
+    // }
+
+    // allocate the peakFootprint and peakFPSpans containers -- these are re-used by pmFootprintsFindAtPoint to minimize allocs in this function
+    pmFootprint *peakFootprint = pmFootprintAlloc(fp->nspans, subImg);
+    pmFootprintSpans *peakFPSpans = pmFootprintSpansAlloc(2*fp->nspans);
+
+    // allocate empty spans for the footprints
+    pmFootprintAllocEmptySpans(peakFootprint, fp->nspans);
+
+    psImage *subMask = psImageCopy(NULL, subImg, PS_TYPE_IMAGE_MASK);
+
+    // if we have many peaks in a large footprint, we waste a lot of time generating nearly identical footprints
+    // here we attempt to cull peaks drawing a single footprint for all peaks in some threshold range
+    // fprintf (stderr, "footprint: %d x %d : %d pix, %d peaks\n", subImg->numCols, subImg->numRows, fp->npix, (int) fp->peaks->n);
+    if ((fp->npix > 30000) && (fp->peaks->n > 10)) {
+
+	// max flux is above threshold for brightest peak
+	pmPeak *maxPeak = NULL;
+	for (int i = 0; i < fp->peaks->n; i++) {
+	    pmPeak *testPeak = fp->peaks->data[i];
+	    float this_peak = useSmoothedImage ? testPeak->smoothFlux : testPeak->rawFlux;
+	
+	    if (isfinite(this_peak)) {
+		maxPeak = fp->peaks->data[i];
+		break;
+	    }
+	}
+	psAssert(maxPeak,"maxPeak was not set in these peaks");
+	//      = fp->peaks->data[0];
+	float maxFlux = useSmoothedImage ? maxPeak->smoothFlux : maxPeak->rawFlux;
+
+	// we have a relationship between the bin and the threshold of:
+	// threshold = 0.25 beta^2 bin^2 + minThreshold
+	// thus, the max bin is: sqrt(4.0*(maxThreshold - minThreshold)/ALPHA^2)
+
+# define ALPHA 0.1
+	
+	float beta = nsigma_delta * ALPHA;
+	float beta2 = PS_SQR(beta);
+	float value = 4.0*(maxFlux - min_threshold)/beta2;
+	int nBins = (value > 1.0) ? sqrt(value) + 10 : 10; // let's be extra generous
+
+	// create a vector to store the threshold bins used for each peak
+	psVector *threshbins = psVectorAlloc(fp->peaks->n, PS_TYPE_S32);
+	threshbins->data.S32[0] = -1; // we skip the first peak
+
+	/// create a vector to track if a peak has been tried or not:
+	psVector *peaktried = psVectorAlloc(fp->peaks->n, PS_TYPE_U8);
+	psVectorInit(peaktried, 0);
+	peaktried->data.U8[0] = true; // we skip the first peak
+
+	psVector *threshbounds = psVectorAlloc(nBins, PS_TYPE_F32);
+	for (int i = 0; i < nBins; i++) {
+	    threshbounds->data.F32[i] = 0.25*beta2*PS_SQR(i) + min_threshold;	    
+	}
+#if (0)
+	if (threshbounds->data.F32[threshbounds->n-1] > maxFlux) {
+	    psWarning ("upper limit: %f does not include max flux: %f",
+		       threshbounds->data.F32[threshbounds->n-1], maxFlux);
+	}
+#endif
+	psHistogram *threshist = psHistogramAllocGeneric(threshbounds);
+
+	// assign the peaks to the histogram bins based on their nominal thresholsd
+	for (int i = 1; i < fp->peaks->n; i++) {
+	    const pmPeak *peak = fp->peaks->data[i];
+	    float flux = useSmoothedImage ? peak->smoothFlux : peak->rawFlux;
+	    float stdev = useSmoothedImage ? peak->smoothFluxStdev : peak->rawFluxStdev;
+
+	    // if flux is negative, careful with fStdev
+	    const float fStdev = fabs(stdev/flux);
+	    const float stdev_pad = fabs(flux * hypot(fStdev, fPadding));
+
+	    float threshold = flux - nsigma_delta*stdev_pad;
+	    psAssert(!isnan(threshold), "impossible");
+
+	    if (threshold <= min_threshold) {
+		threshist->nums->data.F32[0] += 1.0;
+		threshbins->data.S32[i] = 0;
+		continue;
+	    }
+	    int bin = sqrt(4.0*(threshold - min_threshold)/beta2);
+	    psAssert(bin >= 0, "impossible bin");
+
+	    bin = PS_MIN(bin, threshist->nums->n - 1);
+	    threshist->nums->data.F32[bin] += 1.0;
+	    
+	    // record the bin selected for this peak
+	    threshbins->data.S32[i] = bin;
+	}
+
+	// XXX TEST: did we assign correctly
+# if (0)
+	int nPeaks = 1; // we don't put the brightest in the histogram
+	for (int i = 0; i < threshist->nums->n; i++) {
+	    if (threshist->nums->data.F32[i] > 0) {
+		fprintf (stderr, "%f : %f : %d\n", threshist->bounds->data.F32[i], threshist->bounds->data.F32[i+1], (int)threshist->nums->data.F32[i]);
+		nPeaks += threshist->nums->data.F32[i];
+	    }
+	}
+	fprintf (stderr, "%d peaks vs %d in histogram\n", (int) fp->peaks->n, nPeaks);
+# endif
+
+	// XXX for the moment, we will use the alternate cull for all peaks -- it might be
+	// faster to use the standard process for the peaks for which the threshold bin
+	// contains < N sources (N ~ 5-10?)
+
+	// loop over the threshold bins from brightest to faintest
+	for (int i = threshist->nums->n - 1; i >= 0; i--) {
+	    if (threshist->nums->data.F32[i] == 0) continue;
+
+	    // we are going to examine the footprints at this threshold
+	    float threshold = 0.5*(threshist->bounds->data.F32[i] + threshist->bounds->data.F32[i+1]);
+		    
+	    // generate all footprints corresponding to this threshold
+	    psArray *myFP = pmFootprintsFind(subImg, threshold, 5);
+	    if (!myFP) {
+		psWarning ("missing footprint? threshold: %.f", threshold);
+		continue;
+	    }
+	    if (!myFP->n) {
+		psWarning ("empty footprint? threshold: %.f", threshold);
+		psFree (myFP);
+		continue;
+	    }
+
+	    // an array to track if the footprint has a brighter peak or not
+	    psVector *found = psVectorAlloc(myFP->n + 1, PS_TYPE_U8);
+	    psVectorInit(found, 0);
+	    int nFound = 0;
+
+	    // set IDs to distinguish the footprints
+	    psImageInit(idImg, 0);
+	    pmSetFootprintArrayIDsForImage(idImg, myFP, true);
+
+	    // check which footprints contain already-accepted (brighter) peaks 
+	    // (we can give up if/when we found a peak for all footprints)
+	    for (int j = 0; (j < brightPeaks->n) && (nFound < found->n); j++) {
+		const pmPeak *peak = brightPeaks->data[j];
+		int x = peak->x - subImg->col0;
+		int y = peak->y - subImg->row0;
+		int myID = idImg->data.S32[y][x];
+		psAssert(myID >= 0, "impossible");
+		psAssert(myID < found->n, "impossible");
+
+		if (myID == 0) continue; // bright peak is not in a footprint
+		if (found->data.U8[myID]) continue; // we already know this footprint contains a peak
+		found->data.U8[myID] = true;
+		nFound ++;
+	    }
+	
+	    // check the peaks from this threshold bin: if they land in a footprint which has
+	    // been found, we should drop that peak.  otherwise, keep it
+	    for (int j = 0; j < fp->peaks->n; j++) {
+		pmPeak *peak = fp->peaks->data[j];
+		if (peak->assigned) continue; // peak is already claimed by a source -- don't cull
+
+		// skip peaks if we've already tried them
+		if (peaktried->data.U8[j]) continue;
+
+		// is this peak in the threshold bin of interest?
+		if (threshbins->data.S32[j] != i) continue;
+		
+		// do not try this peak again
+		peaktried->data.U8[j] = true;
+
+		int x = peak->x - subImg->col0;
+		int y = peak->y - subImg->row0;
+		int myID = idImg->data.S32[y][x];
+		psAssert(myID < found->n, "impossible");
+
+		// a peak in this threshold bin without a valid footprint comes from a region
+		// with only a handful of pixels (1 or more from the peak itself).  It probably
+		// cannot be joined to a neighbor
+		if (myID == 0) {
+		    psArrayAdd (brightPeaks, 128, peak);
+		    continue;
+		}
+
+		// keep this peak if found->data.U8[myID] == false (no brighter peak in the footprint)
+		if (!found->data.U8[myID]) {
+		    // fprintf (stderr, "keeping %d: %d,%d\n", j, peak->x, peak->y);
+		    psArrayAdd (brightPeaks, 128, peak);
+		    continue;
+		}
+		// fprintf (stderr, "skipping %d: %d,%d\n", j, peak->x, peak->y);
+	    }
+	    psFree (myFP);
+	    psFree (found);
+	}
+	psFree (threshist);
+	psFree (threshbounds);
+	psFree (threshbins);
+	psFree (peaktried);
+
+    } else {
+
+	// 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];
+
+	    float flux  = useSmoothedImage ? peak->smoothFlux      : peak->rawFlux;
+	    float stdev = useSmoothedImage ? peak->smoothFluxStdev : peak->rawFluxStdev;
+
+	    // if flux is negative, careful with fStdev
+	    const float fStdev = fabs(stdev/flux);
+	    const float stdev_pad = fabs(flux * hypot(fStdev, fPadding));
+
+	    float threshold = flux - nsigma_delta*stdev_pad;
+
+	    if (isnan(threshold)) {
+		// min_threshold is assumed to be below the detection threshold,
+		// so all the peaks are pmFootprint, and this isn't the brightest
+		continue;
+	    }
+
+	    // just in case, force the threshold below the peak source flux
+	    if (threshold > flux) {
+		threshold = flux - 10*FLT_EPSILON;
+	    }
+
+	    if (threshold < min_threshold) {
+		threshold = min_threshold;
+	    }
+	    if (threshold > max_threshold) {
+		threshold = max_threshold;
+	    }
+
+	    pmFootprintsFindAtPoint(peakFootprint, peakFPSpans, subImg, subMask, threshold, brightPeaks, peak->y, peak->x);
+	    // if (peakFPSpans->nStartSpans > 2000) {
+	    // 	// dumpfootprints(peakFootprint, peakFPSpans);
+	    // 	// fprintf (stderr, "big footprint %d : %d\n", peakFootprint->nspans, peakFPSpans->nStartSpans);
+	    // 	// fprintf (stderr, "big test footprint: %f %f to %f %f (%d pix)\n", peakFootprint->bbox.x0, peakFootprint->bbox.y0, peakFootprint->bbox.x1, peakFootprint->bbox.y1, peakFootprint->npix);
+	    // }
+
+	    // at this point brightPeaks only has the peaks brighter than the current
+
+	    // we set the IDs to either 1 (in peak) or 0 (not in peak)
+	    pmSetFootprintID (idImg, peakFootprint, IN_PEAK);
+
+	    // If this peak has not already been assigned to a source, then we can look for any
+	    // brighter peaks within its footprint. Check if any of the previous (brighter) peaks
+	    // are within the footprint of this peak If so, the current peak is bogus; drop it.
+	    bool keep = true;
+	    for (int j = 0; keep && !peak->assigned && (j < brightPeaks->n); j++) {
+		// const pmPeak *peak2 = fp->peaks->data[j]; XXX isn't this an error?  we only care about the kept brighter peak, right?
+		const pmPeak *peak2 = brightPeaks->data[j];
+		int x2 = peak2->x - subImg->col0;
+		int y2 = peak2->y - subImg->row0;
+		if (idImg->data.S32[y2][x2] == IN_PEAK) {
+		    // There's a brighter peak within the footprint above threshold; so cull our initial peak
+		    keep = false;
+		}
+	    }
+	    if (!keep) {
+		psAssert (!peak->assigned, "logic error: trying to drop a previously-assigned peak");  // we should not drop any already assigned peaks.
+		continue;
+	    }
+	    psArrayAdd (brightPeaks, 128, fp->peaks->data[i]);
+	}
+    }
+
+    psFree (fp->peaks);
+    fp->peaks = brightPeaks;
+
+    psFree(idImg);
+    psFree(subImg);
+    psFree(subMask);
+    psFree(peakFootprint);
+    psFree(peakFPSpans);
+
+    return PS_ERR_NONE;
+}
+
+
+bool dumpfootprints (pmFootprint *fp, pmFootprintSpans *fpSp) {
+
+    FILE *f1 = fopen ("fp.dat", "w");
+    if (!f1) return false;
+
+    for (int i = 0; i < fp->nspans; i++) {
+	pmSpan *sp = fp->spans->data[i];
+	fprintf (f1, "%d - %d : %d\n", sp->x0, sp->x1, sp->y);
+    }
+    fclose (f1);
+
+    FILE *f2 = fopen ("fpSp.dat", "w");
+    if (!f2) return false;
+
+    for (int i = 0; i < fpSp->nStartSpans; i++) {
+	pmStartSpan *sp = fpSp->startspans->data[i];
+	if (!sp->span) continue;
+	fprintf (f2, "%d - %d : %d\n", sp->span->x0, sp->span->x1, sp->span->y);
+    }
+    fclose (f2);
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmFootprintFind.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmFootprintFind.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmFootprintFind.c	(revision 42651)
@@ -0,0 +1,266 @@
+/* @file  pmFootprintFind.c
+ * find footprints in an image (fast on large scale images)
+ *
+ * @author RHL, Princeton & IfA; EAM, IfA
+ *
+ * @version $Revision: 1.8 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-12-08 02:51:14 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+
+// XXX EAM : why use WSPAN in here rather than pmSpan?
+// XXX WES : can't use pmSpan because does not have an id
+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 *
+pmFootprintsFind(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);
+
+   psImage *floatImg = img->type.type == PS_TYPE_F32 ? psMemIncrRefCounter((psImage*)img) :
+       psImageCopy(NULL, img, PS_TYPE_F32); // Floating-point version of image; casting away const
+
+   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));
+
+       in_span = 0;                      /* not in a span */
+       int id_last_connection = 0;
+       for (j = 0; j < numCols; j++) {
+           double pixVal = floatImg->data.F32[i][j]; // Value of pixel
+           // If pixVal is less than threshold and we are working on a, span end it.
+           if (pixVal < threshold) {
+               // below threshold. If in a span close it out
+               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;
+                   id_last_connection = 0;
+               }
+           } else {                       /* a pixel to fix */
+               // Above theshold. There are 5 choices for the id of this pixel based on whether they are
+               // part of a span (non-zero)
+               // This diagram shows the priority which we check
+               //       
+               //                       col
+               //                   j-1   j   j+1
+               // row i              1    5 
+               // row i + 1          2    3    4
+               //
+               // In case 4 we have a pixel that is not connected to the left are connecting with 
+               // an existing span so need to identify whether it is connected
+               // to the same footprint as the current span (if we are in one)
+               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 && idp[j + 1] != id_last_connection) {
+                   int resolved_lower_right = resolve_alias(aliases, idp[j + 1]);
+                   int resolved_current = resolve_alias(aliases, id);
+                   aliases[resolved_lower_right] = resolved_current;
+
+                   // now we choose the id to continue to use to set pixels in the current span.
+                   // We choose the higher value because future alias resolutions will be faster
+                   // since the alias chain goes from lower ids to higher. This is about 4 times
+                   // faster for complex footprints.
+                   if (resolved_current <= idp[j + 1]) {
+                       idc[j] = id = idp[j + 1];
+                       id_last_connection = 0;
+                   } else {
+                       idc[j] = id = resolved_current;
+                       id_last_connection = 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(floatImg);
+
+   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
+    * XXX replace with a psLib sort call
+    */
+   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);
+   footprints->n = n;
+   /*
+    * clean up
+    */
+   psFree(spans);
+
+   return footprints;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmFootprintFindAtPoint.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmFootprintFindAtPoint.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmFootprintFindAtPoint.c	(revision 42651)
@@ -0,0 +1,345 @@
+/* @file  pmFootprintFindAtPoint.c
+ * find footprints in a small image relative to a reference point
+ *
+ * @author RHL, Princeton & IfA; EAM, IfA
+ *
+ * @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-01-27 06:39:38 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+
+/************************************************************************************************************/
+/*
+ * Search the image for pixels above threshold, starting at a single pmStartSpan.
+ * 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 pmFootprintsFindAtPoint
+ * 
+ * This function is/was ill-defined if pixel values are NAN.  we should either treat NAN as >
+ * threshold or < threshold, but the current (r29004) code is ambiguous.
+ * EAM : change code so NAN is always > threshold
+ */
+bool pmFootprintSpansBuild(pmFootprint *fp, // the footprint that we're building
+			   pmFootprintSpans *fpSpans,
+			   const psImage *img, // the psImage we're working on
+			   psImage *mask, // the associated masks
+			   const float threshold // Threshold
+    ) {
+    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
+    psImageMaskType *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;
+
+    /********************************************************************************************************/
+
+    pmStartSpan *startspan = NULL;
+    for (int i = 0; i < fpSpans->nStartSpans; i++) {
+        startspan = fpSpans->startspans->data[i];
+        if (startspan->direction != PM_STARTSPAN_DONE) {
+            break;
+        }
+        if (startspan->stop) {
+            break;
+        }
+    }
+    if (startspan == NULL || startspan->direction == PM_STARTSPAN_DONE) { // no more pmStartSpans to process
+        return false;
+    }
+    if (startspan->stop) {                  // they don't want any more spans processed
+        return false;
+    }
+
+    /*
+     * Work
+     */
+    const PM_STARTSPAN_DIR dir = startspan->direction;
+    /*
+     * Set initial span to the startspan
+     */
+    int x0 = startspan->span->x0 - col0, x1 = startspan->span->x1 - col0;
+    /*
+     * Go through image identifying objects
+     */
+    int nx0, nx1 = -1;                  // new values of x0, x1
+    const int di = (dir == PM_STARTSPAN_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 = startspan->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_IMAGE_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]);
+	    bool belowThreshold = (pixVal < threshold) && isfinite(pixVal);
+            if ((maskRow[j] & PM_STARTSPAN_DETECTED) || belowThreshold)  {
+                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
+            //
+            for (int j = nx0 + 1; j <= numCols; j++) {
+                double pixVal = (j >= numCols) ? threshold - 100 : (F32 ? imgRowF32[j] : imgRowS32[j]);
+		bool belowThreshold = (pixVal < threshold) && isfinite(pixVal);
+                if ((maskRow[j] & PM_STARTSPAN_DETECTED) || belowThreshold) {
+                    nx1 = j - 1;
+                    break;
+                }
+            }
+
+	    pmSpan *sp = pmFootprintSetSpan(fp, i + row0, nx0 + col0, nx1 + col0);
+	    bool status = pmFootprintSpansSet(fpSpans, sp, mask, PM_STARTSPAN_RESTART);
+	    // fprintf (stderr, "set 1: %d vs %d\n", fp->nspans, fpSpans->nStartSpans);
+            if (status) {
+                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]);
+	    bool aboveThreshold = (pixVal >= threshold) || !isfinite(pixVal);
+            if (!(maskRow[j] & PM_STARTSPAN_DETECTED) && aboveThreshold) {
+                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]);
+		    bool belowThreshold = (pixVal < threshold) && isfinite(pixVal);
+                    if ((maskRow[j] & PM_STARTSPAN_DETECTED) || belowThreshold) { // end of span
+                        sx1 = j;
+                        break;
+                    }
+                }
+                assert (sx1 >= 0);
+
+                pmSpan *sp;
+                if (first) {
+                    if (sx1 <= x1) {
+                        sp = pmFootprintSetSpan(fp, i + row0, sx0 + col0, sx1 + col0 - 1);
+                        bool status = pmFootprintSpansSet(fpSpans, sp, mask, PM_STARTSPAN_DONE);
+			// fprintf (stderr, "set 2: %d vs %d\n", fp->nspans, fpSpans->nStartSpans);
+                        if (status) {
+                            stop = true;
+                            break;
+                        }
+                    } else {            // overhangs to right
+                        sp = pmFootprintSetSpan(fp, i + row0, sx0 + col0, x1 + col0);
+                        bool status = pmFootprintSpansSet(fpSpans, sp, mask, PM_STARTSPAN_DONE);
+			// fprintf (stderr, "set 3: %d vs %d\n", fp->nspans, fpSpans->nStartSpans);
+			if (status) {
+                            stop = true;
+                            break;
+                        }
+                        sp = pmFootprintSetSpan(fp, i + row0, x1 + 1 + col0, sx1 + col0 - 1);
+                        status = pmFootprintSpansSet(fpSpans, sp, mask, PM_STARTSPAN_RESTART);
+			// fprintf (stderr, "set 4: %d vs %d\n", fp->nspans, fpSpans->nStartSpans);
+			if (status) {
+                            stop = true;
+                            break;
+                        }
+                    }
+                    first = false;
+                } else {
+                    sp = pmFootprintSetSpan(fp, i + row0, sx0 + col0, sx1 + col0 - 1);
+                    bool status = pmFootprintSpansSet(fpSpans, sp, mask, PM_STARTSPAN_RESTART);
+		    // fprintf (stderr, "set 5: %d vs %d\n", fp->nspans, fpSpans->nStartSpans);
+		    if (status) {
+                        stop = true;
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (stop || first == false) {   // we're done
+            break;
+        }
+
+        x0 = nx0; x1 = nx1;
+    }
+    /*
+     * Cleanup
+     */
+
+    startspan->direction = PM_STARTSPAN_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 pmFootprintsFind 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()
+ * 
+ * The calling function must supply a footprint allocated with a reasonable amount of space
+ *
+ */
+
+bool pmFootprintsFindAtPoint(pmFootprint *fp,
+			     pmFootprintSpans *fpSpans,
+			     const psImage *img,     // image to search
+			     psImage *mask,
+			     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)
+    psAssert(img, "image must be supplied");
+    psAssert(fp, "footprint must be supplied");
+    psAssert(fpSpans, "footprint spans must be supplied");
+
+    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 false;
+    }
+    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 false;
+    }
+
+    double pixVal = F32 ? img->data.F32[row][col] : img->data.S32[row][col];
+    if (pixVal < threshold) {
+	return true;
+    }
+
+    /*
+     * 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.
+     */
+
+    pmFootprintInit(fp);
+    pmFootprintSpansInit(fpSpans);
+    psImageInit(mask, PM_STARTSPAN_INITIAL);
+
+    // fprintf (stderr, "init: %d vs %d\n", fp->nspans, fpSpans->nStartSpans);
+
+    //
+    // Set stop bits from peaks list
+    //
+    assert (peaks == NULL || peaks->n == 0 || psMemCheckPeak(peaks->data[0]));
+    if (peaks != NULL) {
+	for (int i = 0; i < peaks->n; i++) {
+	    pmPeak *peak = peaks->data[i];
+	    mask->data.PS_TYPE_IMAGE_MASK_DATA[peak->y - mask->row0][peak->x - mask->col0] |= PM_STARTSPAN_STOP;
+	}
+    }
+
+    /*
+     * Find starting span passing through (row, col)
+     */
+    imgRowF32 = img->data.F32[row];      // only one of
+    imgRowS32 = img->data.S32[row];      //      these is valid!
+    psImageMaskType *maskRow = mask->data.PS_TYPE_IMAGE_MASK_DATA[row];
+    {
+	int i;
+	for (i = col; i >= 0; i--) {
+	    pixVal = F32 ? imgRowF32[i] : imgRowS32[i];
+	    bool belowThreshold = (pixVal < threshold) && isfinite(pixVal);
+	    if ((maskRow[i] & PM_STARTSPAN_DETECTED) || belowThreshold) {
+		break;
+	    }
+	}
+	int i0 = i;
+	for (i = col; i < numCols; i++) {
+	    pixVal = F32 ? imgRowF32[i] : imgRowS32[i];
+	    bool belowThreshold = (pixVal < threshold) && isfinite(pixVal);
+	    if ((maskRow[i] & PM_STARTSPAN_DETECTED) || belowThreshold) {
+		break;
+	    }
+	}
+	int i1 = i;
+	pmSpan *sp = pmFootprintSetSpan(fp, row + row0, i0 + col0 + 1, i1 + col0 - 1);
+	pmFootprintSpansSet(fpSpans, sp, mask, PM_STARTSPAN_RESTART);
+	// fprintf (stderr, "first: %d vs %d\n", fp->nspans, fpSpans->nStartSpans);
+    }
+    /*
+     * Now workout from those pmStartSpans, searching for pixels above threshold
+     */
+    while (pmFootprintSpansBuild(fp, fpSpans, img, mask, threshold)) continue;
+    /*
+     * Cleanup
+     */
+    // psFree(mask);
+    // psFree(startspans);                  // restores the image pixel
+
+    return fp;                           // pmFootprint really
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmFootprintIDs.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmFootprintIDs.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmFootprintIDs.c	(revision 42651)
@@ -0,0 +1,122 @@
+/* @file  pmFootprintIDs.c
+ * functions to manipulate footprint IDs 
+ *
+ * @author RHL, Princeton & IfA; EAM, IfA
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-12-08 02:51:14 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+
+/*
+ * 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->nspans; 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;
+       }
+   }
+}
+
+void pmSetFootprintArrayIDsForImage(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) {
+       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
+    */
+   pmSetFootprintArrayIDsForImage(idImage, footprints, relativeIDs);
+
+   return idImage;
+   
+}
+
+/*
+ * Set an image to the value of footprint's ID whever they may fall
+ */
+psImage *pmSetFootprintID(psImage *idImage,
+			  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);
+   
+   if (idImage == NULL) {
+       idImage = psImageAlloc(numCols, numRows, PS_TYPE_S32);
+   } else {
+       assert (idImage->numCols == numCols);
+       assert (idImage->numRows == numRows);
+       // XXX assert on 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;
+   
+}
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmFootprintSpans.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmFootprintSpans.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmFootprintSpans.c	(revision 42651)
@@ -0,0 +1,136 @@
+/* @file  pmFootprintFindAtPoint.c
+ * find footprints in a small image relative to a reference point
+ *
+ * @author RHL, Princeton & IfA; EAM, IfA
+ *
+ * @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-01-27 06:39:38 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+
+static void pmStartSpanFree(pmStartSpan *sspan) {
+    return;
+}
+
+// Allocate an un-assigned pmStartSpan
+pmStartSpan *pmStartSpanAlloc() {
+    pmStartSpan *sspan = psAlloc(sizeof(pmStartSpan));
+    psMemSetDeallocator(sspan, (psFreeFunc)pmStartSpanFree);
+    
+    sspan->span = NULL;
+    sspan->direction = PM_STARTSPAN_NONE;
+    sspan->stop = false;
+    
+    return sspan;
+}
+
+// Assign a pmSpan to this pmStartSpan
+bool pmStartSpanSet(pmStartSpan *sspan,
+		    pmSpan *span,      // The span in question
+		    psImage *mask,           // Pixels that we've already detected
+		    const PM_STARTSPAN_DIR dir   // Should we continue searching towards the top of the image?
+    ) {
+    sspan->span = span; // view on the span (we do not own this memory)
+    sspan->direction = dir;
+    sspan->stop = false;
+    
+    if (mask) {                 // remember that we've detected these pixels
+        psImageMaskType *mpix = &mask->data.PS_TYPE_IMAGE_MASK_DATA[span->y - mask->row0][span->x0 - mask->col0];
+
+        for (int i = 0; i <= span->x1 - span->x0; i++) {
+            mpix[i] |= PM_STARTSPAN_DETECTED;
+            if (mpix[i] & PM_STARTSPAN_STOP) {
+                sspan->stop = true;
+            }
+        }
+    }
+
+    return true;
+}
+
+// remove a span assignment
+bool pmStartSpanUnset(pmStartSpan *sspan) {
+    if (!sspan) return false;
+    sspan->span = NULL;
+    sspan->direction = PM_STARTSPAN_NONE;
+    sspan->stop = false;
+    return true;
+}
+
+static void pmFootprintSpansFree(pmFootprintSpans *fp) {
+    psFree(fp->startspans);
+    return;
+}
+
+// Allocate a pmFootprintSpans structure with unassigned spans
+pmFootprintSpans *pmFootprintSpansAlloc(int nSpans) {
+    pmFootprintSpans *fpSpans = psAlloc(sizeof(pmFootprintSpans));
+    psMemSetDeallocator(fpSpans, (psFreeFunc)pmFootprintSpansFree);
+    
+    // create an footprint with allocated pmStartSpans, but none yet assigned
+    fpSpans->nStartSpans = 0;
+    fpSpans->startspans = psArrayAlloc(nSpans);
+
+    for (int i = 0; i < nSpans; i++) {
+	fpSpans->startspans->data[i] = pmStartSpanAlloc();
+    }
+    return fpSpans;
+}
+
+// Allocate a pmFootprintSpans structure with unassigned spans
+bool pmFootprintSpansInit(pmFootprintSpans *fpSpans) {
+    fpSpans->nStartSpans = 0;
+    for (int i = 0; i < fpSpans->startspans->n; i++) {
+	pmStartSpanUnset(fpSpans->startspans->data[i]);
+    }
+    return true;
+}
+
+// Add a new pmSpan to a pmFootprintSpans.  Iff we see a stop bit, return true
+bool pmFootprintSpansSet(pmFootprintSpans *fpSpans, // the saved pmStartSpans
+			 pmSpan *sp, // the span in question
+			 psImage *mask, // mask of detected/stop pixels
+			 const PM_STARTSPAN_DIR dir) { // the desired direction to search
+    if (dir == PM_STARTSPAN_RESTART) {
+        if (pmFootprintSpansSet(fpSpans, sp, mask, PM_STARTSPAN_UP)) return true;
+	if (pmFootprintSpansSet(fpSpans, sp, NULL, PM_STARTSPAN_DOWN)) return true;
+    } else {
+	int N = fpSpans->nStartSpans;
+	if (N == fpSpans->startspans->n) {
+	    // if we need more space, extend fpSpans->startspans as needed
+	    int Nalloc = fpSpans->startspans->n + 100;
+	    psArrayRealloc(fpSpans->startspans, Nalloc);
+	    fpSpans->startspans->n = Nalloc;
+	    for (int i = N; i < fpSpans->startspans->n; i++) {
+		fpSpans->startspans->data[i] = pmStartSpanAlloc();
+	    }
+	}
+	pmStartSpan *startspan = fpSpans->startspans->data[N];
+	
+	pmStartSpanSet (startspan, sp, mask, dir);
+
+        if (startspan->stop) {              // we detected a stop bit
+            pmStartSpanUnset(startspan);    // don't allocate new span
+            return true;
+        } else {
+	    fpSpans->nStartSpans ++;
+	    return false;
+        }
+    }
+    return false;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmFootprintSpans.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmFootprintSpans.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmFootprintSpans.h	(revision 42651)
@@ -0,0 +1,83 @@
+/* @file  pmFootprintSpans.h
+ *
+ * @author RHL, Princeton & IfA; EAM, IfA
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-12-09 21:16:09 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FOOTPRINT_SPANS_H
+#define PM_FOOTPRINT_SPANS_H
+
+/* We define two helper structures used in building the pmFootprints:
+ *
+ * pmStartSpan      : a smart span which knows how to (re-)start pixel scanning
+ * pmFootprintSpans : a collection of pmStartSpans which can define a footprint
+ *
+ * pmFootprintSpans allows us to allocate the memory for the spans before actually defining them
+ *
+ */
+
+/* pmStartSpan
+ *
+ * A data structure to hold the starting point for a search for pixels above threshold,
+ * used by pmFootprintsFindAtPoint
+ *
+ * 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 pmStartSpan
+//
+typedef enum {PM_STARTSPAN_NONE = 0,	// span is not defined
+	      PM_STARTSPAN_DOWN,	// scan down from this span
+              PM_STARTSPAN_UP,		// scan up from this span
+              PM_STARTSPAN_RESTART,	// restart scanning from this span
+              PM_STARTSPAN_DONE		// this span is processed
+} PM_STARTSPAN_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_STARTSPAN_STOP plane.  It's simpler to be
+// able to check for
+//
+enum {
+    PM_STARTSPAN_INITIAL = 0x0,             // initial state of pixels.
+    PM_STARTSPAN_DETECTED = 0x1,            // we've seen this pixel
+    PM_STARTSPAN_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 {
+    pmSpan *span;			// view on the real span (on a pmFootprint->spans array)
+    PM_STARTSPAN_DIR direction;		// How to continue searching
+    bool stop;                          // should we stop searching?
+} pmStartSpan;
+
+typedef struct {
+    psArray *startspans;
+    int nStartSpans;
+} pmFootprintSpans;
+
+pmStartSpan *pmStartSpanAlloc();
+
+bool pmStartSpanSet(pmStartSpan *sspan,
+		    pmSpan *span,      // The span in question
+		    psImage *mask,           // Pixels that we've already detected
+		    const PM_STARTSPAN_DIR dir   // Should we continue searching towards the top of the image?
+    );
+
+pmFootprintSpans *pmFootprintSpansAlloc(int nSpans);
+
+bool pmFootprintSpansInit(pmFootprintSpans *fpSpans);
+
+bool pmFootprintSpansSet(pmFootprintSpans *fpSpans, // the saved pmStartSpans
+			 pmSpan *sp, // the span in question
+			 psImage *mask, // mask of detected/stop pixels
+			 const PM_STARTSPAN_DIR dir); // the desired direction to search
+
+/// @}
+# endif /* PM_FOOTPRINT_SPANS_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmGrowthCurve.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmGrowthCurve.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmGrowthCurve.c	(revision 42651)
@@ -0,0 +1,115 @@
+/** @file  pmGrowthCurve.c
+ *
+ *  Measure the curve-of-growth for sources
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.15 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-12-08 02:51:14 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.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 );
+}
+
+# define NPTS 25
+pmGrowthCurve *pmGrowthCurveAlloc (psF32 minRadius, psF32 maxRadius, psF32 refRadius)
+{
+
+    pmGrowthCurve *growth = psAlloc (sizeof(pmGrowthCurve));
+    psMemSetDeallocator(growth, (psFreeFunc) pmGrowthCurveFree);
+
+    // set the scaling factor
+    float dR = log10(maxRadius / minRadius) / (float) NPTS;
+    float fR = pow (10.0, dR);
+
+    // Fractional pixel radii are not well defined; use integer pixel radii.  Use 1 pixel steps
+    // until the scaling factor steps in intervals larger than 1 pixel
+    // float Rlin = 1.0 / (fR - 1.0);
+
+    growth->radius = psVectorAllocEmpty (NPTS, PS_DATA_F32);
+    
+    // there will be NPTS radii + a few extras 
+    float radius = minRadius;
+    while (radius < refRadius) {
+	// fprintf (stderr, "r: %f\n", radius);
+	psVectorAppend (growth->radius, radius - 0.001);
+	radius += 1.0;
+    }    
+    growth->refBin = growth->radius->n - 1;
+    while (radius < maxRadius) {
+	// fprintf (stderr, "r: %f\n", radius);
+	psVectorAppend (growth->radius, radius - 0.001);
+	radius *= fR;
+	radius = (int) (radius + 0.5);
+    }    
+    psVectorAppend (growth->radius, radius);
+    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->fitMag = NAN;
+    growth->refMag = NAN;
+    growth->apRef  = NAN;
+    growth->apLoss = NAN;
+
+    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_branches/psModules.20240412/src/objects/pmGrowthCurve.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmGrowthCurve.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmGrowthCurve.h	(revision 42651)
@@ -0,0 +1,37 @@
+/* @file  pmGrowthCurve.h
+ * @brief functions to manipulate the curve-of-growth data
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.8 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-11-10 01:09:20 $
+ * 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 refMag;
+    psF32 apRef;   // apMag[refRadius]
+    psF32 apLoss;  // fitMag - apRef
+    int refBin;
+}
+pmGrowthCurve;
+
+bool psMemCheckGrowthCurve(psPtr ptr);
+
+pmGrowthCurve *pmGrowthCurveAlloc (psF32 minRadius, psF32 maxRadius, psF32 refRadius);
+psF32 pmGrowthCurveCorrect (pmGrowthCurve *growth, psF32 radius);
+
+/// @}
+# endif /* PM_GROWTH_CURVE_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmGrowthCurveGenerate.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmGrowthCurveGenerate.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmGrowthCurveGenerate.c	(revision 42651)
@@ -0,0 +1,380 @@
+/** @file  pmGrowthCurveGenerate.c
+ *
+ *  Generate the curve-of-growth
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-01-27 06:39:38 $
+ *
+ *  Copyright 2004 Institute for Astronomy, 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 "pmFPAMaskWeight.h"
+#include "psVectorBracket.h"
+#include "pmErrorCodes.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmSourcePhotometry.h"
+#include "pmGrowthCurveGenerate.h"
+
+/*****************************************************************************/
+/* FUNCTION IMPLEMENTATION - PUBLIC                                          */
+/*****************************************************************************/
+
+// we generate the growth curve for the center of the image with the specified psf model
+bool pmGrowthCurveGenerate (pmReadout *readout, pmPSF *psf, bool ignore, psImageMaskType maskVal, psImageMaskType markVal)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(readout->image, false);
+
+    // maskVal is used to test for rejected pixels, and must include markVal
+    maskVal |= markVal;
+
+    // XXX something of a hack: measure the growth curve at a number of points in the field and
+    // average them together
+
+    psArray *growths = psArrayAllocEmpty (100);
+
+    for (float ix = -0.4; ix <= +0.4; ix += 0.2) {
+	for (float iy = -0.4; iy <= +0.4; iy += 0.2) {
+
+	    // use the center of the center pixel of the image
+	    // 0.5 PIX: is this offset needed? probably -- the psf model uses 0.5,0.5 as the center, double check
+	    float xc = (int)(ix*readout->image->numCols + 0.5*readout->image->numCols) + readout->image->col0 + 0.5;
+	    float yc = (int)(iy*readout->image->numRows + 0.5*readout->image->numRows) + readout->image->row0 + 0.5;
+
+	    pmGrowthCurve *growth = pmGrowthCurveForPosition (readout->image, psf, ignore, maskVal, markVal, xc, yc);
+	    if (!growth) continue;
+
+	    psArrayAdd (growths, 100, growth);
+	    psFree (growth);
+	}
+    }
+    // psAssert (growths->n, "cannot build growth curve (psf model is invalid everywhere)");
+    // if we cannot generate a curve-of-growth, warn but do not raise an error or abort
+    if (!growths->n) {
+      psWarning ("cannot build growth curve (psf model is invalid everywhere)");
+      psFree (growths);
+      return false;
+    }
+
+    // just use a simple sample median to get the 'best' value from each growth curve...
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN);
+
+    psVector *values = psVectorAlloc (growths->n, PS_DATA_F32);
+
+    // median the values for the fitMags
+    values->n = 0;
+    for (int j = 0; j < growths->n; j++) {
+	pmGrowthCurve *growth = growths->data[j];
+	if (!isfinite(growth->fitMag)) continue;
+	psVectorAppend (values, growth->fitMag);
+    }
+    if (!psVectorStats (stats, values, NULL, NULL, 0)) {
+	// if we cannot generate a curve-of-growth, warn but do not raise an error or abort
+	psWarning("failure to measure stats for curve of growth");
+	psFree (growths);
+	psFree (stats);
+	psFree (values);
+	return false;
+    }
+    psf->growth->fitMag = stats->sampleMedian;
+
+    // loop over a range of source fluxes
+    // no need to interpolate since we have forced the object center
+    // to 0.5, 0.5 above
+    for (int i = 0; i < psf->growth->radius->n; i++) {
+
+	// median the values for each radial bin
+	values->n = 0;
+	for (int j = 0; j < growths->n; j++) {
+	    pmGrowthCurve *growth = growths->data[j];
+	    if (!isfinite(growth->apMag->data.F32[i])) continue;
+	    psVectorAppend (values, growth->apMag->data.F32[i]);
+	}
+	if (!psVectorStats (stats, values, NULL, NULL, 0)) {
+	    // psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	    psWarning("failure to measure stats for curve of growth");
+	    psFree (growths);
+	    psFree (stats);
+	    psFree (values);
+	    return false;
+	}
+	psf->growth->apMag->data.F32[i] = stats->sampleMedian;
+    }
+    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 (fitMag - apMag @ ref : %f - %f)\n", psf->growth->apLoss, psf->growth->fitMag, psf->growth->apRef);
+
+    psFree (growths);
+    psFree (stats);
+    psFree (values);
+
+    return true;
+}
+
+pmGrowthCurve *pmGrowthCurveForPosition (psImage *image, pmPSF *psf, bool ignore, psImageMaskType maskVal, psImageMaskType markVal, float xc, float yc) {
+
+    float fitMag, apMag;
+    float radius;
+
+    assert (psf->growth);
+
+    float minRadius = psf->growth->radius->data.F32[0];
+    pmGrowthCurve *growth = pmGrowthCurveAlloc (minRadius, psf->growth->maxRadius, psf->growth->refRadius);
+
+    float dx = growth->maxRadius + 1;
+    float dy = growth->maxRadius + 1;
+
+    // create template model
+    pmModel *modelRef = pmModelAlloc(psf->type);
+
+    // 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);
+    if (!model) {
+	psFree (growth);
+	return NULL;
+    }
+
+    // measure the fitMag for this model
+    pmSourcePhotometryModel (&fitMag, NULL, model);
+    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 (image, region);
+
+    // the view, image, and mask retain col0,row0
+    psImage *view = psImageSubset (image, region);
+    psImage *pixels = psImageCopy (NULL, view, PS_TYPE_F32);
+    psImage *mask = psImageCopy (NULL, view, PS_TYPE_IMAGE_MASK);
+
+    psImageInit (pixels, 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 (pixels, NULL, model, PM_MODEL_OP_FULL, maskVal);
+
+    // Loop over a range of radii.  No need to interpolate since we have forced the object
+    // center to 0.5, 0.5 above
+    for (int i = 0; i < growth->radius->n; i++) {
+
+        radius = growth->radius->data.F32[i];
+
+        // mask the given aperture and measure the apMag
+        psImageKeepCircle (mask, xc, yc, radius, "OR", markVal);
+        if (!pmSourcePhotometryAper (NULL, &apMag, NULL, NULL, model, pixels, NULL, mask, maskVal)) {
+	    psFree (growth);
+	    psFree (view);
+	    psFree (pixels);
+	    psFree (mask);
+	    psFree (model);
+	    psFree (modelRef);
+	    return NULL;
+        }
+	psImageMaskPixels (mask, "AND", PS_NOT_IMAGE_MASK(markVal)); // clear the circular mask
+
+        // the 'ignore' mode is for testing
+        if (ignore) {
+            growth->apMag->data.F32[i] = fitMag;
+        } else {
+            growth->apMag->data.F32[i] = apMag;
+        }
+    }
+    
+    psFree (view);
+    psFree (pixels);
+    psFree (mask);
+    psFree (model);
+    psFree (modelRef);
+
+    // psLogMsg ("psModules", 4, "GrowthCurve for %f,%f\n", xc, yc);
+
+    return growth;
+}
+
+# define DEBUG 0
+# if (DEBUG)
+static FILE *fgr = NULL;
+# endif
+
+// we generate the growth curve for the center of the image with the specified psf model
+bool pmGrowthCurveGenerateFromSources (pmReadout *readout, pmPSF *psf, psArray *sources, bool INTERPOLATE_AP, psImageMaskType maskVal, psImageMaskType markVal)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(readout->image, false);
+
+    // maskVal is used to test for rejected pixels, and must include markVal
+    maskVal |= markVal;
+
+    pmSourcePhotometryMode photMode = INTERPOLATE_AP ? PM_SOURCE_PHOT_INTERP : 0;
+    
+    // measure the growth curve for each PSF source and average them together
+    psArray *growths = psArrayAllocEmpty (100);
+
+# if (DEBUG)
+    fgr = fopen ("growth.mags.dat", "w");
+# endif
+
+    for (int i = 0; i < sources->n; i++) {
+
+        pmSource *source = sources->data[i];
+
+        if (!(source->mode & PM_SOURCE_MODE_PSFSTAR)) continue;
+
+	pmGrowthCurve *growth = pmGrowthCurveForSource (source, psf, photMode, maskVal, markVal);
+	if (!growth) continue;
+	
+	psArrayAdd (growths, 100, growth);
+	psFree (growth);
+    }
+    psAssert (growths->n, "cannot build growth curve (no valid PSF stars?)");
+
+# if (DEBUG)
+    fclose (fgr);
+# endif
+
+    // just use a simple sample median to get the 'best' value from each growth curve...
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN);
+
+    psVector *values = psVectorAlloc (growths->n, PS_DATA_F32);
+
+    // loop over a range of source fluxes
+    // no need to interpolate since we have forced the object center
+    // to 0.5, 0.5 above
+    for (int i = 0; i < psf->growth->radius->n; i++) {
+
+	// median the values for each radial bin
+	values->n = 0;
+	for (int j = 0; j < growths->n; j++) {
+	    pmGrowthCurve *growth = growths->data[j];
+	    if (!isfinite(growth->apMag->data.F32[i])) continue;
+	    psVectorAppend (values, growth->apMag->data.F32[i] - growth->refMag);
+	}
+	if (values->n == 0) {
+	    psf->growth->apMag->data.F32[i] = NAN;
+	} else {
+	    if (!psVectorStats (stats, values, NULL, NULL, 0)) {
+		// psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+		psWarning("failure to measure stats for curve of growth (from sources)");
+		psFree (growths);
+		psFree (stats);
+		psFree (values);
+		return false;
+	    }
+	    psf->growth->apMag->data.F32[i] = stats->sampleMedian;
+	}
+    }
+
+    psf->growth->fitMag = psf->growth->apMag->data.F32[psf->growth->radius->n-1];
+    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 (fitMag - apMag @ ref : %f - %f)\n", psf->growth->apLoss, psf->growth->fitMag, psf->growth->apRef);
+
+    psFree (growths);
+    psFree (stats);
+    psFree (values);
+
+    return true;
+}
+
+pmGrowthCurve *pmGrowthCurveForSource (pmSource *source, pmPSF *psf, pmSourcePhotometryMode photMode, psImageMaskType maskVal, psImageMaskType markVal) {
+
+    float radius;
+
+    assert (psf->growth);
+
+    float minRadius = psf->growth->radius->data.F32[0];
+    pmGrowthCurve *growth = pmGrowthCurveAlloc (minRadius, psf->growth->maxRadius, psf->growth->refRadius);
+
+    // measure the fitMag for this source (for normalization)
+    // pmSourcePhotometryModel (&fitMag, NULL, source->psfModel);
+    growth->fitMag = source->psfMag;
+
+    float xc = source->peak->xf;
+    float yc = source->peak->yf;
+
+    // Loop over the range of radii
+    for (int i = 0; i < growth->radius->n; i++) {
+
+        radius = growth->radius->data.F32[i];
+
+        // mask the given aperture and measure the apMag
+        psImageKeepCircle (source->maskObj, xc, yc, radius, "OR", markVal);
+
+        if (!pmSourceMagnitudes (source, psf, photMode, maskVal, markVal, radius)) {
+	    psFree (growth);
+	    return NULL;
+        }
+
+        // if (!pmSourcePhotometryAper (NULL, &apMag, NULL, NULL, NULL, source->pixels, NULL, source->maskObj, maskVal)) {
+	//     psFree (growth);
+	//     return NULL;
+        // }
+
+	psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(markVal)); // clear the circular mask
+
+	growth->apMag->data.F32[i] = source->apMag;
+    }
+    psAssert(growth->refBin >= 0, "invalid growth reference bin");
+    psAssert(growth->refBin < growth->apMag->n, "invalid growth reference bin");
+    growth->refMag = growth->apMag->data.F32[growth->refBin];
+
+    // Loop over the range of radii
+# if (DEBUG)
+    for (int i = 0; i < growth->radius->n; i++) {
+	fprintf (fgr, "%f %f  %f %f %f %f\n", xc, yc, growth->radius->data.F32[i], growth->apMag->data.F32[i], growth->fitMag, growth->refMag);
+    }
+# endif
+
+    return growth;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmGrowthCurveGenerate.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmGrowthCurveGenerate.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmGrowthCurveGenerate.h	(revision 42651)
@@ -0,0 +1,22 @@
+/* @file  pmGrowthCurveGenerate.h
+ *
+ * @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_GROWTH_CURVE_GENERATE_H
+# define PM_GROWTH_CURVE_GENERATE_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+bool pmGrowthCurveGenerate (pmReadout *readout, pmPSF *psf, bool ignore, psImageMaskType maskVal, psImageMaskType mark);
+pmGrowthCurve *pmGrowthCurveForPosition (psImage *image, pmPSF *psf, bool ignore, psImageMaskType maskVal, psImageMaskType markVal, float xc, float yc);
+bool pmGrowthCurveGenerateFromSources (pmReadout *readout, pmPSF *psf, psArray *sources, bool INTERPOLATE_AP, psImageMaskType maskVal, psImageMaskType markVal);
+pmGrowthCurve *pmGrowthCurveForSource (pmSource *source, pmPSF *psf, pmSourcePhotometryMode photMode, psImageMaskType maskVal, psImageMaskType markVal);
+
+/// @}
+# endif /* PM_GROWTH_CURVE_GENERATE_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmModel.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmModel.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmModel.c	(revision 42651)
@@ -0,0 +1,428 @@
+/** @file  pmModel.c
+ *
+ *  Functions to define and manipulate object models
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.28 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-16 22:30:26 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+
+static void modelFree(pmModel *tmp)
+{
+    psTrace("psModules.objects", 10, "---- %s() begin ----\n", __func__);
+    if (!tmp) return;
+
+    psFree(tmp->params);
+    psFree(tmp->dparams);
+    psFree(tmp->covar);
+    psTrace("psModules.objects", 10, "---- %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", 10, "---- %s() begin ----\n", __func__);
+
+    pmModelClass *class = pmModelClassSelect (type);
+    if (class == NULL) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return(NULL);
+    }
+
+    pmModel *tmp = (pmModel *) psAlloc(sizeof(pmModel));
+    psMemSetDeallocator(tmp, (psFreeFunc) modelFree);
+
+    tmp->type = type;
+    tmp->mag = NAN;
+    tmp->magErr = NAN;
+    tmp->chisq = NAN;
+    tmp->chisqNorm = NAN;
+    tmp->nDOF  = 0;
+    tmp->nPar  = 0;
+    tmp->nPix  = 0;
+    tmp->nIter = 0;
+    tmp->fitRadius = 0;
+    tmp->flags = PM_MODEL_STATUS_NONE;
+    tmp->residuals = NULL;              // do not free: the model does not own this memory
+    tmp->covar = NULL;
+    tmp->isPCM = false;
+
+    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] = NAN;
+        tmp->dparams->data.F32[i] = NAN;
+    }
+
+    tmp->class = class;
+
+    // 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;
+    // tmp->modelSetLimits     = class->modelSetLimits;
+
+    psTrace("psModules.objects", 10, "---- %s() end ----\n", __func__);
+    return(tmp);
+}
+
+bool psMemCheckModel(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) modelFree);
+}
+
+// copy model to a new structure
+pmModel *pmModelCopy (pmModel *model)
+{
+    if (model == NULL) {
+	return NULL;
+    }
+    pmModel *new = pmModelAlloc (model->type);
+
+    new->chisq     = model->chisq;
+    new->nDOF      = model->nDOF;
+    new->nIter     = model->nIter;
+    new->flags     = model->flags;
+    new->fitRadius = model->fitRadius;
+
+    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", 10, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(image, NAN);
+    PS_ASSERT_PTR_NON_NULL(model, NAN);
+    PS_ASSERT_PTR_NON_NULL(model->params, NAN);
+
+    // 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->class->modelFunc (NULL, model->params, x);
+    psFree(x);
+    psTrace("psModules.objects", 10, "---- %s() end ----\n", __func__);
+    return(tmpF);
+}
+
+psF32 pmModelEvalWithOffset(pmModel *model, psImage *image, psS32 col, psS32 row, int dx, int dy)
+{
+    psTrace("psModules.objects", 10, "---- %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->class->modelFunc (NULL, model->params, x);
+    psFree(x);
+    psTrace("psModules.objects", 10, "---- %s() end ----\n", __func__);
+    return(tmpF);
+}
+
+// XXX this is expensive in terms of malloc calls: the use of image interpolate and the residual images
+// makes this somewhat painful.
+static bool AddOrSubModel(psImage *image,
+                          psImage *mask,
+                          pmModel *model,
+                          pmModelOpMode mode,
+                          bool add,
+                          psImageMaskType maskVal,
+                          int dx,
+                          int dy
+    )
+{
+    psTrace("psModules.objects", 10, "---- %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;
+
+    float imageCol;
+    float imageRow;
+    float 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 XoSave  = params->data.F32[PM_PAR_XPOS];
+    float YoSave  = params->data.F32[PM_PAR_YPOS];
+    float IoSave  = params->data.F32[PM_PAR_I0];
+    float skySave = params->data.F32[PM_PAR_SKY];
+
+    // the options allow us to modify various aspects of the model
+    if (mode & PM_MODEL_OP_NORM) {
+	// if we are including the sky, renormalizing should force use to normalized down the sky flux
+	params->data.F32[PM_PAR_SKY] /= params->data.F32[PM_PAR_I0];
+        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;
+    }
+
+    // apply optional relative offset
+    // params->data.F32[PM_PAR_XPOS] += dx;
+    // params->data.F32[PM_PAR_YPOS] += dy;
+
+    // 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 DX = 0.0;
+    float DY = 0.0;
+    int NX = 0;
+    int NY = 0;
+
+    psF32 **Ro = NULL;
+    psF32 **Rx = NULL;
+    psF32 **Ry = NULL;
+    pmResidMaskType **Rm = NULL;
+
+    if (model->residuals) {
+	DX = xBin*(image->col0 - xCenter - dx) + model->residuals->xCenter + 0.5;
+	DY = yBin*(image->row0 - yCenter - dy) + model->residuals->yCenter + 0.5;
+	Ro = (model->residuals->Ro)   ? model->residuals->Ro->data.F32 : NULL;
+	Rx = (model->residuals->Rx)   ? model->residuals->Rx->data.F32 : NULL;
+	Ry = (model->residuals->Ry)   ? model->residuals->Ry->data.F32 : NULL;
+	Rm = (model->residuals->mask) ? model->residuals->mask->data.PM_TYPE_RESID_MASK_DATA : NULL;
+	if (Ro) {
+	    NX = model->residuals->Ro->numCols;
+	    NY = model->residuals->Ro->numRows;
+	}	    
+    }
+
+    // XXX trying to improve the speed and threadability of this function.
+    // note: model->residuals is a view to the item on pmPSF.
+
+    for (psS32 iy = 0; iy < image->numRows; iy++) {
+        for (psS32 ix = 0; ix < image->numCols; ix++) {
+            if ((mask != NULL) && (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] & maskVal))
+                continue;
+
+            // Convert to coordinate in parent image, with offset (dx,dy)
+	    // 0.5 PIX: the model take pixel coordinates so convert the pixel index here
+            imageCol = ix + 0.5 + image->col0 - dx;
+            imageRow = iy + 0.5 + image->row0 - dy;
+
+            x->data.F32[0] = imageCol;
+            x->data.F32[1] = imageRow;
+
+            pixelValue = 0.0;
+
+            // add in the desired components for this coordinate
+            if (mode & PM_MODEL_OP_FUNC) {
+                pixelValue += model->class->modelFunc (NULL, params, x);
+            }
+
+            // get the contribution from the residual model
+            if (Ro && (mode & PM_MODEL_OP_RES0)) {
+                // residual image position
+                float ry = yBin*iy + DY;
+                float rx = xBin*ix + DX;
+
+                int rx0 = rx - 0.5;
+                int rx1 = rx + 0.5;
+                int ry0 = ry - 0.5;
+                int ry1 = ry + 0.5;
+
+                if (rx0 < 0) goto skip;
+                if (ry0 < 0) goto skip;
+                if (rx1 >= NX) goto skip;
+                if (ry1 >= NY) goto skip;
+
+                // these go from 0.0 to 1.0 between the centers of the pixels
+                float fx = rx - 0.5 - rx0;
+                float Fx = 1.0 - fx;
+                float fy = ry - 0.5 - ry0;
+                float Fy = 1.0 - fy;
+
+                // check the residual image mask (if set). give up if any of the 4 pixels are masked.
+                if (Rm) {
+                    if (Rm[ry0][rx0]) goto skip;
+                    if (Rm[ry0][rx1]) goto skip;
+                    if (Rm[ry1][rx0]) goto skip;
+                    if (Rm[ry1][rx1]) goto skip;
+                }
+
+                // a possible further optimization if we re-use these values
+                // XXX allow for masked pixels, and add pixel weights
+                float V0 = (Ro[ry0][rx0]*Fx + Ro[ry0][rx1]*fx);
+                float V1 = (Ro[ry1][rx0]*Fx + Ro[ry1][rx1]*fx);
+                float Vo = V0*Fy + V1*fy;
+                if (!isfinite(Vo)) goto skip;
+
+                float Vx = 0.0;
+                float Vy = 0.0;
+
+                // skip Rx,Ry if Ro is masked
+                if (Rx && Ry && (mode & PM_MODEL_OP_RES1)) {
+                    V0 = (Rx[ry0][rx0]*Fx + Rx[ry0][rx1]*fx);
+                    V1 = (Rx[ry1][rx0]*Fx + Rx[ry1][rx1]*fx);
+                    Vx = V0*Fy + V1*fy;
+
+                    V0 = (Ry[ry0][rx0]*Fx + Ry[ry0][rx1]*fx);
+                    V1 = (Ry[ry1][rx0]*Fx + Ry[ry1][rx1]*fx);
+                    Vy = V0*Fy + V1*fy;
+                }
+                if (!isfinite(Vx)) goto skip;
+                if (!isfinite(Vy)) goto skip;
+
+                // 2D residual variations are set for the true source position
+                pixelValue += Io*(Vo + XoSave*Vx + YoSave*Vy);
+            }
+
+        skip:
+            // 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_XPOS] = XoSave;
+    params->data.F32[PM_PAR_YPOS] = YoSave;
+    params->data.F32[PM_PAR_I0]   = IoSave;
+    params->data.F32[PM_PAR_SKY]  = skySave;
+
+    psFree(x);
+    psTrace("psModules.objects", 10, "---- %s(true) end ----\n", __func__);
+    return(true);
+}
+
+/******************************************************************************
+ *****************************************************************************/
+bool pmModelAdd(psImage *image,
+                psImage *mask,
+                pmModel *model,
+                pmModelOpMode mode,
+                psImageMaskType maskVal)
+{
+    psTrace("psModules.objects", 10, "---- %s() begin ----\n", __func__);
+    psBool rc = AddOrSubModel(image, mask, model, mode, true, maskVal, 0.0, 0.0);
+    psTrace("psModules.objects", 10, "---- %s(%d) end ----\n", __func__, rc);
+    return(rc);
+}
+
+/******************************************************************************
+ *****************************************************************************/
+bool pmModelSub(psImage *image,
+                psImage *mask,
+                pmModel *model,
+                pmModelOpMode mode,
+                psImageMaskType maskVal)
+{
+    psTrace("psModules.objects", 10, "---- %s() begin ----\n", __func__);
+    psBool rc = AddOrSubModel(image, mask, model, mode, false, maskVal, 0.0, 0.0);
+    psTrace("psModules.objects", 10, "---- %s(%d) end ----\n", __func__, rc);
+    return(rc);
+}
+
+/******************************************************************************
+ *****************************************************************************/
+bool pmModelAddWithOffset(psImage *image,
+                          psImage *mask,
+                          pmModel *model,
+                          pmModelOpMode mode,
+                          psImageMaskType maskVal,
+                          int dx,
+                          int dy)
+{
+    psTrace("psModules.objects", 10, "---- %s() begin ----\n", __func__);
+    psBool rc = AddOrSubModel(image, mask, model, mode, true, maskVal, dx, dy);
+    psTrace("psModules.objects", 10, "---- %s(%d) end ----\n", __func__, rc);
+    return(rc);
+}
+
+/******************************************************************************
+ *****************************************************************************/
+bool pmModelSubWithOffset(psImage *image,
+                          psImage *mask,
+                          pmModel *model,
+                          pmModelOpMode mode,
+                          psImageMaskType maskVal,
+                          int dx,
+                          int dy)
+{
+    psTrace("psModules.objects", 10, "---- %s() begin ----\n", __func__);
+    psBool rc = AddOrSubModel(image, mask, model, mode, false, maskVal, dx, dy);
+    psTrace("psModules.objects", 10, "---- %s(%d) end ----\n", __func__, rc);
+    return(rc);
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmModel.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmModel.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmModel.h	(revision 42651)
@@ -0,0 +1,153 @@
+/* @file  pmModel.h
+ * @brief Functions to define and manipulate object models
+ *
+ * @author GLG, MHPCC
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.19 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-16 22:30:50 $
+ *
+ * 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 */
+
+/** 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.
+    psImage *covar;                     ///< Optional covariance matrix
+    float chisq;                        ///< Fit chi-squared.
+    float chisqNorm;                    ///< re-normalized fit chi-squared.
+    float mag;                          ///< integrated model magnitude
+    float magErr;                       ///< integrated model magnitude error
+    int nPix;                           ///< number of pixels used for fit
+    int nPar;                           ///< number of parameters in fit
+    int nDOF;                           ///< number of degrees of freedom (nDOF = nPix - nPar)
+    int nIter;                          ///< number of iterations to reach min
+    pmModelStatus flags;                ///< model status flags
+    float fitRadius;                    ///< fit radius actually used
+    pmResiduals *residuals;             ///< normalized PSF residuals
+    bool isPCM;				///< is this model fitted with PSF-convolution?
+
+    pmModelClass *class;
+
+    // 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;
+    // pmModelSetLimitsFunc modelSetLimits;
+};
+
+/** pmModelAlloc()
+ *
+ */
+pmModel *pmModelAlloc(pmModelType type);
+bool psMemCheckModel(psPtr ptr);
+
+// copy model to a new structure
+pmModel *pmModelCopy (pmModel *model);
+
+psF32 pmModelEval(pmModel *model, psImage *image, psS32 col, psS32 row);
+psF32 pmModelEvalWithOffset(pmModel *model, psImage *image, psS32 col, psS32 row, int dx, int dy);
+
+/** 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
+    psImageMaskType 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
+    psImageMaskType maskVal             ///< Value to mask
+);
+
+bool pmModelAddWithOffset(psImage *image,
+                          psImage *mask,
+                          pmModel *model,
+                          pmModelOpMode mode,
+                          psImageMaskType maskVal,
+                          int dx,
+                          int dy);
+
+bool pmModelSubWithOffset(psImage *image,
+                          psImage *mask,
+                          pmModel *model,
+                          pmModelOpMode mode,
+                          psImageMaskType 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
+);
+
+
+/// Set the model parameter limits for the given model
+///
+/// Wraps the model-specific pmModelSetLimitsFunc function.
+bool pmModelSetLimits(
+    const pmModel *model,               ///< Model of interest
+    pmModelLimits type                  ///< Type of limits
+    );
+
+
+/// @}
+# endif /* PM_MODEL_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmModelClass.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmModelClass.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmModelClass.c	(revision 42651)
@@ -0,0 +1,277 @@
+/** @file  pmModelClass.c
+ *
+ *  Functions to define and manipulate object model attributes
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-12-08 02:51:14 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+
+#include "pmErrorCodes.h"
+
+// XXX shouldn't these be defined for us in math.h ???
+double hypot(double x, double y);
+double sqrt (double x);
+
+# include "models/pmModel_GAUSS.h"
+# include "models/pmModel_PGAUSS.h"
+# include "models/pmModel_QGAUSS.h"
+# include "models/pmModel_PS1_V1.h"
+# include "models/pmModel_HSC_V1.h"
+# include "models/pmModel_RGAUSS.h"
+# include "models/pmModel_SERSIC.h"
+# include "models/pmModel_EXP.h"
+# include "models/pmModel_DEV.h"
+# include "models/pmModel_TRAIL.h"
+
+static pmModelClass defaultModels[] = {
+    {"PS_MODEL_GAUSS",        7, 0, (pmModelFunc)pmModelFunc_GAUSS,   (pmModelFlux)pmModelFlux_GAUSS,   (pmModelRadius)pmModelRadius_GAUSS,   (pmModelSetFWHM)pmModelSetFWHM_GAUSS,   (pmModelLimits)pmModelLimits_GAUSS,   (pmModelGuessFunc)pmModelGuess_GAUSS,  (pmModelFromPSFFunc)pmModelFromPSF_GAUSS,  (pmModelParamsFromPSF)pmModelParamsFromPSF_GAUSS,  (pmModelFitStatusFunc)pmModelFitStatus_GAUSS,  (pmModelSetLimitsFunc)pmModelSetLimits_GAUSS  },
+    {"PS_MODEL_PGAUSS",       7, 0, (pmModelFunc)pmModelFunc_PGAUSS,  (pmModelFlux)pmModelFlux_PGAUSS,  (pmModelRadius)pmModelRadius_PGAUSS,  (pmModelSetFWHM)pmModelSetFWHM_PGAUSS,  (pmModelLimits)pmModelLimits_PGAUSS,  (pmModelGuessFunc)pmModelGuess_PGAUSS, (pmModelFromPSFFunc)pmModelFromPSF_PGAUSS, (pmModelParamsFromPSF)pmModelParamsFromPSF_PGAUSS, (pmModelFitStatusFunc)pmModelFitStatus_PGAUSS, (pmModelSetLimitsFunc)pmModelSetLimits_PGAUSS },
+    {"PS_MODEL_QGAUSS",       8, 0, (pmModelFunc)pmModelFunc_QGAUSS,  (pmModelFlux)pmModelFlux_QGAUSS,  (pmModelRadius)pmModelRadius_QGAUSS,  (pmModelSetFWHM)pmModelSetFWHM_QGAUSS,  (pmModelLimits)pmModelLimits_QGAUSS,  (pmModelGuessFunc)pmModelGuess_QGAUSS, (pmModelFromPSFFunc)pmModelFromPSF_QGAUSS, (pmModelParamsFromPSF)pmModelParamsFromPSF_QGAUSS, (pmModelFitStatusFunc)pmModelFitStatus_QGAUSS, (pmModelSetLimitsFunc)pmModelSetLimits_QGAUSS },
+    {"PS_MODEL_PS1_V1",       8, 0, (pmModelFunc)pmModelFunc_PS1_V1,  (pmModelFlux)pmModelFlux_PS1_V1,  (pmModelRadius)pmModelRadius_PS1_V1,  (pmModelSetFWHM)pmModelSetFWHM_PS1_V1,  (pmModelLimits)pmModelLimits_PS1_V1,  (pmModelGuessFunc)pmModelGuess_PS1_V1, (pmModelFromPSFFunc)pmModelFromPSF_PS1_V1, (pmModelParamsFromPSF)pmModelParamsFromPSF_PS1_V1, (pmModelFitStatusFunc)pmModelFitStatus_PS1_V1, (pmModelSetLimitsFunc)pmModelSetLimits_PS1_V1 },
+    {"PS_MODEL_RGAUSS",       8, 0, (pmModelFunc)pmModelFunc_RGAUSS,  (pmModelFlux)pmModelFlux_RGAUSS,  (pmModelRadius)pmModelRadius_RGAUSS,  (pmModelSetFWHM)pmModelSetFWHM_RGAUSS,  (pmModelLimits)pmModelLimits_RGAUSS,  (pmModelGuessFunc)pmModelGuess_RGAUSS, (pmModelFromPSFFunc)pmModelFromPSF_RGAUSS, (pmModelParamsFromPSF)pmModelParamsFromPSF_RGAUSS, (pmModelFitStatusFunc)pmModelFitStatus_RGAUSS, (pmModelSetLimitsFunc)pmModelSetLimits_RGAUSS },
+    {"PS_MODEL_SERSIC",       8, 1, (pmModelFunc)pmModelFunc_SERSIC,  (pmModelFlux)pmModelFlux_SERSIC,  (pmModelRadius)pmModelRadius_SERSIC,  (pmModelSetFWHM)pmModelSetFWHM_SERSIC,  (pmModelLimits)pmModelLimits_SERSIC,  (pmModelGuessFunc)pmModelGuess_SERSIC, (pmModelFromPSFFunc)pmModelFromPSF_SERSIC, (pmModelParamsFromPSF)pmModelParamsFromPSF_SERSIC, (pmModelFitStatusFunc)pmModelFitStatus_SERSIC, (pmModelSetLimitsFunc)pmModelSetLimits_SERSIC },
+    {"PS_MODEL_EXP",          7, 1, (pmModelFunc)pmModelFunc_EXP,     (pmModelFlux)pmModelFlux_EXP,     (pmModelRadius)pmModelRadius_EXP,     (pmModelSetFWHM)pmModelSetFWHM_EXP,     (pmModelLimits)pmModelLimits_EXP,     (pmModelGuessFunc)pmModelGuess_EXP,    (pmModelFromPSFFunc)pmModelFromPSF_EXP,    (pmModelParamsFromPSF)pmModelParamsFromPSF_EXP,    (pmModelFitStatusFunc)pmModelFitStatus_EXP,    (pmModelSetLimitsFunc)pmModelSetLimits_EXP    },
+    {"PS_MODEL_DEV",          7, 1, (pmModelFunc)pmModelFunc_DEV,     (pmModelFlux)pmModelFlux_DEV,     (pmModelRadius)pmModelRadius_DEV,     (pmModelSetFWHM)pmModelSetFWHM_DEV,     (pmModelLimits)pmModelLimits_DEV,     (pmModelGuessFunc)pmModelGuess_DEV,    (pmModelFromPSFFunc)pmModelFromPSF_DEV,    (pmModelParamsFromPSF)pmModelParamsFromPSF_DEV,    (pmModelFitStatusFunc)pmModelFitStatus_DEV,    (pmModelSetLimitsFunc)pmModelSetLimits_DEV    },
+    {"PS_MODEL_TRAIL",        7, 0, (pmModelFunc)pmModelFunc_TRAIL,   (pmModelFlux)pmModelFlux_TRAIL,   (pmModelRadius)pmModelRadius_TRAIL,   (pmModelSetFWHM)pmModelSetFWHM_TRAIL,   (pmModelLimits)pmModelLimits_TRAIL,   (pmModelGuessFunc)pmModelGuess_TRAIL,  (pmModelFromPSFFunc)pmModelFromPSF_TRAIL,  (pmModelParamsFromPSF)pmModelParamsFromPSF_TRAIL,  (pmModelFitStatusFunc)pmModelFitStatus_TRAIL,  (pmModelSetLimitsFunc)pmModelSetLimits_TRAIL  },
+    {"PS_MODEL_HSC_V1",       8, 0, (pmModelFunc)pmModelFunc_HSC_V1,  (pmModelFlux)pmModelFlux_HSC_V1,  (pmModelRadius)pmModelRadius_HSC_V1,  (pmModelSetFWHM)pmModelSetFWHM_HSC_V1,  (pmModelLimits)pmModelLimits_HSC_V1,  (pmModelGuessFunc)pmModelGuess_HSC_V1, (pmModelFromPSFFunc)pmModelFromPSF_HSC_V1, (pmModelParamsFromPSF)pmModelParamsFromPSF_HSC_V1, (pmModelFitStatusFunc)pmModelFitStatus_HSC_V1, (pmModelSetLimitsFunc)pmModelSetLimits_HSC_V1 },
+};
+
+static pmModelClass *models = NULL;
+static psVector *modelClassLookupTable = NULL;  // translation between model types in header and here
+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);
+}
+
+bool psMemCheckModelClass(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) ModelClassFree);
+}
+
+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 (models == NULL) {
+        pmModelClassInit();
+    }
+
+    if ((type < 0) || (type >= Nmodels)) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return (NULL);
+    }
+    return (&models[type]);
+}
+
+void pmModelClassCleanup (void)
+{
+    psFree (models);
+    models = NULL;
+    Nmodels = 0;
+    psFree(modelClassLookupTable);
+    modelClassLookupTable = NULL;
+    return;
+}
+
+psS32 pmModelClassParameterCount (pmModelType type)
+{
+    if (models == NULL) {
+        pmModelClassInit();
+    }
+
+    if ((type < 0) || (type >= Nmodels)) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return (0);
+    }
+    return (models[type].nParams);
+}
+
+psS32 pmModelClassGetType (const char *name)
+{
+    if (models == NULL) {
+        pmModelClassInit();
+    }
+
+    for (int i = 0; i < Nmodels; i++) {
+        if (!strcmp(models[i].name, name)) {
+            return (i);
+        }
+    }
+    return (-1);
+}
+
+char *pmModelClassGetName (pmModelType type)
+{
+    if (models == NULL) {
+        pmModelClassInit();
+    }
+
+    if ((type < 0) || (type >= Nmodels)) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return (NULL);
+    }
+    return (models[type].name);
+}
+
+
+void pmModelClassSetLimits(pmModelLimitsType type)
+{
+    if (!models) {
+        pmModelClassInit();
+    }
+
+    for (int i = 0; i < Nmodels; i++) {
+        if (models[i].modelSetLimits) {
+            models[i].modelSetLimits(type);
+        }
+    }
+
+}
+
+
+bool pmModelClassWriteHeader(psMetadata *header)
+{
+    psMetadataAddS32(header, PS_LIST_TAIL, "MTNUM", PS_META_REPLACE, "number of model types", Nmodels);
+    for (int i = 0; i < Nmodels; i++) {
+        char modelNameKey[16];
+        char modelValKey[16];
+        sprintf(modelNameKey, "MTNAM%02d", i);
+        sprintf(modelValKey,  "MTVAL%02d", i);
+        psMetadataAddStr(header, PS_LIST_TAIL, modelNameKey, PS_META_REPLACE, "", models[i].name);
+        psMetadataAddS32(header, PS_LIST_TAIL, modelValKey, PS_META_REPLACE, "", i);
+    }
+
+    return true;
+}
+
+bool pmModelClassReadHeader(psMetadata *header) {
+    psFree(modelClassLookupTable);
+
+    bool status;
+    int numHeaderModels = psMetadataLookupS32(&status, header, "MTNUM");
+    if (!status) {
+        return false;
+    }
+
+    psVector *inputTypes = psVectorAlloc(numHeaderModels, PS_TYPE_S32);
+    psVector *localTypes = psVectorAlloc(numHeaderModels, PS_TYPE_S32);
+    int max_val = -1;
+    for (int i = 0; i < numHeaderModels; i++) {
+        char modelNameKey[16];
+        char modelValKey[16];
+        sprintf(modelNameKey, "MTNAM%02d", i);
+        sprintf(modelValKey,  "MTVAL%02d", i);
+        psString thisName = psMetadataLookupStr(&status, header, modelNameKey);
+        int thisVal = psMetadataLookupS32(&status, header, modelValKey);
+        if (thisVal > max_val) {
+            max_val = thisVal;
+        }
+        inputTypes->data.S32[i] = thisVal;
+        localTypes->data.S32[i] = pmModelClassGetType(thisName);
+    }
+    if (max_val < 0) {
+        psFree(inputTypes);
+        psFree(localTypes);
+        return false;
+    }
+
+    modelClassLookupTable = psVectorAlloc(max_val + 1, PS_TYPE_S32);
+    psVectorInit(modelClassLookupTable, -1);
+
+    for (int i = 0; i < numHeaderModels; i++) {
+        int thisVal = inputTypes->data.S32[i];
+        int localVal = localTypes->data.S32[i];
+        modelClassLookupTable->data.S32[thisVal] = localVal;
+    }
+    psFree(inputTypes);
+    psFree(localTypes);
+
+    return true;
+}
+
+pmModelType pmModelClassGetLocalType(pmModelType inputType) {
+    pmModelType localType = -1;
+
+    if (modelClassLookupTable) {
+        if (inputType >= 0 && inputType < modelClassLookupTable->n) {
+            localType = modelClassLookupTable->data.S32[inputType];
+        }
+    } else {
+        // no lookup table defined
+        // for backwards compatability if inputType refers to a defined model, return it
+        if (inputType >= 0 && pmModelClassGetName(inputType)) {
+            localType = inputType;
+        }
+    }
+
+    return localType;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmModelClass.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmModelClass.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmModelClass.h	(revision 42651)
@@ -0,0 +1,88 @@
+/* @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.4 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-11-27 03:14:57 $
+ * 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;
+    bool useReff;
+    pmModelFunc          modelFunc;
+    pmModelFlux          modelFlux;
+    pmModelRadius        modelRadius;
+    pmModelSetFWHM       modelSetFWHM;
+    pmModelLimits        modelLimits;
+    pmModelGuessFunc     modelGuess;
+    pmModelFromPSFFunc   modelFromPSF;
+    pmModelParamsFromPSF modelParamsFromPSF;
+    pmModelFitStatusFunc modelFitStatus;
+    pmModelSetLimitsFunc modelSetLimits;
+} pmModelClass;
+
+// allocate a pmModelClass to hold nModels entries
+pmModelClass *pmModelClassAlloc (int nModels);
+
+//
+bool psMemCheckModelClass(psPtr ptr);
+
+// 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 (const char *name);
+
+/// Set parameter limits for all models
+void pmModelClassSetLimits(pmModelLimitsType type);
+
+// write keywords to header definining the model type values used by this program
+bool pmModelClassWriteHeader(psMetadata *header);
+// create a lookup table for translating input model type values to local model type values
+bool pmModelClassReadHeader(psMetadata *header);
+// translate input model type value to local value
+pmModelType pmModelClassGetLocalType(pmModelType inputType);
+
+/// @}
+# endif /* PM_MODEL_CLASS_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmModelFuncs.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmModelFuncs.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmModelFuncs.h	(revision 42651)
@@ -0,0 +1,137 @@
+/* @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.4 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-11-27 03:14:57 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_MODEL_FUNCS_H
+# define PM_MODEL_FUNCS_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+// type of model carried by the pmModel structure
+typedef int pmModelType;
+
+typedef enum {
+    PM_MODEL_STATUS_NONE           = 0x0000, ///< model fit not yet attempted, no other info
+    PM_MODEL_STATUS_FITTED         = 0x0001, ///< model fit completed
+    PM_MODEL_STATUS_NONCONVERGE    = 0x0002, ///< model fit did not converge
+    PM_MODEL_STATUS_OFFIMAGE       = 0x0004, ///< model fit drove out of range
+    PM_MODEL_STATUS_BADARGS        = 0x0008, ///< model fit called with invalid args
+    PM_MODEL_STATUS_LIMITS         = 0x0010, ///< model parameters hit limits
+    PM_MODEL_STATUS_WEAK_FIT       = 0x0020, ///< model fit met loose tolerance, but not tight tolerance
+    PM_MODEL_STATUS_NAN_CHISQ      = 0x0040, ///< model fit failed with a NAN chisq 
+    PM_MODEL_SERSIC_PCM_FAIL_GUESS = 0x0080, ///< sersic model fit failed on the initial moments-based guess
+    PM_MODEL_SERSIC_PCM_FAIL_GRID  = 0x0100, ///< sersic model fit failed on the grid search
+    PM_MODEL_PCM_FAIL_GUESS        = 0x0200, ///< non-sersic model fit failed on the initial moments-based guess
+    PM_MODEL_BEST_FIT              = 0x0400, ///< this model was the best fit and was subtracted
+    PM_MODEL_STATUS_NAN_SHAPE      = 0x0800, ///< model ellipse parameters do not transform to valid ellipse
+    PM_MODEL_STATUS_NAN_SHAPE_ERR  = 0x1000, ///< model ellipse parameters errors do not transform to valid ellipse
+    PM_MODEL_STATUS_FAIL_SHAPE_ERR = 0x2000, ///< could not find an MC solution for ellipse parameter errors
+} 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,
+    PM_MODEL_OP_MODELVAR = 0x80,
+} pmModelOpMode;
+
+/// Parameter limit types
+typedef enum {
+    PM_MODEL_LIMITS_NONE,               ///< Apply no limits: suitable for debugging
+    PM_MODEL_LIMITS_IGNORE,             ///< Ignore all limits: fit can go to town
+    PM_MODEL_LIMITS_LAX,                ///< Lax limits: attempting to reproduce even bad data
+    PM_MODEL_LIMITS_MODERATE,           ///< Moderate limits: cope with mildly bad data
+    PM_MODEL_LIMITS_STRICT,             ///< Strict limits: insist on good quality data
+} pmModelLimitsType;
+
+/** 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 center of object
+#define PM_PAR_YPOS 3   ///< Y center 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   ///< Model-dependent parameter
+#define PM_PAR_8    8   ///< Model-dependent parameter
+
+// these are used by pmModel_TRAIL, with refers to L and Theta explicitly
+#define PM_PAR_LENGTH 4 ///< trail length
+#define PM_PAR_THETA  5 ///< position angle
+#define PM_PAR_SIGMA  6 ///< position angle
+
+/*** these prototype classes are used to define elements of the pmModelClass structure below ***/
+ 
+typedef struct pmModel  pmModel;
+typedef struct pmSource pmSource;
+typedef struct pmPSF    pmPSF;
+
+//  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 returns the FWHM given the supplied sigma (major or minor)
+typedef psF64 (*pmModelSetFWHM)(const psVector *params, double sigma);
+
+//  This function provides the model guess parameters based on the details of
+//  the given source.
+typedef bool (*pmModelGuessFunc)(pmModel *model, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal);
+
+//  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, const pmPSF *psf);
+
+//  This function sets the model parameters based on the PSF for a given coordinate and central
+//  intensity
+typedef bool (*pmModelParamsFromPSF)(pmModel *model, const 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);
+
+//  This function sets the parameter limits for the given model
+typedef bool (*pmModelSetLimitsFunc)(pmModelLimitsType type);
+
+/// @}
+# endif /* PM_MODEL_FUNCS_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmModelUtils.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmModelUtils.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmModelUtils.c	(revision 42651)
@@ -0,0 +1,246 @@
+/** @file  pmModelUtils.c
+ *
+ *  Functions to manipulate object models
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-10-08 21:53:08 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+
+#include "pmErrorCodes.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, const pmPSF *psf)
+{
+    PS_ASSERT_PTR_NON_NULL(psf, NULL);
+    PS_ASSERT_PTR_NON_NULL(modelEXT, NULL);
+
+    // 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->class->modelFromPSF (modelPSF, modelEXT, psf)) {
+        psTrace ("psModules.objects", 3, "Failed to set model params from PSF");
+        psFree(modelPSF);
+        return NULL;
+    }
+    // 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 (const pmPSF *psf, float Xo, float Yo, float Io)
+{
+    PS_ASSERT_PTR_NON_NULL(psf, NULL);
+
+    // 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->class->modelParamsFromPSF (modelPSF, psf, Xo, Yo, Io)) {
+        psFree(modelPSF);
+        return NULL;
+    }
+
+    // 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) {
+    PS_ASSERT_PTR_NON_NULL(model, NULL);
+    PS_ASSERT_PTR_NON_NULL(model->params, NULL);
+
+    // set Io to be 1.0
+    model->params->data.F32[PM_PAR_I0] = 1.0;
+
+    // determine the normalized flux
+    float normFlux = model->class->modelFlux (model->params);
+    assert (isfinite(normFlux));
+    assert (normFlux > 0);
+
+    // set the desired normalization
+    model->params->data.F32[PM_PAR_I0] = flux / normFlux;
+
+    return true;
+}
+
+bool pmModelUseReff (pmModelType type) {
+
+    pmModelClass *class = pmModelClassSelect (type);
+    psAssert (class, "undefined model class?");
+    bool useReff = class->useReff;
+    return useReff;
+}
+
+// this function and the one below handle the two cases, where the model shape is uses R_eff or Sigma
+bool pmModelAxesToParams (float *Sxx, float *Sxy, float *Syy, psEllipseAxes axes, bool useReff)  {
+
+    // restrict axex to 0.5 here not below 
+    if (axes.minor < 0.2) axes.minor = 0.2;
+    if (axes.major < 0.2) axes.major = 0.2;
+
+    psEllipseShape shape = psEllipseAxesToShape (axes);
+
+    if (!isfinite(shape.sx))  return false;
+    if (!isfinite(shape.sy))  return false;
+    if (!isfinite(shape.sxy)) return false;
+
+    // set the shape parameters
+    if (useReff) {
+	// *Sxx  = PS_MAX(0.5, shape.sx);
+	// *Syy  = PS_MAX(0.5, shape.sy);
+	*Sxx  = shape.sx;
+	*Syy  = shape.sy;
+	*Sxy  = shape.sxy * 2.0;
+    } else {
+	// *Sxx  = PS_MAX(0.5, M_SQRT2*shape.sx);
+	// *Syy  = PS_MAX(0.5, M_SQRT2*shape.sy);
+	*Sxx  = M_SQRT2*shape.sx;
+	*Syy  = M_SQRT2*shape.sy;
+	*Sxy  = shape.sxy;
+    }
+
+    return true;
+}
+
+bool pmModelParamsToAxes (psEllipseAxes *axes, float Sxx, float Sxy, float Syy, bool useReff)  {
+
+    psEllipseShape shape;
+
+    // set the shape parameters
+    if (useReff) {
+	shape.sx  = Sxx;
+	shape.sy  = Syy;
+	shape.sxy = Sxy / 2.0;
+    } else {
+	shape.sx  = Sxx / M_SQRT2;
+	shape.sy  = Syy / M_SQRT2;
+	shape.sxy = Sxy;
+    }
+
+    if ((shape.sx == 0) || (shape.sy == 0)) {
+        axes->major = 0.0;
+        axes->minor = 0.0;
+        axes->theta = 0.0;
+    } else {
+	// axes ratio < 20
+	// replace with maxAR argument?
+	*axes = psEllipseShapeToAxes (shape, 20.0);
+    }
+
+    return true;
+}
+
+// Reff says if this is a model which uses R_eff (like exp or dev) instead of Sigma
+// set the parameter values SXX, SXY, SYY
+// Scale allows some models to increase the guess size relative to Mxx,Myy
+bool pmModelSetShape (float *Sxx, float *Sxy, float *Syy, pmMoments *moments, bool useReff, float Scale) {
+
+    psEllipseMoments emoments;
+    emoments.x2 = moments->Mxx;
+    emoments.xy = moments->Mxy;
+    emoments.y2 = moments->Myy;
+
+    // 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;
+
+    // set a lower limit to avoid absurd solutions..
+    // NOTE: I should set the lower limit based on the PSF size, if known
+    float Rmajor = PS_MAX(1.0, Scale * axes.major);
+    float Rminor = Rmajor * (axes.minor / axes.major);
+    axes.major = Rmajor;
+    axes.minor = Rminor;
+
+    // EAM 2022.02.05 : Mrf is often much too large, disable this for now
+    // Mxx, Mxy, Myy define the elliptical shape, but Mrf defines the width 
+    // float scale = (isfinite(moments->Mrf) && (moments->Mrf > 0.0)) ? moments->Mrf / axes.major : 1.0;
+    // axes.major *= scale;
+    // axes.minor *= scale;
+
+    pmModelAxesToParams (Sxx, Sxy, Syy, axes, useReff);
+
+    return true;
+}
+
+bool pmModelSetNorm (float *Io, pmSource *source) {
+
+    *Io = source->peak->rawFlux;
+
+#ifndef ALLOW_NONFINITE_PEAK
+    // Gene says fail of peak !finite
+    if (!isfinite(*Io)) return false;
+#else 
+    // This is the way it used to be. Somtimes an infinite value Io made it's way down the pipeline
+    // causing assertion failures
+    if (!isfinite(*Io) && !source->moments) return false;
+
+    *Io = source->moments->Peak;
+    if (!isfinite(*Io)) return false;
+#endif
+
+    return true;
+}
+
+bool pmModelSetPosition (float *Xo, float *Yo, pmSource *source) {
+
+    bool useMoments = pmSourcePositionUseMoments(source);
+    
+    if (useMoments) {
+	*Xo = source->moments->Mx;
+	*Yo = source->moments->My;
+    } else {
+	*Xo = source->peak->xf;
+	*Yo = source->peak->yf;
+    }
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmModelUtils.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmModelUtils.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmModelUtils.h	(revision 42651)
@@ -0,0 +1,56 @@
+/* @file  pmModelUtils.h
+ *
+ * Utility functions for working with pmSources
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-12-15 01:22:11 $
+ * 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
+    const pmPSF *psf                    ///< Add comment
+);
+
+pmModel *pmModelFromPSFforXY (
+    const pmPSF *psf,
+    float Xo,
+    float Yo,
+    float Io
+    );
+
+bool pmModelSetFlux (
+    pmModel *model,
+    float flux
+    );
+
+bool pmModelSetPosition (float *Xo, float *Yo, pmSource *source);
+bool pmModelSetNorm (float *Io, pmSource *source);
+bool pmModelSetShape (float *Sxx, float *Sxy, float *Syy, pmMoments *moments, bool useReff, float Scale);
+
+bool pmModelUseReff (pmModelType type);
+bool pmModelAxesToParams (float *Sxx, float *Sxy, float *Syy, psEllipseAxes axes, bool useReff);
+bool pmModelParamsToAxes (psEllipseAxes *axes, float Sxx, float Sxy, float Syy, bool useReff);
+
+// XXX void pmModelSetModelVarOption (bool option);
+// XXX bool pmModelGetModelVarOption (void);
+
+/// @}
+# endif /* PM_MODEL_UTILS_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmModel_CentralPixel.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmModel_CentralPixel.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmModel_CentralPixel.c	(revision 42651)
@@ -0,0 +1,821 @@
+/* @file  pmModel_CentralPixel.c
+ * @brief Functions to manage the central pixel for sersic-like models
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.19 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-16 22:30:50 $
+ *
+ * Copyright 2013 Institute for Astronomy, University of Hawaii
+ */
+
+/******************************************************************************
+ * this file contains functions to determine the flux of the central pixel(s) for an
+ * exponential / devaucouleur / sersic style galaxy model.  The problem is that (a) these
+ * models are so centrally-peaked that it is necessary to determine the true mean flux in the
+ * central pixel by integration of fractional pixels (at least 0.02 pixels in the case of a
+ * DEV model) and (b) the process of integrating the central pixel is too slow to be used for
+ * real processing.
+
+ * we bypass this problem by defining a set of pre-calculated central pixel images, with
+ * subpixel resolution > 1 pixel (maybe 11 subpixels per real pixel).  These pre-calculated
+ * images are generated for a series of values for the following parameters: sersic index,
+ * effective radius, axial ratio.  We then select the closest image to our specific case, and
+ * integrate over the true sub-pixels relevant for our position and model.  We have thus turned
+ * problem from 2500 evaluations of the full sersic model to ~100 straight additions (possibly
+ * x 6 if we need to interpolate in each of the dimensions).  
+
+ * we need a number of support functions:
+
+ * pmModelCP_Load : load CP model data from the specified file. 
+ * pmModelCP_GetImage : choose an appropriate CP model image for a given set of parameters
+ * pmModelCP_GetValue : calculate the true CP value for the given image and parameters
+
+   *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+#include <pslib.h>
+
+#include "pmModel_CentralPixel.h"
+
+static void pmModelCP_Free(pmModelCP *cp) {
+    psFree (cp->flux);
+}
+
+pmModelCP *pmModelCP_Alloc(void)
+{
+    pmModelCP *tmp = (pmModelCP *) psAlloc(sizeof(pmModelCP));
+    psMemSetDeallocator(tmp, (psFreeFunc) pmModelCP_Free);
+
+    tmp->flux = NULL;
+    tmp->Rmajor = NAN;
+    tmp->Aratio = NAN;
+    tmp->Sindex = NAN;
+
+    return tmp;
+}
+
+static void pmModelCPset_Free(pmModelCPset *tmp) {
+
+    for (int i = 0; i < tmp->RmajorNitem; i++) {
+	for (int j = 0; j < tmp->AratioNitem; j++) {
+	    psFree (tmp->lookupCube[i][j]);
+	}
+	psFree (tmp->lookupCube[i]);
+    }
+    psFree (tmp->lookupCube);
+    psFree (tmp->images);
+}
+
+pmModelCPset *pmModelCPset_Alloc(void)
+{
+    pmModelCPset *tmp = (pmModelCPset *) psAlloc(sizeof(pmModelCPset));
+    psMemSetDeallocator(tmp, (psFreeFunc) pmModelCPset_Free);
+
+    tmp->RmajorMin = NAN;
+    tmp->RmajorMax = NAN;
+    tmp->RmajorDel = NAN;
+
+    tmp->AratioMin = NAN;
+    tmp->AratioMax = NAN;
+    tmp->AratioDel = NAN;
+
+    tmp->SindexMin = NAN;
+    tmp->SindexMax = NAN;
+    tmp->SindexDel = NAN;
+
+    tmp->RmajorNitem = 0;
+    tmp->AratioNitem = 0;
+    tmp->SindexNitem = 0;
+
+    tmp->lookupCube = NULL;
+    tmp->images = NULL;
+
+    return tmp;
+}
+
+// load the central-pixel maps as an array of pmModelCP 
+pmModelCPset *pmModelCP_Load (char *filename) {
+
+    bool status;
+
+    psFits *fits = psFitsOpen (filename, "r");
+    if (!fits) {
+	return false;
+    }
+    
+    // read the PHU -- it defines descriptive metadata
+    psMetadata *PHU = psFitsReadHeader (NULL, fits);
+    if (!PHU) {
+	psFitsClose (fits);
+	return false;
+    }
+
+    pmModelCPset *CPset = pmModelCPset_Alloc();
+
+    // NOTE : RMAJOR refers to the LOG_10 of the major axis
+    CPset->RmajorMin = psMetadataLookupF32 (&status, PHU, "RMAJ_MIN"); psAssert (status, "missing keyword RMAJ_MIN");
+    CPset->RmajorMax = psMetadataLookupF32 (&status, PHU, "RMAJ_MAX"); psAssert (status, "missing keyword RMAJ_MAX");
+    CPset->RmajorDel = psMetadataLookupF32 (&status, PHU, "RMAJ_DEL"); psAssert (status, "missing keyword RMAJ_DEL");
+    CPset->AratioMin = psMetadataLookupF32 (&status, PHU, "ARAT_MIN"); psAssert (status, "missing keyword ARAT_MIN");
+    CPset->AratioMax = psMetadataLookupF32 (&status, PHU, "ARAT_MAX"); psAssert (status, "missing keyword ARAT_MAX");
+    CPset->AratioDel = psMetadataLookupF32 (&status, PHU, "ARAT_DEL"); psAssert (status, "missing keyword ARAT_DEL");
+    CPset->SindexMin = psMetadataLookupF32 (&status, PHU, "SIDX_MIN"); psAssert (status, "missing keyword SIDX_MIN");
+    CPset->SindexMax = psMetadataLookupF32 (&status, PHU, "SIDX_MAX"); psAssert (status, "missing keyword SIDX_MAX");
+    CPset->SindexDel = psMetadataLookupF32 (&status, PHU, "SIDX_DEL"); psAssert (status, "missing keyword SIDX_DEL");
+
+    CPset->RmajorNitem = 1 + (int)(0.5 + (CPset->RmajorMax - CPset->RmajorMin) / CPset->RmajorDel);
+    CPset->AratioNitem = 1 + (int)(0.5 + (CPset->AratioMax - CPset->AratioMin) / CPset->AratioDel);
+    CPset->SindexNitem = 1 + (int)(0.5 + (CPset->SindexMax - CPset->SindexMin) / CPset->SindexDel);
+
+    // array entry = lookupCube[RmajorBin][AratioBin][SindexBin]
+
+    CPset->lookupCube = (int ***) psAlloc (sizeof(int **)*CPset->RmajorNitem);
+    for (int i = 0; i < CPset->RmajorNitem; i++) {
+	CPset->lookupCube[i] = (int **) psAlloc (sizeof(int *)*CPset->AratioNitem);
+	for (int j = 0; j < CPset->AratioNitem; j++) {
+	    CPset->lookupCube[i][j] = (int *) psAlloc (sizeof(int)*CPset->SindexNitem);
+	    for (int k = 0; k < CPset->SindexNitem; k++) {
+		CPset->lookupCube[i][j][k] = -1;
+	    }
+	}
+    }
+    
+    CPset->images = psArrayAllocEmpty (CPset->RmajorNitem*CPset->AratioNitem*CPset->SindexNitem);
+
+    // the CP file contains a set of 2D images; load them all 
+
+    psRegion fullImage = psRegionSet (0, 0, 0, 0);
+    while (true) {
+	bool status = psFitsMoveExtNum (fits, 1, true);
+	if (!status) break;
+
+	psMetadata *header = psFitsReadHeader (NULL, fits);
+	if (!header) {
+	    fprintf (stderr, "error reading header\n");
+	    return false;
+	}
+
+	pmModelCP *cp = pmModelCP_Alloc ();
+
+	cp->flux = psFitsReadImageBuffer (NULL, fits, fullImage, 0);
+	if (!cp->flux) {
+	    fprintf (stderr, "error reading image\n");
+	    return false;
+	}
+
+	cp->Rmajor = psMetadataLookupF32 (&status, header, "R_MAJOR");
+	cp->Aratio = psMetadataLookupF32 (&status, header, "A_RATIO");
+	cp->Sindex = psMetadataLookupF32 (&status, header, "S_INDEX");
+	
+	int RmajorBin = (int)((cp->Rmajor - CPset->RmajorMin) / CPset->RmajorDel); psAssert ((RmajorBin < CPset->RmajorNitem) && (RmajorBin >= 0), "bad bin");
+	int AratioBin = (int)((cp->Aratio - CPset->AratioMin) / CPset->AratioDel); psAssert ((AratioBin < CPset->AratioNitem) && (AratioBin >= 0), "bad bin");
+	int SindexBin = (int)((cp->Sindex - CPset->SindexMin) / CPset->SindexDel); psAssert ((SindexBin < CPset->SindexNitem) && (SindexBin >= 0), "bad bin");
+
+	CPset->lookupCube[RmajorBin][AratioBin][SindexBin] = CPset->images->n;
+
+	psArrayAdd (CPset->images, 121, cp);
+	psFree (cp);
+	psFree (header);
+    }
+
+    psFree (PHU);
+    psFitsClose (fits);
+
+    return CPset;
+}
+
+// choose the closest image to the given coords
+pmModelCP *pmModelCP_GetImage (pmModelCPset *CPset, float Rmajor, float Aratio, float Sindex) {
+
+    // the pmModelCP set is defined for a grid of Rmajor, Aratio, Sindex values
+
+    int RmajorBin = (int)((Rmajor - CPset->RmajorMin) / CPset->RmajorDel); psAssert ((RmajorBin < CPset->RmajorNitem) && (RmajorBin >= 0), "bad bin");
+    int AratioBin = (int)((Aratio - CPset->AratioMin) / CPset->AratioDel); psAssert ((AratioBin < CPset->AratioNitem) && (AratioBin >= 0), "bad bin");
+    int SindexBin = (int)((Sindex - CPset->SindexMin) / CPset->SindexDel); psAssert ((SindexBin < CPset->SindexNitem) && (SindexBin >= 0), "bad bin");
+    
+    int entry = CPset->lookupCube[RmajorBin][AratioBin][SindexBin];
+    
+    pmModelCP *cp = CPset->images->data[entry];
+
+    return (cp);
+}
+
+// XXX for test purposes only:
+# define TEST_IMAGE 0
+# if (TEST_IMAGE)
+static psImage *map = NULL;
+# endif
+
+float pmModelCP_GetFlux_RotSquare (pmModelCP *cp, float dx, float dy, float theta);
+
+float pmModelCP_GetFlux (pmModelCP *cp, float dx, float dy, float theta) {
+
+# if (TEST_IMAGE) 
+    map = psImageCopy (map, cp->flux, PS_TYPE_S32);
+    psImageInit (map, 0.0);
+# endif
+
+    // float flux = pmModelCP_GetFlux_Bresen (cp, dx, dy, theta);
+    // float flux = pmModelCP_GetFlux_Old (cp, dx, dy, theta);
+    float flux = pmModelCP_GetFlux_RotSquare (cp, dx, dy, theta);
+    
+    // RotSquare for theta = 0.0 & Bresen give the same answer 
+    // if I count from x[0] <= ix < x[1]
+
+# if (TEST_IMAGE) 
+    psFits *fits = psFitsOpen ("map.fits", "w");
+    psFitsWriteImage (fits, NULL, map, 0, NULL);
+    psFitsClose (fits);
+    psFree (map);
+# endif
+
+    return flux;
+}
+
+float pmModelCP_GetFlux_Old (pmModelCP *cp, float dx, float dy, float theta) {
+
+    // the cp data is defined for the central 3x3 pixels.  we allow dx,dy to have values of
+    // -1.0 <= dx,dy <= +1.0
+
+    // Xsub = (Xim * cos(theta) - Yim * sin(theta) + 1.5) * Nsub 
+    // Ysub = (Yim * cos(theta) + Xim * sin(theta) + 1.5) * Nsub 
+    
+    // integrate from (dx - 0.5 to dx + 0.5), (dy - 0.5 to dy + 0.5), 
+
+    // get the Xsub,Ysub values for the 4 corners, find the Xmin,Xmax, Ymin,Ymax in the
+    // subrastered image
+
+    float cs = cos(theta*PS_RAD_DEG);
+    float sn = sin(theta*PS_RAD_DEG);
+
+    float Nsub = 11.0;
+    int Xsub00 = ((dx - 0.5)*cs - (dy - 0.5)*sn + 1.5)*Nsub;
+    int Ysub00 = ((dx - 0.5)*sn + (dy - 0.5)*cs + 1.5)*Nsub;
+    int Xsub01 = ((dx - 0.5)*cs - (dy + 0.5)*sn + 1.5)*Nsub;
+    int Ysub01 = ((dx - 0.5)*sn + (dy + 0.5)*cs + 1.5)*Nsub;
+    int Xsub10 = ((dx + 0.5)*cs - (dy - 0.5)*sn + 1.5)*Nsub;
+    int Ysub10 = ((dx + 0.5)*sn + (dy - 0.5)*cs + 1.5)*Nsub;
+    int Xsub11 = ((dx + 0.5)*cs - (dy + 0.5)*sn + 1.5)*Nsub;
+    int Ysub11 = ((dx + 0.5)*sn + (dy + 0.5)*cs + 1.5)*Nsub;
+
+    int Xmin, Xmax, Ymin, Ymax;
+
+    Xmin = PS_MIN(Xsub00,Xsub01);
+    Xmin = PS_MIN(Xsub10,Xmin);
+    Xmin = PS_MIN(Xsub11,Xmin);
+    Xmin = PS_MIN(Xmin, cp->flux->numCols - 1);
+    Xmin = PS_MAX(Xmin, 0);
+    Xmax = PS_MAX(Xsub00,Xsub01);
+    Xmax = PS_MAX(Xsub10,Xmax);
+    Xmax = PS_MAX(Xsub11,Xmax);
+    Xmax = PS_MIN(Xmax, cp->flux->numCols - 1);
+    Xmax = PS_MAX(Xmax, 0);
+    Ymin = PS_MIN(Ysub00,Ysub01);
+    Ymin = PS_MIN(Ysub10,Ymin);
+    Ymin = PS_MIN(Ysub11,Ymin);
+    Ymin = PS_MIN(Ymin, cp->flux->numRows - 1);
+    Ymin = PS_MAX(Ymin, 0);
+    Ymax = PS_MAX(Ysub00,Ysub01);
+    Ymax = PS_MAX(Ysub10,Ymax);
+    Ymax = PS_MAX(Ysub11,Ymax);
+    Ymax = PS_MIN(Ymax, cp->flux->numRows - 1);
+    Ymax = PS_MAX(Ymax, 0);
+
+    // integrate pixels from Xmin,Ymin to Xmax,Ymax, only include pixels contained in the
+    // target pixel
+
+    float flux = 0.0;
+    int   npix = 0;
+    for (int i = Xmin; i < Xmax; i++) {
+	float dX = i / Nsub - 1.5;
+	for (int j = Ymin; j < Ymax; j++) {
+	    float dY = j / Nsub - 1.5;
+
+	    float Xim =  dX*cs + dY*sn;
+	    if (Xim < (dx - 0.5)) continue;
+	    if (Xim > (dx + 0.5)) continue;
+
+	    float Yim = -dX*sn + dY*cs;
+	    if (Yim < (dy - 0.5)) continue;
+	    if (Yim > (dy + 0.5)) continue;
+
+	    flux += cp->flux->data.F32[j][i];
+	    npix ++;
+	}
+    }
+	   
+    float normFlux = flux / npix;
+    return normFlux;
+}
+
+// *** pmSourceRadialProfileSortPair is a utility function for sorting a pair of vectors
+# define COMPARE_INDEX(A,B) (y[A] < y[B])
+# define SWAP_INDEX(TYPE,A,B) {				\
+	int tmp;					\
+	if (A != B) {					\
+	    tmp = x[A];					\
+	    x[A] = x[B];				\
+	    x[B] = tmp;					\
+	    tmp = y[A];					\
+	    y[A] = y[B];				\
+	    y[B] = tmp;					\
+	}						\
+    }
+
+bool pmModelCP_SortCorners (int *x, int *y, int Npar) {
+
+    if (Npar < 2) return true;
+
+    // sort the vector set by the radius
+    PSSORT (Npar, COMPARE_INDEX, SWAP_INDEX, NONE);
+    return true;
+}
+
+float pmModelCP_GetFlux_RotSquare (pmModelCP *cp, float dx, float dy, float theta) {
+
+    // the cp data is defined for the central 3x3 pixels.  we allow dx,dy to have values of
+    // -1.0 <= dx,dy <= +1.0
+
+    // Xsub = (Xim * cos(theta) - Yim * sin(theta) + 1.5) * Nsub 
+    // Ysub = (Yim * cos(theta) + Xim * sin(theta) + 1.5) * Nsub 
+    
+    // integrate from (dx - 0.5 to dx + 0.5), (dy - 0.5 to dy + 0.5), 
+
+    // get the Xsub,Ysub values for the 4 corners, find the Xmin,Xmax, Ymin,Ymax in the
+    // subrastered image
+
+    float cs = cos(theta*PS_RAD_DEG);
+    float sn = sin(theta*PS_RAD_DEG);
+    float Nsub = 11.0;
+
+    int Xsub[4], Ysub[4];
+
+    Xsub[0] = ((dx - 0.5)*cs - (dy - 0.5)*sn + 1.5)*Nsub;
+    Ysub[0] = ((dx - 0.5)*sn + (dy - 0.5)*cs + 1.5)*Nsub;
+    Xsub[1] = ((dx - 0.5)*cs - (dy + 0.5)*sn + 1.5)*Nsub;
+    Ysub[1] = ((dx - 0.5)*sn + (dy + 0.5)*cs + 1.5)*Nsub;
+    Xsub[2] = ((dx + 0.5)*cs - (dy - 0.5)*sn + 1.5)*Nsub;
+    Ysub[2] = ((dx + 0.5)*sn + (dy - 0.5)*cs + 1.5)*Nsub;
+    Xsub[3] = ((dx + 0.5)*cs - (dy + 0.5)*sn + 1.5)*Nsub;
+    Ysub[3] = ((dx + 0.5)*sn + (dy + 0.5)*cs + 1.5)*Nsub;
+
+    // first, sort the corners in order of the Y coordinate:
+    pmModelCP_SortCorners (Xsub, Ysub, 4);
+
+    float flux = 0.0;
+    float npix = 0.0;
+
+    // if (Ysub[0] == Ysub[1]), we have a simple square
+    if (Ysub[0] == Ysub[1]) {
+	psAssert (Ysub[2] == Ysub[3], "not square?");
+	int Xmin = PS_MIN(Xsub[0], Xsub[1]);
+	int Xmax = PS_MAX(Xsub[0], Xsub[1]);
+	for (int iy = Ysub[0]; iy < Ysub[3]; iy++) {
+	    for (int ix = Xmin; ix < Xmax; ix++) {
+		flux += cp->flux->data.F32[iy][ix];
+		npix += 1.0;
+# if (TEST_IMAGE) 
+		fprintf (stderr, "%d %d | %f %f | %f\n", ix, iy, flux, npix, cp->flux->data.F32[iy][ix]);
+		map->data.S32[iy][ix] ++;
+# endif
+	    }
+	}
+	float normFlux = flux / npix;
+	return normFlux;
+    }
+    
+    // second case: Xsub[1] > Xsub[2]:
+    if (Xsub[1] > Xsub[2]) {
+	float dYdXp, dYdXm;
+	// first segment, Ysub[0] to Ysub[1]:
+	dYdXp = (Ysub[1] - Ysub[0]) / (float) (Xsub[1] - Xsub[0]);
+	dYdXm = (Ysub[2] - Ysub[0]) / (float) (Xsub[2] - Xsub[0]);
+	for (int iy = Ysub[0]; iy < Ysub[1]; iy++) {
+	    int Xs = (iy - Ysub[0]) / dYdXm + Xsub[0];
+	    int Xe = (iy - Ysub[0]) / dYdXp + Xsub[0];
+	    for (int ix = Xs; ix < Xe; ix ++) {
+		flux += cp->flux->data.F32[iy][ix];
+		npix += 1.0;
+# if (TEST_IMAGE) 
+		fprintf (stderr, "%d %d | %f %f | %f\n", ix, iy, flux, npix, cp->flux->data.F32[iy][ix]);
+		map->data.S32[iy][ix] ++;
+# endif
+	    }
+	}
+	// 2nd segment, Ysub[1] to Ysub[2]:
+	dYdXp = (Ysub[3] - Ysub[1]) / (float) (Xsub[3] - Xsub[1]);
+	dYdXm = (Ysub[2] - Ysub[0]) / (float) (Xsub[2] - Xsub[0]);
+	for (int iy = Ysub[1]; iy < Ysub[2]; iy++) {
+	    int Xs = (iy - Ysub[0]) / dYdXm + Xsub[0];
+	    int Xe = (iy - Ysub[1]) / dYdXp + Xsub[1];
+	    for (int ix = Xs; ix < Xe; ix ++) {
+		flux += cp->flux->data.F32[iy][ix];
+		npix += 1.0;
+# if (TEST_IMAGE) 
+		fprintf (stderr, "%d %d | %f %f | %f\n", ix, iy, flux, npix, cp->flux->data.F32[iy][ix]);
+		map->data.S32[iy][ix] ++;
+# endif
+	    }
+	}
+	// first segment, Ysub[0] to Ysub[1]:
+	dYdXp = (Ysub[3] - Ysub[1]) / (float) (Xsub[3] - Xsub[1]);
+	dYdXm = (Ysub[3] - Ysub[2]) / (float) (Xsub[3] - Xsub[2]);
+	for (int iy = Ysub[2]; iy < Ysub[3]; iy++) {
+	    int Xs = (iy - Ysub[2]) / dYdXm + Xsub[2];
+	    int Xe = (iy - Ysub[1]) / dYdXp + Xsub[1];
+	    for (int ix = Xs; ix < Xe; ix ++) {
+		flux += cp->flux->data.F32[iy][ix];
+		npix += 1.0;
+# if (TEST_IMAGE) 
+		fprintf (stderr, "%d %d | %f %f | %f\n", ix, iy, flux, npix, cp->flux->data.F32[iy][ix]);
+		map->data.S32[iy][ix] ++;
+# endif
+	    }
+	}
+	float normFlux = flux / npix;
+	return normFlux;
+    }
+
+    // third case: Xsub[1] < Xsub[2]:
+    if (Xsub[2] > Xsub[1]) {
+	// first segment, Ysub[0] to Ysub[1]:
+	float dYdXp, dYdXm;
+	dYdXp = (Ysub[2] - Ysub[0]) / (float) (Xsub[2] - Xsub[0]);
+	dYdXm = (Ysub[1] - Ysub[0]) / (float) (Xsub[1] - Xsub[0]);
+	for (int iy = Ysub[0]; iy < Ysub[1]; iy++) {
+	    int Xs = (iy - Ysub[0]) / dYdXm + Xsub[0];
+	    int Xe = (iy - Ysub[0]) / dYdXp + Xsub[0];
+	    for (int ix = Xs; ix < Xe; ix ++) {
+		flux += cp->flux->data.F32[iy][ix];
+		npix += 1.0;
+# if (TEST_IMAGE) 
+		fprintf (stderr, "%d %d | %f %f | %f\n", ix, iy, flux, npix, cp->flux->data.F32[iy][ix]);
+		map->data.S32[iy][ix] ++;
+# endif
+	    }
+	}
+	// 2nd segment, Ysub[1] to Ysub[2]:
+	dYdXp = (Ysub[2] - Ysub[0]) / (float) (Xsub[2] - Xsub[0]);
+	dYdXm = (Ysub[3] - Ysub[1]) / (float) (Xsub[3] - Xsub[1]);
+	for (int iy = Ysub[1]; iy < Ysub[2]; iy++) {
+	    int Xs = (iy - Ysub[1]) / dYdXm + Xsub[1];
+	    int Xe = (iy - Ysub[0]) / dYdXp + Xsub[0];
+	    for (int ix = Xs; ix < Xe; ix ++) {
+		flux += cp->flux->data.F32[iy][ix];
+		npix += 1.0;
+# if (TEST_IMAGE) 
+		fprintf (stderr, "%d %d | %f %f | %f\n", ix, iy, flux, npix, cp->flux->data.F32[iy][ix]);
+		map->data.S32[iy][ix] ++;
+# endif
+	    }
+	}
+	// first segment, Ysub[0] to Ysub[1]:
+	dYdXp = (Ysub[3] - Ysub[2]) / (float) (Xsub[3] - Xsub[2]);
+	dYdXm = (Ysub[3] - Ysub[1]) / (float) (Xsub[3] - Xsub[1]);
+	for (int iy = Ysub[2]; iy < Ysub[3]; iy++) {
+	    int Xs = (iy - Ysub[1]) / dYdXm + Xsub[1];
+	    int Xe = (iy - Ysub[2]) / dYdXp + Xsub[2];
+	    for (int ix = Xs; ix < Xe; ix ++) {
+		flux += cp->flux->data.F32[iy][ix];
+		npix += 1.0;
+# if (TEST_IMAGE) 
+		fprintf (stderr, "%d %d | %f %f | %f\n", ix, iy, flux, npix, cp->flux->data.F32[iy][ix]);
+		map->data.S32[iy][ix] ++;
+# endif
+	    }
+	}
+	float normFlux = flux / npix;
+	return normFlux;
+    }
+    myAbort ("impossible case?");
+}
+
+float pmModelCP_GetFlux_Bresen (pmModelCP *cp, float dx, float dy, float theta) {
+
+    // the cp data is defined for the central 3x3 pixels.  we allow dx,dy to have values of
+    // -1.0 <= dx,dy <= +1.0
+
+    // Xsub = ( Xim * cos(theta) + Yim * sin(theta) + 1.5) * Nsub 
+    // Ysub = (-Yim * cos(theta) + Xim * sin(theta) + 1.5) * Nsub 
+    
+    // integrate from (dx - 0.5 to dx + 0.5), (dy - 0.5 to dy + 0.5), 
+
+    // get the Xsub,Ysub values for the 4 corners, find the Xmin,Xmax, Ymin,Ymax in the
+    // subrastered image
+
+    float cs = cos(theta*PS_RAD_DEG);
+    float sn = sin(theta*PS_RAD_DEG);
+
+    float Nsub = 11.0;
+    int Xsub00 = 0.5 + ((dx - 0.5)*cs + (dy - 0.5)*sn + 1.5)*Nsub;
+    int Ysub00 = 0.5 + ((dy - 0.5)*cs - (dx - 0.5)*sn + 1.5)*Nsub;
+    int Xsub01 = 0.5 + ((dx - 0.5)*cs + (dy + 0.5)*sn + 1.5)*Nsub;
+    int Ysub01 = 0.5 + ((dy + 0.5)*cs - (dx - 0.5)*sn + 1.5)*Nsub;
+    int Xsub10 = 0.5 + ((dx + 0.5)*cs + (dy - 0.5)*sn + 1.5)*Nsub;
+    int Ysub10 = 0.5 + ((dy - 0.5)*cs - (dx + 0.5)*sn + 1.5)*Nsub;
+    int Xsub11 = 0.5 + ((dx + 0.5)*cs + (dy + 0.5)*sn + 1.5)*Nsub;
+    int Ysub11 = 0.5 + ((dy + 0.5)*cs - (dx + 0.5)*sn + 1.5)*Nsub;
+
+    float flux = pmModelCP_GetFlux_BresenSquare (cp, Xsub00, Ysub00, Xsub10, Ysub10, Xsub01, Ysub01, Xsub11, Ysub11);
+    return flux;
+}
+
+// first line is (X00,Y00) - (X10,Y1) : last line is (X01,Y01) - (X11,Y11)
+float pmModelCP_GetFlux_BresenSquare (pmModelCP *cp, int X00, int Y00, int X10, int Y10, int X01, int Y01, int X11, int Y11) {
+
+    int dX0 = X01 - X00;
+    int dY0 = Y01 - Y00;
+
+    // int dX1 = X11 - X10;
+    // int dY1 = Y11 - Y10;
+
+    // myAssert ((dX0 == dX1) && (dY0 == dY1), "pixel is not square?");
+
+    bool FlipCoords = (abs(dX0) < abs(dY0));
+    bool FlipDirect = FlipCoords ? (Y00 > Y10) : (X00 > X10);
+
+    float flux = 0.0;
+    if (!FlipDirect && !FlipCoords) flux = pmModelCP_GetFlux_BresenSquareBase (cp, X00, Y00, X10, Y10, X01, Y01, X11, Y11, false);
+    if ( FlipDirect && !FlipCoords) flux = pmModelCP_GetFlux_BresenSquareBase (cp, X10, Y10, X00, Y00, X11, Y11, X01, Y01, false);
+    if (!FlipDirect &&  FlipCoords) flux = pmModelCP_GetFlux_BresenSquareBase (cp, Y00, X00, Y10, X10, Y01, X01, Y11, X11, true);
+    if ( FlipDirect &&  FlipCoords) flux = pmModelCP_GetFlux_BresenSquareBase (cp, Y10, X10, Y00, X00, Y11, X11, Y01, X01, true);
+    return flux;
+}
+
+// draw a line between (X00,Y00) & (X01,Y01) and increment to the next line segment until endpoints (X10,Y10) & (X11,Y11)
+float pmModelCP_GetFlux_BresenSquareBase (pmModelCP *cp, int X00, int Y00, int X10, int Y10, int X01, int Y01, int X11, int Y11, bool swapcoords) {
+
+    int dX0 = X01 - X00;
+    int dY0 = Y01 - Y00;
+
+    // int dX1 = X11 - X10;
+    // int dY1 = Y11 - Y10;
+
+    // myAssert ((dX0 == dX1) && (dY0 == dY1), "pixel is not square?");
+
+    float flux = 0.0;
+    float npix = 0.0;
+
+    int Ys = Y00;
+    int Ye = Y10;
+    int e = 0;
+    for (int Xs = X00, Xe = X10; Xs < X01; Xs++, Xe++) {
+	if (swapcoords) {
+	    pmModelCP_GetFlux_BresenLine (&flux, &npix, cp, Ys, Xs, Ye, Xe);
+	} else {
+	    pmModelCP_GetFlux_BresenLine (&flux, &npix, cp, Xs, Ys, Xe, Ye);
+	}
+	e += dY0;
+	float e2 = 2 * e;
+	if (e2 > dX0) {
+	    Ys++;
+	    Ye++;
+	    e -= dX0;
+	} 
+	if (e2 < -dX0) {
+	    Ys--;
+	    Ye--;
+	    e += dX0;
+	}
+    }
+    float normFlux = flux / npix;
+    // fprintf (stderr, "bres: %f %f %f\n", flux, (float) npix, normFlux);
+    return normFlux;
+}
+
+// get the sequence right: 
+// if abs(dY) > abs(dX) : we will run in the Y direction not the X direction (we swap X and Y going in)
+// if the direction (dX or dY) is negative, go the opposite direction
+bool pmModelCP_GetFlux_BresenLine (float *flux, float *npix, pmModelCP *cp, int X0, int Y0, int X1, int Y1) {
+
+  int dX = X1 - X0;
+  int dY = Y1 - Y0;
+
+  bool FlipCoords = (abs(dX) < abs(dY));
+  bool FlipDirect = FlipCoords ? (Y0 > Y1) : (X0 > X1);
+
+  if (!FlipDirect && !FlipCoords) pmModelCP_GetFlux_BresenLineBase (flux, npix, cp, X0, Y0, X1, Y1, false);
+  if ( FlipDirect && !FlipCoords) pmModelCP_GetFlux_BresenLineBase (flux, npix, cp, X1, Y1, X0, Y0, false);
+  if (!FlipDirect &&  FlipCoords) pmModelCP_GetFlux_BresenLineBase (flux, npix, cp, Y0, X0, Y1, X1, true);
+  if ( FlipDirect &&  FlipCoords) pmModelCP_GetFlux_BresenLineBase (flux, npix, cp, Y1, X1, Y0, X0, true);
+  return true;
+}
+
+bool pmModelCP_GetFlux_BresenLineBase (float *flux, float *npix, pmModelCP *cp, int X0, int Y0, int X1, int Y1, bool swapcoords) {
+
+    int dX = X1 - X0;
+    int dY = Y1 - Y0;
+
+    int Y = Y0;
+    int e = 0;
+    for (int X = X0; X < X1; X++) {
+	if (swapcoords) {
+	    *flux += cp->flux->data.F32[X][Y];
+	    *npix += 1.0;
+# if (TEST_IMAGE) 
+	    fprintf (stderr, "%d %d | %f %f | %f\n", X, Y, *flux, *npix, cp->flux->data.F32[X][Y]);
+	    map->data.S32[X][Y] ++;
+# endif
+	} else {
+	    *flux += cp->flux->data.F32[Y][X];
+	    *npix += 1.0;
+# if (TEST_IMAGE) 
+	    fprintf (stderr, "%d %d | %f %f | %f\n", X, Y, *flux, *npix, cp->flux->data.F32[Y][X]);
+	    map->data.S32[Y][X] ++;
+# endif
+	}
+	e += dY;
+	float e2 = 2 * e;
+	if (e2 > dX) {
+	    Y++;
+	    e -= dX;
+	} 
+	if (e2 < -dX) {
+	    Y--;
+	    e += dX;
+	}
+    }
+    return true;
+}
+
+// this is a test function which generates a full sersic model evaluation with sub-pixel sampling
+float pmModelCP_FullSersic (float dx, float dy, float theta, float Rmajor, float Aratio, float Sindex) {
+
+    float flux = 0.0;
+    int   npix = 0;
+
+    float Rminor = Aratio * Rmajor;
+    float f1 = 1.0 / PS_SQR(Rminor) + 1.0 / PS_SQR(Rmajor);
+    float f2 = 1.0 / PS_SQR(Rminor) - 1.0 / PS_SQR(Rmajor);
+    
+    float sxr = 0.5*f1 - 0.5*f2*cos(2.0*theta*PS_RAD_DEG);
+    float syr = 0.5*f1 + 0.5*f2*cos(2.0*theta*PS_RAD_DEG);
+    
+    float Rxx  = +1.0 / sqrt(sxr);
+    float Ryy  = +1.0 / sqrt(syr);
+    float Rxy = -f2*sin(2.0*theta*PS_RAD_DEG);
+    
+    float kappa = -0.275552 + 1.972625*Sindex + 0.003487 * PS_SQR(Sindex);
+    float rindex = 0.5 / Sindex;
+
+    float off = -60.0/(11*11);
+    float delta = 1.0 / (11*11);
+    for (float ix = off; ix < 0.5; ix += delta) {
+	for (float iy = off; iy < 0.5; iy += delta) {
+
+	    float dX = dx + ix;
+	    float dY = dy + iy;
+	    float z = PS_SQR(dX / Rxx) + PS_SQR(dY / Ryy) + dX * dY * Rxy;
+
+	    float q = pow (z, rindex);
+	    float f = exp(-kappa*q);
+
+	    flux += f;
+	    npix ++;
+	}
+    }
+    float normFlux = flux / npix;
+    // fprintf (stderr, "full : %f %f %f\n", flux, (float) npix, normFlux);
+    return normFlux;
+}
+
+// this is a test function which generates a full sersic model evaluation with sub-pixel sampling
+// Nsub is the number of sub-pixel samplings and must be odd
+// dx,dy are the centroid offset
+float pmModelCP_SersicSubpix (float dx, float dy, float Rxx, float Rxy, float Ryy, float Sindex, int Nsub) {
+
+    float flux = 0.0;
+    int   npix = 0;
+
+    // -0.275552 + 1.972625*Sindex + 0.003487 * PS_SQR(Sindex);
+    float kappa = pmSersicKappa (Sindex);
+    float rindex = 0.5 / Sindex;
+
+    // have the resolution be a user-parameter?
+    psAssert (Nsub % 2 == 1, "Nsub is not odd");
+    int Nsub2 = (Nsub - 1) / 2;
+
+    float delta = 1.0 / (float) Nsub;
+    // float off = -Nsub2 * delta;
+
+    int Sx = (int) floor(dx / delta);
+    int Sy = (int) floor(dy / delta);
+
+    for (int ix = -Nsub2; ix <= Nsub2; ix++) {
+      float dX = delta * (Sx + ix);
+      for (int iy = -Nsub2; iy <= Nsub2; iy++) {
+	float dY = delta * (Sy + iy);
+	    float z = PS_SQR(dX / Rxx) + PS_SQR(dY / Ryy) + dX * dY * Rxy;
+
+	    float q = pow (z, rindex);
+	    float f = exp(-kappa*q);
+
+	    // if ((ix == 0) && (iy == 0)) {
+	    //   // fprintf (stderr, "this: %f  %f  %f  --  full : %f %f\n", z, q, f, flux, (float) npix);
+	    // }
+
+	    flux += f;
+	    npix ++;
+	}
+    }
+    float normFlux = flux / npix;
+    // fprintf (stderr, "full : %f %f %f\n", flux, (float) npix, normFlux);
+    return normFlux;
+}
+
+float pmSersicKappa (float Sindex) {
+    // this function is empirically derived from a fit to data for Sindex = 0.5 - 5.5
+    // constrain Sindex or kappa?
+    float kappa = -0.275552 + 1.972625*Sindex + 0.003487 * PS_SQR(Sindex);
+    return kappa;
+}
+
+float pmSersicNorm (float Sindex) {
+
+    float C0 = NAN;
+    float C1 = NAN;
+    float C2 = NAN;
+
+    // y = 0.201545 x^0 -0.950965 x^1 -0.315248 x^2 
+    // y = 0.402084 x^0 -1.357775 x^1 -0.105102 x^2 
+    // y = 0.619093 x^0 -1.591674 x^1 -0.041576 x^2 
+    // y = 0.770263 x^0 -1.696421 x^1 -0.023363 x^2 
+    // y = 0.885891 x^0 -1.755684 x^1 -0.015753 x^2 
+
+    if ((Sindex >= 0.0) && (Sindex < 1.0)) { 
+	C0 = 0.201545; C1 = -0.950965; C2 = -0.315248;
+	// y = 0.201545 x^0 -0.950965 x^1 -0.315248 x^2 
+    }
+    if ((Sindex >= 1.0) && (Sindex < 2.0)) { 
+	C0 = 0.402084; C1 = -1.357775; C2 = -0.105102;
+	// y = 0.402084 x^0 -1.357775 x^1 -0.105102 x^2 
+    }
+    if ((Sindex >= 2.0) && (Sindex < 3.0)) { 
+	C0 = 0.619093; C1 = -1.591674; C2 = -0.041576;
+	// y = 0.619093 x^0 -1.591674 x^1 -0.041576 x^2 
+    }
+    if ((Sindex >= 3.0) && (Sindex < 4.0)) { 
+	C0 = 0.770263; C1 = -1.696421; C2 = -0.023363;
+	// y = 0.770263 x^0 -1.696421 x^1 -0.023363 x^2 
+    }
+    if ((Sindex >= 4.0) && (Sindex < 5.5)) { 
+	C0 = 0.885891; C1 = -1.755684; C2 = -0.015753; 
+	// y = 0.885891 x^0 -1.755684 x^1 -0.015753 x^2 
+    }
+
+    if (isnan(C0)) return NAN;
+
+    float lnorm = C0 + C1*Sindex + C2*Sindex*Sindex;
+    float norm = exp(lnorm);
+    return norm;
+}
+
+# if (0)
+// create a vector containing only the unique entries in the input vector
+psVector *psVectorUniqueSubset (psVector *input) {
+
+    // sort the input vector (to new temp vector)
+    // run through the sorted vector, copying to a new output vector if the current value is
+    // new
+
+    psVector *temp = psVectorSort (input);
+    
+    psVector *output = psVectorAllocEmpty (0.5*input->n, PS_TYPE_F32);
+    
+    psVectorAppend (output, temp->data.F32[0]);
+    float lastValue = temp->data.F32[0];
+    for (int i = 0; i < temp->n; i++) {
+	if (temp->data.F32[i] == lastValue) continue;
+	psVectorAppend (output, temp->data.F32[i]);
+	float lastValue = temp->data.F32[i];
+    }
+    psFree (temp);
+    return output;
+}
+
+getUnique() {
+    // we need to convert the collection of Rmajor, Aratio, Sindex values to a cube
+    // such that entry[RmajorBin][AratioBin][SindexBin] is the CPset array element
+
+    // create full vectors will all Rmajor, Aratio, Sindex values:
+    psVector *RmajorAll = psVectorAllocEmpty (CPset->n, PS_TYPE_F32);
+    psVector *AratioAll = psVectorAllocEmpty (CPset->n, PS_TYPE_F32);
+    psVector *SindexAll  = psVectorAllocEmpty (CPset->n, PS_TYPE_F32);
+    for (int i = 0; i < CPset->n; i++) {
+	pmModelCP *cp = CPset->data[i];
+	psVectorAppend (RmajorAll, cp->Rmajor);
+	psVectorAppend (AratioAll, cp->Aratio);
+	psVectorAppend (SindexAll,  cp->Sindex);
+    }
+    psVector *RmajorUniq = psVectorUniqueSubset (RmajorAll);
+    psVector *AratioUniq = psVectorUniqueSubset (AratioAll);
+    psVector *SindexUniq  = psVectorUniqueSubset (SindexAll);
+}
+# endif
Index: /branches/eam_branches/psModules.20240412/src/objects/pmModel_CentralPixel.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmModel_CentralPixel.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmModel_CentralPixel.h	(revision 42651)
@@ -0,0 +1,69 @@
+/* @file  pmModel_CentralPixel.h
+ * @brief Functions to manage the central pixel for sersic-like models
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.19 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-16 22:30:50 $
+ *
+ * Copyright 2013 Institute for Astronomy, University of Hawaii
+ */
+
+# ifndef PM_MODEL_CENTRAL_PIXEL_H
+# define PM_MODEL_CENTRAL_PIXEL_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+typedef struct {
+    psImage *flux;
+    float Rmajor;
+    float Aratio;
+    float Sindex;
+} pmModelCP;
+
+typedef struct {
+    psArray *images;
+
+    float RmajorMin;
+    float RmajorMax;
+    float RmajorDel;
+
+    float AratioMin;
+    float AratioMax;
+    float AratioDel;
+
+    float SindexMin;
+    float SindexMax;
+    float SindexDel;
+
+    int RmajorNitem;
+    int AratioNitem;
+    int SindexNitem;
+
+    int ***lookupCube;
+
+} pmModelCPset;
+
+pmModelCP    *pmModelCP_Alloc(void);
+pmModelCPset *pmModelCPset_Alloc(void);
+
+pmModelCPset *pmModelCP_Load (char *filename);
+
+pmModelCP    *pmModelCP_GetImage (pmModelCPset *CPset, float Rmajor, float Aratio, float Sindex);
+
+float         pmModelCP_GetFlux (pmModelCP *cp, float dx, float dy, float theta);
+float         pmModelCP_FullSersic (float dx, float dy, float theta, float Rmajor, float Aratio, float Sindex);
+
+bool pmModelCP_GetFlux_BresenLineBase (float *flux, float *npix, pmModelCP *cp, int X0, int Y0, int X1, int Y1, bool swapcoords);
+bool pmModelCP_GetFlux_BresenLine (float *flux, float *npix, pmModelCP *cp, int X0, int Y0, int X1, int Y1);
+float pmModelCP_GetFlux_BresenSquareBase (pmModelCP *cp, int X00, int Y00, int X01, int Y01, int X10, int Y10, int X11, int Y11, bool swapcoords);
+float pmModelCP_GetFlux_BresenSquare (pmModelCP *cp, int X00, int Y00, int X01, int Y01, int X10, int Y10, int X11, int Y11);
+float pmModelCP_GetFlux_Bresen (pmModelCP *cp, float dx, float dy, float theta);
+float pmModelCP_GetFlux_Old (pmModelCP *cp, float dx, float dy, float theta);
+
+float pmModelCP_SersicSubpix (float dx, float dy, float Rxx, float Rxy, float Ryy, float Sindex, int Nsub);
+
+float pmSersicKappa (float Sindex);
+float pmSersicNorm (float Sindex);
+
+# endif
Index: /branches/eam_branches/psModules.20240412/src/objects/pmMoments.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmMoments.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmMoments.c	(revision 42651)
@@ -0,0 +1,74 @@
+/** @file  pmMoments.c
+ *
+ *  Functions defining the pmMoments structure
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-10-03 20:59:16 $
+ *
+ *  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(void)
+{
+    psTrace("psModules.objects", 10, "---- %s() begin ----\n", __func__);
+    pmMoments *tmp = (pmMoments *) psAlloc(sizeof(pmMoments));
+
+    tmp->Mrf = NAN;
+    tmp->Mrh = NAN;
+
+    tmp->KronCore = NAN;
+    tmp->KronCoreErr = NAN;
+
+    tmp->KronFlux = NAN;
+    tmp->KronFluxErr = NAN;
+
+    tmp->KronFinner = NAN;
+    tmp->KronFouter = NAN;
+
+    tmp->KronFluxPSF = NAN;
+    tmp->KronFluxPSFErr = NAN;
+    tmp->KronRadiusPSF = NAN;
+
+    tmp->Mx = NAN;
+    tmp->My = NAN;
+
+    tmp->Mxx = NAN;
+    tmp->Mxy = NAN;
+    tmp->Myy = NAN;
+
+    tmp->Mxxx = NAN;
+    tmp->Mxxy = NAN;
+    tmp->Mxyy = NAN;
+    tmp->Myyy = NAN;
+
+    tmp->Mxxxx = NAN;
+    tmp->Mxxxy = NAN;
+    tmp->Mxxyy = NAN;
+    tmp->Mxyyy = NAN;
+    tmp->Myyyy = NAN;
+
+    tmp->Sum = NAN;
+    tmp->Peak = NAN;
+    tmp->Sky = NAN;
+    tmp->nPixels = 0;
+    tmp->SN = 0;
+
+    psTrace("psModules.objects", 10, "---- %s() end ----\n", __func__);
+    return(tmp);
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmMoments.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmMoments.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmMoments.h	(revision 42651)
@@ -0,0 +1,76 @@
+/* @file  pmMoments.h
+ * @brief Definitions of the moments structure
+ *
+ * @author GLG, MHPCC
+ *
+ * @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-10-03 20:59:16 $
+ * 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 Mrf;    ///< radial first moment
+    float Mrh;    ///< radial half moment
+
+    float Mx;     ///< X-coord of centroid.
+    float My;     ///< Y-coord of centroid.
+    float Mxx;    ///< x-second moment = sigma_x^2 = = (FWHM_x/2.355)^2
+    float Mxy;    ///< xy cross moment = sigma_xy
+    float Myy;    ///< y-second moment = sigma_y^2 = = (FWHM_y/2.355)^2
+
+    float Mxxx;    ///< third moment
+    float Mxxy;    ///< third moment
+    float Mxyy;    ///< third moment
+    float Myyy;    ///< third moment
+
+    float Mxxxx;   ///< fourth moment
+    float Mxxxy;   ///< fourth moment
+    float Mxxyy;   ///< fourth moment
+    float Mxyyy;   ///< fourth moment
+    float Myyyy;   ///< fourth moment
+
+  // float wSum;    ///< window-weighted sum (NOT needed by lensing)
+
+    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.
+
+    float KronFluxPSF; ///< Kron Flux using PSF-optimized window
+    float KronFluxPSFErr; ///< Kron Flux Error using PSF-optimized window
+    float KronRadiusPSF; ///< Kron Radius using PSF-optimized window (Flux in 2.5 Radius)
+
+    float KronCore;    ///< flux in r < 1.0*Mrf
+    float KronCoreErr;    ///< error on flux in r < 1.0*Mrf
+
+    float KronFlux;    ///< Kron flux (flux in r < 2.5*Mrf)
+    float KronFluxErr; ///< Kron flux error
+
+    float KronFinner;    ///< Kron flux (flux in 1.0*Mrf < r < 2.5*Mrf)
+    float KronFouter;    ///< Kron flux (flux in 2.5*Mrf < r < 4.0*Mrf)
+}
+pmMoments;
+
+/** pmMomentsAlloc()
+ *
+ */
+pmMoments *pmMomentsAlloc(void);
+
+/// @}
+# endif
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPCM_MinimizeChisq.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPCM_MinimizeChisq.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPCM_MinimizeChisq.c	(revision 42651)
@@ -0,0 +1,678 @@
+/* @file  pmPCM_MinimizeChisq.c
+ * structures and functions to support PSF-convolved model fitting
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.29 $
+ * @date $Date: 2009-02-16 22:30:50 $
+ * Copyright 2010 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAMaskWeight.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPCMdata.h"
+
+# define SAVE_IMAGES 0
+# if (SAVE_IMAGES) 
+int psphotSaveImage (psMetadata *header, psImage *image, char *filename);
+# endif
+
+# define FACILITY "psModules.objects"
+
+# define USE_FFT 1
+# define PRE_CONVOLVE 1
+# define TESTCOPY 0
+
+bool pmPCM_MinimizeChisq (
+    psMinimization *min,
+    psImage *covar,
+    psVector *params,
+    pmSource *source,
+    pmPCMdata *pcm)
+{
+    psTrace(FACILITY, 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);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(pcm, false);
+    PS_ASSERT_VECTOR_TYPE(pcm->constraint->paramMask, PS_TYPE_VECTOR_MASK, false);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(params, pcm->constraint->paramMask, false);
+
+    psVector *paramMask = pcm->constraint->paramMask;
+
+    psMinimizeLMLimitFunc checkLimits = pcm->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 = NULL;
+    psVector *Beta = NULL;
+
+    // Alpha & Beta only contain elements to represent the unmasked parameters
+    if (!psMinLM_AllocAB (&Alpha, &Beta, params, paramMask)) {
+        psAbort ("programming error: no unmasked parameters to be fit\n");
+    }
+    psAssert (pcm->nPar == Beta->n, "did we set the masked parameters correctly??");
+
+    // allocate internal arrays (current vs Guess)
+    psImage *alpha   = psImageAlloc(pcm->nPar, pcm->nPar, PS_TYPE_F32);
+    psVector *beta   = psVectorAlloc(pcm->nPar, PS_TYPE_F32);
+    psVector *Params = psVectorAlloc(params->n, PS_TYPE_F32);
+
+    psF32 Chisq = 0.0;
+    psF32 lambda = 0.001;
+    psF32 dLinear = 0.0;
+    psF32 nu = 3.0;
+
+# if (USE_FFT && PRE_CONVOLVE)
+    if (pcm->psfFFT) {
+	psFree (pcm->psfFFT);
+    }
+# if (!TESTCOPY)
+    if (!pcm->use1Dgauss) {
+	pcm->psfFFT = psImageConvolveKernelInit(pcm->modelFlux, pcm->psf);
+    }
+# endif
+# endif    
+
+    // calculate initial alpha and beta, set chisq (min->value)
+    min->value = pmPCM_SetABX(alpha, beta, params, paramMask, pcm, source);
+    if (isnan(min->value)) {
+        min->iter = min->maxIter;
+        return(false);
+    }
+    // dump some useful info if trace is defined
+    if (psTraceGetLevel(FACILITY) >= 6) {
+        p_psImagePrint(psTraceGetDestination(), alpha, "alpha guess (0)");
+        p_psVectorPrint(psTraceGetDestination(), beta, "beta guess (0)");
+    }
+    if (psTraceGetLevel(FACILITY) >= 5) {
+        p_psVectorPrint(psTraceGetDestination(), params, "params guess (0)");
+    }
+
+    // iterate until the tolerance is reached, or give up
+    bool done = (min->iter >= min->maxIter);
+    while (!done) {
+        psTrace(FACILITY, 5, "Iteration number %d.  (max iterations is %d).\n", min->iter, min->maxIter);
+
+	if (min->chisqConvergence) {
+	    psTrace(FACILITY, 5, "Last delta is %f.  stop if < %f, accept if < %f\n", min->lastDelta, min->minTol, min->maxTol);
+	} else {
+	    psTrace(FACILITY, 5, "Last delta is %f.  stop if < %f, accept if < %f\n", min->rParSigma, min->minTol*pcm->nPar, min->maxTol*pcm->nPar);
+	}
+
+	if (min->isInteractive) {
+	    fprintf (stderr, "%d : ", min->iter);
+	    for (int ti = 0; ti < params->n; ti++) {
+		fprintf (stderr, "%f  ", params->data.F32[ti]);
+	    }
+	    fprintf (stderr, " : %f\n", min->value);
+	}
+
+	char key[10]; // used for interactive responses
+	bool testValue = false;
+
+        // set a new guess for Alpha, Beta, Params
+        if (!psMinLM_GuessABP(Alpha, Beta, Params, alpha, beta, params, paramMask, checkLimits, lambda, &dLinear)) {
+	    if (false && min->isInteractive) {
+		fprintf (stdout, "guess failed (singular matrix or NaN values), continue? [Y,n] ");
+		if (!fgets(key, 8, stdin)) {
+		    psWarning("Unable to read option");
+		}
+		switch (key[0]) {
+		  case 'n':
+		  case 'N':
+		    done = true;
+		    break;
+		  case 'y':
+		  case 'Y':
+		  case '\n':
+		    lambda *= 10.0;
+		    continue;
+		  default:
+		    lambda *= 10.0;
+		    continue;
+		}
+		if (done) break;
+	    }
+            min->iter ++;
+	    if (min->iter >=  min->maxIter) break;
+            lambda *= 10.0;
+            continue;
+        }
+
+	if (false && min->isInteractive) {
+            p_psVectorPrint(psTraceGetDestination(), Params, "current parameters: ");
+	    fprintf (stdout, "last chisq : %f\n", min->value);
+	    bool getOptions = true;
+	    while (getOptions) {
+		fprintf (stdout, "options: (m)odify, (g)o, (q)uit: ");
+		if (!fgets(key, 8, stdin)) {
+		    psWarning("Unable to read option");
+		}
+		switch (key[0]) {
+		  case 'm':
+		  case 'M':
+		    testValue = TRUE;
+		    fprintf (stdout, "enter (Npar) (value): ");
+		    int Npar = 0;
+		    float value= 0;
+		    int Nscan = fscanf (stdin, "%d %f", &Npar, &value);
+		    if (Nscan != 2) {
+		      fprintf (stderr, "scan failure\n");
+		    }
+		    Params->data.F32[Npar] = value;
+		    break;
+		  case 'g':
+		  case 'G':
+		  case '\n':
+		    getOptions = false;
+		    break;
+		  default:
+		    done = true;
+		    break;
+		}
+		fprintf (stderr, "foo\n");
+	    }
+	    if (done) break;
+	}
+	    
+        // dump some useful info if trace is defined
+        if (psTraceGetLevel(FACILITY) >= 6) {
+            p_psImagePrint(psTraceGetDestination(), Alpha, "Alpha guess (1)");
+            p_psVectorPrint(psTraceGetDestination(), Beta, "Beta guess (1)");
+            p_psVectorPrint(psTraceGetDestination(), beta, "beta current (1)");
+        }
+        if (psTraceGetLevel(FACILITY) >= 5) {
+            p_psVectorPrint(psTraceGetDestination(), Params, "params guess (1)");
+        }
+
+	// calculate the parameter change (rParDelta) and error radius (rParSigma)
+	//    rParDelta : radius of parameter change;
+	//    rParSigma : radius of parameter error 
+	
+	// note that (before SetABX) Alpha[i][i] is the covariance matrix and
+	// Beta is the actual parameter change for this pass
+
+	// note that Alpha & Beta only represent unmasked parameters, while params and Params have all 
+
+	// dParSigma = Alpha[i][i] : error (squared) on parameter i
+	// dParDelta = Params->data.F32[i] - params->data.F32[i]     : change on parameter i
+	float rParSigma = 0.0;
+        for (int j = 0, J = 0; j < Params->n; j++) {
+	    if (paramMask && (paramMask->data.PS_TYPE_VECTOR_MASK_DATA[j])) {
+		continue;
+	    }
+	    rParSigma += PS_SQR(Params->data.F32[j] - params->data.F32[j]) / Alpha->data.F32[J][J];
+	    J++;
+	}
+	rParSigma = sqrt(rParSigma);
+	psTrace(FACILITY, 5, "rParSigma: %f, Niter: %d\n", rParSigma, min->iter);
+	// fprintf (stderr, "rParSigma: %f, Niter: %d\n", rParSigma, min->iter);
+
+        // calculate Chisq for new guess, update Alpha & Beta
+        Chisq = pmPCM_SetABX(Alpha, Beta, Params, paramMask, pcm, source);
+        if (isnan(Chisq)) {
+            min->iter ++;
+	    if (min->iter >=  min->maxIter) break;
+            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(FACILITY, 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(FACILITY) >= 6) {
+            p_psImagePrint(psTraceGetDestination(), Alpha, "alpha guess (2)");
+            p_psVectorPrint(psTraceGetDestination(), Beta, "beta guess (2)");
+        }
+
+	// change in chisq/nDOF since last minimum
+	min->lastDelta = (min->value - Chisq) / pcm->nDOF;
+
+        // rho is positive if the new chisq is smaller; allow for some insignificant change (slight negative rho)
+
+	// XXX the old version of lambda changes:
+	// XXX : Madsen gives suggestion for better use of rho
+        // rho is positive if the new chisq is smaller
+        if (testValue || (rho >= -1e-6)) {
+            min->value = Chisq;
+            alpha  = psImageCopy(alpha, Alpha, PS_TYPE_F32);
+            beta   = psVectorCopy(beta, Beta, PS_TYPE_F32);
+            params = psVectorCopy(params, Params, PS_TYPE_F32);
+
+            // save the new convolved model image
+            psFree (source->modelFlux);
+            source->modelFlux = pmPCMdataSaveImage(pcm);
+        } 
+	switch (min->gainFactorMode) {
+	  case 0:
+	    if (rho >= -1e-6) {
+		lambda *= 0.1;
+	    } else {
+		lambda *= 10.0;
+	    }
+	    break;
+
+	  case 1:
+	    // adjust the gain ratio (lambda) based on rho
+	    if (rho < 0.25) {
+		lambda *= 2.0;
+	    } 
+	    if (rho > 0.75) {
+		lambda *= 0.333;
+	    }
+	    break;
+
+	  case 2:
+	    if (rho > 0.0) {
+		lambda *= PS_MAX(0.33, (1.0 - pow(2.0*rho - 1.0, 3.0)));
+		nu = 3.0;
+	    } else {
+		lambda *= nu;
+		nu *= 3.0;
+	    }
+	    break;
+	}
+        min->iter++;
+
+	// ending conditions:
+	// 1) hard limit : too many iterations
+	done = (min->iter >= min->maxIter);
+	
+	// 2) require deltaChi > 1e-6 (ie, chisq is decreasing, but accept an insignificant change)
+	if (min->lastDelta < -1e-6) {
+	    continue;
+	}
+
+	// save this value in case we stop iterating
+	min->rParSigma = rParSigma;
+
+	// 2) require chisqDOF < maxChisqDOF (if maxChisqDOF is not NAN)
+	// keep iterating regardless of rParSigma in this case
+	float chisqDOF = Chisq / pcm->nDOF;
+	if (isfinite(min->maxChisqDOF) && (chisqDOF > min->maxChisqDOF)) {
+	    continue;
+	}
+
+	// delta-chisq or rParSigma ?
+	if (min->chisqConvergence) {
+	    done |= (min->lastDelta < min->minTol);
+	} else {
+	    done |= (rParSigma < min->minTol*pcm->nPar);
+	}
+    }
+    psTrace(FACILITY, 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(Alpha, Beta, Params, alpha, beta, params, paramMask, NULL, 0.0, NULL)) {
+            psTrace (FACILITY, 5, "failure to calculate covariance matrix\n");
+        }
+        // set covar values which are not masked
+        psImageInit (covar, 0.0);
+        for (int j = 0, J = 0; j < params->n; j++) {
+            if (paramMask && (paramMask->data.PS_TYPE_VECTOR_MASK_DATA[j])) {
+                covar->data.F32[j][j] = 1.0;
+                continue;
+            }
+            for (int k = 0, K = 0; k < params->n; k++) {
+                if (paramMask && (paramMask->data.PS_TYPE_VECTOR_MASK_DATA[k])) continue;
+                covar->data.F32[j][k] = Alpha->data.F32[J][K];
+                K++;
+            }
+            J++;
+        }
+    }
+
+    // free the internal temporary data
+    psFree(alpha);
+    psFree(Alpha);
+    psFree(beta);
+    psFree(Beta);
+    psFree(Params);
+
+    // if the last improvement was at least as good as maxTol, accept the fit:
+    if (min->chisqConvergence) {
+	if (min->lastDelta <= min->maxTol) {
+	    psTrace(FACILITY, 6, "---- end (true) ----\n");
+	    return(true);
+	}
+    } else {
+	if (min->rParSigma <= min->maxTol*pcm->nPar) {
+	    psTrace(FACILITY, 6, "---- end (true) ----\n");
+	    return(true);
+	}
+    }
+    psTrace(FACILITY, 6, "---- end (false) ----\n");
+    return(false);
+}
+
+psF32 pmPCM_SetABX(
+    psImage  *alpha,
+    psVector *beta,
+    const psVector *params,
+    const psVector *paramMask,
+    pmPCMdata *pcm,
+    const pmSource *source)
+{
+    // 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->variance, 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_VECTOR_MASK, false);
+    }
+
+    // 1 *** 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->modelFlux, 0.0);
+    for (int n = 0; n < params->n; n++) {
+        if (!pcm->dmodelsFlux->data[n]) continue;
+        psImageInit (pcm->dmodelsFlux->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.PS_TYPE_IMAGE_MASK_DATA[i][j]) {
+            // continue;
+            // }
+
+            // skip zero-variance points
+            // XXX why is this not masked?
+            // if (source->variance->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 + 0.5 + source->pixels->col0);
+            coord->data.F32[1] = (psF32) (i + 0.5 + source->pixels->row0);
+
+            pcm->modelFlux->data.F32[i][j] = pcm->modelConv->class->modelFunc (deriv, params, coord);
+
+            for (int n = 0; n < params->n; n++) {
+                if ((paramMask != NULL) && (paramMask->data.PS_TYPE_VECTOR_MASK_DATA[n])) { continue; }
+                psImage *dmodel = pcm->dmodelsFlux->data[n];
+                dmodel->data.F32[i][j] = deriv->data.F32[n];
+            }
+        }
+    }
+    psFree(coord);
+    psFree(deriv);
+
+    // convolve model and dmodel arrays with PSF
+    // XXX speed this up by saving the FFTed psf (for each source, obviously)
+
+    // XXX save the FFT'ed psf
+    // XXX create an alternative function which takes a pre-FFTed kernel
+
+# if (USE_FFT)
+# if (PRE_CONVOLVE)
+    // convolve model image and derivative images with pre-convolved kernel
+
+// XXX for a test, just copy, rather than convolve
+# if (TESTCOPY)
+    psImageCopy (pcm->modelConvFlux, pcm->modelFlux, pcm->modelFlux->type.type);
+# else // TESTCOPY
+    if (pcm->use1Dgauss) {
+
+	if (USE_1D_CACHE) {
+	    // do not use the threaded, mask-aware version of this code (psImageSmoothMaskPixelsThread):
+	    // * the model flux is not masked
+	    // * threading takes place above this level
+	    pcm->modelConvFlux = psImageCopy (pcm->modelConvFlux, pcm->modelFlux, pcm->modelFlux->type.type);
+	    psImageSmoothCache_F32 (pcm->modelConvFlux, pcm->smdata);
+	} else {
+	    pcm->modelConvFlux = psImageCopy (pcm->modelConvFlux, pcm->modelFlux, pcm->modelFlux->type.type);
+	    psImageSmooth2dCache_F32 (pcm->modelConvFlux, pcm->smdata2d);
+	}
+    } else {
+	psImageConvolveKernel (pcm->modelConvFlux, pcm->modelFlux, NULL, 0, pcm->psfFFT);
+    }
+# endif // TESTCOPY
+
+    for (int n = 0; n < pcm->dmodelsFlux->n; n++) {
+        if (pcm->dmodelsFlux->data[n] == NULL) continue;
+	if (pcm->constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[n]) continue;
+        psImage *dmodel = pcm->dmodelsFlux->data[n];
+        psImage *dmodelConv = pcm->dmodelsConvFlux->data[n];
+# if (TESTCOPY)
+	psImageCopy (dmodelConv, dmodel, dmodel->type.type);
+# else // TESTCOPY
+	if (pcm->use1Dgauss) {
+	    if (USE_1D_CACHE) {
+		// do not use the threaded, mask-aware version of this code (psImageSmoothMaskPixelsThread):
+		// * the model flux is not masked
+		// * threading takes place above this level
+		dmodelConv = psImageCopy (dmodelConv, dmodel, dmodel->type.type);
+		psImageSmoothCache_F32 (dmodelConv, pcm->smdata);
+	    } else {
+		dmodelConv = psImageCopy (dmodelConv, dmodel, dmodel->type.type);
+		psImageSmooth2dCache_F32 (dmodelConv, pcm->smdata2d);
+	    }
+	} else {
+	    psImageConvolveKernel (dmodelConv, dmodel, NULL, 0, pcm->psfFFT);
+	}
+# endif // TESTCOPY
+    }
+# else // PRE_CONVOLVE
+    // convolve model image and derivative images with psf via FFT
+    psImageConvolveFFT (pcm->modelConvFlux, pcm->modelFlux, NULL, 0, pcm->psf);
+    for (int n = 0; n < pcm->dmodelsFlux->n; n++) {
+        if (pcm->dmodelsFlux->data[n] == NULL) continue;
+	if (pcm->constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[n]) continue;
+        psImage *dmodel = pcm->dmodelsFlux->data[n];
+        psImage *dmodelConv = pcm->dmodelsConvFlux->data[n];
+
+	if (pcm->use1Dgauss) {
+	    if (USE_1D_CACHE) {
+		// do not use the threaded, mask-aware version of this code (psImageSmoothMaskPixelsThread):
+		// * the model flux is not masked
+		// * threading takes place above this level
+		dmodelConv = psImageCopy (dmodelConv, dmodel, dmodel->type.type);
+		psImageSmoothCache_F32 (dmodelConv, pcm->smdata);
+	    } else {
+		dmodelConv = psImageCopy (dmodelConv, dmodel, dmodel->type.type);
+		psImageSmooth2dCache_F32 (dmodelConv, pcm->smdata2d);
+	    }
+	} else {
+	    psImageConvolveFFT (dmodelConv, dmodel, NULL, 0, pcm->psf);
+	}
+    }
+# endif // PRE-CONVOLVE
+# else // USE_FFT
+    // convolve model image and derivative images with psf direct
+    psImageConvolveDirect (pcm->modelConvFlux, pcm->modelFlux, pcm->psf);
+    for (int n = 0; n < pcm->dmodelsFlux->n; n++) {
+        if (pcm->dmodelsFlux->data[n] == NULL) continue;
+	if (pcm->constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[n]) continue;
+        psImage *dmodel = pcm->dmodelsFlux->data[n];
+        psImage *dmodelConv = pcm->dmodelsConvFlux->data[n];
+        psImageConvolveDirect (dmodelConv, dmodel, pcm->psf);
+    }
+# endif // USE_FFT
+
+    // XXX TEST : SAVE IMAGES
+# if (SAVE_IMAGES)
+    static int Npass = 0;
+    char name[128]; 
+    if (!pcm->use1Dgauss) {
+      snprintf (name, 128, "psf.%03d.fits", Npass); psphotSaveImage (NULL, pcm->psf->image, name);
+    }
+    snprintf (name, 128, "mod.%03d.fits", Npass); psphotSaveImage (NULL, pcm->modelFlux, name);
+    snprintf (name, 128, "cnv.%03d.fits", Npass); psphotSaveImage (NULL, pcm->modelConvFlux, name);
+    snprintf (name, 128, "obj.%03d.fits", Npass); psphotSaveImage (NULL, source->pixels, name);
+    snprintf (name, 128, "msk.%03d.fits", Npass); psphotSaveImage (NULL, source->maskObj, name);
+    snprintf (name, 128, "var.%03d.fits", Npass); psphotSaveImage (NULL, source->variance, name);
+    for (int n = 0; n < pcm->dmodelsFlux->n; n++) {
+        psImage *dmodelConv = pcm->dmodelsConvFlux->data[n];
+	if (!dmodelConv) continue;
+	snprintf (name, 128, "dpar.%01d.%03d.fits", n, Npass); psphotSaveImage (NULL, dmodelConv, name);
+    }
+    Npass ++;
+# endif
+
+    // 2 *** accumulate alpha & beta
+
+    // 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.PS_TYPE_IMAGE_MASK_DATA[i][j]) {
+                continue;
+            }
+            // skip zero-variance points
+            if (source->variance->data.F32[i][j] == 0) {
+                continue;
+            }
+            // skip nan value points
+            if (!isfinite(source->pixels->data.F32[i][j])) {
+                continue;
+            }
+
+            float ymodel  = pcm->modelConvFlux->data.F32[i][j];
+
+	    // XXXX note this point here:::
+            float yweight = pcm->poissonErrors ? 1.0 / source->variance->data.F32[i][j] : 1.0;
+            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");
+
+            // alpha & beta only contain unmasked elements
+            for (int n1 = 0, N1 = 0; n1 < params->n; n1++) {
+                if ((paramMask != NULL) && (paramMask->data.PS_TYPE_VECTOR_MASK_DATA[n1])) continue;
+                psImage *dmodel = pcm->dmodelsConvFlux->data[n1];
+                float weight = dmodel->data.F32[i][j] * yweight;
+                for (int n2 = 0, N2 = 0; n2 <= n1; n2++) {
+                    if ((paramMask != NULL) && (paramMask->data.PS_TYPE_VECTOR_MASK_DATA[n2])) continue;
+                    dmodel = pcm->dmodelsConvFlux->data[n2];
+                    alpha->data.F32[N1][N2] += weight * dmodel->data.F32[i][j];
+                    N2++;
+                }
+                beta->data.F32[N1] += weight * delta;
+                N1++;
+            }
+        }
+    }
+
+    // calculate lower-left half of alpha
+    for (psS32 j = 1; j < alpha->numCols; j++) {
+        for (psS32 k = 0; k < j; k++) {
+            alpha->data.F32[k][j] = alpha->data.F32[j][k];
+        }
+    }
+
+    return(chisq);
+}
+
+
+/*
+ *
+ * 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_branches/psModules.20240412/src/objects/pmPCMdata.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPCMdata.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPCMdata.c	(revision 42651)
@@ -0,0 +1,631 @@
+/* @file  pmPCMdata.c
+ * structures and functions to support PSF-convolved model fitting
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.29 $
+ * @date $Date: 2009-02-16 22:30:50 $
+ * Copyright 2010 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAMaskWeight.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPCMdata.h"
+
+# define USE_DELTA_PSF 0
+
+static void pmPCMdataFree (pmPCMdata *pcm) {
+
+    if (pcm == NULL) return;
+
+    psFree (pcm->modelFlux);
+    psFree (pcm->modelConvFlux);
+    psFree (pcm->dmodelsFlux);
+    psFree (pcm->dmodelsConvFlux);
+
+    psFree (pcm->modelConv);
+    psFree (pcm->psf);
+    psFree (pcm->psfFFT);
+    psFree (pcm->constraint);
+
+    psFree (pcm->smdata); // pre-allocated data for psImageSmooth_PreAlloc
+    psFree (pcm->smdata2d); // pre-allocated data for psImageSmooth_PreAlloc
+    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->modelFlux = psImageCopy (NULL, source->pixels, PS_TYPE_F32);
+    pcm->dmodelsFlux = psArrayAlloc (params->n);
+    for (psS32 n = 0; n < params->n; n++) {
+        pcm->dmodelsFlux->data[n] = NULL;
+        if ((paramMask != NULL) && (paramMask->data.PS_TYPE_VECTOR_MASK_DATA[n])) { continue; }
+        pcm->dmodelsFlux->data[n] = psImageCopy (NULL, source->pixels, PS_TYPE_F32);
+    }
+
+    // Allocate storage images for convolved model and derivative images
+    pcm->modelConvFlux = psImageCopy (NULL, source->pixels, PS_TYPE_F32);
+    pcm->dmodelsConvFlux = psArrayAlloc (params->n);
+    for (psS32 n = 0; n < params->n; n++) {
+        pcm->dmodelsConvFlux->data[n] = NULL;
+        if ((paramMask != NULL) && (paramMask->data.PS_TYPE_VECTOR_MASK_DATA[n])) { continue; }
+        pcm->dmodelsConvFlux->data[n] = psImageCopy (NULL, source->pixels, PS_TYPE_F32);
+    }
+
+    pcm->smdata = NULL;
+    pcm->smdata2d = NULL;
+
+    pcm->modelConv = NULL;
+    pcm->psf = NULL;
+    pcm->psfFFT = NULL;
+    pcm->constraint = NULL;
+    pcm->nDOF = 0;
+
+    pcm->poissonErrors = true;
+
+    // full convolution with the PSF is expensive.  if we have to save time, we can do a 1D
+    // convolution with a Gaussian approximation to the kernel
+    pcm->use1Dgauss = false;
+    pcm->nsigma = NAN; // this is set to something defined by the user
+    pcm->sigma = 1.0; // this should be set to something sensible when the psf is known
+
+    return pcm;
+}
+
+psImage *pmPCMdataSaveImage (pmPCMdata *pcm) {
+
+    psImage *model = psImageCopy (NULL, pcm->modelConvFlux, PS_TYPE_F32);
+
+    return model;
+}
+
+psKernel *pmPCMkernelFromPSF (pmSource *source, int nPix) {
+
+    assert (source);
+    assert (source->psfImage); // XXX build if needed?
+
+    int x0 = source->peak->x - source->psfImage->col0;
+    int y0 = source->peak->y - source->psfImage->row0;
+
+    // need to decide on the size: dynamically? statically?
+    psKernel *psf = psKernelAlloc (-nPix, +nPix, -nPix, +nPix);
+
+    // XXX we should just re-construct a PSF at this location 
+    // psModelAdd (psf->image, NULL, source->modelPSF, PM_MODEL_OP_FULL | PM_MODEL_OP_NORM | PM_MODEL_OP_CENTER);
+  
+    // if the realized PSF for this object does not cover the full kernel, give up for now
+    if (x0 + psf->xMin < 0) goto escape;
+    if (x0 + psf->xMax >= source->psfImage->numCols) goto escape;
+    if (y0 + psf->yMin < 0) goto escape;
+    if (y0 + psf->yMax >= source->psfImage->numRows) goto escape;
+
+    double sum = 0.0;
+    for (int j = psf->yMin; j <= psf->yMax; j++) {
+	for (int i = psf->xMin; i <= psf->xMax; i++) {
+	    double value = source->psfImage->data.F32[y0 + j][x0 + i];
+	    psf->kernel[j][i] = value;
+	    sum += value;
+	}
+    }
+
+    if (!(sum > 0.0)) {
+        // Crazy PSF image print out some debugging information ...
+        fprintf(stderr, "invalid kernel sum %f found by pmPCMkernelFromPSF\n", sum);
+
+        if (sum != 0) {
+            // don't bother printing the kernel if its sum is zero
+            for (int j = psf->yMin; j <= psf->yMax; j++) {
+                fprintf(stderr, "Row %d\n", j);
+                for (int i = psf->xMin; i <= psf->xMax; i++) {
+                    double value = source->psfImage->data.F32[y0 + j][x0 + i];
+                    fprintf(stderr, "  %d %f\n", i, value);
+                }
+            }
+        }
+        fflush(stderr);
+        // ... but avoid the asssertion two lines down by escaping
+        goto escape;
+    }
+    assert (sum > 0.0);
+
+    // psf must be normalized (integral = 1.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;
+	}
+    }
+
+    return psf;
+
+escape:
+    psFree (psf);
+    return NULL;
+}
+
+int pmPCMsetParams (psMinConstraint *constraint, pmSourceFitMode mode) {
+
+    // set parameter mask based on fitting mode
+    int nParams = 0;
+    int nParAll = constraint->paramMask->n;
+
+    switch (mode) {
+      case PM_SOURCE_FIT_NORM:
+        // fits only source normalization (Io)
+        nParams = 1;
+        psVectorInit (constraint->paramMask, 1);
+        constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_I0] = 0;
+        break;
+
+      case PM_SOURCE_FIT_PSF:
+        // fits only x,y,Io
+        nParams = 3;
+        psVectorInit (constraint->paramMask, 1);
+        constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_I0] = 0;
+        constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_XPOS] = 0;
+        constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_YPOS] = 0;
+        break;
+
+      case PM_SOURCE_FIT_EXT:
+        // fits all params except sky
+        nParams = nParAll - 1;
+        psVectorInit (constraint->paramMask, 0);
+        constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_SKY] = 1;
+        break;
+
+      case PM_SOURCE_FIT_EXT_AND_SKY:
+        // fits all params including sky
+        nParams = nParAll;
+        psVectorInit (constraint->paramMask, 0);
+        break;
+
+      case PM_SOURCE_FIT_SHAPE:
+	// fits shape (Sxx, Sxy, Syy) and Io
+	nParams = 5;
+	psVectorInit (constraint->paramMask, 1);
+	constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_SKY] = 0;
+	constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_I0] = 0;
+	constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_SXX] = 0;
+	constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_SXY] = 0;
+	constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_SYY] = 0;
+	break;
+
+      case PM_SOURCE_FIT_INDEX:
+        // fits only Io, index (PAR7) -- only Io for models with < 8 params
+	psVectorInit (constraint->paramMask, 1);
+	constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_I0] = 0;
+        if (nParAll == 7) {
+	    nParams = 1;
+	} else {
+	    nParams = 2;
+	    constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_7] = 0;
+	}
+	break;
+
+      case PM_SOURCE_FIT_NO_INDEX:
+        // fits all but index (PAR7) including sky
+	psVectorInit (constraint->paramMask, 0);
+        if (nParAll == 7) {
+	    nParams = nParAll;
+	} else {
+	    nParams = nParAll - 1;
+	    constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_7] = 1;
+	}
+	break;
+      default:
+	psAbort("invalid fitting mode");
+    }
+    return nParams;
+}
+
+static int modelType_GAUSS = -1;
+static int modelType_PS1_V1 = -1;
+
+// generate a Gaussian smoothing kernel for supplied sigma.  sigma here does not need to match
+// that used to allocate the structure, but it is recommended
+bool psImageSmoothCacheKernel_PS1_V1 (psImageSmoothCacheData *smdata, float sigma, float kappa) {
+    // check for NULL structure elements?
+
+    int size = smdata->Nrange;
+
+    psFree (smdata->kernel);
+    smdata->kernel = psVectorAlloc(2 * smdata->Nrange + 1, PS_TYPE_F32);
+
+    double sum = 0.0;			// Sum of Gaussian, for normalization
+    double factor = 1.0 / (sigma * M_SQRT2);	// Multiplier for i -> z
+
+    // PS1_V1 is a power-law with fitted linear term:
+    // 1 / (1 + kappa z + z^1.666)  where z = (r/sigma)^2
+
+    // generate the kernel (not normalized)
+    for (int i = -size, j = 0; i <= size; i++, j++) {
+	float z = PS_SQR(i * factor);
+        sum += smdata->kernel->data.F32[j] = 1.0 / (1 + kappa * z + pow(z,1.666));
+    }
+
+    // renormalize kernel to integral of 1.0
+    for (int i = 0; i < 2 * size + 1; i++) {
+        smdata->kernel->data.F32[i] /= sum;
+    }
+
+    return true;
+}
+
+psImageSmoothCacheData *psImageSmoothCacheSetKernel (float *sigma, float *kappa, float nsigma, psImage *flux, pmModel *modelPSF) {
+
+    psAssert (modelPSF, "psf model must be defined");
+    
+    psEllipseAxes axes;
+    bool useReff = modelPSF->class->useReff;
+    psF32 *PAR = modelPSF->params->data.F32;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], useReff);
+    
+    *sigma = NAN;
+    *kappa = NAN;
+
+    // XXX need to do this more carefully
+    if (modelPSF->type == modelType_GAUSS) {
+	float FWHM_MAJOR = 2*modelPSF->class->modelRadius (modelPSF->params, 0.5*PAR[PM_PAR_I0]);
+	float FWHM_MINOR = FWHM_MAJOR * (axes.minor / axes.major);
+	*sigma = 0.50 * (FWHM_MAJOR + FWHM_MINOR) / 2.35;
+    }
+    if (modelPSF->type == modelType_PS1_V1) {
+	*sigma = 0.5 * (axes.major + axes.minor);
+	*kappa = PAR[PM_PAR_7];
+    }
+    psAssert (isfinite(*sigma), "invalid model type");
+
+    // psImageSmoothCacheAlloc generates a structure but does not assign the smoothing vector
+    psImageSmoothCacheData *smdata = psImageSmoothCacheAlloc (flux, *sigma, nsigma);
+
+    if (modelPSF->type == modelType_GAUSS) {
+	psImageSmoothCacheKernel_Gauss (smdata, *sigma);
+    }
+    if (modelPSF->type == modelType_PS1_V1) {
+	psImageSmoothCacheKernel_PS1_V1 (smdata, *sigma, *kappa);
+    }
+
+    return smdata;
+}
+
+psImageSmooth2dCacheData *psImageSmooth2dCacheSetKernel (float *sigma, float *kappa, float nsigma, psImage *flux, pmModel *modelPSF) {
+
+    psAssert (modelPSF, "psf model must be defined");
+    
+    psEllipseAxes axes;
+    bool useReff = modelPSF->class->useReff;
+    psF32 *PAR = modelPSF->params->data.F32;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], useReff);
+    
+    *sigma = NAN;
+    *kappa = NAN;
+
+    // XXX need to do this more carefully
+    if (modelPSF->type == modelType_GAUSS) {
+	float FWHM_MAJOR = 2*modelPSF->class->modelRadius (modelPSF->params, 0.5*PAR[PM_PAR_I0]);
+	float FWHM_MINOR = FWHM_MAJOR * (axes.minor / axes.major);
+	*sigma = 0.50 * (FWHM_MAJOR + FWHM_MINOR) / 2.35;
+    }
+    if (modelPSF->type == modelType_PS1_V1) {
+	*sigma = 0.5 * (axes.major + axes.minor);
+	*kappa = PAR[PM_PAR_7];
+    }
+    psAssert (isfinite(*sigma), "invalid model type");
+
+    // psImageSmoothCacheAlloc generates a structure but does not assign the smoothing vector
+    psImageSmooth2dCacheData *smdata = psImageSmooth2dCacheAlloc (nsigma);
+
+    if (modelPSF->type == modelType_GAUSS) {
+	psImageSmooth2dCacheKernel_Gauss (smdata, *sigma);
+    }
+    if (modelPSF->type == modelType_PS1_V1) {
+	psImageSmooth2dCacheKernel_PS1_V1 (smdata, *sigma, *kappa);
+    }
+
+    return smdata;
+}
+
+pmPCMdata *pmPCMinit(pmSource *source, pmSourceFitOptions *fitOptions, pmModel *model, psImageMaskType maskVal, float psfSize) {
+
+    modelType_GAUSS = pmModelClassGetType ("PS_MODEL_GAUSS");
+    modelType_PS1_V1 = pmModelClassGetType ("PS_MODEL_PS1_V1");
+
+    // count the number of unmasked pixels:
+    int nPix = 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.PS_TYPE_IMAGE_MASK_DATA[i][j]) {
+                continue;
+            }
+            // skip zero-variance points
+            if (source->variance->data.F32[i][j] == 0) {
+                continue;
+            }
+            // skip nan value points
+            if (!isfinite(source->pixels->data.F32[i][j])) {
+                continue;
+            }
+	    nPix ++;
+	}
+    }    
+
+    psVector *params  = model->params;
+
+    // create the minimization constraints
+    psMinConstraint *constraint = psMinConstraintAlloc();
+    constraint->paramMask = psVectorAlloc (params->n, PS_TYPE_VECTOR_MASK);
+    constraint->checkLimits = model->class->modelLimits;
+
+    int nParams = pmPCMsetParams (constraint, fitOptions->mode);
+
+    if (nPix <  nParams + 1) {
+        psTrace ("psModules.objects", 4, "insufficient valid pixels\n");
+	psFree (constraint);
+        model->flags |= PM_MODEL_STATUS_BADARGS;
+	return NULL;
+    }
+
+    // generate PCM data storage structure
+    pmPCMdata *pcm = pmPCMdataAlloc (params, constraint->paramMask, source);
+    pcm->modelConv = psMemIncrRefCounter(model);
+    pcm->constraint = constraint;
+
+    pcm->poissonErrors = fitOptions->poissonErrors;
+    pcm->nsigma = fitOptions->nsigma;
+
+    pcm->nPix = nPix;
+    pcm->nPar = nParams;
+    pcm->nDOF = nPix - nParams;
+
+# if (USE_1D_GAUSS)
+
+    pcm->use1Dgauss = true;
+    if (USE_1D_CACHE) {
+	pcm->smdata = psImageSmoothCacheSetKernel (&pcm->sigma, &pcm->kappa, pcm->nsigma, source->pixels, source->modelPSF);
+    } else {
+	pcm->smdata2d = psImageSmooth2dCacheSetKernel (&pcm->sigma, &pcm->kappa, pcm->nsigma, source->pixels, source->modelPSF);
+    }
+
+# else
+    // 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
+    psKernel *psf = pmPCMkernelFromPSF (source, psfSize);
+    if (!psf) {
+	// NOTE: this only happens if the source is too close to an edge
+        model->flags |= PM_MODEL_STATUS_BADARGS;
+	return NULL;
+    }
+
+# if (USE_DELTA_PSF)
+    psImageInit (psf->image, 0.0);
+    psf->image->data.F32[(int)(0.5*psf->image->numRows)][(int)(0.5*psf->image->numCols)] = 1.0;
+# endif
+    pcm->psf = psf;
+    pcm->smdata = NULL;
+# endif
+
+    return pcm;
+}
+
+// has the set of fitted terms changed?  has the fitting radius changed?
+bool pmPCMupdate(pmPCMdata *pcm, pmSource *source, pmSourceFitOptions *fitOptions, pmModel *model) {
+
+    bool sameWindow = (source->pixels->numRows == pcm->modelFlux->numRows);
+    sameWindow     &= (source->pixels->numCols == pcm->modelFlux->numCols);
+    sameWindow     &= (source->pixels->col0    == pcm->modelFlux->col0);
+    sameWindow     &= (source->pixels->row0    == pcm->modelFlux->row0);
+
+    // re-count the number of unmasked pixels:
+    if (!sameWindow) {
+	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.PS_TYPE_IMAGE_MASK_DATA[i][j]) {
+		    continue;
+		}
+		// skip zero-variance points
+		if (source->variance->data.F32[i][j] == 0) {
+		    continue;
+		}
+		// skip nan value points
+		if (!isfinite(source->pixels->data.F32[i][j])) {
+		    continue;
+		}
+		pcm->nPix ++;
+	    }
+	}    
+    }
+
+    int nParams = pmPCMsetParams (pcm->constraint, fitOptions->mode);
+
+    if (pcm->nPix <  nParams + 1) {
+        psTrace ("psModules.objects", 4, "insufficient valid pixels\n");
+        model->flags |= PM_MODEL_STATUS_BADARGS;
+	return false;
+    }
+    pcm->nPar = nParams;
+    pcm->nDOF = pcm->nPix - nParams;
+
+    // has the source pixel window changed?
+    if (!sameWindow) {
+
+	// adjust all supporting images:
+	pcm->modelFlux = psImageCopy (pcm->modelFlux, source->pixels, PS_TYPE_F32);
+	for (psS32 n = 0; n < pcm->dmodelsFlux->n; n++) {
+	    if ((pcm->constraint->paramMask != NULL) && (pcm->constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[n])) { continue; }
+	    pcm->dmodelsFlux->data[n] = psImageCopy (pcm->dmodelsFlux->data[n], source->pixels, PS_TYPE_F32);
+	}
+
+	// adjust images for convolved model and derivative images
+	pcm->modelConvFlux = psImageCopy (pcm->modelConvFlux, source->pixels, PS_TYPE_F32);
+	for (psS32 n = 0; n < pcm->dmodelsConvFlux->n; n++) {
+	    if ((pcm->constraint->paramMask != NULL) && (pcm->constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[n])) { continue; }
+	    pcm->dmodelsConvFlux->data[n] = psImageCopy (pcm->dmodelsConvFlux->data[n], source->pixels, PS_TYPE_F32);
+	}
+
+	// If we have changed the window, we need to redefine the smoothing target vectors (but pcm->sigma,kappa,nsigma remain)
+	if (USE_1D_CACHE) {
+	    psFree(pcm->smdata);
+	    pcm->smdata = psImageSmoothCacheAlloc (source->pixels, pcm->sigma, pcm->nsigma);
+
+	    pmModel *modelPSF = source->modelPSF;
+	    if (modelPSF->type == modelType_GAUSS) {
+		psImageSmoothCacheKernel_Gauss (pcm->smdata, pcm->sigma);
+	    }
+	    if (modelPSF->type == modelType_PS1_V1) {
+		psImageSmoothCacheKernel_PS1_V1 (pcm->smdata, pcm->sigma, pcm->kappa);
+	    }
+	} else {
+	    psFree(pcm->smdata2d);
+	    pcm->smdata2d = psImageSmooth2dCacheAlloc (pcm->nsigma);
+
+	    pmModel *modelPSF = source->modelPSF;
+	    if (modelPSF->type == modelType_GAUSS) {
+		// psImageSmooth2dCacheKernel_Gauss (pcm->smdata2d, pcm->sigma);
+	    }
+	    if (modelPSF->type == modelType_PS1_V1) {
+		psImageSmooth2dCacheKernel_PS1_V1 (pcm->smdata2d, pcm->sigma, pcm->kappa);
+	    }
+	}
+    }
+
+    return true;
+}
+
+// construct a realization of the source model
+bool pmPCMCacheModel (pmSource *source, psImageMaskType maskVal, int psfSize, float nsigma) {
+
+    PS_ASSERT_PTR_NON_NULL(source, false);
+
+    // 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);
+
+    // modelFlux always has unity normalization (I0 = 1.0)
+    pmModelAdd (source->modelFlux, source->maskObj, model, PM_MODEL_OP_FULL | PM_MODEL_OP_NORM, maskVal);
+
+    // convolve the model image with the PSF
+    if (USE_1D_GAUSS) {
+	
+	float sigma = NAN;
+	float kappa = NAN;
+
+	if (USE_1D_CACHE) {
+	    psImageSmoothCacheData *smdata = psImageSmoothCacheSetKernel (&sigma, &kappa, nsigma, source->modelFlux, source->modelPSF);
+	    psImageSmoothCache_F32 (source->modelFlux, smdata);
+	    psFree (smdata);
+	} else {
+	    psImageSmooth2dCacheData *smdata = psImageSmooth2dCacheSetKernel (&sigma, &kappa, nsigma, source->modelFlux, source->modelPSF);
+	    psImageSmooth2dCache_F32 (source->modelFlux, smdata);
+	    psFree (smdata);
+	}
+	// old call: psImageSmooth (source->modelFlux, sigma, nsigma);
+    } else {
+	// 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
+	psKernel *psf = pmPCMkernelFromPSF (source, psfSize);
+	if (!psf) {
+	    // NOTE: this only happens if the source is too close to an edge
+	    model->flags |= PM_MODEL_STATUS_BADARGS;
+	    return NULL;
+	}
+
+	// XXX not sure if I can place the output on top of the input
+	psImageConvolveFFT (source->modelFlux, source->modelFlux, NULL, 0, psf);
+    }
+    return true;
+}
+
+// construct a realization of the source model
+bool pmPCMMakeModel (pmSource *source, pmModel *model, float Nsigma, psImageMaskType maskVal, int psfSize) {
+
+    PS_ASSERT_PTR_NON_NULL(source, false);
+
+    // 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);
+
+    // modelFlux always has unity normalization (I0 = 1.0)
+    // pmModelAdd (source->modelFlux, source->maskObj, model, PM_MODEL_OP_FULL | PM_MODEL_OP_NORM, maskVal);
+    pmModelAdd (source->modelFlux, NULL, model, PM_MODEL_OP_FULL | PM_MODEL_OP_SKY | PM_MODEL_OP_NORM, maskVal);
+
+    // convolve the model image with the PSF
+    if (USE_1D_GAUSS) {
+
+	float sigma = NAN;
+	float kappa = NAN;
+
+	if (USE_1D_CACHE) {
+	    psImageSmoothCacheData *smdata = psImageSmoothCacheSetKernel (&sigma, &kappa, Nsigma, source->modelFlux, source->modelPSF);
+	    psImageSmoothCache_F32 (source->modelFlux, smdata);
+	    psFree (smdata);
+	} else {
+	    psImageSmooth2dCacheData *smdata = psImageSmooth2dCacheSetKernel (&sigma, &kappa, Nsigma, source->modelFlux, source->modelPSF);
+	    psImageSmooth2dCache_F32 (source->modelFlux, smdata);
+	    psFree (smdata);
+	}
+	// old call: psImageSmooth (source->modelFlux, sigma, nsigma);
+    } else {
+	// 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
+	psKernel *psf = pmPCMkernelFromPSF (source, psfSize);
+	if (!psf) {
+	    // NOTE: this only happens if the source is too close to an edge
+	    model->flags |= PM_MODEL_STATUS_BADARGS;
+	    return NULL;
+	}
+
+	// XXX not sure if I can place the output on top of the input
+	psImageConvolveFFT (source->modelFlux, source->modelFlux, NULL, 0, psf);
+    }
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPCMdata.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPCMdata.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPCMdata.h	(revision 42651)
@@ -0,0 +1,112 @@
+/* @file  pmPCMdata.h
+ * structures and functions to support PSF-convolved model fitting
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.29 $
+ * @date $Date: 2009-02-16 22:30:50 $
+ * Copyright 2010 Institute for Astronomy, University of Hawaii
+ */
+
+# ifndef PM_PCM_DATA_H
+# define PM_PCM_DATA_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+// XXX this is basically for testing -- when I am happy with the convolution process, I'll strip this out
+# define USE_1D_CACHE 0
+# define USE_1D_GAUSS 1
+
+/** pmPCMdata : PSF Convolved Model data storage structure
+ *
+ * Structure to carry the data needed to generate a PSF-convolve model fit
+ *
+ */
+// 
+typedef struct {
+    psImage *modelFlux;
+    psArray *dmodelsFlux;
+    psImage *modelConvFlux;
+    psArray *dmodelsConvFlux;
+
+    pmModel *modelConv;
+    psKernel *psf;
+    psKernelFFT *psfFFT;
+
+    psMinConstraint *constraint;
+    int nPix;
+    int nPar;
+    int nDOF;
+
+    bool poissonErrors;
+
+    bool use1Dgauss;
+    float kappa;
+    float sigma;
+    float nsigma;
+
+    // psArray *smdata;
+    psImageSmoothCacheData *smdata;
+    psImageSmooth2dCacheData *smdata2d;
+} pmPCMdata;
+
+// structures & functions to support psf-convolved model fitting
+
+// 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);
+
+pmPCMdata *pmPCMinit(pmSource *source, pmSourceFitOptions *fitOptions, pmModel *model, psImageMaskType maskVal, float psfSize);
+bool pmPCMupdate(pmPCMdata *pcm, pmSource *source, pmSourceFitOptions *fitOptions, pmModel *model);
+
+psImage *pmPCMdataSaveImage (pmPCMdata *pcm);
+
+psF32 pmPCM_SetABX(
+    psImage  *alpha,
+    psVector *beta,
+    const psVector *params,
+    const psVector *paramMask,
+    pmPCMdata *pcm,
+    const pmSource *source);
+
+bool pmPCM_MinimizeChisq (
+    psMinimization *min,
+    psImage *covar,
+    psVector *params,
+    pmSource *source,
+    pmPCMdata *pcm);
+
+psKernel *pmPCMkernelFromPSF (pmSource *source, int nPix);
+
+bool pmSourceModelGuessPCM (pmPCMdata *pcm, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal);
+
+bool pmSourceFitPCM (pmPCMdata *pcm, pmSource *source, pmSourceFitOptions *fitOptions, psImageMaskType maskVal, psImageMaskType markVal, int psfSize);
+
+bool pmPCMCacheModel (pmSource *source, psImageMaskType maskVal, int psfSize, float nsigma);
+
+bool pmPCMMakeModel (pmSource *source, pmModel *model, float Nsigma, psImageMaskType maskVal, int psfSize);
+
+/// @}
+# endif /* PM_PCM_DATA_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPSF.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPSF.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPSF.c	(revision 42651)
@@ -0,0 +1,438 @@
+/** @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.39 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-12-08 02:51:14 $
+ *
+ *  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 "pmFPAMaskWeight.h"
+#include "psVectorBracket.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmErrorCodes.h"
+
+
+#define MAX_AXIS_RATIO 20.0             // Maximum axis ratio for PSF model
+
+/*****************************************************************************/
+/* FUNCTION IMPLEMENTATION - PUBLIC                                          */
+/*****************************************************************************/
+
+static void pmPSFOptionsFree (pmPSFOptions *options) {
+
+    if (!options) return;
+
+    psFree (options->stats);
+    psFree (options->fitOptions);
+    return;
+}
+
+pmPSFOptions *pmPSFOptionsAlloc (void) {
+
+    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;
+
+    options->chiFluxTrend = true;
+    options->fitOptions    = NULL; // XXX this has to be set before calling pmPSF fit functions
+
+    options->fitRadius = NAN;
+    options->apRadius = NAN;
+    return options;
+}
+
+bool psMemCheckPSFOptions(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmPSFOptionsFree);
+}
+
+/*****************************************************************************
+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 (const pmPSFOptions *options)
+{
+    PS_ASSERT_PTR_NON_NULL(options, NULL);
+    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;
+}
+
+bool psMemCheckPSF(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmPSFFree);
+}
+
+// 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)
+
+// XXX deprecated
+// input: model->param, output: psf->param[PM_PAR_SXY]
+// XXX double pmPSF_SXYfromModel (psF32 *modelPar)
+// XXX {
+// XXX     PS_ASSERT_PTR_NON_NULL(modelPar, NAN);
+// XXX 
+// XXX     double SXX = modelPar[PM_PAR_SXX];
+// XXX     double SYY = modelPar[PM_PAR_SYY];
+// XXX     double SXY = modelPar[PM_PAR_SXY];
+// XXX 
+// XXX     double par = SXY / PS_SQR(1.0 / PS_SQR(SXX) + 1.0 / PS_SQR(SYY));
+// XXX     return (par);
+// XXX }
+
+// XXX deprecated
+// input: fitted psf->param, output: model->param[PM_PAR_SXY]
+// XXX double pmPSF_SXYtoModel (psF32 *fittedPar)
+// XXX {
+// XXX     PS_ASSERT_PTR_NON_NULL(fittedPar, NAN);
+// XXX 
+// XXX     double SXX = fittedPar[PM_PAR_SXX];
+// XXX     double SYY = fittedPar[PM_PAR_SYY];
+// XXX     double fit = fittedPar[PM_PAR_SXY];
+// XXX 
+// XXX     double SXY = fit * PS_SQR(1.0 / PS_SQR(SXX) + 1.0 / PS_SQR(SYY));
+// XXX 
+// XXX     assert (!isnan(SXY));
+// XXX 
+// XXX     return SXY;
+// XXX }
+
+// The PSF modelling function fits the polarization terms e0, e1, e2:
+
+// the FIT is the 2D representation of the shape using polarization parameters for the elliptical contour
+// the MODEL is the realized psf model for a given location
+
+// convert the parameters (in situ) used in the fitted source model to the parameters used in
+// the 2D PSF model
+bool pmPSF_FitToModel (psF32 *fittedPar, float minMinorAxis, bool useReff)
+{
+    PS_ASSERT_PTR_NON_NULL(fittedPar, false);
+
+    psEllipsePol pol;
+
+    pol.e0 = fittedPar[PM_PAR_E0];
+    pol.e1 = fittedPar[PM_PAR_E1];
+    pol.e2 = fittedPar[PM_PAR_E2];
+
+    psEllipseAxes axes = psEllipsePolToAxes (pol, minMinorAxis);
+    if (!isfinite(axes.major) || !isfinite(axes.minor) || !isfinite(axes.theta)) {
+        psTrace("psModules.objects", 5, "Failed to convert e[012] (%g,%g,%g) to axes", pol.e0, pol.e1, pol.e2);
+        return false;
+    }
+
+    pmModelAxesToParams (&fittedPar[PM_PAR_SXX], &fittedPar[PM_PAR_SXY], &fittedPar[PM_PAR_SYY], axes, useReff);
+    return true;
+}
+
+// convert the parameters (in situ) used in the 2D PSF model fit into the parameters used in
+// the source model
+psEllipsePol pmPSF_ModelToFit (psF32 *modelPar, bool useReff)
+{
+    // must assert non-NULL input parameter
+    psEllipsePol pol;
+    pol.e0 = NAN;
+    pol.e1 = NAN;
+    pol.e2 = NAN;
+    PS_ASSERT_PTR_NON_NULL(modelPar, pol);
+
+    psEllipseAxes axes;
+    pmModelParamsToAxes (&axes, modelPar[PM_PAR_SXX], modelPar[PM_PAR_SXY], modelPar[PM_PAR_SYY], useReff);
+
+    pol = psEllipseAxesToPol (axes);
+
+    return pol;
+}
+
+// convert the parameters used in the fitted source model to the psEllipseAxes representation
+// (major,minor,theta)
+psEllipseAxes pmPSF_ModelToAxes (psF32 *modelPar, bool useReff)
+{
+    psEllipseAxes axes;
+    axes.major = NAN;
+    axes.minor = NAN;
+    axes.theta = NAN;
+
+    PS_ASSERT_PTR_NON_NULL(modelPar, axes);
+
+    pmModelParamsToAxes (&axes, modelPar[PM_PAR_SXX], modelPar[PM_PAR_SXY], modelPar[PM_PAR_SYY], useReff);
+    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, bool useReff)
+{
+    PS_ASSERT_PTR_NON_NULL(modelPar, false);
+
+    modelPar[PM_PAR_SXX] = 0.0;
+    modelPar[PM_PAR_SYY] = 0.0;
+    modelPar[PM_PAR_SXY] = 0.0;
+    
+    if ((axes.major <= 0) || (axes.minor <= 0)) {
+        return true;
+    }
+    
+    pmModelAxesToParams (&modelPar[PM_PAR_SXX], &modelPar[PM_PAR_SXY], &modelPar[PM_PAR_SYY], axes, useReff);
+    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;
+
+    bool useReff = pmModelUseReff (options->type);
+    psEllipsePol pol = pmPSF_ModelToFit(par->data.F32, useReff);
+
+    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;
+}
+
+
+float pmPSFtoFWHM(const pmPSF *psf, float x, float y)
+{
+    PS_ASSERT_PTR_NON_NULL(psf, NAN);
+
+    pmModel *model = pmModelFromPSFforXY(psf, x, y, 1.0); // Model of source
+    if (!model) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to determine PSF model at %f,%f\n", x, y);
+        return NAN;
+    }
+
+    // get the model full-width at half-max
+    float fwhmMajor = 2*model->class->modelRadius (model->params, 0.5);
+
+# if (0)
+    psF32 *params = model->params->data.F32; // Model parameters
+    psEllipseAxes axes = pmPSF_ModelToAxes(params, MAX_AXIS_RATIO, model->class->useReff); // Ellipse axes
+
+    // Curiously, the minor axis can be larger than the major axis, so need to check.
+    float fwhm = 2.355 * PS_MAX(axes.minor, axes.major); // FWHM, converted from sigma
+
+    psFree(model);
+
+    return fwhm;
+# else
+
+    psFree(model);
+
+    return fwhmMajor;
+# endif
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPSF.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPSF.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPSF.h	(revision 42651)
@@ -0,0 +1,122 @@
+/* @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.22 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-01-27 06:39:38 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_PSF_H
+# define PM_PSF_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+/** 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.
+ *
+ */
+struct pmPSF {
+    pmModelType type;                   ///< PSF Model in use
+
+    float chisq;                        ///< PSF goodness statistic (unused??)
+    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
+    int nPSFstars;                      ///< number of stars used to measure PSF
+    int nApResid;                       ///< number of stars used to measure ApResid
+
+    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
+
+    pmTrend2D *ApTrend;                 ///< ApResid vs (x,y)
+    pmTrend2D *FluxScale;               ///< Flux for PSF at (x,y) for normalization = 1.0
+    psPolynomial1D *ChiTrend;           ///< Chisq vs flux fit (correction for systematic errors)
+
+    pmGrowthCurve *growth;              ///< apMag vs Radius
+    pmResiduals *residuals;             ///< normalized residual image (no spatial variation)
+
+    psArray *params;                    ///< Model parameters (psPolynomial2D)
+    psStats *psfTrendStats;             ///< psf parameter trend clipping stats
+
+    pmTrend2DMode psfTrendMode;
+    int trendNx;
+    int trendNy;
+    int fieldNx;
+    int fieldNy;
+    int fieldXo;
+    int fieldYo;
+};
+
+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
+
+    bool          chiFluxTrend;         // Fit a trend in Chi2 as a function of flux?
+    pmSourceFitOptions *fitOptions;
+
+    float         fitRadius;
+    float         apRadius;
+} 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 (const pmPSFOptions *options);
+bool psMemCheckPSF(psPtr ptr);
+pmPSFOptions *pmPSFOptionsAlloc(void);
+bool psMemCheckPSFOptions(psPtr ptr);
+
+double pmPSF_SXYfromModel (psF32 *modelPar);
+double pmPSF_SXYtoModel (psF32 *fittedPar);
+
+pmPSF *pmPSFBuildSimple (char *typeName, float sxx, float syy, float sxy, ...);
+
+bool pmPSF_AxesToModel (psF32 *modelPar, psEllipseAxes axes, bool useReff);
+bool pmPSF_FitToModel (psF32 *fittedPar, float minMinorAxis, bool useReff);
+
+psEllipsePol pmPSF_ModelToFit (psF32 *modelPar, bool useReff);
+psEllipseAxes pmPSF_ModelToAxes (psF32 *modelPar, bool useReff);
+
+/// Calculate FWHM value from a PSF
+float pmPSFtoFWHM(
+    const pmPSF *psf,                   // PSF of interest
+    float x, float y                    // Position of interest
+    );
+
+
+/// @}
+# endif
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPSF_IO.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPSF_IO.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPSF_IO.c	(revision 42651)
@@ -0,0 +1,1427 @@
+/** @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.37 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-01-27 06:39:38 $
+ *
+ *  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 "pmErrorCodes.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPAfileFitsIO.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmPSF_IO.h"
+#include "pmSourceIO.h"
+
+bool pmPSFmodelReadPSFClump (psMetadata *analysis, psMetadata *header);
+bool pmPSFmodelRead_ApTrend (pmPSF *psf, pmFPAfile *file);
+bool pmPSFmodelWrite_ApTrend (pmFPAfile *file, pmPSF *psf);
+
+bool pmPSFmodelRead_GrowthCurve (pmPSF *psf, pmFPAfile *file);
+bool pmPSFmodelWrite_GrowthCurve (pmFPAfile *file, pmPSF *psf);
+
+bool pmPSFmodelCheckDataStatusForView (const pmFPAview *view, const pmFPAfile *file)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa->chips, false);
+    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];
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(chip->cells, false);
+
+    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) {
+
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fpa->chips, false);
+
+    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) {
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+
+    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, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa->chips, false);
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        if (!pmPSFmodelWriteFPA(fpa, view, file, config)) {
+            psError(psErrorCodeLast(), 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(psErrorCodeLast(), false, "Failed to write PSF for chip");
+            return false;
+        }
+        return true;
+    }
+
+    psError(PM_ERR_CONFIG, true, "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, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fpa->chips, false);
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        thisView->chip = i;
+        if (!pmPSFmodelWriteChip (chip, thisView, file, config)) {
+            psError(psErrorCodeLast(), 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, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+
+    // We need the readout as well, because that has the PSF analysis data (e.g., clumps)
+    // There is only one, because photometry is done on chip-mosaicked data.
+    pmFPAview *roView = pmFPAviewAlloc(0); // View to readout
+    *roView = *view;
+    roView->cell = 0;
+    roView->readout = 0;
+    pmReadout *ro = pmFPAviewThisReadout(roView, chip->parent); // Readout with analysis data
+    psFree(roView);
+
+    if (!pmPSFmodelWrite(chip->analysis, ro ? ro->analysis : NULL, view, file, config)) {
+        psError(psErrorCodeLast(), false, "Failed to write PSF for chip");
+        return false;
+    }
+    return true;
+}
+
+// XXX we save the model term identifiers (item) as S32, but they probably should be more flexible
+bool pmTrend2DtoTable (psArray *table, pmTrend2D *trend, char *label, int item) {
+
+    if (trend == NULL) return true; 
+
+    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, label,        0, "", item);
+		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 (table, 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, label,        0, "", item);
+		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 (table, 100, row);
+		psFree (row);
+	    }
+	}
+    }
+    return true;
+}
+
+// extra trend2D elements from a row
+bool pmTrend2DfromTableRow (pmTrend2D *trend, psMetadata *row) {
+
+    bool status = false;
+
+    int xPow = psMetadataLookupS32 (&status, row, "X_POWER");
+    int yPow = psMetadataLookupS32 (&status, row, "Y_POWER");
+
+    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");
+    }
+    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 (const psMetadata *chipAnalysis, const psMetadata *roAnalysis, const pmFPAview *view,
+                      pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+    bool status;
+    char *headName, *tableName, *residName;
+
+    if (!chipAnalysis) {
+        psError(PM_ERR_PROG, true, "No analysis metadata for chip.");
+        return false;
+    }
+    if (!roAnalysis) {
+        psWarning("No analysis metadata for PSF, clump parameters cannot be saved.");
+    }
+
+    // select the current recipe
+    psMetadata *recipe = psMetadataLookupPtr (NULL, config->recipes, "PSPHOT");
+    if (!recipe) {
+        psError(PM_ERR_CONFIG, 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
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // Suitable FPA for writing
+    if (!fpa) {
+        psError(psErrorCodeLast(), false, "Unable to get FPA for writing.");
+        return false;
+    }
+    pmHDU *hdu = psMemIncrRefCounter(pmFPAviewThisHDU(view, fpa));
+    psFree(fpa);
+    if (!hdu) {
+        psError(PM_ERR_CONFIG, false, "Unable to find HDU");
+        return false;
+    }
+
+    // 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(PM_ERR_CONFIG, true, "missing EXTNAME.RULES in camera.config");
+            psFree(hdu);
+            return false;
+        }
+
+        // EXTNAME for image header
+        rule = psMetadataLookupStr(&status, menu, "PSF.HEAD");
+        if (!rule) {
+            psError(PM_ERR_CONFIG, false, "missing entry for PSF.HEAD in EXTNAME.RULES in camera.config");
+            psFree(hdu);
+            return false;
+        }
+        headName = pmFPAfileNameFromRule (rule, file, view);
+
+        // EXTNAME for table data
+        rule = psMetadataLookupStr(&status, menu, "PSF.TABLE");
+        if (!rule) {
+            psError(PM_ERR_CONFIG, false, "missing entry for PSF.TABLE in EXTNAME.RULES in camera.config");
+            psFree (headName);
+            psFree(hdu);
+            return false;
+        }
+        tableName = pmFPAfileNameFromRule (rule, file, view);
+
+        // EXTNAME for resid data
+        rule = psMetadataLookupStr(&status, menu, "PSF.RESID");
+        if (!rule) {
+            psError(PM_ERR_CONFIG, false, "missing entry for PSF.RESID in EXTNAME.RULES in camera.config");
+            psFree (headName);
+            psFree (tableName);
+            psFree(hdu);
+            return false;
+        }
+        residName = pmFPAfileNameFromRule (rule, file, view);
+
+        // EXTNAME for psf image
+        // 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);
+        //     psFree(hdu);
+        //     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;
+        }
+
+        if (!psFitsWriteBlank(file->fits, hdu->header, headName)) {
+            psError(psErrorCodeLast(), false, "Unable to write PSF PHU.");
+            psFree(hdu);
+            return false;
+        }
+        psTrace ("pmFPAfile", 5, "wrote ext head %s (type: %d)\n", file->filename, file->type);
+        file->header = hdu->header;
+        psFree (headName);
+    }
+    psFree(hdu);
+
+    // select the psf of interest
+    pmPSF *psf = psMetadataLookupPtr (&status, chipAnalysis, "PSPHOT.PSF");
+    if (!psf) {
+        psError(PM_ERR_PROG, 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;
+
+        // we now save clump parameters for each region : need to save all of those
+        if (roAnalysis) {
+            int nRegions = psMetadataLookupS32 (&status, roAnalysis, "PSF.CLUMP.NREGIONS");
+            psMetadataAddS32 (header, PS_LIST_TAIL, "PSF_CLN", PS_META_REPLACE, "number of psf clump regions", nRegions);
+            for (int i = 0; i < nRegions; i++) {
+                char regionName[PS_BIGWORD];
+                snprintf (regionName, PS_BIGWORD, "PSF.CLUMP.REGION.%03d", i);
+                psMetadata *regionMD = psMetadataLookupPtr (&status, roAnalysis, regionName);
+
+                psfClump.X  = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.X");   assert (status);
+                psfClump.Y  = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.Y");   assert (status);
+                psfClump.dX = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.DX");  assert (status);
+                psfClump.dY = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.DY");  assert (status);
+
+                char key[PS_SMALLWORD];
+                ps_snprintf_nowarn (key, PS_SMALLWORD, "CLX_%03d", i);
+                psMetadataAddF32 (header, PS_LIST_TAIL, key, PS_META_REPLACE, "psf clump center", psfClump.X);
+                ps_snprintf_nowarn (key, PS_SMALLWORD, "CLY_%03d", i);
+                psMetadataAddF32 (header, PS_LIST_TAIL, key, PS_META_REPLACE, "psf clump center", psfClump.Y);
+                ps_snprintf_nowarn (key, PS_SMALLWORD, "CLDX_%03d", i);
+                psMetadataAddF32 (header, PS_LIST_TAIL, key, PS_META_REPLACE, "psf clump size", psfClump.dX);
+                ps_snprintf_nowarn (key, PS_SMALLWORD, "CLDY_%03d", i);
+                psMetadataAddF32 (header, PS_LIST_TAIL, key, PS_META_REPLACE, "psf clump size", psfClump.dY);
+            }
+        }
+
+        // save the dimensions of each parameter
+        for (int i = 0; i < nPar; i++) {
+            char name[PS_SMALLWORD];
+            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;
+            }
+            ps_snprintf_nowarn (name, PS_SMALLWORD, "PAR%02d_NX", i);
+            psMetadataAddS32 (header, PS_LIST_TAIL, name, 0, "", nX);
+            ps_snprintf_nowarn (name, PS_SMALLWORD, "PAR%02d_NY", i);
+            psMetadataAddS32 (header, PS_LIST_TAIL, name, 0, "", nY);
+            ps_snprintf_nowarn (name, PS_SMALLWORD, "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);
+
+	if (roAnalysis) {
+	  float PSF_APERTURE =  psMetadataLookupF32(&status, roAnalysis, "PSF_APERTURE");
+	  if (status) {
+	    psMetadataAddF32 (header, PS_LIST_TAIL, "PSF_APERTURE", PS_DATA_F32, "aperture for psf objects", PSF_APERTURE);
+	  }
+	  float PSF_FIT_RADIUS =  psMetadataLookupF32(&status, roAnalysis, "PSF_FIT_RADIUS");
+	  if (status) {
+	    psMetadataAddF32 (header, PS_LIST_TAIL, "PSF_FIT_RADIUS", PS_DATA_F32, "aperture for psf objects", PSF_FIT_RADIUS);
+	  }
+	}
+
+        // 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];
+	    pmTrend2DtoTable (psfTable, trend, "MODEL_TERM", i);
+        }
+
+        // write an empty FITS segment if we have no PSF information
+        if (psfTable->n == 0) {
+            psError(PM_ERR_PROG, true, "No PSF data to write.");
+            psFree(tableName);
+            psFree(residName);
+            psFree(psfTable);
+            psFree(header);
+            return false;
+        } else {
+            psTrace ("pmFPAfile", 5, "writing psf data %s\n", tableName);
+            if (!psFitsWriteTable(file->fits, header, psfTable, tableName)) {
+                psError(psErrorCodeLast(), false, "Error 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?
+            if (!psFitsWriteBlank(file->fits, header, residName)) {
+                psError(psErrorCodeLast(), false, "Unable to write blank PSF residual image.");
+                psFree(residName);
+                psFree(header);
+                return false;
+            }
+            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 planes of the image
+        psArray *images = psArrayAllocEmpty (1);
+        psArrayAdd (images, 1, psf->residuals->Ro);  // z = 0 is Ro
+
+        if (psf->residuals->Rx) {
+            psArrayAdd (images, 1, psf->residuals->Rx);
+            psArrayAdd (images, 1, psf->residuals->Ry);
+        }
+
+        // note that all N plane are implicitly of the same type, so we convert the mask
+        if (psf->residuals->mask) {
+            psImage *mask = psImageCopy (NULL, psf->residuals->mask, psf->residuals->Ro->type.type);
+            psArrayAdd (images, 1, mask);
+            psFree (mask);
+        }
+
+        // psFitsWriteImageCube (file->fits, header, images, residName);
+        // psFree (images);
+
+        if (!psFitsWriteImageCube (file->fits, header, images, residName)) {
+            psError(psErrorCodeLast(), false, "Unable to write PSF residuals.");
+            psFree(images);
+            psFree(residName);
+            psFree(header);
+            return false;
+        }
+        psFree (images);
+        psFree (residName);
+        psFree (header);
+    }
+
+    if (!pmPSFmodelWrite_ApTrend(file, psf)) {
+	psError(psErrorCodeLast(), false, "Unable to write PSF ApTrend");
+	return false;
+    }
+
+    if (!pmPSFmodelWrite_GrowthCurve(file, psf)) {
+	psError(psErrorCodeLast(), false, "Unable to write PSF Growth Curve");
+	return false;
+    }
+
+    // write a representation of the psf model
+    {
+        psMetadata *header = psMetadataAlloc ();
+
+        int DX = 65;
+        int DY = 65;
+
+        psImage *psfMosaic = psImageAlloc (DX, DY, PS_TYPE_F32);
+        psImageInit (psfMosaic, 0.0);
+
+        pmModel *modelRef = pmModelAlloc(psf->type);
+
+        // use the center of the center pixel of the image
+        float xc = 0.5*psf->fieldNx;
+        float yc = 0.5*psf->fieldNy;
+
+        // 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] = 1.000;
+        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);
+        if (model) {
+            // place the reference object in the image center
+            pmModelAddWithOffset (psfMosaic, NULL, model, PM_MODEL_OP_FULL | PM_MODEL_OP_CENTER, 0, 0.0, 0.0);
+            psFree (model);
+
+            if (false) {
+                // this call creates an extension with NAXIS3 = 3
+                psArray *images = psArrayAllocEmpty (3);
+                psArrayAdd (images, 1, psfMosaic);
+                // psArrayAdd (images, 1, psfModel);
+                // psArrayAdd (images, 1, psfModel);
+
+                if (!psFitsWriteImageCube (file->fits, header, images, "PSF_MODEL")) {
+                    psError(psErrorCodeLast(), false, "Unable to write PSF representation.");
+                    psFree(images);
+                    psFree(psfMosaic);
+                    psFree(modelRef);
+                    psFree(header);
+                    return false;
+                }
+                psFree (images);
+            } else {
+                // this call creates an extension with NAXIS3 = 1
+                // XXX need to replace PSF_MODEL with rule-based name like residName
+                if (!psFitsWriteImage(file->fits, header, psfMosaic, 0, "PSF_MODEL")) {
+                    psError(psErrorCodeLast(), false, "Unable to write PSF representation.");
+                    psFree(psfMosaic);
+                    psFree(modelRef);
+                    psFree(header);
+                    return false;
+                }
+            }
+        }
+
+        psFree (psfMosaic);
+        psFree (modelRef);
+        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, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+    // 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
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // Suitable FPA for writing
+    if (!fpa) {
+        psError(psErrorCodeLast(), false, "Unable to build FPA to write.");
+        return false;
+    }
+    pmHDU *phu = psMemIncrRefCounter(pmFPAviewThisPHU(view, fpa));
+    psFree(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 {
+        if (!pmConfigConformHeader (outhead, file->format)) {
+            psError(psErrorCodeLast(), false, "Unable to conform header of PSF PHU.");
+            psFree(phu);
+            return false;
+        }
+    }
+    psFree(phu);
+
+    psMetadataAddBool (outhead, PS_LIST_TAIL, "EXTEND", PS_META_REPLACE, "this file has extensions", true);
+    if (!psFitsWriteBlank (file->fits, outhead, "")) {
+        psError(psErrorCodeLast(), false, "Unable to write PHU for PSF.");
+        psFree(outhead);
+        return false;
+    }
+    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)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        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(PM_ERR_CONFIG, true, "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)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+
+    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)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+
+    // We need the readout as well, because that has the PSF analysis data (e.g., clumps)
+    // There may be only one, because photometry is done on chip-mosaicked data.
+    if (chip->cells->n != 1) {
+        psError(PM_ERR_PROG, true, "Chip to receive PSF has %ld cells (should be only one)",
+                chip->cells->n);
+        return false;
+    }
+    pmCell *cell = chip->cells->data[0]; // Cell to receive PSF
+    pmReadout *ro = NULL;                // Readout to receive PSF
+    if (cell->readouts->n == 0) {
+        ro = pmReadoutAlloc(cell);
+        psFree(ro);                     // Drop reference
+    } else if (cell->readouts->n != 1) {
+        psError(PM_ERR_PROG, true, "Cell to receive PSF has %ld readouts (should be only one)",
+                cell->readouts->n);
+        return false;
+    } else {
+        ro = cell->readouts->data[0];
+    }
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    if (!ro->analysis) {
+        ro->analysis = psMetadataAlloc();
+    }
+
+    if (!pmPSFmodelRead(chip->analysis, ro->analysis, view, file, config)) {
+        psError(psErrorCodeLast(), 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 *chipAnalysis, psMetadata *roAnalysis, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    PS_ASSERT_METADATA_NON_NULL(chipAnalysis, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+
+    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(PM_ERR_CONFIG, false, "missing recipe %s", "PSPHOT");
+        return false;
+    }
+
+    // Menu of EXTNAME rules
+    psMetadata *menu = psMetadataLookupMetadata(&status, file->camera, "EXTNAME.RULES");
+    if (!menu) {
+        psError(PM_ERR_CONFIG, true, "missing EXTNAME.RULES in camera.config");
+        return false;
+    }
+    // EXTNAME for table data
+    rule = psMetadataLookupStr(&status, menu, "PSF.TABLE");
+    if (!rule) {
+        psError(PM_ERR_CONFIG, 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(PM_ERR_CONFIG, 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)) {
+        psError(psErrorCodeLast(), false, "cannot find data extension %s in %s", tableName, file->filename);
+        return false;
+    }
+
+    // load the PSF model table header
+    header = psFitsReadHeader (NULL, file->fits);
+    if (!header) {
+        psError(psErrorCodeLast(), false, "Cannot read PSF table header.");
+        return false;
+    }
+
+    pmPSFOptions *options = pmPSFOptionsAlloc();
+
+    // load the PSF model parameters from the FITS table
+    char *modelName = psMetadataLookupStr (&status, header, "PSF_NAME");
+    if (!modelName) {
+        psError(PS_ERR_UNKNOWN, true, "missing model name in psf file %s", file->filename);
+        return false;
+    }
+
+    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;
+    }
+
+    // read the psf clump data for each region
+    status = false;
+    if (roAnalysis) {
+	status = pmPSFmodelReadPSFClump (roAnalysis, header);
+	if (!status) {
+	    psMetadataAddS32 (roAnalysis, PS_LIST_TAIL, "PSF.CLUMP.NREGIONS",  PS_META_REPLACE, "psf clump regions", 0);
+	}
+    } 
+    if (!roAnalysis || !status) {
+	psWarning ("no PSF.CLUMP data available for PSF model");
+    }
+
+    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 (!status) {
+	psError(PS_ERR_UNKNOWN, true, "PSF file %s missing PSF_NPAR value", file->filename);
+	return false;
+    }
+
+    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[PS_SMALLWORD];
+        ps_snprintf_nowarn (name, PS_SMALLWORD, "PAR%02d_NX", i);
+        binning->nXruff = psMetadataLookupS32 (&status, header, name);
+        if (!status) continue;          // not all parameters are defined
+
+	ps_snprintf_nowarn (name, PS_SMALLWORD, "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;
+        }
+
+        ps_snprintf_nowarn (name, PS_SMALLWORD, "PAR%02d_MD", i);
+        char *modeName = psMetadataLookupStr (&status, header, name);
+        if (!status) {
+            psError(PM_ERR_PROG, 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);
+    psFree(options);
+
+    // 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");
+
+    if (roAnalysis) {
+	float PSF_APERTURE =  psMetadataLookupF32(&status, header, "PSF_APERTURE");
+	if (status) {
+	    psMetadataAddF32 (roAnalysis, PS_LIST_TAIL, "PSF_APERTURE", PS_DATA_F32, "aperture for psf objects", PSF_APERTURE);
+	}
+	float PSF_FIT_RADIUS =  psMetadataLookupF32(&status, header, "PSF_FIT_RADIUS");
+	if (status) {
+	    psMetadataAddF32 (roAnalysis, PS_LIST_TAIL, "PSF_FIT_RADIUS", PS_DATA_F32, "aperture for psf objects", PSF_FIT_RADIUS);
+	}
+    } else {
+	psWarning ("unable to read PSF_APERTURE or PSF_FIT_RADIUS");
+    }
+
+    // read the raw table data
+    psArray *table = psFitsReadTable (file->fits);
+    if (!table) {
+        psError(psErrorCodeLast(), false, "Unable to read PSF table.");
+        psFree(header);
+        return false;
+    }
+
+    // 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");
+
+        pmTrend2D *trend = psf->params->data[iPar];
+        if (trend == NULL) {
+            psError(PS_ERR_UNKNOWN, true, "parameter %d not available", iPar);
+            return false;
+        }
+
+	pmTrend2DfromTableRow(trend, row);
+    }
+    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)) {
+        psError(psErrorCodeLast(), false, "Cannot find PSF data extension %s in %s",
+                imageName, file->filename);
+        return false;
+    }
+
+    header = psFitsReadHeader (NULL, file->fits);
+    if (!header) {
+        psError(psErrorCodeLast(), false, "Unable to read PSF header.");
+        return false;
+    }
+    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");
+	if (!status) {
+            psError(psErrorCodeLast(), false, "XBIN not set in PSF residual image header.");
+            return false;
+        }
+        int yBin  = psMetadataLookupS32 (&status, header, "YBIN");
+	if (!status) {
+            psError(psErrorCodeLast(), false, "YBIN not set in PSF residual image header.");
+            return false;
+        }
+
+        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};
+        if (!psFitsReadImageBuffer(psf->residuals->Ro, file->fits, fullImage, 0)) {
+            psError(psErrorCodeLast(), false, "Unable to read PSF residual image.");
+            return false;
+        }
+
+        // note that all N plane are implicitly of the same type, so we convert the mask
+        psImage *mask = psImageCopy(NULL, psf->residuals->mask, psf->residuals->Ro->type.type);
+        psImageInit (psf->residuals->mask, 0);
+        psImageInit (psf->residuals->Rx, 0.0);
+        psImageInit (psf->residuals->Ry, 0.0);
+        switch (Nz) {
+          case 1: // Ro only
+            break;
+          case 2: // Ro and mask
+            if (!psFitsReadImageBuffer(mask, file->fits, fullImage, 1)) {
+                psError(psErrorCodeLast(), false, "Unable to read PSF residual image.");
+                return false;
+            }
+            psImageCopy (psf->residuals->mask, mask, PM_TYPE_RESID_MASK);
+            break;
+          case 3: // Ro, Rx and Ry, no mask
+            if (!psFitsReadImageBuffer(psf->residuals->Rx, file->fits, fullImage, 1)) {
+                psError(psErrorCodeLast(), false, "Unable to read PSF residual image.");
+                return false;
+            }
+            if (!psFitsReadImageBuffer(psf->residuals->Ry, file->fits, fullImage, 2)) {
+                psError(psErrorCodeLast(), false, "Unable to read PSF residual image.");
+                return false;
+            }
+            break;
+          case 4: // Ro, Rx, Ry, and mask:
+            if (!psFitsReadImageBuffer(psf->residuals->Rx, file->fits, fullImage, 1)) {
+                psError(psErrorCodeLast(), false, "Unable to read PSF residual image.");
+                return false;
+            }
+            if (!psFitsReadImageBuffer(psf->residuals->Ry, file->fits, fullImage, 2)) {
+                psError(psErrorCodeLast(), false, "Unable to read PSF residual image.");
+                return false;
+            }
+            if (!psFitsReadImageBuffer(mask, file->fits, fullImage, 3)) {
+                psError(psErrorCodeLast(), false, "Unable to read PSF residual image.");
+                return false;
+            }
+            psImageCopy (psf->residuals->mask, mask, PM_TYPE_RESID_MASK);
+            break;
+        }
+        psFree (mask);
+    }
+
+    if (!pmPSFmodelRead_ApTrend (psf, file)) {
+	psError(psErrorCodeLast(), false, "Unable to read PSF ApTrend data.");
+	return false;
+    }
+
+    if (!pmPSFmodelRead_GrowthCurve(psf, file)) {
+	psError(psErrorCodeLast(), false, "Unable to read PSF Growth Curve");
+	return false;
+    }
+
+    psMetadataAdd (chipAnalysis, PS_LIST_TAIL, "PSPHOT.PSF",     PS_DATA_UNKNOWN,  "psphot psf", psf);
+    psFree (psf);
+
+    psFree (tableName);
+    psFree (imageName);
+    psFree (header);
+
+    return true;
+}
+
+// write aperture trend to a FITS table
+bool pmPSFmodelWrite_ApTrend (pmFPAfile *file, pmPSF *psf) {
+
+    pmTrend2D *trend = psf->ApTrend;
+    if (trend == NULL) { 
+	psWarning ("no PSF ApTrend to write out, skipping");
+	return true; 
+    }
+
+    // we need to write a header for the table,
+    psMetadata *header = psMetadataAlloc();
+
+    int nX = 0, nY = 0;
+    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;
+    }
+    psMetadataAddS32 (header, PS_LIST_TAIL, "TREND_NX", 0, "", nX);
+    psMetadataAddS32 (header, PS_LIST_TAIL, "TREND_NY", 0, "", nY);
+    char *modeName = pmTrend2DModeToString (trend->mode);
+    psMetadataAddStr (header, PS_LIST_TAIL, "TREND_MD", 0, "", modeName);
+    psFree (modeName);
+
+    // build a FITS table of the ApTrend (only 1)
+    psArray *table = psArrayAllocEmpty (100);
+    pmTrend2DtoTable (table, trend, "APTREND", 0);
+
+    // write an empty FITS segment if we have no PSF information
+    if (table->n == 0) {
+	psError(PM_ERR_PROG, true, "No PSF data to write.");
+	psFree(table);
+	psFree(header);
+	return false;
+    } 
+
+    psTrace ("pmFPAfile", 5, "writing psf ApTrend data %s\n", "AP_TREND");
+    if (!psFitsWriteTable(file->fits, header, table, "AP_TREND")) {
+	psError(psErrorCodeLast(), false, "Error writing psf table data %s\n", "AP_TREND");
+	psFree(table);
+	psFree(header);
+	return false;
+    }
+
+    psFree (table);
+    psFree (header);
+    return true;
+}
+
+// read aperture trend to a FITS table
+bool pmPSFmodelRead_ApTrend (pmPSF *psf, pmFPAfile *file) {
+
+    bool status;
+
+    // move fits pointer to AP_TREND section
+    // advance to the table data extension
+    if (!psFitsMoveExtNameClean (file->fits, "AP_TREND")) {
+	psWarning ("no Aperture Trend data in PSF file, skipping");
+	return true;
+    }
+
+    psMetadata *header = psFitsReadHeader (NULL, file->fits);
+    if (!header) {
+	psError(psErrorCodeLast(), false, "Unable to read AP_TREND header.");
+	return false;
+    }
+	
+    // read the raw table data
+    psArray *table = psFitsReadTable (file->fits);
+    if (!table) {
+	psError(psErrorCodeLast(), false, "Unable to read AP_TREND table.");
+	psFree(header);
+	return false;
+    }
+
+    // XXX allow user to set this optionally?
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+
+    psImageBinning *binning = psImageBinningAlloc();
+    binning->nXfine = psf->fieldNx;
+    binning->nYfine = psf->fieldNy;
+    binning->nXruff = psMetadataLookupS32 (&status, header, "TREND_NX");
+    binning->nYruff = psMetadataLookupS32 (&status, header, "TREND_NY");
+    psImageBinningSetScale (binning, PS_IMAGE_BINNING_CENTER);
+    char *modeName  = psMetadataLookupStr (&status, header, "TREND_MD");
+    if (!status) {
+	psError(PM_ERR_PROG, true, "inconsistent PSF header: NX & NY defined for AP TREND, but not MD");
+	psFree (header);
+	psFree (stats);
+	psFree (table);
+	return false;
+    }
+    pmTrend2DMode psfTrendMode = pmTrend2DModeFromString (modeName);
+    if (psfTrendMode == PM_TREND_NONE) {
+	psfTrendMode = PM_TREND_POLY_ORD;
+    }
+
+    // measure Trend2D for the current spatial scale
+    pmTrend2D *apTrend = pmTrend2DNoImageAlloc (PM_TREND_MAP, binning, stats);
+
+    // fill in the matching psf->params entries
+    for (int i = 0; i < table->n; i++) {
+	psMetadata *row = table->data[i];
+	pmTrend2DfromTableRow(apTrend, row);
+    }
+    psf->ApTrend = apTrend;
+
+    psFree (binning);
+    psFree (header);
+    psFree (stats);
+    psFree (table);
+    return true;
+}
+
+// write aperture trend to a FITS table
+bool pmPSFmodelWrite_GrowthCurve (pmFPAfile *file, pmPSF *psf) {
+
+    pmGrowthCurve *growth = psf->growth;
+    if (growth == NULL) { 
+	psWarning ("no PSF Growth Curve to write out, skipping");
+	return true; 
+    }
+
+    // we need to write a header for the table,
+    psMetadata *header = psMetadataAlloc();
+
+    psMetadataAddF32 (header, PS_LIST_TAIL, "GROWTH_MIN_RAD", 0, "", growth->radius->data.F32[0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "GROWTH_MAX_RAD", 0, "", growth->maxRadius);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "GROWTH_REF_RAD", 0, "", growth->refRadius);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "GROWTH_AP_LOSS", 0, "", growth->apLoss);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "GROWTH_AP_REF",  0, "", growth->apRef);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "GROWTH_FIT_MAG", 0, "", growth->fitMag);
+
+    // build a FITS table of the ApTrend (only 1)
+    psArray *table = psArrayAllocEmpty (100);
+    for (int i = 0; i < growth->apMag->n; i++) {
+	psMetadata *row = psMetadataAlloc ();
+	psMetadataAddF32 (row, PS_LIST_TAIL, "RADIUS", 0, "", growth->radius->data.F32[i]);
+	psMetadataAddF32 (row, PS_LIST_TAIL, "AP_MAG", 0, "", growth->apMag->data.F32[i]);
+	psArrayAdd (table, 100, row);
+	psFree (row);
+    }
+
+    // write an empty FITS segment if we have no PSF information
+    if (table->n == 0) {
+	psError(PM_ERR_PROG, true, "No PSF data to write.");
+	psFree(table);
+	psFree(header);
+	return false;
+    } 
+
+    psTrace ("pmFPAfile", 5, "writing psf Growth Curve data %s\n", "GROWTH_CURVE");
+    if (!psFitsWriteTable(file->fits, header, table, "GROWTH_CURVE")) {
+	psError(psErrorCodeLast(), false, "Error writing psf table data %s\n", "GROWTH_CURVE");
+	psFree(table);
+	psFree(header);
+	return false;
+    }
+
+    psFree (table);
+    psFree (header);
+    return true;
+}
+
+// read aperture trend to a FITS table
+bool pmPSFmodelRead_GrowthCurve (pmPSF *psf, pmFPAfile *file) {
+
+    bool status;
+
+    // move fits pointer to AP_TREND section
+    // advance to the table data extension
+    if (!psFitsMoveExtNameClean (file->fits, "GROWTH_CURVE")) {
+	psWarning ("no Growth Curve data in PSF file, skipping");
+	return true;
+    }
+
+    psMetadata *header = psFitsReadHeader (NULL, file->fits);
+    if (!header) {
+	psError(psErrorCodeLast(), false, "Unable to read GROWTH_CURVE header.");
+	return false;
+    }
+	
+    // read the raw table data
+    psArray *table = psFitsReadTable (file->fits);
+    if (!table) {
+	psError(psErrorCodeLast(), false, "Unable to read GROWTH_CURVE table.");
+	psFree(header);
+	return false;
+    }
+
+    float minRadius = psMetadataLookupF32 (&status, header, "GROWTH_MIN_RAD"); if (!status) return false;
+    float maxRadius = psMetadataLookupF32 (&status, header, "GROWTH_MAX_RAD"); if (!status) return false;
+    float refRadius = psMetadataLookupF32 (&status, header, "GROWTH_REF_RAD"); if (!status) return false;
+
+    psf->growth = pmGrowthCurveAlloc(minRadius, maxRadius, refRadius);
+
+    psf->growth->apLoss = psMetadataLookupF32 (&status, header, "GROWTH_AP_LOSS"); if (!status) return false;
+    psf->growth->apRef  = psMetadataLookupF32 (&status, header, "GROWTH_AP_REF"); if (!status) return false;
+    psf->growth->fitMag = psMetadataLookupF32 (&status, header, "GROWTH_FIT_MAG"); if (!status) return false;
+
+    // fill in the matching psf->params entries
+    for (int i = 0; i < table->n; i++) {
+	psMetadata *row = table->data[i];
+	psf->growth->apMag->data.F32[i] = psMetadataLookupF32 (&status, row, "AP_MAG"); if (!status) return false;
+    }
+
+    psFree (header);
+    psFree (table);
+    return true;
+}
+
+bool pmPSFmodelReadPSFClump (psMetadata *analysis, psMetadata *header) {
+
+    bool status = false;;
+
+    int nRegions = psMetadataLookupS32 (&status, header, "PSF_CLN");
+    if (!status) {
+	// read old-style psf clump data
+
+	char regionName[PS_BIGWORD];
+	ps_snprintf_nowarn (regionName, PS_BIGWORD, "PSF.CLUMP.REGION.000");
+	psMetadata *regionMD = psMetadataLookupPtr (&status, analysis, regionName);
+
+	if (!regionMD) {
+	    regionMD = psMetadataAlloc();
+	    psMetadataAddMetadata (analysis, PS_LIST_TAIL, regionName, PS_META_REPLACE, "psf clump region", regionMD);
+	    psFree (regionMD);
+	}
+
+	// psf clump data
+	pmPSFClump psfClump;
+	psfClump.X  = psMetadataLookupF32 (&status, header, "PSF_CLX" );  if (!status) return false;
+	psfClump.Y  = psMetadataLookupF32 (&status, header, "PSF_CLY" );  if (!status) return false;
+	psfClump.dX = psMetadataLookupF32 (&status, header, "PSF_CLDX");  if (!status) return false;
+	psfClump.dY = psMetadataLookupF32 (&status, header, "PSF_CLDY");  if (!status) return false;
+
+	psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.X",  PS_META_REPLACE, "psf clump center", psfClump.X);
+	psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.Y",  PS_META_REPLACE, "psf clump center", psfClump.Y);
+	psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.DX", PS_META_REPLACE, "psf clump center", psfClump.dX);
+	psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.DY", PS_META_REPLACE, "psf clump center", psfClump.dY);
+	psMetadataAddS32 (analysis, PS_LIST_TAIL, "PSF.CLUMP.NREGIONS",  PS_META_REPLACE, "psf clump regions", 1);
+    } else {
+	for (int i = 0; i < nRegions; i++) {
+	    char key[PS_SMALLWORD];
+	    char regionName[PS_BIGWORD];
+	    ps_snprintf_nowarn (regionName, PS_BIGWORD, "PSF.CLUMP.REGION.%03d", i);
+
+	    psMetadata *regionMD = psMetadataLookupPtr (&status, analysis, regionName);
+	    if (!regionMD) {
+		regionMD = psMetadataAlloc();
+		psMetadataAddMetadata (analysis, PS_LIST_TAIL, regionName, PS_META_REPLACE, "psf clump region", regionMD);
+		psFree (regionMD);
+	    }
+
+	    // psf clump data
+	    pmPSFClump psfClump;
+
+	    ps_snprintf_nowarn (key, PS_SMALLWORD, "CLX_%03d", i);
+	    psfClump.X  = psMetadataLookupF32 (&status, header, key);  if (!status) return false;
+	    ps_snprintf_nowarn (key, PS_SMALLWORD, "CLY_%03d", i);
+	    psfClump.Y  = psMetadataLookupF32 (&status, header, key);  if (!status) return false;
+	    ps_snprintf_nowarn (key, PS_SMALLWORD, "CLDX_%03d", i);
+	    psfClump.dX = psMetadataLookupF32 (&status, header, key);  if (!status) return false;
+	    ps_snprintf_nowarn (key, PS_SMALLWORD, "CLDY_%03d", i);
+	    psfClump.dY = psMetadataLookupF32 (&status, header, key);  if (!status) return false;
+
+	    psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.X",  PS_META_REPLACE, "psf clump center", psfClump.X);
+	    psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.Y",  PS_META_REPLACE, "psf clump center", psfClump.Y);
+	    psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.DX", PS_META_REPLACE, "psf clump center", psfClump.dX);
+	    psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.DY", PS_META_REPLACE, "psf clump center", psfClump.dY);
+	}
+	psMetadataAddS32 (analysis, PS_LIST_TAIL, "PSF.CLUMP.NREGIONS",  PS_META_REPLACE, "psf clump regions", nRegions);
+    }
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPSF_IO.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPSF_IO.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPSF_IO.h	(revision 42651)
@@ -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.11 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-07-17 22:38:15 $
+ * 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, pmConfig *config);
+bool pmPSFmodelWriteFPA (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, pmConfig *config);
+bool pmPSFmodelWriteChip (pmChip *chip, const pmFPAview *view, pmFPAfile *file, pmConfig *config);
+bool pmPSFmodelWrite (const psMetadata *chipAnalysis, const psMetadata *roAnalysis, const pmFPAview *view, pmFPAfile *file, pmConfig *config);
+
+bool pmPSFmodelWritePHU (const pmFPAview *view, pmFPAfile *file, 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 *chipAnalysis, psMetadata *roAnalysis, 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_branches/psModules.20240412/src/objects/pmPSFtoMetadata.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPSFtoMetadata.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPSFtoMetadata.c	(revision 42651)
@@ -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_branches/psModules.20240412/src/objects/pmPSFtry.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPSFtry.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPSFtry.c	(revision 42651)
@@ -0,0 +1,203 @@
+/** @file  pmPSFtry.c
+ *
+ *  XXX: need description of file purpose
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.69 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-01-27 06:39:38 $
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmSourcePhotometry.h"
+#include "pmSourceVisual.h"
+
+// ********  pmPSFtry functions  **************************************************
+// * pmPSFtry holds a single pmPSF model test, with the input sources, the freely
+// * fitted version of the model, the pmPSF fit to the fitted model parameters,
+// * and the PSF fits to the source. It also includes the statistics from the
+// * fits, both the individual sources, and the collection
+
+// free a pmPSFtry structure
+static void pmPSFtryFree (pmPSFtry *test)
+{
+    if (test == NULL) return;
+
+    psFree (test->psf);
+    psFree (test->sources);
+    psFree (test->metric);
+    psFree (test->metricErr);
+    psFree (test->fitMag);
+    psFree (test->mask);
+    return;
+}
+
+// allocate a pmPSFtry based on the desired sources and the model (identified by name)
+pmPSFtry *pmPSFtryAlloc (const psArray *sources, const pmPSFOptions *options)
+{
+    pmPSFtry *test = (pmPSFtry *) psAlloc(sizeof(pmPSFtry));
+    psMemSetDeallocator(test, (psFreeFunc) pmPSFtryFree);
+
+    test->psf       = NULL; 
+    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_VECTOR_MASK);
+
+    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++) {
+	pmSource *sourceOld = sources->data[i];
+	pmSource *sourceNew = pmSourceCopy (sourceOld);
+
+	// save a reference so we can get back to the original
+	// this is specifically used in psphotChooosePSF to unflag the candidate PSF sources
+	// which were not actually used to generate a PSF model
+	sourceNew->parent = sourceOld; 
+
+        test->sources->data[i] = sourceNew;
+    }
+
+    return (test);
+}
+
+bool psMemCheckPSFtry(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmPSFtryFree);
+}
+
+float psVectorSystematicError (psVector *residuals, psVector *errors, float clipFraction) {
+
+    psAssert(residuals, "residuals cannot be NULL");
+    psAssert(errors, "errors cannot be NULL");
+    psAssert(residuals->n == errors->n, "residuals and errors must be the same length");
+
+    // given a vector of residuals and their formal errors, calculated the necessary systematic
+    // error needed to yield a reduced chisq of 1.0, after first tossing out the clipFraction
+    // highest chi-square contributors (allowed outliers)
+
+    psVector *mask  = psVectorAlloc(residuals->n, PS_TYPE_VECTOR_MASK);
+    psVector *chisq = psVectorAlloc(residuals->n, PS_TYPE_F32);
+
+    // calculate the chisq vector:
+    int Ngood = 0;
+    for (int i = 0; i < residuals->n; i++) {
+	chisq->data.F32[i] = PS_MAX_F32;
+	if (!isfinite(residuals->data.F32[i])) continue;
+	if (!isfinite(errors->data.F32[i])) continue;
+	if (errors->data.F32[i] <= 0.0) continue;
+	chisq->data.F32[i] = PS_SQR(residuals->data.F32[i] / errors->data.F32[i]);
+	Ngood ++;
+    }
+
+    psVector *index = psVectorSortIndex(NULL, chisq);
+
+    // toss out the clipFraction highest chisq values
+    for (int i = 0; i < residuals->n; i++) {
+	int n = index->data.S32[i];
+	if (i < (1.0 - clipFraction)*Ngood) {
+	    mask->data.PS_TYPE_VECTOR_MASK_DATA[n] = 0;
+	} else {
+	    mask->data.PS_TYPE_VECTOR_MASK_DATA[n] = 1;
+	}
+    }
+
+    // Ndof ~= Ngood
+    // Chisq_Ndof = sum(residuals_i^2 / error_i^2) / Ndof
+    // choose S2 such than Chisq^sys_Ndof = sum(residuals_i^2 / (error_i^2 + S2)) / Ndof = 1.0
+    
+    // use Newton-Raphson to solve for S2:
+
+    // use median sigma to calculate the initial guess for S2:
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN);
+    psVectorStats (stats, errors, NULL, mask, 1);
+    float errorMedian = stats->sampleMedian;
+    
+    float nPts = 0.0;
+    float res2mean = 0.0;
+    float ChiSq = 0.0;
+    for (int i = 0; i < residuals->n; i++) {
+	int n = index->data.S32[i];
+	if (mask->data.PS_TYPE_VECTOR_MASK_DATA[n]) continue;
+	res2mean += PS_SQR(residuals->data.F32[n]);
+	ChiSq += PS_SQR(residuals->data.F32[n]) / PS_SQR(errors->data.F32[n]);
+	nPts += 1.0;
+    }
+    res2mean /= nPts;
+    ChiSq /= nPts;
+    
+    float S2guess = res2mean - PS_SQR(errorMedian);
+
+    psLogMsg ("psModules", 10, "ChiSquare: %f, Ntotal: %ld, Ngood: %d, Nkeep: %.0f, S2 guess: %f\n", 
+	      ChiSq, residuals->n, Ngood, nPts, S2guess);
+
+    for (int iter = 0; iter < 10; iter++) {
+
+	ChiSq = 0.0;
+	float dRdS = 0.0;
+	for (int i = 0; i < residuals->n; i++) {
+	    int n = index->data.S32[i];
+	    if (mask->data.PS_TYPE_VECTOR_MASK_DATA[n]) continue;
+	    float error2 = PS_SQR(errors->data.F32[n]) + S2guess;
+	    ChiSq += PS_SQR(residuals->data.F32[n]) / error2;
+	    dRdS += PS_SQR(residuals->data.F32[n]) / PS_SQR(error2);
+	}
+	ChiSq /= nPts;
+	dRdS /= nPts;
+
+	// Note the sign on dS: dRdS above is -1 * dR/dS formally
+	float dS = (ChiSq - 1.0) / dRdS;
+	S2guess += dS;
+	S2guess = PS_MAX(0.0, S2guess);
+
+	psLogMsg ("psModules", 10, "ChiSquare: %f, dS: %f, S2 guess: %f\n", ChiSq, dS, S2guess);
+    }
+
+    // free local allocations
+    psFree (mask);
+    psFree (chisq);
+    psFree (stats);
+    psFree (index);
+
+    return (sqrt(S2guess));
+}
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPSFtry.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPSFtry.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPSFtry.h	(revision 42651)
@@ -0,0 +1,148 @@
+/* @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.22 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-01-27 06:39:38 $
+ * 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;                   ///< PS_TYPE_VECTOR_MASK to flag good and bad sources 
+    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 (const psArray *sources, const pmPSFOptions *options);
+bool psMemCheckPSFtry(psPtr ptr);
+
+/** 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 (
+    const psArray *sources,		///< PSF sources to use in the pmPSF model analysis
+    const char *modelName,  		///< human-readable name of desired model
+    pmPSFOptions *options, 
+    psImageMaskType maskVal, 
+    psImageMaskType mark
+    );
+
+/** fit EXT models to all possible psf sources */
+bool pmPSFtryFitEXT (pmPSFtry *psfTry, pmPSFOptions *options, psImageMaskType maskVal, psImageMaskType markVal);
+bool pmPSFtryFitEXT_Threaded (psThreadJob *job);
+
+bool pmPSFtryMakePSF (bool *pGoodFit, pmPSFtry *psfTry);
+
+bool pmPSFtryFitPSF (pmPSFtry *psfTry, pmPSFOptions *options, psImageMaskType maskVal, psImageMaskType markVal);
+bool pmPSFtryFitPSF_Threaded (psThreadJob *job);
+
+bool pmPSFThreads (void);
+
+/** 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);
+
+/** 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.
+);
+
+bool pmPSFFitShapeParams (bool *pGoodFit, pmPSF *psf, psArray *sources, psVector *x, psVector *y, psVector *srcMask);
+
+float psVectorSystematicError (psVector *residuals, psVector *errors, float clipFraction);
+
+/// @}
+# endif
+
+/**
+ *
+ * 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).
+ *
+ */
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPSFtryFitEXT.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPSFtryFitEXT.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPSFtryFitEXT.c	(revision 42651)
@@ -0,0 +1,191 @@
+/** @file  pmPSFtry.c
+ *
+ *  XXX: need description of file purpose
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.69 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-01-27 06:39:38 $
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceUtils.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmSourcePhotometry.h"
+#include "pmSourceVisual.h"
+
+bool pmPSFThreads (void) {
+
+    psThreadTask *task = NULL;
+
+    task = psThreadTaskAlloc("PSF_TRY_FIT_EXT", 6);
+    task->function = &pmPSFtryFitEXT_Threaded;
+    psThreadTaskAdd(task);
+    psFree(task);
+
+    task = psThreadTaskAlloc("PSF_TRY_FIT_PSF", 6);
+    task->function = &pmPSFtryFitPSF_Threaded;
+    psThreadTaskAdd(task);
+    psFree(task);
+
+    return true;
+}
+
+static int Next = 0;
+
+// Fit an EXT model to all candidates PSF sources.
+// Note: this is independent of the modeled 2D variations in the PSF.
+bool pmPSFtryFitEXT (pmPSFtry *psfTry, pmPSFOptions *options, psImageMaskType maskVal, psImageMaskType markVal) {
+
+    psTimerStart ("psf.fit");
+
+    // in this segment, we are fitting the full PSF model class (shape unconstrained)
+    options->fitOptions->mode = PM_SOURCE_FIT_EXT;
+
+    // maskVal is used to test for rejected pixels, and must include markVal
+    maskVal |= markVal;
+
+    Next = 0;
+    for (int i = 0; i < psfTry->sources->n; i++) {
+
+        pmSource *source = psfTry->sources->data[i];
+
+	if (!source->moments) {
+	    psfTry->mask->data.PS_TYPE_VECTOR_MASK_DATA[i] = PSFTRY_MASK_EXT_FAIL;
+	    psTrace ("psModules.objects", 4, "masking %d (%d,%d) : no moments\n", i, source->peak->x, source->peak->y);
+	    continue;
+	}
+	if (!source->moments->nPixels) {
+	    psTrace ("psModules.objects", 4, "masking %d (%d,%d) : no pixels\n", i, source->peak->x, source->peak->y);
+	    psfTry->mask->data.PS_TYPE_VECTOR_MASK_DATA[i] = PSFTRY_MASK_EXT_FAIL;
+	    continue;
+	}
+	// If mask object does not exist, mark the source as bad.
+	// We cannot proceed with it because psImageMaskPixels leaves an uncleared error code last which causes
+	// psphot to exit with a fault. 
+	if (source->maskObj == NULL) {
+	    psTrace ("psModules.objects", 4, "source %d (%d,%d) : null maskObj\n", i, source->peak->x, source->peak->y);
+	    psfTry->mask->data.PS_TYPE_VECTOR_MASK_DATA[i] = PSFTRY_MASK_EXT_FAIL;
+	    continue;
+	}
+
+	source->modelEXT = pmSourceModelGuess (source, options->type, maskVal, markVal);
+	if (source->modelEXT == NULL) {
+	    psTrace ("psModules.objects", 4, "masking %d (%d,%d) : failed to generate model guess\n", i, source->peak->x, source->peak->y);
+	    psfTry->mask->data.PS_TYPE_VECTOR_MASK_DATA[i] = PSFTRY_MASK_EXT_FAIL;
+	    continue;
+	}
+
+	// do some actual work on this source
+	psThreadJob *job = psThreadJobAlloc ("PSF_TRY_FIT_EXT");
+	psArrayAdd(job->args, 1, source);
+	psArrayAdd(job->args, 1, psfTry);
+	psArrayAdd(job->args, 1, options);
+	
+	PS_ARRAY_ADD_SCALAR(job->args, i,        PS_TYPE_S32);
+
+	PS_ARRAY_ADD_SCALAR(job->args, maskVal,  PS_TYPE_IMAGE_MASK);
+	PS_ARRAY_ADD_SCALAR(job->args, markVal,  PS_TYPE_IMAGE_MASK);
+
+# if (1)
+	if (!psThreadJobAddPending(job)) {
+	    psError(PS_ERR_UNKNOWN, false, "Unable to create psf model.");
+	    return false;
+	}
+# else
+	if (!pmPSFtryFitEXT_Threaded(job)) {
+	    psError(PS_ERR_UNKNOWN, false, "Unable to create psf model.");
+	    return false;
+	}
+	psFree(job);
+# endif
+    }
+
+    // wait for the threads to finish and manage results
+    if (!psThreadPoolWait (false, true)) {
+	psError(PS_ERR_UNKNOWN, false, "failure to model psf");
+	return false;
+    }
+
+    // we have only supplied one type of job, so we can assume the types here
+    psThreadJob *job = NULL;
+    while ((job = psThreadJobGetDone()) != NULL) {
+	// we have no returned data from this operation
+	if (job->args->n < 1) fprintf (stderr, "error with job\n");
+	psFree(job);
+    }
+
+    psLogMsg ("psphot.psftry", PS_LOG_MINUTIA, "fit ext:   %f sec for %d of %ld sources\n", psTimerMark ("psf.fit"), Next, psfTry->sources->n);
+    psTrace ("psModules.object", 3, "keeping %d of %ld PSF candidates (EXT)\n", Next, psfTry->sources->n);
+
+    if (Next == 0) {
+        psError(PS_ERR_UNKNOWN, false, "No sources with good extended fits from which to determine PSF.");
+        return false;
+    }
+
+    return true;
+}
+
+bool pmPSFtryFitEXT_Threaded (psThreadJob *job) {
+
+    pmSource *source =      job->args->data[0];
+    pmPSFtry *psfTry =      job->args->data[1];
+    pmPSFOptions *options = job->args->data[2];
+
+    int i = PS_SCALAR_VALUE(job->args->data[3], S32);
+
+    psImageMaskType maskVal = PS_SCALAR_VALUE(job->args->data[4],PS_TYPE_IMAGE_MASK_DATA);
+    psImageMaskType markVal = PS_SCALAR_VALUE(job->args->data[5],PS_TYPE_IMAGE_MASK_DATA);
+
+    // set object mask to define valid pixels
+    // XXX 0.5 PIX: is the circle symmetric about the peak coordinate (given 0.5,0.5 center)?
+    psImageKeepCircle (source->maskObj, source->peak->x, source->peak->y, options->fitRadius, "OR", markVal);
+
+    // fit model as EXT, not PSF
+    bool status = pmSourceFitModel (source, source->modelEXT, options->fitOptions, maskVal);
+
+    // clear object mask to define valid pixels
+    psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(markVal)); // clear the circular mask
+
+    // exclude the poor fits
+    if (!status) {
+	psTrace ("psModules.objects", 4, "masking %d (%d,%d) : status is poor\n", i, source->peak->x, source->peak->y);
+	psfTry->mask->data.PS_TYPE_VECTOR_MASK_DATA[i] = PSFTRY_MASK_EXT_FAIL;
+	return true;
+    }
+    Next ++;
+    
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPSFtryFitPSF.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPSFtryFitPSF.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPSFtryFitPSF.c	(revision 42651)
@@ -0,0 +1,220 @@
+/** @file  pmPSFtryFitPSF.c
+ *  @brief Fit the PSF model to the candidate PSF stars (part of PSF model generation)
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.69 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-01-27 06:39:38 $
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmSourcePhotometry.h"
+#include "pmSourceVisual.h"
+
+static int Npsf = 0;
+
+// stage 3: Refit with fixed shape parameters.  This function uses the LMM fitting, but could
+// be re-written to use the simultaneous linear fitting (see psphotFitSourcesLinear.c)
+bool pmPSFtryFitPSF (pmPSFtry *psfTry, pmPSFOptions *options, psImageMaskType maskVal, psImageMaskType markVal) {
+
+    psTimerStart ("psf.fit");
+
+    // in this segment, we are fitting the fitted PSF model class (shape constrained)
+    options->fitOptions->mode = PM_SOURCE_FIT_PSF;
+
+    // maskVal is used to test for rejected pixels, and must include markVal
+    maskVal |= markVal;
+
+    Npsf = 0;
+    for (int i = 0; i < psfTry->sources->n; i++) {
+
+        pmSource *source = psfTry->sources->data[i];
+	psAssert (source->moments, "how can a psf source not have moments?");
+
+        // masked for: bad model fit, outlier in parameters
+        if (psfTry->mask->data.PS_TYPE_VECTOR_MASK_DATA[i] & PSFTRY_MASK_ALL) {
+            psTrace ("psModules.objects", 4, "dropping %d (%d,%d) : source is masked\n", i, source->peak->x, source->peak->y);
+            continue;
+        }
+
+	// set shape for this model based on PSF
+	psFree (source->modelPSF);
+	source->modelPSF = pmModelFromPSF (source->modelEXT, psfTry->psf);
+	if (source->modelPSF == NULL) {
+	    psfTry->mask->data.PS_TYPE_VECTOR_MASK_DATA[i] = PSFTRY_MASK_BAD_MODEL;
+	    psTrace ("psModules.objects", 4, "dropping %d (%d,%d) : bad PSF fit\n", i, source->peak->x, source->peak->y);
+	    continue;
+	}
+
+	// do some actual work on this source
+	psThreadJob *job = psThreadJobAlloc ("PSF_TRY_FIT_PSF");
+	psArrayAdd(job->args, 1, source);
+	psArrayAdd(job->args, 1, psfTry);
+	psArrayAdd(job->args, 1, options);
+	
+	PS_ARRAY_ADD_SCALAR(job->args, i,        PS_TYPE_S32);
+
+	PS_ARRAY_ADD_SCALAR(job->args, maskVal,  PS_TYPE_IMAGE_MASK);
+	PS_ARRAY_ADD_SCALAR(job->args, markVal,  PS_TYPE_IMAGE_MASK);
+
+# if (1)
+	if (!psThreadJobAddPending(job)) {
+	    psError(PS_ERR_UNKNOWN, false, "Unable to create psf model.");
+	    return false;
+	}
+# else
+	if (!pmPSFtryFitPSF_Threaded(job)) {
+	    psError(PS_ERR_UNKNOWN, false, "Unable to create psf model.");
+	    return false;
+	}
+	psFree(job);
+# endif
+    }
+
+    // wait for the threads to finish and manage results
+    if (!psThreadPoolWait (false, true)) {
+	psError(PS_ERR_UNKNOWN, false, "failure to model psf");
+	return false;
+    }
+
+    // we have only supplied one type of job, so we can assume the types here
+    psThreadJob *job = NULL;
+    while ((job = psThreadJobGetDone()) != NULL) {
+	// we have no returned data from this operation
+	if (job->args->n < 1) fprintf (stderr, "error with job\n");
+	psFree(job);
+    }
+    psfTry->psf->nPSFstars = Npsf;
+
+    pmSourceVisualShowModelFits (psfTry->psf, psfTry->sources, maskVal);
+
+    psLogMsg ("psphot.psftry", PS_LOG_MINUTIA, "fit psf:   %f sec for %d of %ld sources (%d x %d model)\n", psTimerMark ("psf.fit"), Npsf, psfTry->sources->n, psfTry->psf->trendNx, psfTry->psf->trendNy);
+    psTrace ("psModules.object", 3, "keeping %d of %ld PSF candidates (PSF)\n", Npsf, psfTry->sources->n);
+
+    if (Npsf == 0) {
+#if 0
+	// DEBUG code: save the PSF model fit data in detail
+
+	char hostname[256];
+	gethostname (hostname, 256);
+
+	int pid = getpid();
+
+	char filename[64];
+	snprintf (filename, 64, "psffit.%s.%d.%dx%d.dat", hostname, pid, psfTry->psf->trendNx, psfTry->psf->trendNy);
+	FILE *f = fopen (filename, "w");
+	psAssert (f, "failed open");
+
+	for (int i = 0; i < psfTry->sources->n; i++) {
+
+	    // skip masked sources
+	  // if (psfTry->mask->data.PS_TYPE_VECTOR_MASK_DATA[i] & PSFTRY_MASK_ALL) continue;
+
+	    pmSource *source = psfTry->sources->data[i];
+
+            if (!source->modelPSF) continue;
+
+	    float par7 = (source->modelPSF->params->n == 7) ? -100 : source->modelPSF->params->data.F32[PM_PAR_7];
+	    fprintf (f, "%6.1f %6.1f : %6.1f %6.1f : %8.3f %8.3f %8.3f : %f : %f %f %f : %f %d\n",
+		     source->peak->xf, source->peak->yf, 
+		     source->modelPSF->params->data.F32[PM_PAR_XPOS], source->modelPSF->params->data.F32[PM_PAR_YPOS], 
+		     source->psfMag, source->apMag, source->psfMagErr,
+		     source->modelPSF->params->data.F32[PM_PAR_I0], 
+		     source->modelPSF->params->data.F32[PM_PAR_SXX], source->modelPSF->params->data.F32[PM_PAR_SXY], 
+		     source->modelPSF->params->data.F32[PM_PAR_SYY], par7,
+		     psfTry->mask->data.PS_TYPE_VECTOR_MASK_DATA[i]);
+	}
+	fclose (f);
+#endif
+	psError(PS_ERR_UNKNOWN, false, "No sources with good PSF fits after model is built.");
+	return false;
+    }
+
+    return true;
+}
+
+bool pmPSFtryFitPSF_Threaded (psThreadJob *job) {
+
+    pmSource *source =      job->args->data[0];
+    pmPSFtry *psfTry =      job->args->data[1];
+    pmPSFOptions *options = job->args->data[2];
+
+    int i = PS_SCALAR_VALUE(job->args->data[3], S32);
+
+    psImageMaskType maskVal = PS_SCALAR_VALUE(job->args->data[4],PS_TYPE_IMAGE_MASK_DATA);
+    psImageMaskType markVal = PS_SCALAR_VALUE(job->args->data[5],PS_TYPE_IMAGE_MASK_DATA);
+
+    // PSF fit and aperture mags use different radii
+    source->modelPSF->fitRadius = options->fitRadius;
+    source->apRadius            = options->apRadius;
+
+    // set object mask to define valid pixels for PSF model fit
+    psImageKeepCircle (source->maskObj, source->peak->x, source->peak->y, options->fitRadius, "OR", markVal);
+
+    // fit the PSF model to the source
+    bool status = pmSourceFitModel (source, source->modelPSF, options->fitOptions, maskVal);
+
+    // skip poor fits
+    if (!status) {
+	psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(markVal)); // clear the circular mask
+	psfTry->mask->data.PS_TYPE_VECTOR_MASK_DATA[i] = PSFTRY_MASK_PSF_FAIL;
+	psTrace ("psModules.objects", 4, "dropping %d (%d,%d) : failed PSF fit\n", i, source->peak->x, source->peak->y);
+	return true;
+    }
+
+    // set object mask to define valid pixels for APERTURE magnitude
+    if (options->fitRadius != options->apRadius) {
+	psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(markVal)); // clear the circular mask
+	psImageKeepCircle (source->maskObj, source->peak->x, source->peak->y, options->apRadius, "OR", markVal);
+    }
+
+    // This function calculates the psf and aperture magnitudes
+    status = pmSourceMagnitudes (source, psfTry->psf, PM_SOURCE_PHOT_INTERP, maskVal, markVal, options->apRadius); // raw PSF mag, AP mag
+    psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(markVal)); // clear the circular mask
+
+    if (!status || isnan(source->apMag)) {
+	psfTry->mask->data.PS_TYPE_VECTOR_MASK_DATA[i] = PSFTRY_MASK_BAD_PHOT;
+	psTrace ("psModules.objects", 4, "dropping %d (%d,%d) : poor photometry\n", i, source->peak->x, source->peak->y);
+	return true;
+    }
+
+    psfTry->fitMag->data.F32[i] = source->psfMag;
+    psfTry->metric->data.F32[i] = source->apMag - source->psfMag;
+    psfTry->metricErr->data.F32[i] = source->psfMagErr;
+
+    psTrace ("psModules.object", 6, "keeping source %d (%d) of %ld\n", i, Npsf, psfTry->sources->n);
+    Npsf ++;
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPSFtryMakePSF.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPSFtryMakePSF.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPSFtryMakePSF.c	(revision 42651)
@@ -0,0 +1,378 @@
+/** @file  pmPSFtry.c
+ *  @brief generate a pmPSF from a collection of EXT measurments of likely PSF stars.
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.69 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-01-27 06:39:38 $
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmSourcePhotometry.h"
+#include "pmSourceVisual.h"
+
+bool pmPSF_DataDump (char *filename, psVector *x, psVector *y, psVector *e0, psVector *e1, psVector *e2, psVector *mask);
+
+/*****************************************************************************
+pmPSFFromPSFtry (psfTry): build a PSF model from a collection of source->modelEXT entries
+using the specified order in X,Y.  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 pmPSFtryMakePSF (bool *pGoodFit, pmPSFtry *psfTry)
+{
+    PS_ASSERT_PTR_NON_NULL(psfTry, false);
+    PS_ASSERT_PTR_NON_NULL(psfTry->sources, false);
+
+    pmPSF *psf = psfTry->psf;
+    psVector *srcMask = psfTry->mask;
+
+    // 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);
+
+    // construct the x,y terms
+    for (int i = 0; i < psfTry->sources->n; i++) {
+        if (srcMask->data.PS_TYPE_VECTOR_MASK_DATA[i]) continue;
+
+        pmSource *source = psfTry->sources->data[i];
+        assert (source->modelEXT); // all unmasked sources should have modelEXT
+
+        x->data.F32[i] = source->modelEXT->params->data.F32[PM_PAR_XPOS];
+        y->data.F32[i] = source->modelEXT->params->data.F32[PM_PAR_YPOS];
+    }
+
+    // fit the shape parameters (SXX, SYY, SXY) as a function of position
+    if (!pmPSFFitShapeParams (pGoodFit, psf, psfTry->sources, x, y, srcMask)) {
+        psFree(x);
+        psFree(y);
+        return false;
+    }
+    if (!*pGoodFit) {
+	psWarning ("poor fit to PSF shape parameters for trend order %d, %d, skipping\n", psf->trendNx, psf->trendNy);
+	psFree(x);
+	psFree(y);
+	return true;
+    }
+
+    // vector to store the other parameter values
+    psVector *z  = psVectorAlloc (psfTry->sources->n, PS_TYPE_F32);
+
+    // skip the unfitted parameters (X, Y, Io, Sky) and the shape parameters (SXX, SYY, SXY);
+    // fit 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++) {
+	    // skip any masked sources (failed to fit one of the model steps or get a magnitude)
+	    if (srcMask->data.PS_TYPE_VECTOR_MASK_DATA[j]) continue;
+
+            pmSource *source = psfTry->sources->data[j];
+	    assert (source->modelEXT); // all unmasked sources should have modelEXT
+
+            z->data.F32[j] = source->modelEXT->params->data.F32[i];
+        }
+
+	pmTrend2D *trend = psf->params->data[i];
+
+        // 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 (pGoodFit, trend, srcMask, 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;
+        }
+	if (!*pGoodFit) {
+	    // if we do not get a good fit (but do not actually hit an error), 
+	    // tell the calling program to try something else
+	    psWarning ("poor fit to PSF parameter %d for trend order %d, %d, skipping\n", i, psf->trendNx, psf->trendNy);
+            psFree(x);
+            psFree(y);
+            psFree(z);
+            return true;
+	}
+	if (trend->mode == PM_TREND_MAP) {
+	    psImageMapRepair (trend->map->map);
+	}
+    }
+
+    // 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);
+            if (!modelPSF) {
+                fprintf(f, "modelPSF is NULL\n");
+                break;
+            }
+            if (!source->modelEXT) {
+                fprintf(f, "source->modelEXT is NULL\n");
+                break;
+            }
+
+            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;
+}
+
+// fit the shape parameters using the supplied order (pmPSF->trendNx,trendNy)
+bool pmPSFFitShapeParams (bool *pGoodFit, 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.  the source mask (srcMask) is 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.
+
+    // XXX re-read the sextractor manual on handling 'infinitely thin' sources...
+
+    // storage vectors for the polarization terms & mags
+    psVector *e0   = psVectorAlloc (sources->n, PS_TYPE_F32);
+    psVector *e1   = psVectorAlloc (sources->n, PS_TYPE_F32);
+    psVector *e2   = psVectorAlloc (sources->n, PS_TYPE_F32);
+
+    // convert the measured source shape paramters to polarization terms
+    for (int i = 0; i < sources->n; i++) {
+        // skip any masked sources (failed to fit one of the model steps or get a magnitude)
+        if (srcMask->data.PS_TYPE_VECTOR_MASK_DATA[i]) continue;
+
+        pmSource *source = sources->data[i];
+        assert (source->modelEXT); // all unmasked sources should have modelEXT
+
+	bool useReff = source->modelEXT->class->useReff;
+        psEllipsePol pol = pmPSF_ModelToFit (source->modelEXT->params->data.F32, useReff);
+
+        e0->data.F32[i] = pol.e0;
+        e1->data.F32[i] = pol.e1;
+        e2->data.F32[i] = pol.e2;
+    }
+
+    // weed out extreme e0 outliers here: find the median and exclude points not in the
+    // range MEDIAN / 5 < e0 < 5 * MEDIAN
+    { 
+      psStats *e0stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN);
+      if (psVectorStats (e0stats, e0, NULL, srcMask, 0xff)) {
+	float e0med = e0stats->sampleMedian;
+    
+	for (int i = 0; i < sources->n; i++) {
+	  // skip any masked sources (failed to fit one of the model steps or get a magnitude)
+	  if (srcMask->data.PS_TYPE_VECTOR_MASK_DATA[i]) continue;
+
+	  if (e0->data.F32[i] < 0.2*e0med) {
+	    srcMask->data.PS_TYPE_VECTOR_MASK_DATA[i] = PSFTRY_MASK_OUTLIER;
+	  }
+	  if (e0->data.F32[i] > 5.0*e0med) {
+	    srcMask->data.PS_TYPE_VECTOR_MASK_DATA[i] = PSFTRY_MASK_OUTLIER;
+	  }
+	}
+      }
+      psFree (e0stats);
+    }
+
+    // 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
+    // NOTE : trend->stats (below) points to the same data as psfTrendStats; set the value to 1
+    // to limit the iteration within the loop
+    int nIter = psf->psfTrendStats->clipIter;
+    psf->psfTrendStats->clipIter = 1;
+    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?
+	// XXX we hardwire this to SAMPLE stats above (psphotChoosePSF.c), hardwire here instead?
+     
+	pmTrend2D *trend = NULL;
+
+	// XXX we are using the same stats structure on each pass: do we need to re-init it?
+	bool status = true;
+
+	trend = psf->params->data[PM_PAR_E0];
+	trend->stats->clipIter = 1; // in allocation, this value is set to the value of nIter, but we should use 1 here
+	status &= pmTrend2DFit (pGoodFit, trend, srcMask, 0xff, x, y, e0, NULL);
+	if (!*pGoodFit) {
+	    psFree (e0);
+	    psFree (e1);
+	    psFree (e2);
+	    return true;
+	}
+	if (trend->mode == PM_TREND_MAP) {
+	    psImageMapRepair (trend->map->map);
+	}
+
+# if (PS_TRACE_ON)
+	float mean, stdev;
+	psStatsOptions meanOption = psStatsMeanOption(psf->psfTrendStats->options);
+	psStatsOptions stdevOption = psStatsStdevOption(psf->psfTrendStats->options);
+	mean = psStatsGetValue (trend->stats, meanOption);
+	stdev = psStatsGetValue (trend->stats, stdevOption);
+	psTrace ("psModules.objects", 4, "clipped E0 : %f +/- %f keeping %ld of %ld\n", mean, stdev, psf->psfTrendStats->clippedNvalues, e0->n);
+# endif
+
+        if (psf->psfTrendMode == PM_TREND_MAP) psImageMapCleanup (trend->map);
+	pmSourceVisualPSFModelResid (trend, x, y, e0, srcMask);
+
+	trend = psf->params->data[PM_PAR_E1];
+	trend->stats->clipIter = 1; // in allocation, this value is set to the value of nIter, but we should use 1 here
+	status &= pmTrend2DFit (pGoodFit, trend, srcMask, 0xff, x, y, e1, NULL);
+	if (!*pGoodFit) {
+	    psFree (e0);
+	    psFree (e1);
+	    psFree (e2);
+	    return true;
+	}
+	if (trend->mode == PM_TREND_MAP) {
+	    psImageMapRepair (trend->map->map);
+	}
+
+# if (PS_TRACE_ON)
+	mean = psStatsGetValue (trend->stats, meanOption);
+	stdev = psStatsGetValue (trend->stats, stdevOption);
+	psTrace ("psModules.objects", 4, "clipped E1 : %f +/- %f keeping %ld of %ld\n", mean, stdev, psf->psfTrendStats->clippedNvalues, e1->n);
+# endif
+        if (psf->psfTrendMode == PM_TREND_MAP) psImageMapCleanup (trend->map);
+	pmSourceVisualPSFModelResid (trend, x, y, e1, srcMask);
+
+	trend = psf->params->data[PM_PAR_E2];
+	trend->stats->clipIter = 1; // in allocation, this value is set to the value of nIter, but we should use 1 here
+	status &= pmTrend2DFit (pGoodFit, trend, srcMask, 0xff, x, y, e2, NULL);
+	if (!*pGoodFit) {
+	    psFree (e0);
+	    psFree (e1);
+	    psFree (e2);
+	    return true;
+	}
+	if (trend->mode == PM_TREND_MAP) {
+	    psImageMapRepair (trend->map->map);
+	}
+
+# if (PS_TRACE_ON)
+	mean = psStatsGetValue (trend->stats, meanOption);
+	stdev = psStatsGetValue (trend->stats, stdevOption);
+	psTrace ("psModules.objects", 4, "clipped E2 : %f +/- %f keeping %ld of %ld\n", mean, stdev, psf->psfTrendStats->clippedNvalues, e2->n);
+# endif
+        if (psf->psfTrendMode == PM_TREND_MAP) psImageMapCleanup (trend->map);
+	pmSourceVisualPSFModelResid (trend, x, y, e2, srcMask);
+
+	if (!status) {
+	    psError (PS_ERR_UNKNOWN, true, "failed to fit PSF shape params");
+	    psFree (e0);
+	    psFree (e1);
+	    psFree (e2);
+	    return false;
+	}
+    }
+    psf->psfTrendStats->clipIter = nIter;
+
+    // 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.PS_TYPE_VECTOR_MASK_DATA[i]);
+        }
+        fclose (f);
+    }
+
+    psFree (e0);
+    psFree (e1);
+    psFree (e2);
+    return true;
+}
+
+bool pmPSF_DataDump (char *filename, psVector *x, psVector *y, psVector *e0, psVector *e1, psVector *e2, psVector *mask) {
+
+
+  FILE *f = fopen (filename, "w");
+  if (!f) return false;
+
+  for (int i = 0; i < x->n; i++) {
+
+    fprintf (f, "%6.1f %6.1f : %5.2f %5.2f %5.2f : %2d\n", 
+	     x->data.F32[i], 
+	     y->data.F32[i], 
+	     e0->data.F32[i], 
+	     e1->data.F32[i], 
+	     e2->data.F32[i], 
+	     mask->data.U8[i]);
+  }
+  fclose (f);
+  return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPSFtryMetric.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPSFtryMetric.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPSFtryMetric.c	(revision 42651)
@@ -0,0 +1,94 @@
+/** @file  pmPSFtry.c
+ *  @brief: measure the systematic error in the aperture-psf magnitude
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.69 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-01-27 06:39:38 $
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmSourcePhotometry.h"
+#include "pmSourceVisual.h"
+
+// The quality of the PSF model is determined by the systematic scatter in the fit - aperture
+// magnitude.  We use the moments->Sum value, calculated with a Gaussian window, as a proxy for
+// the aperture magnitude.  We measure the systmatic scatter with the function
+// psVectorSystematicError which solves for the value of SysErr that needs to be added in
+// quadrature to the errors of a set of residual measurements in order to yield a ChiSq of 1.0.
+bool pmPSFtryMetric (pmPSFtry *psfTry)
+{
+    PS_ASSERT_PTR_NON_NULL(psfTry, false);
+    PS_ASSERT_PTR_NON_NULL(psfTry->sources, false);
+
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN);
+
+    if (!psVectorStats (stats, psfTry->metric, NULL, psfTry->mask, PSFTRY_MASK_ALL)) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to measured clipped mean");
+	psFree (stats);
+        return false;
+    }
+
+    // generate a residual vector
+    psVector *resid = psVectorAllocEmpty (psfTry->metric->n, PS_TYPE_F32);
+    psVector *error = psVectorAllocEmpty (psfTry->metric->n, PS_TYPE_F32);
+    int n = 0;
+    for (int i = 0; i < psfTry->metric->n; i++) {
+	if (psfTry->mask && (psfTry->mask->data.PS_TYPE_VECTOR_MASK_DATA[i] && PSFTRY_MASK_ALL)) continue;
+	// if (psfTry->metricErr->data.F32[i] > 0.005) continue;
+	resid->data.F32[n] = psfTry->metric->data.F32[i] - stats->sampleMedian;
+	error->data.F32[n] = psfTry->metricErr->data.F32[i];
+	n++;
+    }
+    resid->n = error->n = n;
+
+    float psfSysErr = psVectorSystematicError (resid, error, 0.05);
+
+    psLogMsg ("pmPSFtryMetric", 4, "apresid: %f +/- %f (systematic error) from statistics of %ld psf stars (%d used)\n", 
+	      stats->sampleMedian, psfSysErr, psfTry->sources->n, n);
+
+    psfTry->psf->ApResid  = stats->sampleMedian;
+    psfTry->psf->dApResid = psfSysErr;
+
+    pmSourceVisualPlotPSFMetric (psfTry);
+    pmSourceVisualPlotPSFMetricSubpix (psfTry);
+
+    psFree (stats);
+    psFree (resid);
+    psFree (error);
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPSFtryModel.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPSFtryModel.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPSFtryModel.c	(revision 42651)
@@ -0,0 +1,293 @@
+/** @file  pmPSFtry.c
+ *
+ *  XXX: need description of file purpose
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.69 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-01-27 06:39:38 $
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmSourceVisual.h"
+
+// 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:
+
+// XXX some test code (delete eventually)
+// bool countMaskedSources(pmPSFtry *try, char *msg) {
+// 
+//     int N1 = 0;
+//     int N2 = 0;
+//     for (int i = 0; i < try->sources->n; i++) {
+//         pmSource *source = try->sources->data[i];
+//     	// fprintf (stderr, "%llx : %d\n", (long long int) source, (source->mode & PM_SOURCE_MODE_PSFSTAR));
+//     	if (source->mode & PM_SOURCE_MODE_PSFSTAR) {
+//     	    N1 ++;
+//         }
+//     	if (try->mask->data.PS_TYPE_VECTOR_MASK_DATA[i]) {
+//     	    N2 ++;
+//         }
+//     }
+//     fprintf (stderr, "%s : masked: %d or %d of %ld\n", msg, N1, N2, try->sources->n);
+//     return true;
+// }
+
+// generate a pmPSFtry with a copy of the test PSF sources
+pmPSFtry *pmPSFtryModel (const psArray *sources, const char *modelName, pmPSFOptions *options, psImageMaskType maskVal, psImageMaskType markVal)
+{
+    assert (options->fitOptions);
+
+    // validate the requested model name
+    options->type = pmModelClassGetType (modelName);
+    if (options->type == -1) {
+        psError (PS_ERR_UNKNOWN, true, "invalid model name %s", modelName);
+        return NULL;
+    }
+
+    pmPSFtry *psfTry = pmPSFtryAlloc (sources, options);
+    if (psfTry == NULL) {
+        psError (PS_ERR_UNKNOWN, false, "failed to allocate psf model");
+        return NULL;
+    }
+
+    // maskVal is used to test for rejected pixels, and must include markVal
+    maskVal |= markVal;
+
+    // stage 1:  fit an EXT model to all candidates PSF sources -- this is independent of the modeled 2D variations in the PSF
+    if (!pmPSFtryFitEXT(psfTry, options, maskVal, markVal)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to fit EXT models to sources for psf model");
+        psFree(psfTry);
+        return NULL;
+    }
+
+    // hard limit on minimum number of stars
+    if ((sources->n <  3)) {
+        psError (PS_ERR_UNKNOWN, true, "failed to determine PSF parameters");
+        return NULL;
+    }
+
+    // this is a bit tricky, because we have two cases (MAP vs POLY), and they have a different
+    // definition for 'order' (order_MAP = order_POLY + 1).  in addition, we have a
+    // user-specified MAX order, which we should respect, regardless of the mode
+
+    // set the max order (0 = constant) which the number of psf stars can support:
+    int MaxOrderForStars = 0;
+
+    // we require only 3 stars for n = 0, increase stars / cell for higher order
+    if (sources->n >=  16) MaxOrderForStars = 1; //  4 cells, 4 per cell
+    if (sources->n >=  54) MaxOrderForStars = 2; //  9 cells, 6 per cell
+    if (sources->n >= 128) MaxOrderForStars = 3; // 16 cells, 8 per cell
+    if (sources->n >= 300) MaxOrderForStars = 4; // 25 cells, 12 per cell
+    if (sources->n >  576) MaxOrderForStars = 5; // 36 cells, 16 per cell
+
+    // rule of thumb: require 3 stars per 'cell' (order+1)^2
+    // if (sources->n >= 12) MaxOrderForStars = 1; // 4 cells
+    // if (sources->n >= 27) MaxOrderForStars = 2; // 9 cells
+    // if (sources->n >= 48) MaxOrderForStars = 3; // 16 cells
+    // if (sources->n >= 75) MaxOrderForStars = 4; // 25 cells
+    // if (sources->n > 108) MaxOrderForStars = 5; // 36 cells
+
+    int orderMax = PS_MAX (options->psfTrendNx, options->psfTrendNy);
+    int orderMin = 0;
+    if (options->psfTrendMode == PM_TREND_MAP) {
+        MaxOrderForStars ++;
+        orderMin ++;
+    }
+    orderMax = PS_MIN (orderMax, MaxOrderForStars);
+
+    // save the raw source mask (generated by pmPSFtryFitEXT)
+    psVector *srcMask = psVectorCopy (NULL, psfTry->mask, PS_TYPE_VECTOR_MASK);
+
+    // we will save the PSF with the best fit (min systematic error)
+    pmPSF *minPSF = NULL;
+    psVector *minMask = NULL;
+
+    // as we loop over orders, we need to refer to the initial selection, but we modify the
+    // option values to match the current guess: save the max values here:
+    int Nx = (options->psfTrendMode == PM_TREND_MAP) ? options->psfTrendNx : options->psfTrendNx + 1;
+    int Ny = (options->psfTrendMode == PM_TREND_MAP) ? options->psfTrendNy : options->psfTrendNy + 1;
+    for (int i = orderMin; i <= orderMax; i++) {
+
+        if (Nx > Ny) {
+            options->psfTrendNx = i;
+            options->psfTrendNy = PS_MAX (orderMin, (int)(i * (Ny / (float) Nx) + 0.5));
+        } else {
+            options->psfTrendNy = i;
+            options->psfTrendNx = PS_MAX (orderMin, (int)(i * (Nx / (float) Ny) + 0.5));
+        }
+
+	fprintf (stderr, "fitting %d x %d model for PSF\n", options->psfTrendNx, options->psfTrendNy);
+
+        // free existing data, if any
+        psFree(psfTry->psf);
+        psFree(psfTry->mask);
+
+        // allocate a mask and a psf model using the current Nx,Ny order values;
+        psfTry->psf = pmPSFAlloc (options);
+        psfTry->mask = psVectorCopy (NULL, srcMask, PS_TYPE_VECTOR_MASK);
+
+        // stage 2: construct a psf (pmPSF) from this collection of model fits, including the 2D variation
+	bool goodFit = false;
+        if (!pmPSFtryMakePSF (&goodFit, psfTry)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to construct a psf model from collection of sources");
+            psFree(psfTry);
+            return NULL;
+        }
+	if (!goodFit) {
+	    psWarning ("poor psf fit for order %d, skipping\n", i);
+	    continue;
+	}
+
+        // stage 3: refit with fixed shape parameters, measure pmPSFtry->metric
+        if (!pmPSFtryFitPSF (psfTry, options, maskVal, markVal)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to construct a psf model from collection of sources");
+            psFree(psfTry);
+            return NULL;
+        }
+
+        // stage 4: measure systematic error in pmPSFtry->metric
+        if (!pmPSFtryMetric (psfTry)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to measure systematic error of metric");
+            psFree(psfTry);
+            return NULL;
+        }
+
+        if (!minPSF) {
+            minPSF = psMemIncrRefCounter(psfTry->psf);
+            minMask = psMemIncrRefCounter(psfTry->mask);
+        }
+
+        if (psfTry->psf->dApResid < minPSF->dApResid) {
+            psFree (minPSF);
+            psFree (minMask);
+            minPSF = psMemIncrRefCounter(psfTry->psf);
+            minMask = psMemIncrRefCounter(psfTry->mask);
+        }
+    }
+    psFree (srcMask);
+
+    if (!minPSF) {
+	psError(PS_ERR_UNKNOWN, false, "failed to construct a valid psf model from the sources");
+	psFree(psfTry);
+	return NULL;
+    }
+
+    // keep the ones matching the min systematic error:
+    psFree (psfTry->psf);
+    psFree (psfTry->mask);
+    psfTry->psf = minPSF;
+    psfTry->mask = minMask;
+
+    // XXXXX this is probably not used any more.  Are the chisq of the fits so bad? can we
+    // fix them by softening the errors on the brightest pixels?
+
+    // 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_VECTOR_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.PS_TYPE_VECTOR_MASK_DATA[i] = 0xff;
+            psTrace ("psModules.objects", 4, "dropping %d (%d,%d) : no PSF model\n", i, source->peak->x, source->peak->y);
+        } 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.PS_TYPE_VECTOR_MASK_DATA[i] = 0;
+        }
+    }
+
+    // use 3hi/3lo sigma clipping on the chisq fit
+    psStats *stats = options->stats;
+
+    // linear clipped fit of chisq trend vs flux
+    if (options->chiFluxTrend) {
+
+	if (0) {
+	    FILE *f = fopen ("test.psf.dat", "w");
+	    int fd = fileno (f);
+	    p_psVectorPrint (fd, flux, "flux");
+	    p_psVectorPrint (fd, chisq, "chisq");
+	    p_psVectorPrint (fd, mask, "mask");
+	    fclose (f);
+	}
+
+        bool result = psVectorClipFitPolynomial1D(psfTry->psf->ChiTrend, options->stats,
+                                                  mask, 0xff, chisq, NULL, flux);
+        psStatsOptions meanStat = psStatsMeanOption(options->stats->options); // Statistic for mean
+        psStatsOptions stdevStat = psStatsStdevOption(options->stats->options); // Statistic for stdev
+
+        psLogMsg ("pmPSFtry", 4, "chisq vs flux fit: %f +/- %f\n",
+                  psStatsGetValue(stats, meanStat), psStatsGetValue(stats, stdevStat));
+
+        if (!result) {
+            psError(PS_ERR_UNKNOWN, false, "Failed to fit psf->ChiTrend");
+            psFree(psfTry);
+            psFree(flux);
+            psFree(mask);
+            psFree(chisq);
+            return NULL;
+        }
+    }
+
+    psFree(flux);
+    psFree(mask);
+    psFree(chisq);
+
+    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));
+    }
+
+    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);
+}
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPeaks.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPeaks.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPeaks.c	(revision 42651)
@@ -0,0 +1,636 @@
+/** @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.26 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-16 22:30:14 $
+ *
+ *  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 "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.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", 10, "---- 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
+    // XXX need to calculate dx, dy correctly
+    // 0.5 PIX: peaks are calculated using the pixel index and converted here to pixel coords
+    if ((fabs(min.x) < 1.5) && (fabs(min.y) < 1.5)) {
+        peak->xf = min.x + ix + image->col0 + 0.5;
+        peak->yf = min.y + iy + image->row0 + 0.5;
+
+	// These errors are fractional errors, and should be scaled by the 
+	// error on the peak pixel (see, eg, psphotFindPeaks)
+	peak->dx = min.xErr;
+	peak->dy = min.yErr;
+	
+	// xf,yf must land on image with 0 pixel border
+	peak->xf = PS_MAX (PS_MIN (peak->xf, image->numCols - 1), image->col0);
+	peak->yf = PS_MAX (PS_MIN (peak->yf, image->numRows - 1), image->row0);
+    } else {
+        peak->xf = ix + 0.5;
+        peak->yf = iy + 0.5; 
+	peak->dx = NAN;
+	peak->dy = NAN;
+    }
+
+    psArrayAdd(peaks, 100, peak);
+    psFree (peak);
+
+    psTrace("psModules.objects", 10, "---- 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", 10, "---- %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", 10, "---- %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.
+*****************************************************************************/
+# if (0)
+static bool isItInThisRegion(const psRegion valid,
+                             psS32 x,
+                             psS32 y)
+{
+    psTrace("psModules.objects", 10, "---- %s() begin ----\n", __func__);
+    if ((x >= valid.x0) &&
+            (x <= valid.x1) &&
+            (y >= valid.y0) &&
+            (y <= valid.y1)) {
+        psTrace("psModules.objects", 10, "---- %s(true) end ----\n", __func__);
+        return(true);
+    }
+    psTrace("psModules.objects", 10, "---- %s(false) end ----\n", __func__);
+    return(false);
+}
+# endif
+
+/******************************************************************************
+pmPeakAlloc(): Allocate the pmPeak data structure and set appropriate members.
+*****************************************************************************/
+static void peakFree(pmPeak *tmp)
+{
+    if (!tmp) return;
+    psFree (tmp->saddlePoints);
+    return;
+}
+
+pmPeak *pmPeakAlloc(psS32 x,
+                    psS32 y,
+                    psF32 value,
+                    pmPeakType type)
+{
+    psTrace("psModules.objects", 10, "---- %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->xf = x;
+    tmp->yf = y;
+    tmp->dx = NAN;
+    tmp->dy = NAN;
+    tmp->detValue      	 = value;
+    tmp->rawFlux       	 = value; // set this by default: it is up to the user to supply a better value
+    tmp->rawFluxStdev  	 = NAN;
+    tmp->smoothFlux    	 = value; // set this by default: it is up to the user to supply a better value
+    tmp->smoothFluxStdev = NAN;
+    tmp->assigned = false;
+    tmp->type = type;
+    tmp->footprint = NULL;
+    tmp->saddlePoints = NULL;
+
+    psMemSetDeallocator(tmp, (psFreeFunc) peakFree);
+
+    psTrace("psModules.objects", 10, "---- %s() end ----\n", __func__);
+    return(tmp);
+}
+
+// copy to an already allocated peak
+bool pmPeakCopy(pmPeak *out, pmPeak *in)
+{
+    out->x  		 = in->x;
+    out->y  		 = in->y;
+    out->xf 		 = in->xf;
+    out->yf 		 = in->yf;
+    out->dx 		 = in->dx;
+    out->dy 		 = in->dy;
+    out->detValue      	 = in->detValue;
+    out->rawFlux       	 = in->rawFlux;
+    out->rawFluxStdev  	 = in->rawFluxStdev;
+    out->smoothFlux    	 = in->smoothFlux;
+    out->smoothFluxStdev = in->smoothFluxStdev;
+    out->assigned        = in->assigned;
+    out->type      	 = in->type;
+    out->footprint       = in->footprint;
+
+    return true;
+}
+
+bool psMemCheckPeak(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) peakFree);
+}
+
+
+// psSort comparison functions for peaks
+// XXX: Add error-checking for NULL args
+int pmPeaksSortByDetValueAscend (const void **a, const void **b)
+{
+    pmPeak *A = *(pmPeak **)a;
+    pmPeak *B = *(pmPeak **)b;
+
+    psF32 diff;
+
+    diff = A->detValue - B->detValue;
+    if (diff < FLT_EPSILON) {
+        return (-1);
+    } else if (diff > FLT_EPSILON) {
+        return (+1);
+    }
+    return (0);
+}
+int pmPeaksSortByDetValueDescend (const void **a, const void **b)
+{
+    pmPeak *A = *(pmPeak **)a;
+    pmPeak *B = *(pmPeak **)b;
+
+    psF32 diff;
+
+    diff = A->detValue - B->detValue;
+    if (diff < FLT_EPSILON) {
+        return (+1);
+    } else if (diff > FLT_EPSILON) {
+        return (-1);
+    }
+    return (0);
+}
+int pmPeaksSortByRawFluxAscend (const void **a, const void **b)
+{
+    pmPeak *A = *(pmPeak **)a;
+    pmPeak *B = *(pmPeak **)b;
+
+    psF32 diff;
+
+    diff = A->rawFlux - B->rawFlux;
+    if (diff < FLT_EPSILON) {
+        return (-1);
+    } else if (diff > FLT_EPSILON) {
+        return (+1);
+    }
+    return (0);
+}
+int pmPeaksSortByRawFluxDescend (const void **a, const void **b)
+{
+    pmPeak *A = *(pmPeak **)a;
+    pmPeak *B = *(pmPeak **)b;
+
+    psF32 diff;
+
+    diff = A->rawFlux - B->rawFlux;
+    if (diff < FLT_EPSILON) {
+        return (+1);
+    } else if (diff > FLT_EPSILON) {
+        return (-1);
+    }
+    return (0);
+}
+int pmPeaksSortBySmoothFluxAscend (const void **a, const void **b)
+{
+    pmPeak *A = *(pmPeak **)a;
+    pmPeak *B = *(pmPeak **)b;
+
+    psF32 diff;
+
+    diff = A->smoothFlux - B->smoothFlux;
+    if (diff < FLT_EPSILON) {
+        return (-1);
+    } else if (diff > FLT_EPSILON) {
+        return (+1);
+    }
+    return (0);
+}
+int pmPeaksSortBySmoothFluxDescend (const void **a, const void **b)
+{
+    pmPeak *A = *(pmPeak **)a;
+    pmPeak *B = *(pmPeak **)b;
+
+    psF32 diff;
+
+    diff = A->smoothFlux - B->smoothFlux;
+    if (diff < FLT_EPSILON) {
+        return (+1);
+    } else if (diff > FLT_EPSILON) {
+        return (-1);
+    }
+    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->flux;
+//     psF32 fB = B->flux;
+//     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", 10, "---- %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", 10, "---- %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", 10, "---- %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", 10, "---- %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", 10, "---- %s(NULL) end ----\n", __func__);
+        return(NULL);
+    }
+    psVector *tmpRow = NULL;
+    psU32 col = 0;
+    psU32 row = 0;
+    psArray *list = psArrayAllocEmpty(100);
+
+    // 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 {
+            psLogMsg ("psModules.objects", 5, "peak specified outside valid column range.");
+        }
+    }
+    psFree (tmpRow);
+    psFree (row1);
+
+    //
+    // Exit if this image has a single row.
+    //
+    if (image->numRows == 1) {
+        psTrace("psModules.objects", 10, "---- %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 {
+		psLogMsg ("psModules.objects", 5, "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 {
+            psLogMsg ("psModules.objects", 5, "peak specified outside valid column range.");
+        }
+    }
+    psFree (tmpRow);
+    psFree (row1);
+    psTrace("psModules.objects", 10, "---- %s() end ----\n", __func__);
+    return(list);
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPeaks.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPeaks.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPeaks.h	(revision 42651)
@@ -0,0 +1,160 @@
+/* @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.15 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-12-09 21:16:09 $
+ * 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_SUSPECT_SATURATION,         ///< Peak is probably saturated
+    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:
+ *
+ *  There are 3 values which define the amplitude of the peak and which may be used to sort the
+ *  peaks: 
+ *  * detValue - the peak in the detection image (nominally, the S/N)
+ *  * rawFlux - the peak in the unsmoothed image
+ *  * smoothFlux - the peak in the smoothed image
+ * 
+ *  For a given image, peaks do necesarily not have the same sequence for these three values.
+ *  Depending on the analysis, it may make sense to sort by one or the other of these values
+ */
+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 detValue;                     ///< peak flux in detection image (= S/N)
+    float rawFlux;                      ///< peak flux in unsmoothed signal image
+    float rawFluxStdev;                 ///< peak stdev in unsmoothed signal image
+    float smoothFlux;                   ///< peak flux in smoothed signal image
+    float smoothFluxStdev;              ///< peak stdev in smoothed signal image
+    bool assigned;                      ///< is peak assigned to a source?
+    pmPeakType type;                    ///< Description of peak.
+    pmFootprint *footprint;		///< reference to containing footprint (just a view, not a memcopy)
+    psArray *saddlePoints;		///< set of saddle points between this peak and near neighbors
+}
+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 psMemCheckPeak(psPtr ptr);
+
+bool pmPeakCopy(pmPeak *out, pmPeak *in);
+
+/** 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
+);
+
+
+/** 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 pmPeaksSortByDetValueAscend (const void **a, const void **b);
+int pmPeaksSortByDetValueDescend (const void **a, const void **b);
+int pmPeaksSortByRawFluxAscend (const void **a, const void **b);
+int pmPeaksSortByRawFluxDescend (const void **a, const void **b);
+int pmPeaksSortBySmoothFluxAscend (const void **a, const void **b);
+int pmPeaksSortBySmoothFluxDescend (const void **a, const void **b);
+
+/// @}
+# endif /* PM_PEAKS_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPetrosian.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPetrosian.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPetrosian.c	(revision 42651)
@@ -0,0 +1,107 @@
+/* @file  pmPetrosian.c
+ * low-level petrosian functions
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: $
+ * @date $Date: $
+ * Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "pmPetrosian.h"
+
+static void pmPetrosianFree(pmPetrosian *petrosian)
+{
+    if (!petrosian) {
+        return;
+    }
+    psFree(petrosian->radii);
+    psFree(petrosian->fluxes);
+    psFree(petrosian->theta);
+    psFree(petrosian->isophotalRadii);
+
+    psFree(petrosian->radiusElliptical);
+    psFree(petrosian->fluxElliptical);
+
+    psFree(petrosian->binSB);
+    psFree(petrosian->binSBstdev);
+    psFree(petrosian->radialBins);
+    psFree(petrosian->area);
+}
+
+pmPetrosian *pmPetrosianAlloc()
+{
+    pmPetrosian *petrosian = (pmPetrosian *)psAlloc(sizeof(pmPetrosian));
+    psMemSetDeallocator(petrosian, (psFreeFunc) pmPetrosianFree);
+
+    petrosian->radii = NULL;
+    petrosian->fluxes = NULL;
+    petrosian->theta = NULL;
+    petrosian->isophotalRadii = NULL;
+
+    petrosian->radiusElliptical = NULL;
+    petrosian->fluxElliptical = NULL;
+
+    petrosian->radialBins = NULL;
+    petrosian->area = NULL;
+    petrosian->binSB = NULL;
+    petrosian->binSBstdev = NULL;
+
+    petrosian->petrosianRadius = NAN;
+    petrosian->petrosianFlux = NAN;
+
+    // petrosian->axes = {0.0, 0.0, 0.0};
+
+    return petrosian;
+}
+
+bool pmPetrosianFreeVectors(pmPetrosian *petrosian) {
+
+    psFree(petrosian->radii);
+    psFree(petrosian->fluxes);
+    psFree(petrosian->theta);
+    psFree(petrosian->isophotalRadii);
+
+    psFree(petrosian->radiusElliptical);
+    psFree(petrosian->fluxElliptical);
+
+    psFree(petrosian->binSB);
+    psFree(petrosian->binSBstdev);
+    psFree(petrosian->radialBins);
+    psFree(petrosian->area);
+
+    petrosian->radii = NULL;
+    petrosian->fluxes = NULL;
+    petrosian->theta = NULL;
+    petrosian->isophotalRadii = NULL;
+
+    petrosian->radiusElliptical = NULL;
+    petrosian->fluxElliptical = NULL;
+
+    petrosian->radialBins = NULL;
+    petrosian->area = NULL;
+    petrosian->binSB = NULL;
+    petrosian->binSBstdev = NULL;
+    
+    return true;
+}
+
+# define COMPARE_INDEX(A,B) (index->data.F32[A] < index->data.F32[B])
+# define SWAP_INDEX(TYPE,A,B) { \
+  float tmp; \
+  if (A != B) { \
+    tmp = index->data.F32[A]; \
+    index->data.F32[A] = index->data.F32[B]; \
+    index->data.F32[B] = tmp; \
+    tmp = extra->data.F32[A]; \
+    extra->data.F32[A] = extra->data.F32[B]; \
+    extra->data.F32[B] = tmp; \
+  } \
+}
+
+bool pmPetrosianSortPair (psVector *index, psVector *extra) {
+
+    // sort the vector set by the radius
+    PSSORT (index->n, COMPARE_INDEX, SWAP_INDEX, NONE);
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPetrosian.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPetrosian.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPetrosian.h	(revision 42651)
@@ -0,0 +1,44 @@
+/* @file  pmPetrosian.h
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: $
+ * @date $Date: $
+ * Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_PETROSIAN_H
+#define PM_PETROSIAN_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+typedef struct {
+    psArray  *radii;			// radii for raw radial profiles at evenly-spaced angles
+    psArray  *fluxes;			// fluxes measured at above radii
+    psVector *theta;			// angles corresponding to above radial profiles
+    psVector *isophotalRadii;		// isophotal radius for the above angles
+
+    psVector *radiusElliptical;		// normalized radial coordinates for all relevant pixels
+    psVector *fluxElliptical;		// flux for the above radial coordinates
+
+    psVector *binSB;			// mean surface brightness within radial bins
+    psVector *binSBstdev;		// scatter of mean surface brightness within radial bins
+    psVector *radialBins;		// radii corresponding to above binnedBlux
+    psVector *area;			// differential area of the non-overlapping radial bins
+
+    psEllipseAxes axes;			// shape of elliptical contour
+
+    float petrosianRadius;
+    float petrosianFlux;
+
+} pmPetrosian;
+
+pmPetrosian *pmPetrosianAlloc();
+
+bool pmPetrosianFreeVectors(pmPetrosian *petrosian);
+bool pmPetrosianSortPair (psVector *index, psVector *extra);
+
+/// @}
+
+# endif /* PM_PETROSIAN_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPhotObj.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPhotObj.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPhotObj.c	(revision 42651)
@@ -0,0 +1,121 @@
+/** @file  pmPhotObj.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-09-15 09:49:01 $
+ * Copyright 2009 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"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+
+#include "pmPhotObj.h"
+
+
+static void pmPhotObjFree (pmPhotObj *tmp)
+{
+    if (!tmp) return;
+    psFree(tmp->sources);
+}
+
+
+pmPhotObj *pmPhotObjAlloc() 
+{
+    static int id = 1;
+    pmPhotObj *obj = (pmPhotObj *) psAlloc(sizeof(pmPhotObj));
+    psMemSetDeallocator(obj, (psFreeFunc) pmPhotObjFree);
+
+    *(int *)&obj->id = id; // cast away the const to set the id.
+    id++;
+
+    obj->sources = NULL;
+    return obj;
+}
+
+bool pmPhotObjAddSource(pmPhotObj *object, pmSource *source) {
+
+    psAssert (source, "programming error: NULL source");
+    if (!source->peak) {
+	psError(PS_ERR_UNKNOWN, true, "source missing peak");
+	return false; 
+    }
+    if (!isfinite(source->peak->xf)) {
+	psError(PS_ERR_UNKNOWN, true, "NAN peak coordinate");
+	return false; 
+    }
+    if (!isfinite(source->peak->yf)) {
+	psError(PS_ERR_UNKNOWN, true, "NAN peak coordinate");
+	return false; 
+    }
+
+    // XXX we should probably use the fitted position if it exists
+    if (!object->sources) {
+	object->sources = psArrayAllocEmpty(1);
+	object->x  = source->peak->xf;
+	object->y  = source->peak->yf;
+	object->flux = source->peak->rawFlux;
+    } else {
+	object->flux = PS_MAX(object->flux, source->peak->rawFlux);
+    }
+    psArrayAdd (object->sources, 1, source);
+    return true;
+}
+
+// sort by flux (descending)
+int pmPhotObjSortByFlux (const void **a, const void **b)
+{
+    pmPhotObj *objA = *(pmPhotObj **)a;
+    pmPhotObj *objB = *(pmPhotObj **)b;
+
+    psF32 fA = objA->flux;
+    psF32 fB = objB->flux;
+
+    psF32 diff = fA - fB;
+    if (diff > FLT_EPSILON) return (-1);
+    if (diff < FLT_EPSILON) return (+1);
+    return (0);
+}
+
+// sort by X (ascending)
+int pmPhotObjSortByX (const void **a, const void **b)
+{
+    pmPhotObj *objA = *(pmPhotObj **)a;
+    pmPhotObj *objB = *(pmPhotObj **)b;
+
+    psF32 fA = objA->x;
+    psF32 fB = objB->x;
+
+    psF32 diff = fA - fB;
+    if (diff > FLT_EPSILON) return (+1);
+    if (diff < FLT_EPSILON) return (-1);
+    return (0);
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmPhotObj.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmPhotObj.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmPhotObj.h	(revision 42651)
@@ -0,0 +1,48 @@
+/* @file  pmPhotObj.h
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: $
+ * @date $Date: 2009-02-16 22:30:50 $
+ * Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# ifndef PM_PHOT_OBJ_H
+# define PM_PHOT_OBJ_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+
+/** pmPhotObj data structure
+ *
+ *  A Photometry Object is a connected set of source measurements with a common
+ *  connection.  Each source that comprises the object represents the detection of some
+ *  astronomical object on a single image.  The object represents the related collection
+ *  of measurements.  The fits are coupled in some way.  For example, they may all have
+ *  the same position, but independent fluxes.  Or, they may have a common set of
+ *  positions and shape parameters.  Or the position in each image may be related by a
+ *  function.
+ *
+ *  XXX is thre any info that is neaded for each object beyond that carried by the sources
+ *  (besides ID)?
+ */
+typedef struct {
+    int id;                            ///< ID for output (generated on write OR set on read)
+    psArray *sources;
+    int flags;
+    float x;
+    float y;
+    float flux;				// max of peak->rawFlux for all matched sources
+} pmPhotObj;
+
+pmPhotObj *pmPhotObjAlloc(void);
+
+bool pmPhotObjAddSource(pmPhotObj *object, pmSource *source);
+
+int pmPhotObjSortByFlux (const void **a, const void **b);
+int pmPhotObjSortByX (const void **a, const void **b);
+
+/// @}
+# endif /* PM_PHOT_OBJ_H */
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmResiduals.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmResiduals.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmResiduals.c	(revision 42651)
@@ -0,0 +1,61 @@
+/** @file pmResiduals.c
+ *
+ * Functions to manipulate the residual tables (data - model).
+ *
+ * @author EAM, IfA
+ * @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-06 02:31:25 $
+ * 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->variance);
+    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->variance = psImageAlloc (nX, nY, PS_TYPE_F32);
+    resid->mask   = psImageAlloc (nX, nY, PM_TYPE_RESID_MASK);
+
+    // NOTE : the residual mask is internal only : 1 byte is sufficient
+
+    resid->xBin = xBin;
+    resid->yBin = yBin;
+    resid->xCenter = 0.5*(nX - 1);
+    resid->yCenter = 0.5*(nY - 1);
+    return resid;
+}
+
+bool psMemCheckResiduals(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmResidualsFree);
+}
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmResiduals.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmResiduals.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmResiduals.h	(revision 42651)
@@ -0,0 +1,41 @@
+/** @file pmResiduals.h
+ *
+ * Functions to manipulate the residual tables (data - model).
+ *
+ * @author EAM, IfA
+ * @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-06 02:31:25 $
+ * 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 *variance;
+    psImage *mask;
+    int xBin;
+    int yBin;
+    int xCenter;
+    int yCenter;
+} pmResiduals;
+
+pmResiduals *pmResidualsAlloc (int xSize, int ySize, int xBin, int yBin);
+bool psMemCheckResiduals(psPtr ptr);
+
+// macros to abstract the resid mask type : these values must be consistent
+#define PM_TYPE_RESID_MASK PS_TYPE_U8        /**< the psElemType to use for mask image */
+#define PM_TYPE_RESID_MASK_DATA U8           /**< the data member to use for mask image */
+#define PM_TYPE_RESID_MASK_NAME "psU8"       /**< the data type for mask as a string */
+typedef psU8 pmResidMaskType;               ///< the C datatype for a mask image
+#define PM_NOT_RESID_MASK(A)(UINT8_MAX-(A))
+
+/// @}
+# endif
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSource.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSource.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSource.c	(revision 42651)
@@ -0,0 +1,1390 @@
+/** @file  pmSource.c
+ *
+ *  Functions to define and manipulate sources on images
+ *
+ *  @author EAM, IfA
+ *  @author GLG, MHPCC (initial code base)
+ *
+ *  @version $Revision: 1.70 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-16 22:29:59 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAMaskWeight.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourcePhotometry.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+
+static void sourceFree(pmSource *tmp)
+{
+    if (!tmp)
+        return;
+
+    psTrace("psModules.objects", 10, "---- begin ----\n");
+    psFree(tmp->peak);
+    psFree(tmp->pixels);
+    psFree(tmp->variance);
+    psFree(tmp->modelVar);
+    psFree(tmp->maskObj);
+    psFree(tmp->maskView);
+    psFree(tmp->modelFlux);
+    psFree(tmp->psfImage);
+    psFree(tmp->moments);
+    psFree(tmp->modelPSF);
+    psFree(tmp->modelEXT);
+    psFree(tmp->modelFits);
+    psFree(tmp->extFitPars);
+    psFree(tmp->blends);
+    psFree(tmp->satstar);
+    psFree(tmp->extpars);
+    psFree(tmp->diffStats);
+    psFree(tmp->galaxyFits);
+    psFree(tmp->radialAper);
+    psFree(tmp->lensingOBJ);
+    psFree(tmp->lensingPSF);
+    psTrace("psModules.objects", 10, "---- end ----\n");
+}
+
+// free only the pixel data associated with this source
+void pmSourceFreePixels(pmSource *source)
+{
+
+    if (!source)
+        return;
+
+    psFree (source->pixels);
+    psFree (source->variance);
+    psFree (source->modelVar);
+    psFree (source->maskObj);
+    psFree (source->maskView);
+    psFree (source->modelFlux);
+    psFree (source->psfImage);
+
+    source->pixels = NULL;
+    source->variance = NULL;
+    source->modelVar = NULL;
+    source->maskObj = NULL;
+    source->maskView = NULL;
+    source->modelFlux = NULL;
+    source->psfImage = NULL;
+    return;
+}
+
+bool psMemCheckSource(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) sourceFree);
+}
+
+/******************************************************************************
+pmSourceAlloc(): Allocate the pmSource structure and initialize its members
+to NULL.
+*****************************************************************************/
+
+pmSource *pmSourceAlloc()
+{
+    psTrace("psModules.objects", 10, "---- begin ----\n");
+    static int id = 1;
+    pmSource *source = (pmSource *) psAlloc(sizeof(pmSource));
+    P_PM_SOURCE_SET_ID(source, id++);
+
+    source->seq = -1;
+    source->peak = NULL;
+    source->pixels = NULL;
+    source->variance = NULL;
+    source->modelVar = NULL;
+    source->maskObj = NULL;
+    source->maskView = NULL;
+    source->modelFlux = NULL;
+    source->psfImage = NULL;
+    source->moments = NULL;
+    source->modelPSF = NULL;
+    source->modelEXT = NULL;
+    source->modelFits = NULL;
+    source->extFitPars = NULL;
+
+    source->type = PM_SOURCE_TYPE_UNKNOWN;
+    source->mode = PM_SOURCE_MODE_DEFAULT;
+    source->mode2 = PM_SOURCE_MODE_DEFAULT;
+    source->tmpFlags = 0;
+
+    // default values are NAN
+    source->psfMag     	     = NAN;
+    source->psfMagErr 	     = NAN;
+    source->psfFlux    	     = NAN;
+    source->psfFluxErr 	     = NAN;
+    source->extMag 	     = NAN;    
+    source->apMag  	     = NAN;
+    source->apMagRaw  	     = NAN;
+    source->apRadius  	     = NAN;
+    source->apNpixels  	     = 0;
+    source->apFlux    	     = NAN;
+    source->apFluxErr 	     = NAN; 
+
+    source->windowRadius     = NAN;
+    source->skyRadius  	     = NAN;
+    source->skyFlux    	     = NAN;
+    source->skySlope   	     = NAN;
+
+    source->pixWeightNotBad  = NAN;
+    source->pixWeightNotPoor = NAN;
+
+    source->psfChisq         = NAN;
+    source->crNsigma         = NAN;
+    source->extNsigma        = NAN;
+    source->sky    	     = NAN;
+    source->skyErr 	     = NAN;    
+    source->extSN  	     = NAN;    
+
+    source->region = psRegionSet(NAN, NAN, NAN, NAN);
+    source->blends = NULL;
+    source->satstar = NULL;
+    source->extpars = NULL;
+    source->diffStats = NULL;
+    source->galaxyFits = NULL;
+    source->lensingOBJ = NULL;
+    source->lensingPSF = NULL;
+    source->radialAper = NULL;
+    source->parent = NULL;
+    source->tmpPtr = NULL;
+    source->chipNum = -1;
+    source->chipX = -1000;
+    source->chipY = -1000;
+    source->imageID = -1;
+    source->nFrames = 0;
+
+    psMemSetDeallocator(source, (psFreeFunc) sourceFree);
+
+    psTrace("psModules.objects", 10, "---- end ----\n");
+    return(source);
+}
+
+/******************************************************************************
+pmSourceCopy(): copy the pmSource, yielding a copy of the source that can be used without
+affecting the original.  This Copy can be used to allow multiple fit attempts on the same
+object.  The pixels, variance, and mask arrays all point to the same original subarrays.  The
+peak and moments point at the original values.  The models, blends, and XXX are NOT copied
+*****************************************************************************/
+pmSource *pmSourceCopy(pmSource *in)
+{
+    if (in == NULL) {
+        return(NULL);
+    }
+    pmSource *source = pmSourceAlloc ();
+
+    // peak has the same values as the original
+    if (in->peak != NULL) {
+        source->peak = pmPeakAlloc (in->peak->x, in->peak->y, in->peak->detValue, in->peak->type);
+	pmPeakCopy(source->peak, in->peak);
+    }
+
+    // copy the values in the moments structure
+    if (in->moments != NULL) {
+        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.  Modifying these pixels (ie, subtracting the model) will affect the pixels seen
+    // by all copies.
+    source->pixels   = in->pixels   ? psImageCopyView(NULL, in->pixels)   : NULL;
+    source->variance = in->variance ? psImageCopyView(NULL, in->variance) : NULL;
+    source->modelVar = NULL;
+    source->maskView = in->maskView ? psImageCopyView(NULL, in->maskView) : NULL;
+
+    // the maskObj is a unique mask array; create a new mask image
+    source->maskObj = in->maskObj   ? psImageCopy (NULL, in->maskObj, PS_TYPE_IMAGE_MASK) : NULL;
+
+    // NOTE : because of the const id element, we cannot just assign *source = *in
+
+    source->imageID          = in->imageID;
+
+    source->type     	     = in->type;
+    source->mode     	     = in->mode;
+    source->mode2    	     = in->mode2;
+    source->tmpFlags 	     = in->tmpFlags;
+
+    source->psfMag     	     = in->psfMag;
+    source->psfMagErr 	     = in->psfMagErr;
+    source->psfFlux    	     = in->psfFlux;
+    source->psfFluxErr 	     = in->psfFluxErr;
+    source->extMag 	     = in->extMag;
+    source->apMag  	     = in->apMag;
+    source->apMagRaw  	     = in->apMagRaw;
+    source->apRadius  	     = in->apRadius;
+    source->apNpixels  	     = in->apNpixels;
+    source->apFlux    	     = in->apFlux;
+    source->apFluxErr 	     = in->apFluxErr;
+
+    source->windowRadius     = in->windowRadius;
+    source->skyRadius  	     = in->skyRadius;  	
+    source->skyFlux    	     = in->skyFlux;    	
+    source->skySlope   	     = in->skySlope;   	
+
+    source->pixWeightNotBad  = in->pixWeightNotBad;
+    source->pixWeightNotPoor = in->pixWeightNotPoor;
+
+    source->psfChisq         = in->psfChisq;
+    source->crNsigma         = in->crNsigma;
+    source->extNsigma        = in->extNsigma;
+    source->sky    	     = in->sky;
+    source->skyErr 	     = in->skyErr;
+
+    source->region           = in->region;
+
+    // XXX I am not copying the pointers to things like the blends, satstar profile, galaxyFits, etc
+
+    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)
+{
+    PS_ASSERT_PTR_NON_NULL(mySource, false);
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(readout->image, false);
+    PS_ASSERT_INT_POSITIVE(Radius, false);
+
+    psRegion sourceRegion;
+
+    // Grab a subimage of the original image of size (2 * outerRadius).
+    sourceRegion = psRegionForSquare (x, y, Radius);
+    sourceRegion = psRegionForImage (readout->image, sourceRegion);
+
+    // these images are subset images of the equivalent parents
+    mySource->pixels = psImageSubset(readout->image, sourceRegion);
+    if (readout->variance) {
+        mySource->variance = psImageSubset(readout->variance, sourceRegion);
+    }
+    if (readout->mask) {
+        mySource->maskView = psImageSubset(readout->mask,  sourceRegion);
+        // the object mask is a copy, and used to define the source pixels
+        mySource->maskObj = psImageCopy(NULL, mySource->maskView, PS_TYPE_IMAGE_MASK);
+    }
+    mySource->region   = sourceRegion;
+    mySource->windowRadius = Radius;
+
+    return true;
+}
+
+bool pmSourceRedefinePixels(pmSource *mySource,
+                            const pmReadout *readout,
+                            psF32 x,
+                            psF32 y,
+                            psF32 Radius)
+{
+    PS_ASSERT_PTR_NON_NULL(mySource, false);
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(readout->image, false);
+    PS_ASSERT_INT_POSITIVE(Radius, false);
+
+    bool extend;
+    psRegion newRegion;
+
+    // check to see if new region is completely contained within old region
+    newRegion = psRegionForSquare (x, y, Radius);
+    newRegion = psRegionForImage (readout->image, newRegion);
+
+    // re-define if required by region or absence of pixels
+    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->variance == NULL);
+    extend |= (mySource->maskObj == NULL);
+    extend |= (mySource->maskView == NULL);
+
+    // if ((fabs(x - 2020) < 5) && (fabs(y - 366) < 5)) {
+    // 	if (extend) {
+    // 	    fprintf (stderr, "extend T, %f, %f : %f, %f vs %f, %f : %f, %f\n", 
+    // 		     newRegion.x0, newRegion.y0, newRegion.x1, newRegion.y1, 
+    // 		     mySource->region.x0, mySource->region.y0, mySource->region.x1, mySource->region.y1);
+    // 	} else {
+    // 	    fprintf (stderr, "extend F, %f, %f : %f, %f vs %f, %f : %f, %f\n", 
+    // 		     newRegion.x0, newRegion.y0, newRegion.x1, newRegion.y1, 
+    // 		     mySource->region.x0, mySource->region.y0, mySource->region.x1, mySource->region.y1);
+    // 	}
+    // }
+
+    if (extend) {
+        // re-create the subimage
+        psFree (mySource->pixels);
+        psFree (mySource->variance);
+        psFree (mySource->maskView);
+
+        mySource->pixels   = psImageSubset(readout->image,  newRegion);
+        mySource->variance = psImageSubset(readout->variance, 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_IMAGE_MASK);
+
+        // drop the old modelFlux pixels and force the user to re-create
+        psFree (mySource->modelFlux);
+        mySource->modelFlux = NULL;
+
+        // drop the old psfImage pixels and force the user to re-create
+        psFree (mySource->psfImage);
+        mySource->psfImage = NULL;
+    }
+    mySource->windowRadius = Radius;
+    return extend;
+}
+
+bool pmSourceRedefinePixelsByRegion(pmSource *mySource,
+				    const pmReadout *readout,
+				    psRegion newRegion)
+{
+    PS_ASSERT_PTR_NON_NULL(mySource, false);
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(readout->image, false);
+
+    // re-create the subimage
+    psFree (mySource->pixels);
+    psFree (mySource->variance);
+    psFree (mySource->maskView);
+	
+    mySource->pixels   = psImageSubset(readout->image,    newRegion);
+    mySource->variance = psImageSubset(readout->variance, 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_IMAGE_MASK);
+
+    // drop the old modelFlux pixels and force the user to re-create
+    psFree (mySource->modelFlux);
+    mySource->modelFlux = NULL;
+
+    // drop the old psfImage pixels and force the user to re-create
+    psFree (mySource->psfImage);
+    mySource->psfImage = NULL;
+
+    return true;
+}
+
+/******************************************************************************
+    pmSourcePSFClump(source, recipe): Find the likely PSF clump in the
+    sigma-x, sigma-y plane. return 0,0 clump in case of error.
+*****************************************************************************/
+
+pmPSFClump pmSourcePSFClump(psImage **savedImage, psRegion *region, psArray *sources, float PSF_SN_LIM, float PSF_CLUMP_GRID_SCALE, psF32 SX_MAX, psF32 SY_MAX, psF32 SX_MIN, psF32 SY_MIN, psF32 AR_MAX)
+{
+    psTrace("psModules.objects", 10, "---- begin ----\n");
+
+    psArray *peaks  = NULL;
+    pmPSFClump errorClump = {-1.0, -1.0, 0.0, 0.0, 0, 0.0};
+    pmPSFClump emptyClump = {+0.0, +0.0, 0.0, 0.0, 0, 0.0};
+    pmPSFClump psfClump;
+
+    PS_ASSERT_PTR_NON_NULL(sources, errorClump);
+
+    // find the sigmaX, sigmaY clump
+    {
+        psF32 AR_MIN = 1.0 / AR_MAX;
+
+        // construct a sigma-plane image
+        int numCols = 1 + SX_MAX / PSF_CLUMP_GRID_SCALE; // Size of sigma-plane image
+	int numRows = 1 + SY_MAX / PSF_CLUMP_GRID_SCALE; // Size of sigma-plane image
+        psTrace("psModules.objects", 10, "sigma-plane dimensions: %dx%d\n", numCols, numRows);
+        psImage *splane = psImageAlloc(numCols, numRows, PS_TYPE_F32); // sigma-plane image
+        psImageInit(splane, 0);
+
+        // place the sources in the sigma-plane image (ignore 0,0 values?)
+        int nValid = 0;                 // Number of valid sources
+        for (int i = 0; i < sources->n; i++) {
+            pmSource *source = sources->data[i]; // Source of interest
+            if (!source || !source->moments) {
+                continue;
+            }
+
+	    if (region) {
+		int x = source->peak->x, y = source->peak->y; // Coordinates of peak
+		if (x < region->x0 || x > region->x1 || y < region->y0 || y > region->y1) {
+		    continue;
+		}
+	    }
+
+            if (source->mode & PM_SOURCE_MODE_BLEND) {
+                continue;
+            }
+
+            if (!source->moments->nPixels) continue;
+
+            if (source->moments->SN < PSF_SN_LIM) {
+                psTrace("psModules.objects", 10, "Rejecting source from clump because of low S/N (%f)\n",
+                        source->moments->SN);
+                continue;
+            }
+
+            float Mxx = source->moments->Mxx, Myy = source->moments->Myy; // Second moments
+            float ar = Mxx / Myy;       // Radius
+
+            if (!isfinite(Mxx) || !isfinite(Myy)) {
+                psTrace("psModules.objects", 10,
+                        "Rejecting source from clump because of non-finite moments (%f,%f)\n",
+                        Mxx, Myy);
+                continue;
+            }
+
+            // Sx,Sy are limited at 0.  a peak at 0,0 is artificial
+            if (fabs(Mxx) < SX_MIN || fabs(Myy < SY_MIN)) {
+                psTrace("psModules.objects", 10,
+                        "Rejecting source from clump because of low moments (%f,%f)\n",
+                        Mxx, Myy);
+                continue;
+            }
+            if (Mxx > SX_MAX || Myy > SY_MAX) {
+                psTrace("psModules.objects", 10,
+                        "Rejecting source from clump because of high moments (%f,%f)\n",
+                        Mxx, Myy);
+                continue;
+            }
+            if (ar > AR_MAX || ar < AR_MIN) {
+                psTrace("psModules.objects", 10, "Rejecting source from clump because of Ar (%f)\n", ar);
+                continue;
+            }
+
+            // for the moment, force splane dimensions to be 10x10 image pix
+            int binX = Mxx / PSF_CLUMP_GRID_SCALE, binY = Myy /  PSF_CLUMP_GRID_SCALE; // Position on splane
+            psAssert(binX >= 0 && binX < numCols && binY >= 0 && binY < numRows, "We checked it already");
+
+            splane->data.F32[binY][binX] += 1.0;
+            nValid++;
+        }
+
+        // find the peak in this image
+        psStats *stats = psStatsAlloc (PS_STAT_MAX | PS_STAT_SAMPLE_STDEV);
+        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);
+
+	psfClump.nSigma = stats->sampleStdev;
+	psfClump.nTotal = nValid;
+
+	if (savedImage) {
+	    *savedImage = psMemIncrRefCounter(splane);
+        }
+        psFree (splane);
+        psFree (stats);
+
+        // if we failed to find a valid peak, return the empty clump (failure signal)
+	if (peaks == NULL) {
+            psError(PS_ERR_UNKNOWN, false, "failure in peak analysis for PSF clump.\n");
+	    psFree (peaks);
+            return emptyClump;
+	}
+
+        if (peaks->n == 0)
+        {
+            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");
+            }
+	    psFree (peaks);
+            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 (note that we only have detValue, not rawFlux, etc
+        psArraySort (peaks, pmPeaksSortByDetValueDescend);
+        clump = peaks->data[0];
+        psTrace ("psModules.objects", 2, "clump is at %d, %d (%f)\n", clump->x, clump->y, clump->detValue);
+
+	// XXX store the mean sigma?
+	float meanSigma = psfClump.nSigma;
+	psfClump.nStars = clump->detValue;
+	psfClump.nSigma = clump->detValue / meanSigma;
+
+        // define section window for clump
+        minSx = clump->x * PSF_CLUMP_GRID_SCALE - 2.0*PSF_CLUMP_GRID_SCALE;
+        maxSx = clump->x * PSF_CLUMP_GRID_SCALE + 2.0*PSF_CLUMP_GRID_SCALE;
+        minSy = clump->y * PSF_CLUMP_GRID_SCALE - 2.0*PSF_CLUMP_GRID_SCALE;
+        maxSy = clump->y * PSF_CLUMP_GRID_SCALE + 2.0*PSF_CLUMP_GRID_SCALE;
+
+        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_SN_LIM)
+                continue;
+
+	    if (region) {
+		if (tmpSrc->peak->x < region->x0) continue;
+		if (tmpSrc->peak->x > region->x1) continue;
+		if (tmpSrc->peak->y < region->y0) continue;
+		if (tmpSrc->peak->y > region->y1) continue;
+	    }
+
+            if (tmpSrc->moments->Mxx < minSx)
+                continue;
+            if (tmpSrc->moments->Mxx > maxSx)
+                continue;
+            if (tmpSrc->moments->Myy < minSy)
+                continue;
+            if (tmpSrc->moments->Myy > maxSy)
+                continue;
+            tmpSx->data.F32[tmpSx->n] = tmpSrc->moments->Mxx;
+            tmpSy->data.F32[tmpSy->n] = tmpSrc->moments->Myy;
+            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);
+
+        if (!psVectorStats (stats, tmpSx, NULL, NULL, 0)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to measure Sx stats");
+            return (emptyClump);
+        }
+        psfClump.X  = stats->clippedMean;
+        psfClump.dX = hypot(stats->clippedStdev, PSF_CLUMP_GRID_SCALE);
+
+        if (!psVectorStats (stats, tmpSy, NULL, NULL, 0)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to measure Sy stats");
+            return (emptyClump);
+        }
+        psfClump.Y  = stats->clippedMean;
+        psfClump.dY = hypot(stats->clippedStdev, PSF_CLUMP_GRID_SCALE);
+
+        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", 10, "---- end ----\n");
+    return (psfClump);
+}
+
+/******************************************************************************
+    pmSourceRoughClass(source, recipe): make a guess at the source
+    classification.
+    XXX: How can this function ever return FALSE?
+*****************************************************************************/
+
+bool pmSourceRoughClass(psRegion *region, psArray *sources, float PSF_SN_LIM, float PSF_CLUMP_NSIGMA, pmPSFClump clump, psImageMaskType maskSat)
+{
+    psTrace("psModules.objects", 10, "---- begin ----");
+
+    PS_ASSERT_PTR_NON_NULL(sources, 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);
+
+    pmSourceMode noMoments = PM_SOURCE_MODE_MOMENTS_FAILURE | PM_SOURCE_MODE_SKYVAR_FAILURE | PM_SOURCE_MODE_SKY_FAILURE | PM_SOURCE_MODE_BELOW_MOMENTS_SN;
+
+    // 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];
+
+        // psf clumps are found for image subregions:
+        // skip sources not in this region
+        if (source->peak->x <  region->x0) continue;
+        if (source->peak->x >= region->x1) continue;
+        if (source->peak->y <  region->y0) continue;
+        if (source->peak->y >= region->y1) continue;
+
+# define DEBUG 0
+# define TEST_X 2792
+# define TEST_Y 1741
+# if (DEBUG) 
+	if ((fabs(source->peak->x - TEST_X) < 5) && (fabs(source->peak->y - TEST_Y) < 5)) {
+	  fprintf (stderr, "test peak\n");
+	}
+# endif	
+
+        // should be set by pmSourceAlloc
+        psAssert (source->type == PM_SOURCE_TYPE_UNKNOWN, "source type was not init-ed?");
+
+        // we are basically classifying by moments; use the default if not found
+        if (!source->moments) {
+            source->type = PM_SOURCE_TYPE_STAR;
+            psAssert (source->mode & noMoments, "why is this source missing moments?");
+            Nstar++;
+            continue;
+        }
+
+        psF32 sigX = source->moments->Mxx;
+        psF32 sigY = source->moments->Myy;
+
+        // XXX EAM : can we use the value of SATURATE if mask is NULL?
+	// XXX a 5x5 box centered on the peak is a rather small region to check for
+        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;
+            // why do we recalculate moments here?
+	    // we already attempt to do this in psphotSourceStats
+            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_SATURATED;
+            Nsat ++;
+            continue;
+        }
+
+        // The following determinations require the use of moments
+        if (!(source->mode & noMoments)) {
+            // likely defect (bright, but too small to be stellar)
+	    // XXX eliminate the classification?
+            if ((source->moments->SN > 10) && (sigX < 0.05 || sigY < 0.05)) {
+                source->type = PM_SOURCE_TYPE_DEFECT;
+                source->mode |= PM_SOURCE_MODE_DEFECT;
+                Ncr ++;
+                continue;
+            }
+
+	    // check for insignificant sources or excessively low-surface brightness
+	    float coreSN = source->moments->KronCore / source->moments->KronCoreErr;
+	    float coreKR = source->moments->KronCore / source->moments->KronFlux;
+
+	    // XXX these values need to be in the recipe...
+	    if (false && isfinite(coreSN) && (coreSN < 5.0)) {
+                source->type = PM_SOURCE_TYPE_DEFECT;
+                source->mode |= PM_SOURCE_MODE_DEFECT;
+                Ncr ++;
+                continue;
+            }
+	    if (false && isfinite(coreKR) && (coreKR < 0.1)) {
+                source->type = PM_SOURCE_TYPE_DEFECT;
+                source->mode |= PM_SOURCE_MODE_DEFECT;
+                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;
+                Next ++;
+                continue;
+            }
+
+            // the rest are probable stellar objects
+	    // the vectors below are accumulated to give user feedback on the S/N ranges
+            starsn_moments->data.F32[starsn_moments->n] = source->moments->SN;
+            starsn_moments->n ++;
+            starsn_peaks->data.F32[starsn_peaks->n] = sqrt(source->peak->detValue);
+            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->tmpFlags |= PM_SOURCE_TMPF_CANDIDATE_PSFSTAR;
+                Npsf ++;
+                continue;
+            }
+        }
+
+        // random type of star
+        source->type = PM_SOURCE_TYPE_STAR;
+    }
+
+    psLogMsg("psModules.objects", PS_LOG_INFO, "Rough classifications: %d %d %d %d %d %d",
+             Nstar, Npsf, Next, Nsatstar, Nsat, Ncr);
+
+    if (starsn_moments->n) {
+        psStats *stats = NULL;
+        stats = psStatsAlloc (PS_STAT_MIN | PS_STAT_MAX);
+
+        if (!psVectorStats (stats, starsn_moments, NULL, NULL, 0)) {
+	  psErrorClear(); // XXX it is probably excessive for psVectorStats to raise an error here (no valid values)
+	  fprintf (stderr, "failed to measure SN / moments stats (no valid moments)\n");
+	  // psError(PS_ERR_UNKNOWN, false, "failed to measure SN / moments stats");
+	  // psFree (stats);
+	  // psFree (starsn_peaks);
+	  // return false;
+        }
+        psLogMsg ("pmObjects", 3, "SN range (moments): %f - %f\n", stats->min, stats->max);
+        psFree (stats);
+    }
+    psFree (starsn_moments);
+
+    if (starsn_peaks->n) {
+        psStats *stats = NULL;
+        stats = psStatsAlloc (PS_STAT_MIN | PS_STAT_MAX);
+        if (!psVectorStats (stats, starsn_peaks, NULL, NULL, 0)) {
+	  psErrorClear(); // XXX it is probably excessive for psVectorStats to raise an error here (no valid values)
+	  fprintf (stderr, "failed to measure SN / peak stats (no valid peaks)\n");
+	  // psError(PS_ERR_UNKNOWN, false, "failed to measure SN / moments stats");
+	  // psFree (stats);
+	  // psFree (starsn_peaks);
+	  // return false;
+        }
+        psLogMsg ("psModules.objects", 3, "SN range (peaks)  : %f - %f (%ld)\n", stats->min, stats->max, starsn_peaks->n);
+        psFree (stats);
+    }
+    psFree (starsn_peaks);
+
+    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", 10, "---- end ----\n");
+    return true;
+}
+
+// construct a realization of the source model
+bool pmSourceCacheModel (pmSource *source, psImageMaskType maskVal) {
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    // 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 PSF model at the location of this source
+bool pmSourceCachePSF (pmSource *source, psImageMaskType maskVal) {
+    PS_ASSERT_PTR_NON_NULL(source, false);
+
+    // 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->psfImage = psImageCopy (source->psfImage, source->pixels, PS_TYPE_F32);
+    psImageInit (source->psfImage, 0.0);
+
+    // in some places (psphotEnsemble), we need a normalized version
+    // in others, we just want the model.  which is more commonly used?
+    // psfImage always has unity normalization (I0 = 1.0)
+    pmModelAdd (source->psfImage, 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, psImageMaskType maskVal, int dx, int dy)
+{
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+    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
+
+    bool addNoise = mode & PM_MODEL_OP_NOISE;
+
+    bool addModelVar = mode & PM_MODEL_OP_MODELVAR;
+    if (addModelVar) psAssert (source->modelVar, "programming error");
+
+    // require the use of pmModelAddWithOffset if we are adding noise (because the model size and norm are rescaled)
+    if (!addNoise && 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;
+        }
+
+        psImageMaskType **mask = NULL;
+        if (source->maskObj) {
+            mask = source->maskObj->data.PS_TYPE_IMAGE_MASK_DATA;
+        }
+
+	psF32 **target = NULL;
+	target = addModelVar ? source->modelVar->data.F32 : source->pixels->data.F32;
+
+        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;
+                }
+            }
+        }
+	// do not change the flag here if we are adding/subtracting from modelVar
+	if (!addModelVar) {
+	  if (add) {
+	    source->tmpFlags &= ~PM_SOURCE_TMPF_SUBTRACTED;
+	  } else {
+	    source->tmpFlags |= PM_SOURCE_TMPF_SUBTRACTED;
+	  }
+	}
+        return true;
+    }
+
+    psImage *target = source->pixels;
+    if (addNoise) {
+        target = source->variance;
+    }
+    if (addModelVar) {
+      target = source->modelVar;
+    }
+
+    if (add) {
+	status = pmModelAddWithOffset (target, source->maskObj, model, PM_MODEL_OP_FULL, maskVal, dx, dy);
+	if (!addNoise && !addModelVar) source->tmpFlags &= ~PM_SOURCE_TMPF_SUBTRACTED;
+    } else {
+	status = pmModelSubWithOffset (target, source->maskObj, model, PM_MODEL_OP_FULL, maskVal, dx, dy);
+	if (!addNoise && !addModelVar) source->tmpFlags |= PM_SOURCE_TMPF_SUBTRACTED;
+    }
+    if (!status) {
+	// XXX maybe raise an error or warning?
+    }
+
+    return true;
+}
+
+// should we call pmSourceCacheModel if it does not exist?
+bool pmSourceNoiseOp (pmSource *source, pmModelOpMode mode, float FACTOR, float SIZE, bool add, psImageMaskType maskVal, int dx, int dy)
+{
+    assert (mode & PM_MODEL_OP_NOISE);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+
+    if (add) {
+        psTrace ("psphot", 3, "adding noise to object at %f,%f\n", source->peak->xf, source->peak->yf);
+    } else {
+        psTrace ("psphot", 3, "removing noise from object at %f,%f\n", source->peak->xf, source->peak->yf);
+    }
+
+    pmSourceNoiseOpModel (source->modelPSF, source, mode, FACTOR, SIZE, add, maskVal, dx, dy);
+
+    if (source->modelEXT) {
+	pmSourceNoiseOpModel (source->modelEXT, source, mode, FACTOR, SIZE, add, maskVal, dx, dy);
+    }
+
+    return true;
+}
+
+bool pmSourceNoiseOpModel (pmModel *model, pmSource *source, pmModelOpMode mode, float FACTOR, float SIZE, bool add, psImageMaskType maskVal, int dx, int dy) 
+{
+    bool status;
+    psEllipseShape oldshape;
+    psEllipseAxes axes;
+
+    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];
+
+    // XXX can this be done more intelligently?
+    if (oldI0 == 0.0) return false;
+    if (!isfinite(oldI0)) return false;
+
+    bool useReff = model->class->useReff;
+    pmModelParamsToAxes (&axes, PAR[PM_PAR_SXX], PAR[PM_PAR_SXY], PAR[PM_PAR_SYY], useReff);
+
+    // increase size and height of source
+    axes.major *= SIZE;
+    axes.minor *= SIZE;
+
+    pmModelAxesToParams (&PAR[PM_PAR_SXX], &PAR[PM_PAR_SXY], &PAR[PM_PAR_SYY], axes, useReff);
+    PAR[PM_PAR_I0]  = FACTOR*oldI0;
+
+    psImage *target = source->variance;
+
+    if (add) {
+	status = pmModelAddWithOffset (target, source->maskObj, model, mode, maskVal, dx, dy);
+    } else {
+	status = pmModelSubWithOffset (target, source->maskObj, model, mode, maskVal, dx, dy);
+    }
+    if (!status) {
+	// XXX raise an error or warning?
+    }
+
+    // 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;
+
+    return true;
+}
+
+bool pmSourceSmoothOp (pmSource *source, pmModelOpMode mode, psImage *target, float sigma, bool add, psImageMaskType maskVal, int dx, int dy)
+{
+//    assert (mode & PM_MODEL_OP_NOISE);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+    PS_ASSERT_PTR_NON_NULL(target, false);
+
+    if (add) {
+        psTrace ("psphot", 3, "adding smoothed object at %f,%f\n", source->peak->xf, source->peak->yf);
+    } else {
+        psTrace ("psphot", 3, "removing smooted object at %f,%f\n", source->peak->xf, source->peak->yf);
+    }
+
+    pmModel *model = pmSourceGetModel(NULL, source);
+    if (!model) {
+        return false;
+    }
+    pmSourceSmoothOpModel (model, source, mode, target, sigma, add, maskVal, dx, dy);
+
+    return true;
+}
+
+bool pmSourceSmoothOpModel (pmModel *model, pmSource *source, pmModelOpMode mode, psImage *target, float sigma, bool add, psImageMaskType maskVal, int dx, int dy) 
+{
+    bool status;
+    psEllipseShape oldshape;
+    psEllipseShape newshape;
+    psEllipseAxes axes;
+
+    if (add) {
+	psTrace ("psphot", 4, "adding smoothed object at %f,%f\n", model->params->data.F32[PM_PAR_XPOS], model->params->data.F32[PM_PAR_YPOS]);
+    } else {
+	psTrace ("psphot", 4, "removing smoothed 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;
+
+    // Isn't this hanging around somewhere?
+    float oldFlux = NAN;
+    if (!pmSourcePhotometryModel (NULL, &oldFlux, model)) return false;
+
+    // save original values
+    float oldI0  = PAR[PM_PAR_I0];
+    float oldsxx = PAR[PM_PAR_SXX];
+    float oldsyy = PAR[PM_PAR_SYY];
+    float oldsxy = PAR[PM_PAR_SXY];
+
+    // Since we are going to scale the flux correctly we need to get our
+    // factors of sqrt(2) right
+    oldshape.sx  = oldsxx / M_SQRT2;
+    oldshape.sy  = oldsyy / M_SQRT2;
+    oldshape.sxy = oldsxy;
+
+    // XXX can this be done more intelligently?
+    if (oldI0 == 0.0) return false;
+    if (!isfinite(oldI0)) return false;
+
+    // increase size and height of source
+    axes = psEllipseShapeToAxes (oldshape, 20.0);
+    axes.major = sqrt(PS_SQR(axes.major) + PS_SQR(sigma));
+    axes.minor = sqrt(PS_SQR(axes.minor) + PS_SQR(sigma));
+    newshape = psEllipseAxesToShape (axes);
+    PAR[PM_PAR_SXX] = newshape.sx * M_SQRT2;
+    PAR[PM_PAR_SYY] = newshape.sy * M_SQRT2;
+    PAR[PM_PAR_SXY] = newshape.sxy;
+
+    PAR[PM_PAR_I0]  = 1.0;
+
+    float newFlux;
+    if (!pmSourcePhotometryModel (NULL, &newFlux, model)) return false;
+
+    PAR[PM_PAR_I0]  = oldFlux / newFlux;
+
+    if (add) {
+	status = pmModelAddWithOffset (target, source->maskObj, model, mode, maskVal, dx, dy);
+    } else {
+	status = pmModelSubWithOffset (target, source->maskObj, model, mode, maskVal, dx, dy);
+    }
+    if (!status) {
+	// XXX raise an error or warning?
+    }
+
+    // restore original values
+    PAR[PM_PAR_I0]  = oldI0;
+    PAR[PM_PAR_SXX] = oldsxx;
+    PAR[PM_PAR_SYY] = oldsyy;
+    PAR[PM_PAR_SXY] = oldsxy;
+
+    return true;
+}
+
+bool pmSourceAdd (pmSource *source, pmModelOpMode mode, psImageMaskType maskVal) {
+    return pmSourceOp (source, mode, true, maskVal, 0, 0);
+}
+
+bool pmSourceSub (pmSource *source, pmModelOpMode mode, psImageMaskType maskVal) {
+    return pmSourceOp (source, mode, false, maskVal, 0, 0);
+}
+
+bool pmSourceAddWithOffset (pmSource *source, pmModelOpMode mode, psImageMaskType maskVal, int dx, int dy) {
+    return pmSourceOp (source, mode, true, maskVal, dx, dy);
+}
+
+bool pmSourceSubWithOffset (pmSource *source, pmModelOpMode mode, psImageMaskType 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)
+{
+    PS_ASSERT_PTR_NON_NULL(source, NULL);
+
+    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;
+
+        // the 'best' extended model is saved in source->modelEXT (may be a pointer to one of
+        // the elements of source->modelFits)
+      case PM_SOURCE_TYPE_EXTENDED:
+        model = source->modelEXT;
+        if (!model && source->modelPSF) {
+            // XXX raise an error or warning here?
+            if (isPSF) {
+                *isPSF = true;
+            }
+            return source->modelPSF;
+        }
+        return (model);
+        break;
+
+      default:
+        return NULL;
+    }
+    return NULL;
+}
+
+// this function decides if the source position should be based on the peak or the moments.
+// this is only used if we know we should not use a model fit position (eg, no model, or no
+// model yet)
+bool pmSourcePositionUseMoments(pmSource *source) {
+
+    if (!source->moments) return false;		 // can't if there are no moments
+    if (!source->moments->nPixels) return false; // can't if the moments were not measured
+    if (source->mode & PM_SOURCE_MODE_MOMENTS_FAILURE) return false; // can't if the moments failed...
+
+    if (source->mode & PM_SOURCE_MODE_SATSTAR) return true; // moments are best for SATSTARs
+
+    float dX = source->moments->Mx - source->peak->xf;
+    float dY = source->moments->My - source->peak->yf;
+    float dR = hypot(dX, dY);
+    
+    // only use the moments position if the moment-peak offset is small or the star is saturated
+    if (dR > 1.5) return false;
+
+    return true;
+}
+
+// sort by SN (descending)
+int pmSourceSortByFlux (const void **a, const void **b)
+{
+    pmSource *A = *(pmSource **)a;
+    pmSource *B = *(pmSource **)b;
+
+    psF32 fA = (A->peak == NULL) ? 0 : A->peak->rawFlux;
+    psF32 fB = (B->peak == NULL) ? 0 : B->peak->rawFlux;
+    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 SN (descending)
+int pmSourceSortByParentFlux (const void **a, const void **b)
+{
+    pmSource *Ao = *(pmSource **)a;
+    pmSource *Bo = *(pmSource **)b;
+    pmSource *A  = Ao->parent;
+    pmSource *B  = Bo->parent;
+
+    psF32 fA = (A->peak == NULL) ? 0 : A->peak->rawFlux;
+    psF32 fB = (B->peak == NULL) ? 0 : B->peak->rawFlux;
+    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);
+}
+
+// sort by X (ascending)
+int pmSourceSortByX (const void **a, const void **b)
+{
+    pmSource *A = *(pmSource **)a;
+    pmSource *B = *(pmSource **)b;
+
+    psF32 fA = (A->peak == NULL) ? 0 : A->peak->x;
+    psF32 fB = (B->peak == NULL) ? 0 : B->peak->x;
+
+    psF32 diff = fA - fB;
+    if (diff > FLT_EPSILON) return (+1);
+    if (diff < FLT_EPSILON) return (-1);
+    return (0);
+}
+
+// sort by Seq (ascending)
+int pmSourceSortBySeq (const void **a, const void **b)
+{
+    pmSource *A = *(pmSource **)a;
+    pmSource *B = *(pmSource **)b;
+
+    int iA = A->seq;
+    int iB = B->seq;
+
+    int diff = iA - iB;
+    if (diff > 0) return (+1);
+    if (diff < 0) return (-1);
+    return (0);
+}
+
+// sort by Seq (ascending)
+int pmSourceSortByParentSeq (const void **a, const void **b)
+{
+    pmSource *Ao = *(pmSource **)a;
+    pmSource *Bo = *(pmSource **)b;
+    pmSource *A  = Ao->parent;
+    pmSource *B  = Bo->parent;
+
+    int iA = A->seq;
+    int iB = B->seq;
+
+    int diff = iA - iB;
+    if (diff > 0) return (+1);
+    if (diff < 0) return (-1);
+    return (0);
+}
+
+pmSourceMode pmSourceModeFromString (const char *name) {
+  if (!strcasecmp (name, "DEFAULT"   )) return PM_SOURCE_MODE_DEFAULT;
+  if (!strcasecmp (name, "PSFMODEL"  )) return PM_SOURCE_MODE_PSFMODEL;
+  if (!strcasecmp (name, "EXTMODEL"  )) return PM_SOURCE_MODE_EXTMODEL;
+  if (!strcasecmp (name, "FITTED"    )) return PM_SOURCE_MODE_FITTED;
+  if (!strcasecmp (name, "FAIL"      )) return PM_SOURCE_MODE_FAIL;
+  if (!strcasecmp (name, "POOR"      )) return PM_SOURCE_MODE_POOR;
+  if (!strcasecmp (name, "PAIR"      )) return PM_SOURCE_MODE_PAIR;
+  if (!strcasecmp (name, "PSFSTAR"   )) return PM_SOURCE_MODE_PSFSTAR;
+  if (!strcasecmp (name, "SATSTAR"   )) return PM_SOURCE_MODE_SATSTAR;
+  if (!strcasecmp (name, "BLEND"     )) return PM_SOURCE_MODE_BLEND;
+  if (!strcasecmp (name, "EXTERNAL"  )) return PM_SOURCE_MODE_EXTERNAL;
+  if (!strcasecmp (name, "BADPSF"    )) return PM_SOURCE_MODE_BADPSF;
+  if (!strcasecmp (name, "DEFECT"    )) return PM_SOURCE_MODE_DEFECT;
+  if (!strcasecmp (name, "SATURATED" )) return PM_SOURCE_MODE_SATURATED;
+  if (!strcasecmp (name, "CRLIMIT"   )) return PM_SOURCE_MODE_CR_LIMIT;
+  if (!strcasecmp (name, "EXTLIMIT"  )) return PM_SOURCE_MODE_EXT_LIMIT;
+  return PM_SOURCE_MODE_DEFAULT;
+}
+
+char *pmSourceModeToString (const pmSourceMode mode) {
+  switch (mode) {
+    case PM_SOURCE_MODE_DEFAULT    : return psStringCopy ("DEFAULT"   );
+    case PM_SOURCE_MODE_PSFMODEL   : return psStringCopy ("PSFMODEL"  );
+    case PM_SOURCE_MODE_EXTMODEL   : return psStringCopy ("EXTMODEL"  );
+    case PM_SOURCE_MODE_FITTED     : return psStringCopy ("FITTED"    );
+    case PM_SOURCE_MODE_FAIL       : return psStringCopy ("FAIL"      );
+    case PM_SOURCE_MODE_POOR       : return psStringCopy ("POOR"      );
+    case PM_SOURCE_MODE_PAIR       : return psStringCopy ("PAIR"      );
+    case PM_SOURCE_MODE_PSFSTAR    : return psStringCopy ("PSFSTAR"   );
+    case PM_SOURCE_MODE_SATSTAR    : return psStringCopy ("SATSTAR"   );
+    case PM_SOURCE_MODE_BLEND      : return psStringCopy ("BLEND"     );
+    case PM_SOURCE_MODE_EXTERNAL   : return psStringCopy ("EXTERNAL"  );
+    case PM_SOURCE_MODE_BADPSF     : return psStringCopy ("BADPSF"    );
+    case PM_SOURCE_MODE_DEFECT     : return psStringCopy ("DEFECT"    );
+    case PM_SOURCE_MODE_SATURATED  : return psStringCopy ("SATURATED" );
+    case PM_SOURCE_MODE_CR_LIMIT   : return psStringCopy ("CRLIMIT"   );
+    case PM_SOURCE_MODE_EXT_LIMIT  : return psStringCopy ("EXTLIMIT"  );
+    default:
+      return NULL;
+  }
+  return NULL;
+}
+
+// Function to estimate the memory consumed by a source.
+#define IMAGE_BYTES(_im, _pix_size) (_im ? (sizeof(psImage) + (_im->numRows * sizeof(void*)) + (_im->numRows * _im->numCols * _pix_size * (_im->parent ? 0 : 1))) : 0)
+
+#define VECTOR_BYTES(_v, _elem_size) (_v ? (sizeof(psVector) + (_v->n * _elem_size)) : 0)
+
+// estimate the memory consumed by this source. 
+// It doesn't count everything (a couple of psArrays), the big stuff is counted
+psU64 pmSourceMemoryUse (pmSource *source) {
+    psU64 bytes = sizeof(pmSource) + sizeof(pmPeak) + sizeof(pmMoments);
+
+    bytes += IMAGE_BYTES(source->pixels, 4);
+    bytes += IMAGE_BYTES(source->variance, 4);
+    bytes += IMAGE_BYTES(source->modelVar, 4);
+    bytes += IMAGE_BYTES(source->maskObj, 2);
+    bytes += IMAGE_BYTES(source->maskView, 2);
+    bytes += IMAGE_BYTES(source->modelFlux, 4);
+    bytes += IMAGE_BYTES(source->psfImage, 4);
+
+    if (source->modelFits) {
+        for (int i = 0; i < source->modelFits->n; i++) {
+            pmModel *model = source->modelFits->data[i];
+            if (!model) continue;
+            bytes += sizeof(pmModel);
+            bytes += IMAGE_BYTES(model->covar, 4);
+            bytes += VECTOR_BYTES(model->params, 4);
+            bytes += VECTOR_BYTES(model->dparams, 4);
+            if (model->residuals) {
+                bytes += sizeof(pmResiduals);
+                bytes += IMAGE_BYTES(model->residuals->Ro, 4);
+                bytes += IMAGE_BYTES(model->residuals->Rx, 4);
+                bytes += IMAGE_BYTES(model->residuals->Ry, 4);
+                bytes += IMAGE_BYTES(model->residuals->variance, 4);
+                bytes += IMAGE_BYTES(model->residuals->mask, 2);
+            }
+        }
+    }
+    if (source->radialAper) {
+        for (int i = 0; i < source->radialAper->n; i++) {
+            pmSourceRadialApertures *radialAper = source->radialAper->data[i];
+            if (radialAper) {
+                bytes += sizeof(pmSourceRadialApertures);
+                bytes += VECTOR_BYTES(radialAper->flux, 4);
+                bytes += VECTOR_BYTES(radialAper->fluxStdev, 4);
+                bytes += VECTOR_BYTES(radialAper->fluxErr, 4);
+                bytes += VECTOR_BYTES(radialAper->fill, 4);
+            }
+        }
+    }
+
+    return bytes;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSource.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSource.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSource.h	(revision 42651)
@@ -0,0 +1,339 @@
+/* @file  pmSource.h
+ *
+ * @author EAM, IfA; GLG, MHPCC
+ *
+ * @version $Revision: 1.29 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-16 22:30:50 $
+ * 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.
+ *
+ */
+typedef enum {
+    PM_SOURCE_TYPE_UNKNOWN,             ///< not yet classified
+    PM_SOURCE_TYPE_DEFECT,              ///< a cosmic-ray
+    PM_SOURCE_TYPE_SATURATED,           ///< random saturated pixels (eg, bleed trails)
+    PM_SOURCE_TYPE_STAR,                ///< a good-quality star (subtracted model is PSF)
+    PM_SOURCE_TYPE_EXTENDED,            ///< an extended object (eg, galaxy) (subtracted model is EXT)
+} pmSourceType;
+
+typedef enum {
+    PM_SOURCE_TMPF_MODEL_GUESS       = 0x0001,
+    PM_SOURCE_TMPF_SUBTRACTED        = 0x0002,
+    PM_SOURCE_TMPF_SIZE_MEASURED     = 0x0004,
+    PM_SOURCE_TMPF_SIZE_CR_CANDIDATE = 0x0008,
+    PM_SOURCE_TMPF_MOMENTS_MEASURED  = 0x0010,
+    PM_SOURCE_TMPF_CANDIDATE_PSFSTAR = 0x0020,
+    PM_SOURCE_TMPF_RADIAL_KEEP       = 0x0040,
+    PM_SOURCE_TMPF_RADIAL_SKIP       = 0x0080,
+    PM_SOURCE_TMPF_PETRO_KEEP        = 0x0100,
+    PM_SOURCE_TMPF_PETRO_SKIP        = 0x0200,
+    PM_SOURCE_TMPF_EXT_FIT           = 0x0400,  // not just galaxies (trails as well)
+    PM_SOURCE_TMPF_PETRO             = 0x0800,
+} pmSourceTmpF;
+
+/** 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:
+ *
+ * a pmSource is the information about a (possible) blob of flux in a specific image.  A source
+ * may represent an insignificant or undetected source.  There may be multiple representations
+ * of an image (eg, alternate smoothed copies); sources on alternate images may have a pointer
+ * to the version on the primary image (source->parent).  A set of sources on different, but
+ * related images (eg, multiple exposures or different filters) which (may) represent the same
+ * astronomical object are grouped together with the pmPhotObj type (set pmPhotObj.h).
+ * 
+ * A single source may be fitted with multiple models (not at the same time!).  The PSF model
+ * fit is a fit of the position (optionally) and the flux to the PSF model at the location of
+ * the source.  Alternate model fits are extended source models. The best model fit is used to
+ * subtract the object from the image.
+ *
+ *  XXX do I have to re-organize this (again!) to allow an arbitrary set of extended model fits??
+ *  XXX put the Mag and Err inside the pmModel?
+ *  XXX keep the modelEXT or add to the psArray
+ *
+ *
+ */
+struct pmSource {
+    const int id;                       ///< Unique ID for object (generated on alloc)
+    int seq;                            ///< ID for output (generated on write OR set on read)
+    pmPeak  *peak;                      ///< Description of peak pixel.
+    psImage *pixels;                    ///< Rectangular region including object pixels.
+    psImage *variance;			///< Image variance.
+    psImage *modelVar;			///< variance based on current models
+    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 *psfImage;			///< cached copy of the psf model for this source
+    pmMoments *moments;                 ///< Basic moments measured for the object.
+    pmModel *modelPSF;                  ///< PSF Model fit (parameters and type)
+    pmModel *modelEXT;                  ///< EXT Model fit used for subtraction (parameters and type)
+    psArray *modelFits;                 ///< collection of extended source models (best == modelEXT)
+    psArray *extFitPars;		///< extra extended fit parameters
+    pmSourceType type;                  ///< Best identification of object.
+    pmSourceMode mode;                  ///< analysis flags set for object.
+    pmSourceMode2 mode2;                ///< analysis flags set for object.
+    pmSourceTmpF tmpFlags;              ///< internal-only flags
+
+    float psfMag;                       ///< calculated from flux in modelPSF
+    float psfMagErr;                    ///< error in psfMag
+    float psfFlux;                      ///< calculated from flux in modelPSF
+    float psfFluxErr;                   ///< error in psfFlux
+    float extMag;                       ///< calculated from flux in modelEXT -- NOTE this is not actually used
+    float apMag;                        ///< apMag corresponding to psfMag or extMag (depending on type)
+    float apMagRaw;                     ///< raw mag in given aperture
+    float apRadius;			///< radius for aperture magnitude
+    int   apNpixels;			///< number of unmasked pixels in aperture
+    float apFlux;                       ///< apFlux corresponding to psfMag or extMag (depending on type)
+    float apFluxErr;                    ///< apFluxErr corresponding to psfMag or extMag (depending on type)
+
+    float windowRadius;			///< size of box used for full analysis
+    float skyRadius;			///< radius at which profile hits local sky (or goes flat)
+    float skyFlux;			///< mean flux per pixel in aperture at which profile hits local sky (or goes flat)
+    float skySlope;			///< mean flux slope at which profile hits local sky (or goes flat)
+
+    float pixWeightNotBad;              ///< PSF-weighted coverage of unmasked (not BAD) pixels
+    float pixWeightNotPoor;             ///< PSF-weighted coverage of unmasked (not POOR) pixels
+
+    float psfChisq;                     ///< probability of PSF
+    float crNsigma;                     ///< Nsigma deviation from PSF to CR
+    float extNsigma;                    ///< Nsigma deviation from PSF to EXT
+    float sky;				///< The sky at the center of the object 
+    float skyErr;			///< The sky error at the center of the object
+    float extSN;                        ///< for externally supplied source the kron signal to noise (used by full force)
+
+    psRegion region;                    ///< area on image covered by selected pixels
+    psArray *blends;                    ///< collection of sources thought to be confused with object
+    pmSourceSatstar *satstar;
+    pmSourceExtendedPars *extpars;      ///< extended source parameters
+    pmSourceDiffStats *diffStats;       ///< extra parameters for difference detections
+    psArray *galaxyFits;                ///< fits to galaxy models (psphotFullForce only)
+    pmSourceLensing *lensingOBJ;        ///< lensing moments parameters (per object)
+    pmSourceLensing *lensingPSF;        ///< lensing moments parameters (psf, interpolated)
+    psArray *radialAper;		///< radial flux in circular apertures
+    pmSource *parent;			///< reference to the master source from which this is derived
+    psPtr *tmpPtr;                      ///< pointer that may be used to store data in a particular module. e.g. psphotKronIterate.
+    short chipNum;                      ///< camera dependent of chip suppling pixels for fullforce source
+    short chipX;                        ///< chip space X coord of fullforce source
+    short chipY;                        ///< chip space Y coord of fullforce source
+    int imageID;
+    psU16 nFrames;
+};
+
+/** 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;
+    int nStars;
+    int nTotal;
+    float nSigma;
+}
+pmPSFClump;
+
+// private macro to set the source ID (a const)
+#define P_PM_SOURCE_SET_ID(S,V) { *(int *)&(S)->id = (V); }
+
+/** pmSourceAlloc()
+ *
+ */
+pmSource  *pmSourceAlloc(void);
+
+/** pmSourceCopy()
+ *
+ */
+
+bool psMemCheckSource(psPtr ptr);
+
+pmSource  *pmSourceCopy(pmSource *source);
+pmSource *pmSourceCopyData(pmSource *in);
+
+// free just the pixels for a source, keeping derived data
+void pmSourceFreePixels(pmSource *source);
+
+/** 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
+);
+
+bool pmSourceRedefinePixelsByRegion (
+    pmSource *mySource,   ///< source to be re-defined
+    const pmReadout *readout,   ///< base the source on this readout
+    psRegion newRegion ///< region for source pixel definition
+);
+
+/** 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.
+ */
+
+pmPSFClump pmSourcePSFClump(
+    psImage **savedImage, 
+    psRegion *region,                   ///< restrict measurement to specified region
+    psArray *source,                    ///< The input pmSource
+    float PSF_SN_LIM, 
+    float PSF_CLUMP_GRID_SCALE, 
+    psF32 SX_MAX, 
+    psF32 SY_MAX, 
+    psF32 SX_MIN, 
+    psF32 SY_MIN, 
+    psF32 AR_MAX
+);
+
+/** 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(
+    psRegion *region,                   ///< restrict measurement to specified region
+    psArray *sources,                    ///< The input pmSources
+    float PSF_SN_LIM,			 ///< min S/N for source to be used for PSF model
+    float PSF_CLUMP_NSIGMA,		 ///< size of region around peak of clump for PSF stars
+    pmPSFClump clump,                   ///< Statistics about the PSF clump
+    psImageMaskType 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
+    float sigma,      ///< size of Gaussian window function (<= 0.0 -> skip window)
+    float minSN,	      ///< minimum pixel significance
+    float minKronRadius,      ///< minimum pixel significance
+    psImageMaskType maskVal
+);
+
+/** pmSourceMoments()
+ *
+ * Measure 1st moments for the given source, using the peak coordinates as the initial
+ * source location. The resulting moment values are applied to the source.moments
+ * entry. 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 pmSourceMomentsGetCentroid(
+  pmSource *source, 
+  psF32 radius, 
+  psF32 sigma, 
+  psF32 minSN, 
+  psImageMaskType maskVal, 
+  float xGuess, float yGuess);
+
+float pmSourceMinKronRadius(psArray *sources, float PSF_SN_LIM);
+
+pmModel *pmSourceGetModel (bool *isPSF, const pmSource *source);
+
+bool pmSourceAdd (pmSource *source, pmModelOpMode mode, psImageMaskType maskVal);
+bool pmSourceSub (pmSource *source, pmModelOpMode mode, psImageMaskType maskVal);
+bool pmSourceAddWithOffset (pmSource *source, pmModelOpMode mode, psImageMaskType maskVal, int dx, int dy);
+bool pmSourceSubWithOffset (pmSource *source, pmModelOpMode mode, psImageMaskType maskVal, int dx, int dy);
+
+bool pmSourceNoiseOpModel (pmModel *model, pmSource *source, pmModelOpMode mode, float FACTOR, float SIZE, bool add, psImageMaskType maskVal, int dx, int dy);
+bool pmSourceNoiseOp (pmSource *source, pmModelOpMode mode, float FACTOR, float SIZE, bool add, psImageMaskType maskVal, int dx, int dy);
+
+bool pmSourceSmoothOp (pmSource *source, pmModelOpMode mode, psImage *target, float sigma, bool add, psImageMaskType maskVal, int dx, int dy);
+bool pmSourceSmoothOpModel (pmModel *model, pmSource *source, pmModelOpMode mode, psImage *target, float sigma, bool add, psImageMaskType maskVal, int dx, int dy);
+
+bool pmSourceOp (pmSource *source, pmModelOpMode mode, bool add, psImageMaskType maskVal, int dx, int dy);
+bool pmSourceCacheModel (pmSource *source, psImageMaskType maskVal);
+bool pmSourceCachePSF (pmSource *source, psImageMaskType maskVal);
+
+bool pmSourcePositionUseMoments(pmSource *source);
+
+int  pmSourceSortByY (const void **a, const void **b);
+int  pmSourceSortByX (const void **a, const void **b);
+int  pmSourceSortBySeq (const void **a, const void **b);
+int  pmSourceSortByParentSeq (const void **a, const void **b);
+int  pmSourceSortByFlux (const void **a, const void **b);
+int  pmSourceSortByParentFlux (const void **a, const void **b);
+
+pmSourceMode pmSourceModeFromString (const char *name);
+char *pmSourceModeToString (const pmSourceMode mode);
+
+psU64 pmSourceMemoryUse(pmSource *source);
+
+/// @}
+# endif /* PM_SOURCE_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceContour.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceContour.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceContour.c	(revision 42651)
@@ -0,0 +1,472 @@
+/** @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.13 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-12-08 02:51:14 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.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", 10, "---- %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", 10, "---- %s() end ----\n", __func__);
+            return (subCol);
+        }
+        subCol += incr;
+    }
+    psTrace("psModules.objects", 10, "---- %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", 10, "---- %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", 10, "---- %s() end ----\n", __func__);
+            if (subCol == x) {
+                return (subCol);
+            }
+            return (subCol);
+        }
+        subCol += incr;
+    }
+    psTrace("psModules.objects", 10, "---- %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", 10, "---- %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", 10, "---- %s(NAN) end ----\n", __func__);
+        return(NAN);
+    }
+    if (!((0 <= subRow) && (subRow < source->pixels->numRows))) {
+        psTrace("psModules.objects", 10, "---- %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", 10, "---- %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", 10, "---- %s() end ----\n", __func__);
+            return((psF32) (subCol + source->pixels->col0));
+        }
+
+        if ((newValue <= level) && (level <= oldValue)) {
+            // This is simple linear interpolation.
+            psTrace("psModules.objects", 10, "---- %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", 10, "---- %s() end ----\n", __func__);
+            return( ((psF32) (subCol + source->pixels->col0)) + ((psF32) incr) * ((level - oldValue) / (newValue - oldValue)) );
+        }
+
+        subCol+=incr;
+    }
+
+    psTrace("psModules.objects", 10, "---- %s(NAN) end ----\n", __func__);
+    return(NAN);
+}
+
+/******************************************************************************
+new implementation of source contour function
+*****************************************************************************/
+psArray *pmSourceContour (psImage *image, int xc, int yc, float threshold)
+{
+    psTrace("psModules.objects", 10, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(image, NULL);
+
+    int xR, yR, x0, x1, x0s, x1s;
+    int x = xc - image->col0;
+    int y = yc - image->row0;
+
+    // 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;
+    }
+
+    // the requested point must be within the contour
+    if (image->data.F32[y][x] < threshold) {
+        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;
+
+    psArray *tmpArray = psArrayAlloc(2);
+
+    tmpArray->data[0] = (psPtr *) xVec;
+    tmpArray->data[1] = (psPtr *) yVec;
+    psTrace("psModules.objects", 10, "---- %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", 10, "---- %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", 10, "---- %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", 10, "---- %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", 10, "---- %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", 10, "---- %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", 10, "---- %s() end ----\n", __func__);
+    return(tmpArray);
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceContour.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceContour.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceContour.h	(revision 42651)
@@ -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_branches/psModules.20240412/src/objects/pmSourceDiffStats.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceDiffStats.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceDiffStats.c	(revision 42651)
@@ -0,0 +1,47 @@
+/** @file  pmSourceDiffStats.c
+ *
+ *  Functions defining the pmSourceDiffStats structure and associated measurements
+
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-10-03 20:59:16 $
+ *
+ *  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 "pmSourceDiffStats.h"
+
+/** initialization function for a pmSourceDiffStats object
+ **/
+void pmSourceDiffStatsInit(pmSourceDiffStats *diffStats)
+{
+    diffStats->fRatio = NAN;
+    diffStats->nRatioBad = NAN;
+    diffStats->nRatioMask = NAN;
+    diffStats->nRatioAll = NAN;
+    diffStats->nGood = 0;
+
+    diffStats->SNp = NAN;
+    diffStats->SNm = NAN;
+    diffStats->Rp = NAN;
+    diffStats->Rm = NAN;
+}
+
+/******************************************************************************
+pmSourceDiffStatsAlloc(): Allocate the pmSourceDiffStats structure and initialize the members
+to zero.
+*****************************************************************************/
+pmSourceDiffStats *pmSourceDiffStatsAlloc(void)
+{
+    pmSourceDiffStats *tmp = (pmSourceDiffStats *) psAlloc(sizeof(pmSourceDiffStats));
+    pmSourceDiffStatsInit(tmp);
+    return(tmp);
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceDiffStats.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceDiffStats.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceDiffStats.h	(revision 42651)
@@ -0,0 +1,48 @@
+/* @file  pmSourceDiffStats.h
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.29 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-16 22:30:50 $
+ * Copyright 2004 Institute for Astronomy, University of Hawaii
+ */
+
+# ifndef PM_SOURCE_DIFF_STATS_H
+# define PM_SOURCE_DIFF_STATS_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+/* The elements of this structure are used to characterize diff image detections.
+ * The values in the structure are derived from the following measurements:
+ * fGood = \sum(flux) for pixels with S/N > +SN_LIMIT
+ * fBad  = \sum(flux) for pixels with S/N < -SN_LIMIT
+ * nGood = \sum(pixels) with S/N > +SN_LIMIT
+ * nBad  = \sum(pixels) with S/N < -SN_LIMIT
+ * nMask = \sum(pixels) masked
+ */
+
+typedef struct {
+    float fRatio;			// = fGood / (fGood + fBad)
+    float nRatioBad;			// = nGood / (nGood + nBad)
+    float nRatioMask;			// = nGood / (nGood + nMask)
+    float nRatioAll;			// = nGood / (nGood + nMask + nBad)
+    int   nGood;			// nGood as defined above
+    float SNp;				// S/N of matched source in positive image
+    float SNm;				// S/N of matched source in negative image
+    float Rp;				// radius of matched source in positive image
+    float Rm;				// radius of matched source in negative image
+} pmSourceDiffStats;
+
+
+/** pmSourceDiffStatsAlloc()
+ */
+pmSourceDiffStats *pmSourceDiffStatsAlloc(void);
+
+/** pmSourceDiffStatsInit()
+ * function to initialize the values in the structure
+ */
+void pmSourceDiffStatsInit(pmSourceDiffStats *diffStats);
+
+/// @}
+# endif
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceExtendedPars.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceExtendedPars.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceExtendedPars.c	(revision 42651)
@@ -0,0 +1,316 @@
+/** @file  pmSourceExtendedPars.c
+ *
+ *  Functions to define and manipulate sources on images
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA: significant modifications.
+ *
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-06 02:31:25 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+#include "pmSourceExtendedPars.h"
+
+// pmSourceRadialFlux carries the raw radial flux information, including angular bins
+static void pmSourceRadialFluxFree(pmSourceRadialFlux *flux)
+{
+    if (!flux) return;
+    psFree(flux->radii);
+    psFree(flux->fluxes);
+    psFree(flux->theta);
+    psFree(flux->isophotalRadii);
+}
+
+pmSourceRadialFlux *pmSourceRadialFluxAlloc()
+{
+    pmSourceRadialFlux *flux = (pmSourceRadialFlux *)psAlloc(sizeof(pmSourceRadialFlux));
+    psMemSetDeallocator(flux, (psFreeFunc) pmSourceRadialFluxFree);
+
+    flux->radii = NULL;
+    flux->fluxes = NULL;
+    flux->theta = NULL;
+    flux->isophotalRadii = NULL;
+
+    return flux;
+}
+
+bool psMemCheckSourceRadialFlux(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmSourceRadialFluxFree);
+}
+
+// pmSourceRadialApertures carries the raw radial flux information, including angular bins
+static void pmSourceRadialAperturesFree(pmSourceRadialApertures *radial)
+{
+    if (!radial) return;
+    psFree(radial->flux);
+    psFree(radial->fluxErr);
+    psFree(radial->fluxStdev);
+    psFree(radial->fill);
+}
+
+pmSourceRadialApertures *pmSourceRadialAperturesAlloc()
+{
+    pmSourceRadialApertures *radial = (pmSourceRadialApertures *)psAlloc(sizeof(pmSourceRadialApertures));
+    psMemSetDeallocator(radial, (psFreeFunc) pmSourceRadialAperturesFree);
+
+    radial->flux = NULL;
+    radial->fluxErr = NULL;
+    radial->fluxStdev = NULL;
+    radial->fill = NULL;
+    return radial;
+}
+
+bool psMemCheckSourceRadialApertures(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmSourceRadialAperturesFree);
+}
+
+// pmSourceEllipticalFlux carries the elliptical renormalized radial flux info
+static void pmSourceEllipticalFluxFree(pmSourceEllipticalFlux *flux)
+{
+    if (!flux) return;
+    psFree(flux->radiusElliptical);
+    psFree(flux->fluxElliptical);
+}
+
+pmSourceEllipticalFlux *pmSourceEllipticalFluxAlloc()
+{
+    pmSourceEllipticalFlux *flux = (pmSourceEllipticalFlux *)psAlloc(sizeof(pmSourceEllipticalFlux));
+    psMemSetDeallocator(flux, (psFreeFunc) pmSourceEllipticalFluxFree);
+
+    flux->radiusElliptical = NULL;
+    flux->fluxElliptical = NULL;
+
+    return flux;
+}
+
+bool psMemCheckSourceEllipticalFlux(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmSourceEllipticalFluxFree);
+}
+
+// pmSourceRadialProfile defines flux information in radial bins
+static void pmSourceRadialProfileFree(pmSourceRadialProfile *profile)
+{
+    if (!profile) return;
+    psFree(profile->binSB);
+    psFree(profile->binSBstdev);
+    psFree(profile->binSBerror);
+    psFree(profile->binSum);
+    psFree(profile->binFill);
+    psFree(profile->radialBins);
+    psFree(profile->area);
+}
+
+pmSourceRadialProfile *pmSourceRadialProfileAlloc()
+{
+    pmSourceRadialProfile *profile = (pmSourceRadialProfile *)psAlloc(sizeof(pmSourceRadialProfile));
+    psMemSetDeallocator(profile, (psFreeFunc) pmSourceRadialProfileFree);
+
+    profile->binSB = NULL;
+    profile->binSBstdev = NULL;
+    profile->binSBerror = NULL;
+    profile->binSum = NULL;
+    profile->binFill = NULL;
+    profile->radialBins = NULL;
+    profile->area = NULL;
+    return profile;
+}
+
+bool psMemCheckSourceRadialProfile(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmSourceRadialProfileFree);
+}
+
+# if (0)
+// pmSourceRadialProfileFreeVectors frees the intermediate data values
+bool pmSourceRadialProfileFreeVectors(pmSourceRadialProfile *profile) {
+
+    psFree(profile->radii);
+    psFree(profile->fluxes);
+    psFree(profile->theta);
+    psFree(profile->isophotalRadii);
+
+    psFree(profile->radiusElliptical);
+    psFree(profile->fluxElliptical);
+
+    // psFree(profile->binSB);
+    // psFree(profile->binSBstdev);
+    // psFree(profile->binSBerror);
+    
+    // psFree(profile->radialBins);
+    psFree(profile->area);
+
+    profile->radii = NULL;
+    profile->fluxes = NULL;
+    profile->theta = NULL;
+    profile->isophotalRadii = NULL;
+
+    profile->radiusElliptical = NULL;
+    profile->fluxElliptical = NULL;
+
+    // profile->binSB = NULL;
+    // profile->binSBstdev = NULL;
+    // profile->binSBerror = NULL;
+    
+    // profile->radialBins = NULL;
+    profile->area = NULL;
+
+    return true;
+}
+# endif
+
+// *** pmSourceRadialProfileSortPair is a utility function for sorting a pair of vectors
+# define COMPARE_INDEX(A,B) (index->data.F32[A] < index->data.F32[B])
+# define SWAP_INDEX(TYPE,A,B) { \
+  float tmp; \
+  if (A != B) { \
+    tmp = index->data.F32[A]; \
+    index->data.F32[A] = index->data.F32[B]; \
+    index->data.F32[B] = tmp; \
+    tmp = extra->data.F32[A]; \
+    extra->data.F32[A] = extra->data.F32[B]; \
+    extra->data.F32[B] = tmp; \
+  } \
+}
+
+bool pmSourceRadialProfileSortPair (psVector *index, psVector *extra) {
+
+    psAssert (index->n == extra->n, "mismatched vector lengths");
+    if (index->n < 2) return true;
+
+    // sort the vector set by the radius
+    PSSORT (index->n, COMPARE_INDEX, SWAP_INDEX, NONE);
+    return true;
+}
+
+// *** pmSourceExtendedPars describes the possible collection of extended flux measurements for a source
+static void pmSourceExtendedParsFree (pmSourceExtendedPars *pars) {
+    if (!pars) return;
+
+    psFree(pars->radFlux);
+    psFree(pars->ellipticalFlux);
+    psFree(pars->radProfile);
+    psFree(pars->petProfile);
+    return;
+}
+
+pmSourceExtendedPars *pmSourceExtendedParsAlloc (void) {
+    pmSourceExtendedPars *pars = (pmSourceExtendedPars *) psAlloc(sizeof(pmSourceExtendedPars));
+    psMemSetDeallocator(pars, (psFreeFunc) pmSourceExtendedParsFree);
+
+    pars->radFlux = NULL;
+    pars->ellipticalFlux = NULL;
+    pars->radProfile = NULL;
+    pars->petProfile = NULL;
+
+    pars->petrosianFlux = NAN;
+    pars->petrosianFluxErr = NAN;
+    pars->petrosianRadius = NAN;
+    pars->petrosianRadiusErr = NAN;
+    pars->petrosianR90 = NAN;
+    pars->petrosianR90Err = NAN;
+    pars->petrosianR50 = NAN;
+    pars->petrosianR50Err = NAN;
+    pars->ghalfLightRadius = NAN;
+    pars->gRT = NAN;
+    pars->gRA = NAN;
+    pars->gS2 = NAN;
+    pars->gA = NAN;
+    pars->gbumpy = NAN;
+    return pars;
+}
+
+bool psMemCheckSourceExtendedPars(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmSourceExtendedParsFree);
+}
+
+
+// *** pmSourceExtendedFlux describes the flux within an elliptical aperture of some kind 
+static void pmSourceExtendedFluxFree (pmSourceExtendedFlux *flux) {
+    if (!flux) return;
+    return;
+}
+
+pmSourceExtendedFlux *pmSourceExtendedFluxAlloc (void) {
+
+    pmSourceExtendedFlux *flux = (pmSourceExtendedFlux *) psAlloc(sizeof(pmSourceExtendedFlux));
+    psMemSetDeallocator(flux, (psFreeFunc) pmSourceExtendedFluxFree);
+
+    flux->flux = 0.0;
+    flux->fluxErr = 0.0;
+    flux->radius = 0.0;
+    flux->radiusErr = 0.0;
+
+    return flux;
+}
+
+
+bool psMemCheckSourceExtendedFlux(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmSourceExtendedFluxFree);
+}
+
+// *** pmSourceExtFitPars describes extra metadata related to an extended fit
+static void pmSourceExtFitParsFree (pmSourceExtFitPars *pars) {
+    return;
+}
+
+pmSourceExtFitPars *pmSourceExtFitParsAlloc (void) {
+
+    pmSourceExtFitPars *pars = (pmSourceExtFitPars *) psAlloc(sizeof(pmSourceExtFitPars));
+    psMemSetDeallocator(pars, (psFreeFunc) pmSourceExtFitParsFree);
+
+    pars->Mxx = NAN;
+    pars->Mxy = NAN;
+    pars->Myy = NAN;
+
+    pars->Mrf    = NAN;
+    pars->Mrh    = NAN;
+
+    pars->apMag  = NAN;
+    pars->krMag  = NAN;
+    pars->psfMag = NAN;
+    pars->peakMag = NAN;
+
+    return pars;
+}
+
+// *** pmSourceExtFitPars describes extra metadata related to an extended fit
+static void pmSourceGalaxyFitsFree (pmSourceGalaxyFits *tmp) {
+  
+    psFree (tmp->Flux);
+    psFree (tmp->dFlux);
+    psFree (tmp->chisq);
+
+    return;
+}
+
+pmSourceGalaxyFits *pmSourceGalaxyFitsAlloc (void) {
+
+    pmSourceGalaxyFits *tmp = (pmSourceGalaxyFits *) psAlloc(sizeof(pmSourceGalaxyFits));
+    psMemSetDeallocator(tmp, (psFreeFunc) pmSourceGalaxyFitsFree);
+
+    tmp->Flux  = psVectorAllocEmpty (25, PS_TYPE_F32);
+    tmp->dFlux = psVectorAllocEmpty (25, PS_TYPE_F32);
+    tmp->chisq = psVectorAllocEmpty (25, PS_TYPE_F32);
+    tmp->nPix = 0;
+
+    return tmp;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceExtendedPars.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceExtendedPars.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceExtendedPars.h	(revision 42651)
@@ -0,0 +1,135 @@
+/* @file  pmSourceExtendedPars.h
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-06 02:31:25 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_SOURCE_EXTENDED_PARS_H
+# define PM_SOURCE_EXTENDED_PARS_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+typedef struct {
+    psArray  *radii;			// radii for raw radial profiles at evenly-spaced angles
+    psArray  *fluxes;			// fluxes measured at above radii
+    psVector *theta;			// angles corresponding to above radial profiles
+    psVector *isophotalRadii;		// isophotal radius for the above angles
+} pmSourceRadialFlux;
+
+typedef struct {
+    psVector *flux;			// fluxes measured at above radii
+    psVector *fluxStdev;		// scatter (standard deviation) of flux
+    psVector *fluxErr;			// formal error on the fluxes (sqrt\sum(variance))
+    psVector *fill;			// angles corresponding to above radial profiles
+} pmSourceRadialApertures;
+
+typedef struct {
+    psVector *radiusElliptical;		// normalized radial coordinates for all relevant pixels
+    psVector *fluxElliptical;		// flux for the above radial coordinates
+} pmSourceEllipticalFlux;
+
+typedef struct {
+    psVector *binSB;			// mean surface brightness within radial bins
+    psVector *binSBstdev;		// scatter of mean surface brightness within radial bins
+    psVector *binSBerror;		// formal error on mean surface brightness within radial bins
+    psVector *binSum;			// sum of flux within radial bins
+    psVector *binFill;			// fraction of area actually lit
+    psVector *radialBins;		// radii corresponding to above binnedFlux
+    psVector *area;			// differential area of the non-overlapping radial bins
+} pmSourceRadialProfile;
+
+typedef struct {
+    float flux;
+    float fluxErr;
+    float radius;
+    float radiusErr;
+} pmSourceExtendedFlux;
+
+typedef struct {
+    pmSourceRadialFlux     *radFlux;	    // raw radial flux information
+    pmSourceEllipticalFlux *ellipticalFlux; // flux for elliptically-renormalized radii
+    pmSourceRadialProfile  *radProfile;	    // surface brightness profile in specified fixed bins
+    pmSourceRadialProfile  *petProfile;	    // surface brightness profile in petrosian bins
+    psEllipseAxes axes;			    // shape of elliptical contour
+    float petrosianFlux;
+    float petrosianFluxErr;
+    float petrosianRadius;
+    float petrosianRadiusErr;
+    float petrosianR90;
+    float petrosianR90Err;
+    float petrosianR50;
+    float petrosianR50Err;
+    float petrosianFill;
+    float ghalfLightRadius;                 
+    float gRT;                           // total residual (from eliptically symmetric model) Simard 2002
+    float gRA;                           // assymetric residual (from eliptically symmetric model) Simard 2002
+    float gS2;                           // (un) smoothness  (Simard, 2011 Cheng 2011)
+    float gA;                            // assymetry index (Gyory & Bell 2010)
+    float gbumpy;                        // Blakeslee bumpiness
+} pmSourceExtendedPars;
+
+// additional measurements related to the model fits
+typedef struct {
+    float Mxx;
+    float Mxy;
+    float Myy;
+    
+    float Mrf;
+    float Mrh;
+
+    float apMag;
+    float krMag;
+    float psfMag;
+    float peakMag;
+} pmSourceExtFitPars;
+
+typedef struct {
+  int       modelType;
+  psVector *Flux;
+  psVector *dFlux;
+  psVector *chisq;
+  int       nPix;
+  bool      reducedTrials;
+  float     fRmajorMin;
+  float     fRmajorMax;
+  float     fRmajorDel;
+  float     fRminorMin;
+  float     fRminorMax;
+  float     fRminorDel;
+} pmSourceGalaxyFits;
+
+pmSourceRadialFlux *pmSourceRadialFluxAlloc();
+bool psMemCheckSourceRadialFlux(psPtr ptr);
+
+pmSourceRadialApertures *pmSourceRadialAperturesAlloc();
+bool psMemCheckSourceRadialApertures(psPtr ptr);
+
+pmSourceEllipticalFlux *pmSourceEllipticalFluxAlloc();
+bool psMemCheckSourceEllipticalFlux(psPtr ptr);
+
+// *** pmSourceRadialProfile describes the radial profile of a source in elliptical contours, and 
+// intermediate data used to measure the profile
+pmSourceRadialProfile *pmSourceRadialProfileAlloc();
+bool psMemCheckSourceRadialProfile(psPtr ptr);
+
+// *** pmSourceExtendedPars describes the possible collection of extended flux measurements for a source
+pmSourceExtendedPars *pmSourceExtendedParsAlloc (void);
+bool psMemCheckSourceExtendedPars(psPtr ptr);
+
+// *** pmSourceExtendedFlux describes the flux within an elliptical aperture of some kind 
+pmSourceExtendedFlux *pmSourceExtendedFluxAlloc(void);
+bool psMemCheckSourceExtendedFlux(psPtr ptr);
+
+// *** pmSourceRadialProfileSortPair is a utility function for sorting a pair of vectors
+bool pmSourceRadialProfileSortPair(psVector *index, psVector *extra);
+
+pmSourceExtFitPars *pmSourceExtFitParsAlloc (void);
+
+pmSourceGalaxyFits *pmSourceGalaxyFitsAlloc (void);
+
+/// @}
+# endif /* PM_SOURCE_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceFitModel.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceFitModel.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceFitModel.c	(revision 42651)
@@ -0,0 +1,346 @@
+/** @file  pmSourceFitModel.c
+ *
+ *  fit single source models to image pixels
+ *
+ *  @author EAM, IfA
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.31 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-16 22:29:09 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourcePhotometry.h"
+#include "pmSourceFitModel.h"
+
+void pmSourceFitOptionsFree(pmSourceFitOptions *opt)
+{
+    return;
+}
+
+pmSourceFitOptions *pmSourceFitOptionsAlloc(void) {
+
+    pmSourceFitOptions *opt = (pmSourceFitOptions *) psAlloc(sizeof(pmSourceFitOptions));
+    psMemSetDeallocator(opt, (psFreeFunc) pmSourceFitOptionsFree);
+
+    opt->mode = PM_SOURCE_FIT_PSF;
+    opt->nIter  = 15;
+    opt->minTol = 0.01;
+    opt->maxTol = 1.00;
+    opt->weight = 1.00;
+    opt->nsigma = 5.00;
+    opt->maxChisqDOF = NAN;
+    opt->poissonErrors = true;
+    opt->saveCovariance = false;
+
+    // we default to the old algorithm
+    opt->gainFactorMode = 0;
+    opt->chisqConvergence = true;
+    opt->isInteractive = false;
+    opt->useReweighting = false;
+
+    return opt;
+}
+
+bool pmSourceFitModel (pmSource *source,
+                       pmModel *model,
+                       pmSourceFitOptions *options,
+                       psImageMaskType maskVal)
+{
+    psTrace("psModules.objects", 10, "---- %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);
+
+    // XXX if variance is NULL, use pixels instead (wrong, but not badly wrong)
+    // PS_ASSERT_PTR_NON_NULL(source->variance, 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);
+
+    // XXX for a test, skip the central pixel in the sersic fit
+    bool skipCenter = false && (model->type == pmModelClassGetType("PS_MODEL_SERSIC"));
+    float Xo = model->params->data.F32[PM_PAR_XPOS];
+    float Yo = model->params->data.F32[PM_PAR_YPOS];
+
+    // if variance is NULL, we pretend pixels == variance
+    float **vWgt = source->variance ? source->variance->data.F32 : source->pixels->data.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.PS_TYPE_IMAGE_MASK_DATA[i][j] & maskVal) {
+                continue;
+            }
+            // skip zero-variance points
+            if (vWgt[i][j] == 0) {
+                continue;
+            }
+            // skip nan values in image
+            if (!isfinite(source->pixels->data.F32[i][j])) {
+		fprintf (stderr, "WARNING: unmasked nan in image : %x vs %x\n", source->maskObj->data.PS_TYPE_IMAGE_MASK_DATA[i][j], maskVal);
+                continue;
+            }
+
+            // skip nan values in image
+            if (!isfinite(vWgt[i][j])) {
+		fprintf (stderr, "WARNING: unmasked nan in variance : %x vs %x\n", source->maskObj->data.PS_TYPE_IMAGE_MASK_DATA[i][j], maskVal);
+		continue;
+            }
+
+            // Convert i/j to image space:
+	    // 0.5 PIX: the coordinate values must be in pixel coords, not index	    
+            float Xv = (psF32) (j + 0.5 + source->pixels->col0);
+            float Yv = (psF32) (i + 0.5 + source->pixels->row0);
+
+	    // XXX possible skip of center pixel:
+	    if (skipCenter) {
+		float r = hypot(Xv - Xo, Yv - Yo);
+		if (r < 0.75) {
+		    continue;
+		}
+	    }
+
+            psVector *coord = psVectorAlloc(2, PS_TYPE_F32);
+            coord->data.F32[0] = Xv;
+            coord->data.F32[1] = Yv;
+            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 variance to avoid the bias from systematic errors here we would just use the
+            // source sky variance
+            if (options->poissonErrors) {
+                yErr->data.F32[nPix] = 1.0 / vWgt[i][j];
+            } else {
+                yErr->data.F32[nPix] = 1.0 / options->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_VECTOR_MASK);
+    constraint->checkLimits = model->class->modelLimits;
+
+    // set parameter mask based on fitting mode
+    int nParams = 0;
+    switch (options->mode) {
+      case PM_SOURCE_FIT_NORM:
+        // NORM-only model fits only source normalization (Io)
+        nParams = 1;
+        psVectorInit (constraint->paramMask, 1);
+        constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[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.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_I0] = 0;
+        constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_XPOS] = 0;
+        constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_YPOS] = 0;
+        break;
+      case PM_SOURCE_FIT_EXT:
+        // EXT model fits all shape params and Io (not Xo, Yo, sky)
+        nParams = params->n - 3;
+        psVectorInit (constraint->paramMask, 0);
+        constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_XPOS] = 1;
+        constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_YPOS] = 1;
+        constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_SKY] = 1;
+        break;
+      case PM_SOURCE_FIT_TRAIL:
+        // special mode for pmModel_TRAIL: Io, Xo, Yo, Length, and Theta (not Io or Sigma)
+        nParams = params->n - 3;
+        psVectorInit (constraint->paramMask, 0);
+        constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_SKY] = 1;
+        constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_SIGMA] = 1;
+        break;
+      case PM_SOURCE_FIT_INDEX:
+        // PSF model only fits Io, index (PAR7) -- only Io for models with < 8 params
+	psVectorInit (constraint->paramMask, 1);
+	constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_I0] = 0;
+        if (params->n == 7) {
+	    nParams = 1;
+	} else {
+	    nParams = 2;
+	    constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_7] = 0;
+	}
+	break;
+      case PM_SOURCE_FIT_NO_INDEX:
+        // PSF model only fits Io, Sxx, Sxy, Syy
+	psVectorInit (constraint->paramMask, 0);
+        constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_XPOS] = 1;
+        constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_YPOS] = 1;
+	constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_SKY] = 1;
+        if (params->n == 7) {
+	    nParams = params->n - 3;
+	} else {
+	    nParams = params->n - 4;
+	    constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[PM_PAR_7] = 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->class->modelLimits (PS_MINIMIZE_PARAM_MIN, i, params->data.F32, NULL);
+	model->class->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 (options->nIter, options->minTol, options->maxTol);
+    myMin->gainFactorMode = options->gainFactorMode;
+    myMin->chisqConvergence = options->chisqConvergence;
+    myMin->isInteractive = options->isInteractive;
+    myMin->useReweighting = options->useReweighting;
+
+    psImage *covar = psImageAlloc (params->n, params->n, PS_TYPE_F32);
+
+    fitStatus = psMinimizeLMChi2(myMin, covar, params, constraint, x, y, yErr, model->class->modelFunc);
+    for (int i = 0; i < dparams->n; i++) {
+        if ((constraint->paramMask != NULL) && constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[i])
+            continue;
+        dparams->data.F32[i] = sqrt(covar->data.F32[i][i]);
+        psTrace ("psModules.objects", 4, "%f +/- %f", params->data.F32[i], dparams->data.F32[i]);
+    }
+    if (options->saveCovariance) {
+	psFree (model->covar);
+	model->covar = psMemIncrRefCounter(covar);
+    }
+    model->nIter = myMin->iter;
+    model->nPar = nParams;
+
+    psTrace ("psModules.objects", 4, "niter: %d, chisq: %f", myMin->iter, myMin->value);
+
+    // save the resulting chisq, nDOF, nIter
+    // NOTE: if (!options->poissonErrors) chisq will be wrong : recalculate
+    if (options->poissonErrors) {
+	model->chisq = myMin->value;
+	model->nPix  = y->n;
+	model->nDOF  = y->n - model->nPar;
+	model->chisqNorm = model->chisq / model->nDOF;
+    } else {
+	pmSourceChisqUnsubtracted (source, model, maskVal);
+    }
+
+    // set the model success or failure status
+    model->flags |= PM_MODEL_STATUS_FITTED;
+    if (!fitStatus) {
+	if (isnan(myMin->value)) {
+	  model->flags |= PM_MODEL_STATUS_NAN_CHISQ;
+	} else {
+	  model->flags |= PM_MODEL_STATUS_NONCONVERGE;
+	}
+    }
+
+    if (myMin->chisqConvergence) {
+      if (myMin->lastDelta > myMin->minTol) model->flags |= PM_MODEL_STATUS_WEAK_FIT;
+    } else {
+      if (myMin->rParSigma > myMin->minTol*nParams) model->flags |= PM_MODEL_STATUS_WEAK_FIT;
+    }
+
+    // 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_VECTOR_MASK);
+        altmask->data.PS_TYPE_VECTOR_MASK_DATA[0] = 1;
+        for (int i = 1; i < dparams->n; i++) {
+            altmask->data.PS_TYPE_VECTOR_MASK_DATA[i] = (constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[i]) ? 0 : 1;
+        }
+        psMinimizeGaussNewtonDelta(delta, params, altmask, x, y, yErr, model->class->modelFunc);
+
+        for (int i = 0; i < dparams->n; i++) {
+            if (!constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[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", 10, "---- %s(%d) end ----\n", __func__, rc);
+    return(rc);
+}
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceFitModel.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceFitModel.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceFitModel.h	(revision 42651)
@@ -0,0 +1,102 @@
+/* @file  pmSourceFitModel.h
+ *
+ * @author EAM, IfA; GLG, MHPCC
+ *
+ * @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-01-27 06:39:38 $
+ * 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,
+    PM_SOURCE_FIT_INDEX,
+    PM_SOURCE_FIT_SHAPE,
+    PM_SOURCE_FIT_NO_INDEX,
+    PM_SOURCE_FIT_TRAIL,
+} pmSourceFitMode;
+
+typedef struct {
+    pmSourceFitMode mode;		///< optionally fit all or a subset of parameters
+    float nIter;			///< max number of allowed iterations
+    float minTol;			///< convergence criterion
+    float maxTol;			///< convergence criterion
+    float maxChisqDOF;			///< convergence criterion
+    float weight;			///< use this weight for constant-weight fits
+    float covarFactor;			///< covariance factor for calculating the chisq
+    float nsigma;                       ///< how far out to convolve
+    bool poissonErrors;			///< use poisson errors for fits?
+    bool saveCovariance;
+    int gainFactorMode;
+    bool chisqConvergence; 
+    bool isInteractive;
+    bool useReweighting;
+} pmSourceFitOptions;
+
+// the pmSourceFitOptions structure is used to control details of the fitting process
+pmSourceFitOptions *pmSourceFitOptionsAlloc(void);
+
+// bool pmSourceFitModelInit(
+//     pmSourceFitMode mode,		///< what parameter set should be fitted?
+//     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
+    pmSourceFitOptions *options,	///< define parameters to be fitted
+    psImageMaskType 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
+//     psImageMaskType maskVal		///< Vale to mask
+// 
+// );
+
+/// @}
+# endif /* PM_SOURCE_FIT_MODEL_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceFitPCM.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceFitPCM.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceFitPCM.c	(revision 42651)
@@ -0,0 +1,222 @@
+/* @file  pmSourceFitPCM.c
+ * structures and functions to support PSF-convolved model fitting
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.29 $
+ * @date $Date: 2009-02-16 22:30:50 $
+ * Copyright 2010 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAMaskWeight.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourcePhotometry.h"
+#include "pmSourceFitModel.h"
+#include "pmPCMdata.h"
+
+# define FACILITY "psModules.objects"
+
+// 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.
+
+# define TIMING 0
+
+bool pmSourceChisqModelFlux (pmSource *source, pmModel *model, psImageMaskType maskVal);
+
+bool pmSourceFitPCM (pmPCMdata *pcm, pmSource *source, pmSourceFitOptions *fitOptions, psImageMaskType maskVal, psImageMaskType markVal, int psfSize) {
+    
+    if (TIMING) { psTimerStart ("pmSourceFitPCM"); }
+
+    psVector *params  = pcm->modelConv->params;
+    psVector *dparams  = pcm->modelConv->dparams;
+
+    // force the floating parameters to fall within the contraint ranges
+    for (int i = 0; i < params->n; i++) {
+	pcm->modelConv->class->modelLimits (PS_MINIMIZE_PARAM_MIN, i, params->data.F32, NULL);
+	pcm->modelConv->class->modelLimits (PS_MINIMIZE_PARAM_MAX, i, params->data.F32, NULL);
+    }
+
+    // set up the minimization process
+    psMinimization *myMin = psMinimizationAlloc (fitOptions->nIter, fitOptions->minTol, fitOptions->maxTol);
+    myMin->chisqConvergence = fitOptions->chisqConvergence;
+    myMin->gainFactorMode = fitOptions->gainFactorMode;
+    myMin->isInteractive = fitOptions->isInteractive;
+
+    psImage *covar = psImageAlloc (params->n, params->n, PS_TYPE_F32);
+    // NOTE : 4 allocs to here
+
+    float t1, t2, t3, t4, t5;
+    if (TIMING) { t1 = psTimerMark ("pmSourceFitPCM"); }
+
+    // NOTE : 996 allocs in here
+    bool fitStatus = pmPCM_MinimizeChisq (myMin, covar, params, source, pcm);
+    if (TIMING) { t2 = psTimerMark ("pmSourceFitPCM"); }
+
+    for (int i = 0; i < dparams->n; i++) {
+        if ((pcm->constraint->paramMask != NULL) && pcm->constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[i])
+            continue;
+        dparams->data.F32[i] = sqrt(covar->data.F32[i][i]);
+        psTrace ("psModules.objects", 4, "%f +/- %f", params->data.F32[i], dparams->data.F32[i]);
+    }
+    if (fitOptions->saveCovariance) {
+	psFree(pcm->modelConv->covar);
+	pcm->modelConv->covar = psMemIncrRefCounter(covar);
+    }
+    psTrace ("psphot", 4, "niter: %d, chisq: %f", myMin->iter, myMin->value);
+    if (TIMING) { t3 = psTimerMark ("pmSourceFitPCM"); }
+
+    if (fitStatus && source->modelFlux) {
+        // renormalize output model image (generated by fitting process)
+        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;
+            }
+        }
+    }
+    pcm->modelConv->nIter = myMin->iter;
+    pcm->modelConv->nPar = pcm->nPar;
+
+    // save the resulting chisq, nDOF, nIter
+    if (fitOptions->poissonErrors) {
+	pcm->modelConv->chisq = myMin->value;
+	pcm->modelConv->nPix = pcm->nPix;
+	pcm->modelConv->nDOF = pcm->nDOF;
+	pcm->modelConv->chisqNorm = pcm->modelConv->chisq / pcm->modelConv->nDOF;
+    } else {
+	// xxx this is wrong because it does not convolve with the psf
+	pmPCMMakeModel (source, pcm->modelConv, pcm->nsigma, maskVal, psfSize);
+	pmSourceChisqModelFlux (source, pcm->modelConv, maskVal);
+    }
+    if (TIMING) { t4 = psTimerMark ("pmSourceFitPCM"); }
+
+    // set the model success or failure status
+    pcm->modelConv->flags |= PM_MODEL_STATUS_FITTED;
+
+    if (!fitStatus) {
+	if (isnan(myMin->value)) {
+	    pcm->modelConv->flags |= PM_MODEL_STATUS_NAN_CHISQ;
+	} else {
+	    pcm->modelConv->flags |= PM_MODEL_STATUS_NONCONVERGE;
+	}
+    }
+
+    if (myMin->chisqConvergence) {
+      if (myMin->lastDelta > myMin->minTol) pcm->modelConv->flags |= PM_MODEL_STATUS_WEAK_FIT;
+    } else {
+      if (myMin->rParSigma > myMin->minTol*pcm->nPar) pcm->modelConv->flags |= PM_MODEL_STATUS_WEAK_FIT;
+    }
+
+    // once we have fitted a model, we need to record that this model is a PCM model:
+    pcm->modelConv->isPCM = true;
+
+    // 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) pcm->modelConv->flags |= PM_MODEL_STATUS_OFFIMAGE;
+
+    source->mode |= PM_SOURCE_MODE_FITTED; // XXX is this needed?
+    if (TIMING) { t5 = psTimerMark ("pmSourceFitPCM"); }
+
+     if (TIMING) {
+ 	fprintf (stderr, "nIter: %2d, npix: %5d, t1: %6.4f, t2: %6.4f, t3: %6.4f, t4: %6.4f, t5: %6.4f\n", myMin->iter, pcm->nPix, t1, t2, t3, t4, t5);
+     }
+
+    psFree(myMin);
+    psFree(covar);
+
+    return true;
+}
+
+// XXX deprecate this function or merge with the empirical model
+bool pmSourceModelGuessPCM (pmPCMdata *pcm, pmSource *source, psImageMaskType maskVal, psImageMaskType markVal) {
+
+    if (!pcm->modelConv->class->modelGuess(pcm->modelConv, source, maskVal, markVal)) {
+	return false;
+    }
+    return true;
+
+    // the following was an attempt to make analytical modifications to the shape terms based on the psf
+    // this has been replaced with a more empirical approach
+# if (0)
+    // 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...)
+
+    // XXX test : modify the Io, SXX, SYY terms based on the psf SXX, SYY terms:
+    // SXX,SYY in model parameters are sqrt(2) * shape.Sxx,Syy
+    psEllipseShape psfShape;
+    psfShape.sx  = source->modelPSF->params->data.F32[PM_PAR_SXX] / M_SQRT2;
+    psfShape.sxy = source->modelPSF->params->data.F32[PM_PAR_SXY];
+    psfShape.sy  = source->modelPSF->params->data.F32[PM_PAR_SYY] / M_SQRT2;
+
+    psEllipseAxes psfAxes = psEllipseShapeToAxes (psfShape, 20.0);
+    if (!isfinite(psfAxes.major)) return false;
+    if (!isfinite(psfAxes.minor)) return false;
+    if (!isfinite(psfAxes.theta)) return false;
+
+    // XXX test : modify the Io, SXX, SYY terms based on the psf SXX, SYY terms:
+    psEllipseShape extShape;
+    extShape.sx  = pcm->modelConv->params->data.F32[PM_PAR_SXX] / M_SQRT2;
+    extShape.sxy = pcm->modelConv->params->data.F32[PM_PAR_SXY];
+    extShape.sy  = pcm->modelConv->params->data.F32[PM_PAR_SYY] / M_SQRT2;
+
+    psEllipseAxes extAxes = psEllipseShapeToAxes (extShape, 20.0);
+    if (!isfinite(extAxes.major)) return false;
+    if (!isfinite(extAxes.minor)) return false;
+    if (!isfinite(extAxes.theta)) return false;
+
+    // decrease the initial guess ellipse by psf_minor axis:
+    psEllipseAxes extAxesMod;
+    extAxesMod.major = sqrt (PS_MAX (0.25, PS_SQR(extAxes.major) - PS_SQR(psfAxes.minor)));
+    extAxesMod.minor = sqrt (PS_MAX (0.25, PS_SQR(extAxes.minor) - PS_SQR(psfAxes.minor)));
+    extAxesMod.theta = extAxes.theta;
+
+    psEllipseShape extShapeMod = psEllipseAxesToShape (extAxesMod);
+    if (!isfinite(extShapeMod.sx))  return false;
+    if (!isfinite(extShapeMod.sy))  return false;
+    if (!isfinite(extShapeMod.sxy)) return false;
+
+    pcm->modelConv->params->data.F32[PM_PAR_SXX] = extShapeMod.sx * M_SQRT2;
+    pcm->modelConv->params->data.F32[PM_PAR_SXY] = extShapeMod.sxy;
+    pcm->modelConv->params->data.F32[PM_PAR_SYY] = extShapeMod.sy * M_SQRT2;
+
+    // increase the initial guess central intensity by 2pi r^2:
+    pcm->modelConv->params->data.F32[PM_PAR_I0] *= (1.0 + PS_SQR(psfAxes.minor) / PS_SQR(extAxesMod.minor));
+
+    return true;
+# endif
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceFitSet.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceFitSet.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceFitSet.c	(revision 42651)
@@ -0,0 +1,636 @@
+/** @file  pmSourceFitModel.c
+ *
+ *  fit single source models to image pixels
+ *
+ *  @author EAM, IfA
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.15 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-06 02:31:25 $
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourcePhotometry.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 ***************************/
+
+// these functions currently need to use this static variable because of the way psMinimizeLMM
+// is implemented.  We could re-work that structure, but for now it is probably easier to make
+// this thread safe by pre-allocating separate static variables for each thread.
+
+static psArray *fitSets = NULL;
+static pthread_mutex_t fitSetInitMutex = PTHREAD_MUTEX_INITIALIZER;
+
+// call this before launching the threads
+bool pmSourceFitSetInit (int nThreads) {
+
+    if (!fitSets) {
+        fitSets = psArrayAlloc (PS_MAX (1, nThreads));
+    }
+
+    // the allocated elements should be NULL on psArrayAlloc,
+    // and a previously allocated array of fitSets should have been cleared
+    // before pmSourceFitSetInit is called
+    for (int i = 0; i < fitSets->n; i++) {
+        psAssert(fitSets->data[i] == NULL, "failure to init or clear fitSets?");
+    }
+    return true;
+}
+
+void pmSourceFitSetDone (void) {
+    psFree(fitSets);
+    fitSets = NULL;
+}
+
+static void pmSourceFitSetDataFree (pmSourceFitSetData *set) {
+    if (!set) return;
+
+    psFree (set->modelSet);
+    psFree (set->paramSet);
+    psFree (set->derivSet);
+    return;
+}
+
+pmSourceFitSetData *pmSourceFitSetDataAlloc (psArray *modelSet) {
+    PS_ASSERT_PTR_NON_NULL(modelSet, NULL);
+
+    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;
+    set->thread    = pthread_self();
+
+    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;
+}
+
+bool psMemCheckSourceFitSetData(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmSourceFitSetDataFree);
+}
+
+pmSourceFitSetData *pmSourceFitSetDataSet (psArray *modelSet)
+{
+    psAssert(fitSets, "pmSourceFitSetInit not called");
+
+    // find the fitSet used by this thread
+    pthread_t id = pthread_self();
+
+    // If our ID is already on the stack, abort.
+    // We do need to lock on this because someone might pull one of the fitSets out from under us
+    pthread_mutex_lock(&fitSetInitMutex);
+    for (int i = 0; i < fitSets->n; i++) {
+        pmSourceFitSetData *thisSet = fitSets->data[i];
+        if (!thisSet) continue;
+        if (thisSet->thread == id) {
+            psAbort("pmSourceFitSetDataSet() called but previous entry not cleared");
+        }
+    }
+
+    // Find an open slot
+    for (int i = 0; i < fitSets->n; i++) {
+        if (fitSets->data[i]) continue;
+        pmSourceFitSetData *thisSet = fitSets->data[i] = pmSourceFitSetDataAlloc(modelSet);
+        pthread_mutex_unlock(&fitSetInitMutex);
+        return thisSet;
+    }
+    pthread_mutex_unlock(&fitSetInitMutex);
+    psAbort("no empty slot for new pmSourceFitSetData");
+    return NULL;
+}
+
+pmSourceFitSetData *pmSourceFitSetDataGet (void) {
+
+    psAssert(fitSets, "pmSourceFitSetInit not called");
+
+    // Find the fitSet used by this thread
+    // We do need to lock on this because someone might pull one of the fitSets out from under us
+    pthread_t id = pthread_self();
+    pthread_mutex_lock(&fitSetInitMutex);
+    for (int i = 0; i < fitSets->n; i++) {
+        pmSourceFitSetData *thisSet = fitSets->data[i];
+        if (!thisSet) continue;
+        if (thisSet->thread == id) {
+            pthread_mutex_unlock(&fitSetInitMutex);
+            return thisSet;
+        }
+    }
+    psAbort("pmSourceFitSetDataGet() called, but no entry found");
+}
+
+void pmSourceFitSetDataClear (void)
+{
+    psAssert (fitSets, "pmSourceFitSetInit not called");
+
+    // Find the fitSet used by this thread
+    // We do need to lock on this because someone might pull one of the fitSets out from under us
+    pthread_t id = pthread_self();
+    pthread_mutex_lock(&fitSetInitMutex);
+    for (int i = 0; i < fitSets->n; i++) {
+        pmSourceFitSetData *thisSet = fitSets->data[i];
+        if (!thisSet) continue;
+        if (thisSet->thread == id) {
+            psFree(thisSet);
+            fitSets->data[i] = NULL;
+            pthread_mutex_unlock(&fitSetInitMutex);
+            return;
+        }
+    }
+    psAbort("pmSourceFitSetDataClear() called, but no entry found");
+
+    return;
+}
+
+// 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)
+{
+    PS_ASSERT_PTR_NON_NULL(fitSets, false);
+    pmSourceFitSetData *thisSet = pmSourceFitSetDataGet();
+
+    // 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->class->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)
+{
+    PS_ASSERT_PTR_NON_NULL(set, false);
+    PS_ASSERT_PTR_NON_NULL(set->paramSet, false);
+    PS_ASSERT_PTR_NON_NULL(set->derivSet, false);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(set->paramSet, set->derivSet, false);
+    int n = 0;
+    int sum = 0;
+    for (int i = 0; i < set->paramSet->n; i++) {
+        sum+= set->paramSet->n;
+    }
+
+    // Must assert that the deriv and param psVectors are large enough, or else
+    // a seg fault occurs.
+    // XXX: Put the correct error call in here:
+    if (0) {
+        if (deriv->n < sum || param->n < sum) {
+            PS_ASSERT_PTR_NON_NULL(0, false);
+        }
+    }
+
+    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));
+
+        // if we are setting derive, derivOne and paramOne must be same length
+        assert ((deriv == 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)
+{
+    PS_ASSERT_VECTOR_NON_NULL(param, false);
+    PS_ASSERT_PTR_NON_NULL(set, false);
+    PS_ASSERT_PTR_NON_NULL(set->paramSet, false);
+    PS_ASSERT_PTR_NON_NULL(set->derivSet, false);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(set->paramSet, set->derivSet, false);
+
+    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;
+}
+
+// set the model parameters for this fit set
+bool pmSourceFitSetValues (pmSourceFitSetData *set, 
+			   const psVector *dparam, const psVector *param, const psImage *covar, 
+			   pmSource *source, psMinimization *myMin, int nPix, 
+			   bool fitStatus, pmSourceFitOptions *options, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(set, false);
+    PS_ASSERT_PTR_NON_NULL(set->paramSet, false);
+    PS_ASSERT_VECTOR_NON_NULL(dparam, false);
+    PS_ASSERT_VECTOR_NON_NULL(param, false);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(source->pixels, false);
+    PS_ASSERT_PTR_NON_NULL(myMin, false);
+
+    bool onPic = true;
+
+    int n = 0;
+    int nStart = 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++) {
+            model->params->data.F32[j] = param->data.F32[n];
+            model->dparams->data.F32[j] = dparam->data.F32[n];
+            psTrace ("psModules.objects", 4, "%f +/- %f", param->data.F32[n], dparam->data.F32[n]);
+        }
+	if (options->saveCovariance) {
+	    // we only save the covar matrix for this object with itself (ignore cross terms between objects)
+	    model->covar = psImageAlloc(model->params->n, model->params->n, PS_TYPE_F32);
+	    for (int ix = 0; ix < model->params->n; ix++) {
+		for (int iy = 0; iy < model->params->n; iy++) {
+		    model->covar->data.F32[iy][ix] = covar->data.F32[nStart+iy][nStart+ix];
+		}
+	    }
+	}
+	nStart += model->params->n;
+        psTrace ("psModules.objects", 4, " src %d", i);
+
+	model->nIter = myMin->iter;
+	// model->nPar is set by pmSourceFitSetMasks
+
+        // save the resulting chisq, nDOF, nIter
+        // these are not unique for any one source
+	if (options->poissonErrors) {
+	    model->chisq = myMin->value;
+	    model->nPix  = nPix;
+	    model->nDOF  = nPix - model->nPar;
+	    model->chisqNorm = model->chisq / model->nDOF;
+	} else {
+	    pmSourceChisqUnsubtracted (source, model, maskVal);
+	}
+
+        // set the model success or failure status
+        model->flags |= PM_MODEL_STATUS_FITTED;
+	if (!fitStatus) {
+	  if (isnan(myMin->value)) {
+	    model->flags |= PM_MODEL_STATUS_NAN_CHISQ;
+	  } else {
+	    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)
+{
+    pmSourceFitSetData *thisSet = pmSourceFitSetDataGet();
+
+    float chisqSum = 0.0;
+    float chisqOne = 0.0;
+    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->class->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)
+{
+    PS_ASSERT_PTR_NON_NULL(set, false);
+    PS_ASSERT_PTR_NON_NULL(constraint, false);
+
+    // unmask everyone
+    psVectorInit (constraint->paramMask, 0);
+
+    int n = 0;
+    for (int i = 0; i < set->paramSet->n; i++) {
+        psVector *paramOne = set->paramSet->data[i];
+        pmModel  *modelOne = set->modelSet->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.PS_TYPE_VECTOR_MASK_DATA[n + j] = 1;
+		modelOne->nPar = 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.PS_TYPE_VECTOR_MASK_DATA[n + j] = 1;
+		modelOne->nPar = 3;
+            }
+            break;
+          case PM_SOURCE_FIT_EXT:
+            // EXT model fits all params (except sky)
+            constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[n + PM_PAR_SKY] = 1;
+	    modelOne->nPar = paramOne->n - 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,
+		     pmSourceFitOptions *options,
+                     psImageMaskType maskVal)
+{
+    psTrace("psModules.objects", 10, "---- %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->variance, 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.PS_TYPE_IMAGE_MASK_DATA[i][j] & maskVal) {
+                continue;
+            }
+            // skip zero-variance points
+            if (source->variance->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:
+            // 0.5 PIX: the coordinate values must be in pixel coords, not index
+            coord->data.F32[0] = (psF32) (j + 0.5 + source->pixels->col0);
+            coord->data.F32[1] = (psF32) (i + 0.5 + 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 variance to avoid the bias from systematic errors here we would just use the
+            // source sky variance
+            if (options->poissonErrors) {
+		yErr->data.F32[nPix] = 1.0 / source->variance->data.F32[i][j];
+	    } else {
+		yErr->data.F32[nPix] = 1.0 / options->weight;
+	    }
+	    nPix++;
+	}
+    }
+    x->n = nPix;
+    y->n = nPix;
+    yErr->n = nPix;
+
+// create the FitSet for this thread and set the initial parameter guesses
+    pmSourceFitSetData *thisSet = pmSourceFitSetDataSet(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_VECTOR_MASK);
+    constraint->checkLimits = pmSourceFitSetCheckLimits;
+
+    pmSourceFitSetMasks (constraint, thisSet, options->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.PS_TYPE_VECTOR_MASK_DATA[i]);
+	}
+    }
+
+    if (nPix <  thisSet->nParamSet + 1) {
+	psTrace (__func__, 4, "insufficient valid pixels\n");
+	psTrace("psModules.objects", 10, "---- %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);
+	pmSourceFitSetDataClear(); // frees thisSet and removes if from the array of fitSets
+	return(false);
+    }
+
+    psMinimization *myMin = psMinimizationAlloc (options->nIter, options->minTol, options->maxTol);
+    myMin->gainFactorMode = options->gainFactorMode;
+    myMin->chisqConvergence = options->chisqConvergence;
+    myMin->isInteractive = options->isInteractive;
+
+    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.PS_TYPE_VECTOR_MASK_DATA[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_VECTOR_MASK);
+	altmask->data.PS_TYPE_VECTOR_MASK_DATA[0] = 1;
+	for (int i = 1; i < dparams->n; i++) {
+	    altmask->data.PS_TYPE_VECTOR_MASK_DATA[i] = (constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[i]) ? 0 : 1;
+	}
+	psMinimizeGaussNewtonDelta(delta, params, altmask, x, y, yErr, pmSourceFitSetFunction);
+
+	for (int i = 0; i < dparams->n; i++) {
+	    if (!constraint->paramMask->data.PS_TYPE_VECTOR_MASK_DATA[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, covar, source, myMin, y->n, fitStatus, options, maskVal);
+    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);
+    pmSourceFitSetDataClear(); // frees thisSet and removes if from the array of fitSets
+
+    bool rc = (onPic && fitStatus);
+    psTrace("psModules.objects", 10, "---- %s end (%d) ----\n", __func__, rc);
+    return(rc);
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceFitSet.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceFitSet.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceFitSet.h	(revision 42651)
@@ -0,0 +1,71 @@
+/* @file  pmSourceFitSet.h
+ *
+ * @author EAM, IfA; GLG, MHPCC
+ *
+ * @version $Revision: 1.8 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-01-27 06:39:38 $
+ * 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;
+    pthread_t thread;
+} pmSourceFitSetData;
+
+// use this function to init the fit sets based on the number of threads
+bool pmSourceFitSetInit (int nThreads);
+void pmSourceFitSetDone (void);
+
+// initialize data for a group of object models
+pmSourceFitSetData *pmSourceFitSetDataAlloc (psArray *modelSet);
+bool psMemCheckSourceFitSetData(psPtr ptr);
+
+// functions for selecting the FitSet corresponding to the current thread
+pmSourceFitSetData *pmSourceFitSetDataSet (psArray *modelSet);
+pmSourceFitSetData *pmSourceFitSetDataGet (void);
+void pmSourceFitSetDataClear (void);
+
+// 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, const psImage *covar, 
+			   pmSource *source, psMinimization *myMin, int nPix, 
+			   bool fitStatus, pmSourceFitOptions *options, psImageMaskType maskVal);
+
+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
+    pmSourceFitOptions *options,	///< define options for fitting process
+    psImageMaskType maskVal             ///< Value to mask
+
+);
+
+bool pmSourcePrintModelSet (FILE *file, psArray *modelSet);
+bool pmSourceFitSetPrint (FILE *file, pmSourceFitSetData *set);
+
+/// @}
+# endif /* PM_SOURCE_FIT_MODEL_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceGroups.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceGroups.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceGroups.c	(revision 42651)
@@ -0,0 +1,225 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmSourceGroups.h"
+
+// the strategy here is to divide the image into 2x2 blocks of cells and cycle through
+// the four discontiguous sets of cells, threading all within a set and blocking between
+// sets
+
+// we divide the image region into 2*2 blocks of size Nx*Ny, the image will have
+// Cx*Cy blocks so that (2Nx)Cx = numCols, (2Ny)Cy = numRows.  We want to choose Cx and
+// Cy so that (2Nx)Cx * (2Ny)Cy = 4 * NFILL * nThreads -- each of the four sets of cells
+// has enough cells to allow NFILL cells for each thread (to better distribute heavy and
+// light load cells
+
+// the array runs from readout->image->col0 to readout->image->col0 + readout->image->numCols
+
+
+static void sourceGroupsFree(pmSourceGroups *groups)
+{
+    psFree(groups->groups);
+    return;
+}
+
+
+bool pmSourceGroupsCoordToCell(int *group, // Group number, returned
+                               int *cell,  // Cell number, returned
+                               float x, float y, // Coordinates
+                               const pmSourceGroups *groups // Groups
+                               )
+{
+    // XXX need to handle edges
+    int ix = (x - groups->Xo) / (2 * groups->Nx);
+    ix = PS_MAX(0, PS_MIN(ix, groups->Cx - 1));
+
+    int iy = (y - groups->Yo) / (2 * groups->Ny);
+    iy = PS_MAX(0, PS_MIN(iy, groups->Cy - 1));
+
+    int jx = (((int)(x - groups->Xo)) % (2 * groups->Nx)) / groups->Nx;
+    jx = PS_MAX(0, PS_MIN(jx, groups->Nx - 1));
+
+    int jy = (((int)(y - groups->Yo)) % (2 * groups->Ny)) / groups->Ny;
+    jy = PS_MAX(0, PS_MIN(jy, groups->Ny - 1));
+
+    *group = jx + 2 * jy;
+    *cell  = ix + groups->Cx * iy;
+
+    return true;
+}
+
+
+pmSourceGroups *pmSourceGroupsAlloc(const pmReadout *readout, int nThreads)
+{
+    pmSourceGroups *groups = psAlloc(sizeof(pmSourceGroups)); // Groups, to return
+    psMemSetDeallocator(groups, (psFreeFunc)sourceGroupsFree);
+
+    groups->Xo = readout->image->col0;
+    groups->Yo = readout->image->row0;
+
+    if (nThreads == 0 || nThreads == 1) {
+        // Trivial case
+        groups->Cx = groups->Cy = 1;
+        groups->Nx = readout->image->numCols;
+        groups->Ny = readout->image->numRows;
+        groups->groups = psArrayAlloc(1);
+        return groups;
+    }
+
+    int nCells = nThreads * 2*2;        // number of cells in a single set
+    int C = sqrt(nCells) + 0.5;
+    int Cx = 1, Cy = 1;                 // Number of cells in x and y
+
+    // we need to assign Cx and Cy based on the dimensionality of the image
+    // crude way to find most evenly balanced factors of nCells:
+    for (int i = C; i >= 1; i--) {
+        int C1 = nCells / C;
+        int C2 = nCells / C1;
+        if (C1*C2 != nCells) continue;
+
+        if (readout->image->numRows > readout->image->numCols) {
+            Cx = PS_MAX(C1, C2);
+            Cy = PS_MIN(C1, C2);
+        } else {
+            Cx = PS_MAX(C1, C2);
+            Cy = PS_MIN(C1, C2);
+        }
+    }
+
+    groups->Cx = Cx;
+    groups->Cy = Cy;
+    groups->Nx = readout->image->numCols / (Cx*2);
+    groups->Ny = readout->image->numRows / (Cy*2);
+    groups->groups = psArrayAlloc(4);
+
+    return groups;
+}
+
+
+
+pmSourceGroups *pmSourceGroupsFromSources(const pmReadout *readout, const psArray *sources, int nThreads)
+{
+    PM_ASSERT_READOUT_NON_NULL(readout, NULL);
+    PM_ASSERT_READOUT_IMAGE(readout, NULL);
+    PS_ASSERT_ARRAY_NON_NULL(sources, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(nThreads, NULL);
+
+    pmSourceGroups *groups = pmSourceGroupsAlloc(readout, nThreads);
+
+    int numSources = sources->n;        // Number of sources
+    psVector *x = psVectorAlloc(numSources, PS_TYPE_F32), *y = psVectorAlloc(numSources, PS_TYPE_F32);
+    for (int i = 0; i < numSources; i++) {
+        pmSource *source = sources->data[i]; // Source of interest
+        x->data.F32[i] = source->peak->xf;
+        y->data.F32[i] = source->peak->yf;
+    }
+
+    if (!pmSourceGroupsPopulate(groups, x, y)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to populate source groups");
+        psFree(x);
+        psFree(y);
+        psFree(groups);
+        return NULL;
+    }
+
+    psFree(x);
+    psFree(y);
+    return groups;
+}
+
+pmSourceGroups *pmSourceGroupsFromVectors(const pmReadout *readout, const psVector *x,
+                                          const psVector *y, int nThreads)
+{
+    PM_ASSERT_READOUT_NON_NULL(readout, NULL);
+    PM_ASSERT_READOUT_IMAGE(readout, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(x, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(y, NULL);
+    PS_ASSERT_VECTOR_TYPE(x, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTOR_TYPE(y, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(x, y, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(nThreads, NULL);
+
+    pmSourceGroups *groups = pmSourceGroupsAlloc(readout, nThreads);
+    if (!pmSourceGroupsPopulate(groups, x, y)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to populate source groups");
+        psFree(groups);
+        return NULL;
+    }
+
+    return groups;
+}
+
+
+bool pmSourceGroupsPopulate(pmSourceGroups *groups, const psVector *x, const psVector *y)
+{
+    PS_ASSERT_PTR_NON_NULL(groups, false);
+    PS_ASSERT_VECTOR_NON_NULL(x, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(y, NULL);
+    PS_ASSERT_VECTOR_TYPE(x, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTOR_TYPE(y, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(x, y, NULL);
+
+    int numSources = x->n;                // Number of sources
+
+    // Populate the groups
+    long total = groups->Cx * groups->Cy; // Total size
+    for (int i = 0; i < groups->groups->n; i++) {
+        psArray *cells = psArrayAlloc(total); // Cells within a group
+        groups->groups->data[i] = cells;
+        for (int j = 0; j < cells->n; j++) {
+            cells->data[j] = psVectorAllocEmpty(numSources / total, PS_TYPE_S32);
+        }
+    }
+
+    // Populate the cells
+    if (total == 1) {
+        // Trivial case: Cx == Cy == 1
+        psArray *cells = groups->groups->data[0]; // Cell
+        psVector *cellSources = cells->data[0];   // Indices of sources for cell
+        for (int i = 0; i < numSources; i++) {
+            cellSources->data.S32[i] = i;
+        }
+	cellSources->n = numSources;
+    } else {
+        for (int i = 0; i < numSources; i++) {
+            int group = 0, cell = 0;        // Group and cell index for source
+            pmSourceGroupsCoordToCell(&group, &cell, x->data.F32[i], y->data.F32[i], groups);
+
+            psArray *cells = groups->groups->data[group]; // Cells for group
+            psVector *cellSources = cells->data[cell];    // Indices of sources for cell within group
+            psVectorAppend(cellSources, i);
+        }
+    }
+
+    return groups;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceGroups.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceGroups.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceGroups.h	(revision 42651)
@@ -0,0 +1,50 @@
+#ifndef PM_SOURCE_GROUPS_H
+#define PM_SOURCE_GROUPS_H
+
+/// Groups of sources
+///
+/// We divide up the sources for threading.
+typedef struct {
+    int Xo, Yo;                         // Offset
+    int Nx, Ny;                         // Size of cells
+    int Cx, Cy;                         // Number of cells in x and y
+    psArray *groups;                    // Cell groups
+} pmSourceGroups;
+
+/// Allocate the source groups
+pmSourceGroups *pmSourceGroupsAlloc(
+    const pmReadout *readout,           // Readout on which the sources are defined
+    int nThreads                        // Number of threads
+    );
+
+
+/// Return the group and cell indices given x,y coordinates
+bool pmSourceGroupsCoordToCell(
+    int *group,                         // Group number, returned
+    int *cell,                          // Cell number, returned
+    float x, float y,                   // Coordinates
+    const pmSourceGroups *groups        // Groups
+    );
+
+/// Populate the source groups
+bool pmSourceGroupsPopulate(
+    pmSourceGroups *groups,              // Source groups to populate
+    const psVector *x, const psVector *y // Coordinates of sources
+    );
+
+
+/// Generate source groups from an array of sources
+pmSourceGroups *pmSourceGroupsFromSources(
+    const pmReadout *readout,           // Readout on which the sources are defined
+    const psArray *sources,             // Array of sources
+    int nThreads                        // Number of threads
+    );
+
+/// Generate source groups from vectors with source positions
+pmSourceGroups *pmSourceGroupsFromVectors(
+    const pmReadout *readout,             // Readout on which the sources are defined
+    const psVector *x, const psVector *y, // Coordinates of sources
+    int nThreads                          // Number of threads
+    );
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO.c	(revision 42651)
@@ -0,0 +1,1571 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.70 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-16 22:30:50 $
+ *
+ *  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 "pmFPAfileFitsIO.h"
+#include "pmConceptsRead.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+
+#include "pmDetections.h"
+#include "pmDetEff.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
+static bool pmReadoutReadXSRC(pmFPAfile *file, pmReadout *readout, char * exttype, psMetadata *hduHeader, psString xsrcname, psArray *sources, long *sourceIndex);
+static bool pmReadoutReadXFIT(pmFPAfile *file, pmReadout *readout, char * exttype, psMetadata *hduHeader, psString xfitname, psArray *sources, long *sourceIndex);
+static bool pmReadoutReadXRAD(pmFPAfile *file, pmReadout *readout, char * exttype, psMetadata *hduHeader, psString xfitname, psArray *sources, long *sourceIndex);
+static bool pmReadoutReadXGAL(pmFPAfile *file, pmReadout *readout, char * exttype, psMetadata *hduHeader, psString xfitname, psArray *sources, long *sourceIndex);
+
+// lookup the EXTNAME values used for table data and image header segments
+bool pmSourceIOextnames(psString *headname,    // Extension name for image header
+			psString *dataname,    // Extension name for PSF table data
+			psString *deteffname,  // Extension name for detection efficiency
+			psString *xsrcname,    // Extension name for extended non-parametric measurements
+			psString *xfitname,    // Extension name for extended fitted measurements
+			psString *xradname,    // Extension name for radial apertures
+			psString *xgalname,    // Extension name for galaxy shapes
+			const pmFPAfile *file, // File of interest
+			const pmFPAview *view  // View to level of interest
+    )
+{
+    bool status;                        // Status of MD lookup
+
+    // 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
+    if (headname) {
+        const char *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 PSF table data
+    if (dataname) {
+        const char *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);
+    }
+
+    // EXTNAME for detection efficiency
+    if (deteffname) {
+        const char *rule = psMetadataLookupStr(&status, menu, "CMF.DETEFF");
+        if (!rule) {
+            psError(PS_ERR_UNKNOWN, true, "missing entry for CMF.DETEFF in EXTNAME.RULES in camera.config");
+            return false;
+        }
+        *deteffname = pmFPAfileNameFromRule(rule, file, view);
+    }
+
+    // EXTNAME for extended source non-parametric measurements
+    if (xsrcname) {
+        const char *rule = psMetadataLookupStr(&status, menu, "CMF.XSRC");
+        if (!rule) {
+            psError(PS_ERR_UNKNOWN, true, "missing entry for CMF.XSRC in EXTNAME.RULES in camera.config");
+            return false;
+        }
+        *xsrcname = pmFPAfileNameFromRule (rule, file, view);
+    }
+
+    // EXTNAME for extended source fitted measurements
+    if (xfitname) {
+        const char *rule = psMetadataLookupStr(&status, menu, "CMF.XFIT");
+        if (!rule) {
+            psError(PS_ERR_UNKNOWN, true, "missing entry for CMF.XFIT in EXTNAME.RULES in camera.config");
+            return false;
+        }
+        *xfitname = pmFPAfileNameFromRule (rule, file, view);
+    }
+
+    // EXTNAME for radial apertures
+    if (xradname) {
+        const char *rule = psMetadataLookupStr(&status, menu, "CMF.XRAD");
+        if (!rule) {
+            psError(PS_ERR_UNKNOWN, true, "missing entry for CMF.XRAD in EXTNAME.RULES in camera.config");
+            return false;
+        }
+        *xradname = pmFPAfileNameFromRule (rule, file, view);
+    }
+
+    // EXTNAME for radial apertures
+    if (xgalname) {
+        const char *rule = psMetadataLookupStr(&status, menu, "CMF.XGAL");
+        if (!rule) {
+            psError(PS_ERR_UNKNOWN, true, "missing entry for CMF.XGAL in EXTNAME.RULES in camera.config");
+            return false;
+        }
+        *xgalname = pmFPAfileNameFromRule (rule, file, view);
+    }
+
+    return true;
+}
+
+
+// translations between psphot object types and dophot object types
+int pmSourceGetDophotType (pmSource *source)
+{
+    PS_ASSERT_PTR_NON_NULL(source, -1);
+
+    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)
+{
+    PS_ASSERT_PTR_NON_NULL(source, false);
+
+    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, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // Suitable FPA for writing
+
+    if (view->chip == -1) {
+        if (!pmFPAWriteObjects (fpa, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write objects from fpa");
+            psFree(fpa);
+            return false;
+        }
+        psFree(fpa);
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        psError(PS_ERR_UNKNOWN, false, "Writing chip == %d (>= chips->n == %ld)", view->chip, fpa->chips->n);
+        psFree(fpa);
+        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");
+            psFree(fpa);
+            return false;
+        }
+        psFree(fpa);
+        return true;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        psError(PS_ERR_UNKNOWN, false, "Writing cell == %d (>= cells->n == %ld)",
+                view->cell, chip->cells->n);
+        psFree(fpa);
+        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");
+            psFree(fpa);
+            return false;
+        }
+        psFree(fpa);
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        psError(PS_ERR_UNKNOWN, false, "Writing readout == %d (>= readouts->n == %ld)",
+                view->readout, cell->readouts->n);
+        psFree(fpa);
+        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);
+        psFree(fpa);
+        return false;
+    }
+
+    psFree(fpa);
+    return true;
+}
+
+// read in all chip-level Objects files for this FPA
+bool pmFPAWriteObjects (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fpa->chips, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        thisView->chip = i;
+        if (!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)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(chip->cells, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        thisView->cell = i;
+        if (!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)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(cell->readouts, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        thisView->readout = i;
+        if (!pmReadoutWriteObjects (readout, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write %dth readout", i);
+            psFree (thisView);
+            return false;
+        }
+    }
+    psFree (thisView);
+    return true;
+}
+
+# define PM_SOURCES_WRITE(NAME,TYPE)					\
+    if (!strcmp (exttype, NAME)) {					\
+	status = pmSourcesWrite_##TYPE(file->fits, readout, sources, file->header, outhead, dataname, recipe); \
+	if (xsrcname) {							\
+	    status &= pmSourcesWrite_##TYPE##_XSRC(file->fits, readout, sources, file->header, xsrcname, recipe); \
+	}								\
+	if (xfitname) {							\
+	    status &= pmSourcesWrite_##TYPE##_XFIT (file->fits, readout, sources, file->header, xfitname); \
+	}								\
+	if (xradname) {							\
+	    status &= pmSourcesWrite_##TYPE##_XRAD (file->fits, readout, sources, file->header, xradname, recipe); \
+	}								\
+	if (xgalname) {							\
+	    status &= pmSourcesWrite_##TYPE##_XGAL (file->fits, readout, sources, xgalname, recipe); \
+	}								\
+    }
+
+// write out all readout-level Objects files for this cell
+bool pmReadoutWriteObjects (pmReadout *readout, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    pmCell *cell = readout->parent;
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    pmChip *chip = cell->parent;
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    pmFPA *fpa = chip->parent;
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+
+    bool status;
+    pmHDU *hdu;
+    psMetadata *updates;
+
+    // if sources is NULL, write out an empty table
+    // input / output sources are stored on the readout->analysis as "PSPHOT.DETECTIONS"
+
+    psArray *sources = NULL;
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    if (detections) {
+	sources = detections->allSources;
+    }
+    if (!sources) {
+	detections = pmDetectionsAlloc();
+        sources = psArrayAlloc(0);
+	detections->allSources = sources;
+        psMetadataAddPtr(readout->analysis, PS_LIST_TAIL, "PSPHOT.DETECTIONS", PS_DATA_UNKNOWN | PS_META_REPLACE, "Blank array of sources", detections);
+        psFree(detections); // Held onto by the metadata, so we can continue to use
+    }
+
+    // the older types (RAW, OBJ, SX, CMP) are for backwards compatibility -- deprecate eventually?
+    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, fpa);
+	  if (!hdu) {
+	      psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find HDU to write sources.");
+	      return false;
+	  }
+
+	  // copy the header to an output header, add the output header data
+	  psMetadata *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, fpa);
+        if (!hdu) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find HDU to write sources.");
+            return false;
+        }
+
+        // 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;
+        }
+
+        // if none of these are TRUE, the output files only contain the psf measurements.
+	bool doPetrosian = psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_PETROSIAN");
+	bool doAnnuli    = psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_ANNULI");
+        bool XSRC_OUTPUT = doPetrosian || doAnnuli;
+        bool XFIT_OUTPUT = psMetadataLookupBool(&status, recipe, "EXTENDED_SOURCE_FITS");
+        bool XRAD_OUTPUT = psMetadataLookupBool(&status, recipe, "RADIAL_APERTURES");
+        bool XGAL_OUTPUT = psMetadataLookupBool(&status, recipe, "GALAXY_SHAPES");
+
+        // define the EXTNAME values for the different data segments:
+        psString headname = NULL;
+        psString dataname = NULL;
+        psString deteffname = NULL;
+        psString xsrcname = NULL;
+        psString xfitname = NULL;
+        psString xradname = NULL;
+        psString xgalname = NULL;
+        if (!pmSourceIOextnames(&headname, &dataname, &deteffname, 
+				XSRC_OUTPUT ? &xsrcname : NULL,
+				XFIT_OUTPUT ? &xfitname : NULL, 
+				XRAD_OUTPUT ? &xradname : NULL, 
+				XGAL_OUTPUT ? &xgalname : NULL, 
+				file, view)) {
+            return false;
+        }
+
+        // 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 strips out the NAXISn keywords, forcing CFITSIO to take care of them
+                // save NAXIS1,NAXIS2 as IMNAXIS1,IMNAXIS2
+                int numCols = 0, numRows = 0; // Size of image
+                if (readout->image) {
+                    numCols = readout->image->numCols;
+                    numRows = readout->image->numRows;
+                } else {
+                    numCols = psMetadataLookupS32(&status, hdu->header, "IMNAXIS1");
+                    if (!status) {
+                        numCols = psMetadataLookupS32(&status, hdu->header, "NAXIS1");
+                    }
+                    numRows = psMetadataLookupS32(&status, hdu->header, "IMNAXIS2");
+                    if (!status) {
+                        numRows = psMetadataLookupS32(&status, hdu->header, "NAXIS2");
+                    }
+                }
+                if (numCols == 0 || numRows == 0) {
+                    psWarning("Output source file has invalid IMNAXIS1, IMNAXIS2.");
+                }
+                psMetadataAddS32(hdu->header, PS_LIST_TAIL, "IMNAXIS1", PS_META_REPLACE,
+                                 "Number of columns in original image", numCols);
+                psMetadataAddS32(hdu->header, PS_LIST_TAIL, "IMNAXIS2", PS_META_REPLACE,
+                                 "Number of rows in original image", numRows);
+
+                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 Object TABLE data segment(s)
+        {
+            // create a header to hold the output data
+            psMetadata *outhead = psMetadataAlloc ();
+	    
+	    char *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);
+
+            // if we request XSRC output, add the XSRC name to this header
+            if (xsrcname) {
+		psMetadataAddStr (outhead, PS_LIST_TAIL, "XSRCNAME", PS_META_REPLACE, "name of XSRC table extension", xsrcname);
+            }
+            if (xfitname) {
+		psMetadataAddStr (outhead, PS_LIST_TAIL, "XFITNAME", PS_META_REPLACE, "name of XFIT table extension", xfitname);
+            }
+            if (xradname) {
+		psMetadataAddStr (outhead, PS_LIST_TAIL, "XRADNAME", PS_META_REPLACE, "name of XRAD table extension", xradname);
+            }
+            if (xgalname) {
+		psMetadataAddStr (outhead, PS_LIST_TAIL, "XGALNAME", PS_META_REPLACE, "name of XGAL table extension", xgalname);
+            }
+
+            // these are case-sensitive since the EXTYPE is case-sensitive
+            status = false;
+	    PM_SOURCES_WRITE("SMPDATA",   SMPDATA);
+	    PM_SOURCES_WRITE("PS1_DEV_0", PS1_DEV_0);
+	    PM_SOURCES_WRITE("PS1_DEV_1", PS1_DEV_1);
+	    PM_SOURCES_WRITE("PS1_CAL_0", PS1_CAL_0);
+	    PM_SOURCES_WRITE("PS1_V1",    CMF_PS1_V1);
+	    PM_SOURCES_WRITE("PS1_V2",    CMF_PS1_V2);
+	    PM_SOURCES_WRITE("PS1_V3",    CMF_PS1_V3);
+	    PM_SOURCES_WRITE("PS1_V4",    CMF_PS1_V4);
+	    PM_SOURCES_WRITE("PS1_V5",    CMF_PS1_V5);
+	    PM_SOURCES_WRITE("PS1_SV1",   CMF_PS1_SV1);
+	    PM_SOURCES_WRITE("PS1_SV2",   CMF_PS1_SV2);
+	    PM_SOURCES_WRITE("PS1_SV3",   CMF_PS1_SV3);
+	    PM_SOURCES_WRITE("PS1_SV4",   CMF_PS1_SV4);
+	    PM_SOURCES_WRITE("PS1_DV1",   CMF_PS1_DV1);
+	    PM_SOURCES_WRITE("PS1_DV2",   CMF_PS1_DV2);
+	    PM_SOURCES_WRITE("PS1_DV3",   CMF_PS1_DV3);
+	    PM_SOURCES_WRITE("PS1_DV4",   CMF_PS1_DV4);
+	    PM_SOURCES_WRITE("PS1_DV5",   CMF_PS1_DV5);
+
+	    psFree (outhead);
+	    psFree (exttype);
+
+            if (!status) {
+                psError(PS_ERR_IO, false, "writing CMF data to %s with format %s\n", file->filename, exttype);
+		goto escape;
+            }
+        }
+
+	// write out the detection efficiency TABLE segments
+	if (deteffname) {
+            // create a header to hold the output data
+            psMetadata *outhead = psMetadataAlloc ();
+            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", "DETEFF");
+
+	    status = pmReadoutWriteDetEff(file->fits, readout, outhead, deteffname);
+	    psFree (outhead);
+
+            if (!status) {
+                psError(PS_ERR_IO, false, "writing DETEFF data to %s\n", file->filename);
+		goto escape;
+            }
+	}
+	psFree (headname);
+	psFree (dataname);
+	psFree (xsrcname);
+	psFree (xfitname);
+	psFree (xradname);
+	psFree (xgalname);
+	psFree (deteffname);
+
+        psTrace ("pmFPAfile", 5, "wrote ext data %s (type: %d)\n", file->filename, file->type);
+        break;
+
+      escape:
+	psFree (headname);
+	psFree (dataname);
+	psFree (xsrcname);
+	psFree (xfitname);
+	psFree (xradname);
+	psFree (xgalname);
+	psFree (deteffname);
+	return false;
+
+      case PM_FPA_FILE_CFF: {
+        // 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;
+        }
+
+        hdu = pmFPAviewThisHDU (view, fpa);
+        pmConfigConformHeader(hdu->header, file->format);
+        psFitsWriteBlank (file->fits, hdu->header, NULL);
+        file->header = hdu->header;
+        file->wrote_phu = true;
+	if (!pmSourcesWrite_CFF(readout, file->fits, sources, hdu->header, recipe)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to write CFF");
+            return false;
+        }
+        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, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    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) {
+        pmSourceIO_WriteMatchedRefs (file->fits, file->fpa, config);
+        return true;
+    }
+
+    // find the FPA phu
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // Suitable FPA for writing
+    pmHDU *phu = psMemIncrRefCounter(pmFPAviewThisPHU(view, 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);
+    }
+    psFree(phu);
+
+    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....
+    // XXX why are these not correctly inserted by pmConfigConformHeader??
+
+    // Because these concepts are not part of the "RULE" in the camera format, pmConfigConformHeader only adds
+    // what is required by the "RULE".
+    // Though we can configure Ohana to look at particular header keywords, it doesn't like having the
+    // important values in "HIERARCH FPA.TIME", etc, as is done for skycells, so we stoop to its level,
+    // putting in additional header keywords that it can understand.
+
+    psMetadata *headers = psMetadataLookupMetadata(NULL, 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);
+        psFree(fpa);
+        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);
+            psFree(fpa);
+            return false;
+        }
+        psTime *time = psMetadataLookupTime(NULL, 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);
+            psFree(fpa);
+            return false;
+        }
+        float exptime = psMetadataLookupF32(NULL, 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);
+            psFree(fpa);
+            return false;
+        }
+        float airmass = psMetadataLookupF32(NULL, 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(&status, fileData, "FPA.OBS");
+    if (fpaNameHdr && strlen(fpaNameHdr) > 0) {
+        const char *fpaName = psMetadataLookupStr(&status, fpa->concepts, "FPA.OBS");
+	if (fpaName) {
+	    psMetadataAddStr(outhead, PS_LIST_TAIL, fpaNameHdr, PS_META_REPLACE, "FPA observation identifier", fpaName);
+	}
+    }
+
+    // if we have mosaic-level astrometry information, add it here:
+    psMetadata *updates = psMetadataLookupPtr (&status, fpa->analysis, "PSASTRO.HEADER");
+    if (updates) {
+        psMetadataCopy (outhead, updates);
+    }
+    psFree(fpa);
+
+    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);
+
+    pmSourceIO_WriteMatchedRefs (file->fits, file->fpa, config);
+    pmSourceIO_WriteGlints (file->fits, file->fpa, config);
+    pmSourceIO_WriteGhosts (file->fits, file->fpa, config);
+    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)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+
+    pmFPA *fpa = file->fpa;
+
+    pmSourceIO_ReadMatchedRefs (file->fits, fpa, config);
+
+    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)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa->chips, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        thisView->chip = i;
+        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)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(chip->cells, false);
+
+    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)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(cell->readouts, false);
+
+    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)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    bool status;
+    psArray *sources = NULL;
+    pmHDU *hdu;
+
+    // define the EXTNAME values for the different data segments:
+    psString headname = NULL;
+    psString dataname = NULL;
+    psString deteffname = NULL;
+    psString xsrcname = NULL;
+    psString xfitname = NULL;
+    psString xradname = NULL;
+    psString xgalname = NULL;
+
+    psMetadata *tableHeader = NULL;
+    char *xtension = NULL;
+
+    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:
+      case PM_FPA_FILE_WCS:  // "WCS" is CMF without detected objects
+        // read in header, if not yet loaded
+        hdu = pmFPAviewThisHDU (view, file->fpa);
+
+        // determine the output table format. Assume if we need to output extendend source
+        // parameters that they may exist in the input. 
+        // XXX: Perhaps we should use different recipe values.
+        // I.E. EXTENDED_SOURCE_ANALYSIS_READ or something like that
+        psMetadata *recipe = psMetadataLookupMetadata(&status, config->recipes, "PSPHOT");
+        if (!status) {
+	    psError(PS_ERR_UNKNOWN, true, "missing recipe PSPHOT in config data");
+	    return false;
+        }
+
+        // if none of these are TRUE, we only read the psf measurements
+        // XXX: shouldn't we look for these extensions and read the regardless of the recipe values?
+	bool doPetrosian = psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_PETROSIAN");
+	bool doAnnuli    = psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_ANNULI");
+        bool XSRC_OUTPUT = doPetrosian || doAnnuli;
+        bool XFIT_OUTPUT = psMetadataLookupBool(&status, recipe, "EXTENDED_SOURCE_FITS");
+        bool XRAD_OUTPUT = psMetadataLookupBool(&status, recipe, "RADIAL_APERTURES");
+        bool XGAL_OUTPUT = psMetadataLookupBool(&status, recipe, "GALAXY_SHAPES");
+
+        if (!pmSourceIOextnames(&headname, &dataname, &deteffname, 
+                XSRC_OUTPUT ? &xsrcname : NULL, 
+                XFIT_OUTPUT ? &xfitname : NULL, 
+                XRAD_OUTPUT ? &xradname : NULL,
+                XGAL_OUTPUT ? &xgalname : NULL,
+                file, view)) {
+            return false;
+        }
+
+        bool saveSourcesHeader = psMetadataLookupBool(&status, recipe, "SAVE.INPUT.SOURCES.HEADER");;
+        // advance to the IMAGE HEADER extension
+        if (saveSourcesHeader || hdu->header == NULL) {
+            // if the IMAGE header does not exist, we have no data for this view
+            if (!psFitsMoveExtNameClean (file->fits, headname)) {
+                if (hdu->header == NULL) {
+                    readout->data_exists = false;
+                }
+                psFree (headname);
+                psFree (dataname);
+                psFree (deteffname);
+                return true;
+            }
+            psMetadata *sourcesHeader = psFitsReadHeader (NULL, file->fits);
+            // Save the hdu header for the sources on readout->analysis. 
+            // psphotStack uses this in updateMode
+            psMetadataAddPtr(readout->analysis, PS_LIST_TAIL, "INPUT.SOURCES.HEADER", 
+                PS_DATA_UNKNOWN | PS_META_REPLACE, "fits header from input sources file", sourcesHeader);
+            if (hdu->header == NULL) {
+                hdu->header = sourcesHeader;
+            } else {
+                psFree(sourcesHeader);
+            }
+        }
+
+	// "WCS" is CMF without detected objects
+	if (file->type == PM_FPA_FILE_WCS) {
+	  psTrace("psModules.objects", 6, "read CMF table from %s : %s : %s", file->filename, headname, dataname);
+	  psFree (headname);
+	  psFree (dataname);
+	  psFree (deteffname);
+	  break;
+	}
+
+	// EXTDATA is the PSF data associated with this image header
+
+        // we need to find the corresponding table EXTNAME.
+        // first check the header
+        char *extdata = psMetadataLookupStr (&status, 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);
+        }
+
+        tableHeader = psFitsReadHeader(NULL, file->fits); // The FITS header
+        if (!tableHeader) psAbort("cannot read table header");
+
+        xtension = psMetadataLookupStr (NULL, tableHeader, "XTENSION");
+        if (!xtension) psAbort("cannot read table type");
+	if (strcmp (xtension, "BINTABLE")) {
+	    psWarning ("no binary table in extension %s, skipping\n", dataname);
+            psFree(tableHeader);
+	    return false;
+	}
+
+        char *exttype = psMetadataLookupStr (NULL, tableHeader, "EXTTYPE");
+        if (!exttype) psAbort("cannot read table type");
+
+# define PM_SOURCES_READ_PSF(NAME,TYPE)				 \
+	if (!strcmp (exttype, NAME)) {				 \
+	    sources = pmSourcesRead_##TYPE(file->fits, hdu->header);	\
+	}									
+
+        // XXX these are case-sensitive since the EXTYPE is case-sensitive
+        if (file->type == PM_FPA_FILE_CMF) {
+	    PM_SOURCES_READ_PSF("SMPDATA",   SMPDATA);
+	    PM_SOURCES_READ_PSF("PS1_DEV_0", PS1_DEV_0);
+	    PM_SOURCES_READ_PSF("PS1_DEV_1", PS1_DEV_1);
+	    PM_SOURCES_READ_PSF("PS1_CAL_0", PS1_CAL_0);
+	    PM_SOURCES_READ_PSF("PS1_V1",    CMF_PS1_V1);
+	    PM_SOURCES_READ_PSF("PS1_V2",    CMF_PS1_V2);
+	    PM_SOURCES_READ_PSF("PS1_V3",    CMF_PS1_V3);
+	    PM_SOURCES_READ_PSF("PS1_V4",    CMF_PS1_V4);
+	    PM_SOURCES_READ_PSF("PS1_V5",    CMF_PS1_V5);
+	    PM_SOURCES_READ_PSF("PS1_SV1",   CMF_PS1_SV1);
+	    PM_SOURCES_READ_PSF("PS1_SV2",   CMF_PS1_SV2);
+	    PM_SOURCES_READ_PSF("PS1_SV3",   CMF_PS1_SV3);
+	    PM_SOURCES_READ_PSF("PS1_SV4",   CMF_PS1_SV4);
+	    PM_SOURCES_READ_PSF("PS1_DV1",   CMF_PS1_DV1);
+	    PM_SOURCES_READ_PSF("PS1_DV2",   CMF_PS1_DV2);
+	    PM_SOURCES_READ_PSF("PS1_DV3",   CMF_PS1_DV3);
+	    PM_SOURCES_READ_PSF("PS1_DV4",   CMF_PS1_DV4);
+	    PM_SOURCES_READ_PSF("PS1_DV5",   CMF_PS1_DV5);
+
+            if (!sources) {
+                psError(PS_ERR_IO, false, "reading CMF data from %s with format %s\n", file->filename, exttype);
+		return false;
+            }
+
+            long *sourceIndex = NULL;
+            if (XSRC_OUTPUT || XFIT_OUTPUT || XRAD_OUTPUT || XGAL_OUTPUT) {
+                // Build sourceIndex. Lookup table from source->seq to index in sources array.
+                // Consists of an array of length max(source->seq) + 1.
+
+                // find maximum sequence number
+                long seq_max = -1;
+                for (long i = sources->n -1; i >= 0; i--) {
+                    pmSource *source = sources->data[i];
+                    if (source->seq < 0) {
+                        // This can happen cmf files that have been corrupted
+                        psError(PS_ERR_IO, true, "seq < 0 for source %ld: Suspect %s is corrupt", i, file->origname);
+                        return false;
+                    }
+                    if (source->seq > seq_max) {
+                        seq_max = source->seq;
+                    }
+                }
+                // allocate and initialize the index
+                sourceIndex = psAlloc((seq_max + 1) * sizeof(long));
+                for (long i = 0; i < seq_max; i++) {
+                    sourceIndex[i] = -1;
+                }
+                // populate the index
+                for (long i = 0; i < sources->n; i++) {
+                    pmSource *source = sources->data[i];
+                    sourceIndex[source->seq] = i;
+                }
+            }
+            if (XSRC_OUTPUT && xsrcname) {
+		// a cmf file may have an XSRC extension, but it is not required
+                if (!pmReadoutReadXSRC(file, readout, exttype, hdu->header, xsrcname, sources, sourceIndex)) {
+		    // do anything?
+                }
+                psFree(xsrcname);
+            }
+            if (XFIT_OUTPUT && xfitname) {
+		// a cmf file may have an XFIT extension, but it is not required
+                if (!pmReadoutReadXFIT(file, readout, exttype, hdu->header, xfitname, sources, sourceIndex)) {
+		    // do anything?
+                }
+                psFree(xfitname);
+            }
+            if (XRAD_OUTPUT && xradname) {
+		// a cmf file may have an XRAD extension, but it is not required
+                if (!pmReadoutReadXRAD(file, readout, exttype, hdu->header, xradname, sources, sourceIndex)) {
+		    // do anything?
+                }
+                psFree(xradname);
+            }
+            if (XGAL_OUTPUT && xgalname) {
+		// a cmf file may have an XGAL extension, but it is not required
+                if (!pmReadoutReadXGAL(file, readout, exttype, hdu->header, xgalname, sources, sourceIndex)) {
+		    // do anything?
+                }
+                psFree(xgalname);
+            }
+            psFree(sourceIndex);
+
+            if (!pmReadoutReadDetEff(file->fits, readout, deteffname)) {
+#if 0
+                psError(PS_ERR_IO, false, "Unable to read detection efficiency");
+                return false;
+#else
+                // No great loss
+                psErrorClear();
+#endif
+            }
+        }
+
+        psTrace("psModules.objects", 6, "read CMF table from %s : %s : %s", file->filename, headname, dataname);
+        psFree (headname);
+        psFree (dataname);
+	psFree (deteffname);
+        psFree (tableHeader);
+        break;
+
+      case PM_FPA_FILE_CFF: {
+        // 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;
+        }
+        // read in header, if not yet loaded
+        hdu = pmFPAviewThisHDU (view, file->fpa);
+
+	// look these up in the camera config?
+	// headrule = {CHIP.NAME}.hdr
+	// datarule = {CHIP.NAME}.cff
+
+        // define the EXTNAME values for the different data segments:
+        headname = pmFPAfileNameFromRule("{CHIP.NAME}.hdr", file, view);
+        dataname = pmFPAfileNameFromRule("{CHIP.NAME}.cff", 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 (!psFitsMoveExtNameClean (file->fits, headname)) {
+                readout->data_exists = false;
+                psFree (headname);
+                psFree (dataname);
+                return true;
+            }
+            hdu->header = psFitsReadHeader (NULL, file->fits);
+        }
+
+        // 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);
+        }
+
+        tableHeader = psFitsReadHeader(NULL, file->fits); // The FITS header
+        if (!tableHeader) psAbort("cannot read table header");
+
+	// verify this is a binary table
+        char *xtension = psMetadataLookupStr (NULL, tableHeader, "XTENSION");
+        if (!xtension) psAbort("cannot read table type");
+	if (strcmp (xtension, "BINTABLE")) {
+	    psWarning ("no binary table in extension %s, skipping\n", dataname);
+            psFree(tableHeader);
+	    return false;
+	}
+
+	sources = pmSourcesRead_CFF(file->fits, hdu->header, recipe);
+
+        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;
+
+    // if we have a prior set of detections on this readout, we will replace them here
+    pmDetections *detections = pmDetectionsAlloc();
+    detections->allSources = sources;
+    status = psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSPHOT.DETECTIONS", PS_DATA_ARRAY | PS_META_REPLACE, "input sources", detections);
+    psFree (detections);
+    return true;
+}
+
+bool pmFPAviewCheckDataStatusForSources (const pmFPAview *view, const pmFPAfile *file)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        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)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fpa->chips, false);
+
+    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)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(chip->cells, false);
+
+    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)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(cell->readouts, false);
+
+    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)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+
+    bool status;
+
+    // select the detections of interest
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    if (!detections) return false;
+    if (!detections->allSources) return false;
+    return true;
+}
+
+// XXX: We might be able to macroize this and reuse for the other types
+
+static bool pmReadoutReadXSRC(pmFPAfile *file, pmReadout *readout, char *exttype, psMetadata *hduHeader, psString xsrcname, psArray *sources, long *sourceIndex) 
+{
+    if (!psFitsMoveExtNameClean (file->fits, xsrcname)) {
+        psTrace ("pmFPAfile", 1, "cannot find xsrc extension %s in %s, skipping", xsrcname, file->filename);
+        return false;
+    }
+
+    psMetadata *tableHeader = psFitsReadHeader(NULL, file->fits); // The FITS header
+    if (!tableHeader) psAbort("cannot read table header");
+
+    char *xtension = psMetadataLookupStr (NULL, tableHeader, "XTENSION");
+    if (!xtension) psAbort("cannot read table type");
+    if (strcmp (xtension, "BINTABLE")) {
+        psFree(tableHeader);
+        psWarning ("no binary table in extension %s, skipping\n", xsrcname);
+        return false;
+    }
+
+# define PM_SOURCES_READ_XSRC(NAME,TYPE)				\
+    if (!strcmp (exttype, NAME)) {					\
+	status = pmSourcesRead_##TYPE##_XSRC(file->fits, readout, hduHeader, tableHeader, sources, sourceIndex); \
+    }									
+
+    bool status = false;
+    if (file->type == PM_FPA_FILE_CMF) {
+	PM_SOURCES_READ_XSRC("PS1_V1",    CMF_PS1_V1);
+	PM_SOURCES_READ_XSRC("PS1_V2",    CMF_PS1_V2);
+	PM_SOURCES_READ_XSRC("PS1_V3",    CMF_PS1_V3);
+	PM_SOURCES_READ_XSRC("PS1_V4",    CMF_PS1_V4);
+	PM_SOURCES_READ_XSRC("PS1_V5",    CMF_PS1_V5);
+	PM_SOURCES_READ_XSRC("PS1_SV1",   CMF_PS1_SV1);
+	PM_SOURCES_READ_XSRC("PS1_SV2",   CMF_PS1_SV2);
+	PM_SOURCES_READ_XSRC("PS1_SV3",   CMF_PS1_SV3);
+	PM_SOURCES_READ_XSRC("PS1_SV4",   CMF_PS1_SV4);
+	PM_SOURCES_READ_XSRC("PS1_DV1",   CMF_PS1_DV1);
+	PM_SOURCES_READ_XSRC("PS1_DV2",   CMF_PS1_DV2);
+	PM_SOURCES_READ_XSRC("PS1_DV3",   CMF_PS1_DV3);
+	PM_SOURCES_READ_XSRC("PS1_DV4",   CMF_PS1_DV4);
+	PM_SOURCES_READ_XSRC("PS1_DV5",   CMF_PS1_DV5);
+    }
+    psFree(tableHeader);
+    return status;
+}
+
+static bool pmReadoutReadXFIT(pmFPAfile *file, pmReadout *readout, char *exttype, psMetadata *hduHeader, psString extname, psArray *sources, long *sourceIndex) 
+{
+    if (!psFitsMoveExtNameClean (file->fits, extname)) {
+        psTrace ("pmFPAfile", 1, "cannot find xfit extension %s in %s, skipping", extname, file->filename);
+        return false;
+    }
+
+    psMetadata *tableHeader = psFitsReadHeader(NULL, file->fits); // The FITS header
+    if (!tableHeader) psAbort("cannot read table header");
+
+    char *xtension = psMetadataLookupStr (NULL, tableHeader, "XTENSION");
+    if (!xtension) psAbort("cannot read table type");
+    if (strcmp (xtension, "BINTABLE")) {
+        psWarning ("no binary table in extension %s, skipping\n", extname);
+        psFree(tableHeader);
+        return false;
+    }
+
+# define PM_SOURCES_READ_XFIT(NAME,TYPE)				\
+    if (!strcmp (exttype, NAME)) {					\
+	status = pmSourcesRead_##TYPE##_XFIT(file->fits, readout, hduHeader, tableHeader, sources, sourceIndex); \
+    }									
+
+    bool status = false;
+    if (file->type == PM_FPA_FILE_CMF) {
+	PM_SOURCES_READ_XFIT("PS1_V1",    CMF_PS1_V1);
+	PM_SOURCES_READ_XFIT("PS1_V2",    CMF_PS1_V2);
+	PM_SOURCES_READ_XFIT("PS1_V3",    CMF_PS1_V3);
+	PM_SOURCES_READ_XFIT("PS1_V4",    CMF_PS1_V4);
+	PM_SOURCES_READ_XFIT("PS1_V5",    CMF_PS1_V5);
+	PM_SOURCES_READ_XFIT("PS1_SV1",   CMF_PS1_SV1);
+	PM_SOURCES_READ_XFIT("PS1_SV2",   CMF_PS1_SV2);
+	PM_SOURCES_READ_XFIT("PS1_SV3",   CMF_PS1_SV3);
+	PM_SOURCES_READ_XFIT("PS1_SV4",   CMF_PS1_SV4);
+	PM_SOURCES_READ_XFIT("PS1_DV1",   CMF_PS1_DV1);
+	PM_SOURCES_READ_XFIT("PS1_DV2",   CMF_PS1_DV2);
+	PM_SOURCES_READ_XFIT("PS1_DV3",   CMF_PS1_DV3);
+	PM_SOURCES_READ_XFIT("PS1_DV4",   CMF_PS1_DV4);
+	PM_SOURCES_READ_XFIT("PS1_DV5",   CMF_PS1_DV5);
+    }
+    psFree(tableHeader);
+    return status;
+}
+static bool pmReadoutReadXRAD(pmFPAfile *file, pmReadout *readout, char *exttype, psMetadata *hduHeader, psString extname, psArray *sources, long *sourceIndex) 
+{
+    if (!psFitsMoveExtNameClean (file->fits, extname)) {
+        psTrace ("pmFPAfile", 1, "cannot find xrad extension %s in %s, skipping", extname, file->filename);
+        return false;
+    }
+
+    psMetadata *tableHeader = psFitsReadHeader(NULL, file->fits); // The FITS header
+    if (!tableHeader) psAbort("cannot read table header");
+
+    char *xtension = psMetadataLookupStr (NULL, tableHeader, "XTENSION");
+    if (!xtension) psAbort("cannot read table type");
+    if (strcmp (xtension, "BINTABLE")) {
+        psWarning ("no binary table in extension %s, skipping\n", extname);
+        psFree(tableHeader);
+        return false;
+    }
+
+# define PM_SOURCES_READ_XRAD(NAME,TYPE)				\
+    if (!strcmp (exttype, NAME)) {					\
+	status = pmSourcesRead_##TYPE##_XRAD(file->fits, readout, hduHeader, tableHeader, sources, sourceIndex); \
+    }									
+
+    bool status = false;
+    if (file->type == PM_FPA_FILE_CMF) {
+	PM_SOURCES_READ_XRAD("PS1_V1",    CMF_PS1_V1);
+	PM_SOURCES_READ_XRAD("PS1_V2",    CMF_PS1_V2);
+	PM_SOURCES_READ_XRAD("PS1_V3",    CMF_PS1_V3);
+	PM_SOURCES_READ_XRAD("PS1_V4",    CMF_PS1_V4);
+	PM_SOURCES_READ_XRAD("PS1_V5",    CMF_PS1_V5);
+	PM_SOURCES_READ_XRAD("PS1_SV1",   CMF_PS1_SV1);
+	PM_SOURCES_READ_XRAD("PS1_SV2",   CMF_PS1_SV2);
+	PM_SOURCES_READ_XRAD("PS1_SV3",   CMF_PS1_SV3);
+	PM_SOURCES_READ_XRAD("PS1_SV4",   CMF_PS1_SV4);
+	PM_SOURCES_READ_XRAD("PS1_DV1",   CMF_PS1_DV1);
+	PM_SOURCES_READ_XRAD("PS1_DV2",   CMF_PS1_DV2);
+	PM_SOURCES_READ_XRAD("PS1_DV3",   CMF_PS1_DV3);
+	PM_SOURCES_READ_XRAD("PS1_DV4",   CMF_PS1_DV4);
+	PM_SOURCES_READ_XRAD("PS1_DV5",   CMF_PS1_DV5);
+    }
+    psFree(tableHeader);
+    return status;
+}
+static bool pmReadoutReadXGAL(pmFPAfile *file, pmReadout *readout, char *exttype, psMetadata *hduHeader, psString xgalname, psArray *sources, long *sourceIndex) 
+{
+    if (!psFitsMoveExtNameClean (file->fits, xgalname)) {
+        psTrace ("pmFPAfile", 1, "cannot find xgal extension %s in %s, skipping", xgalname, file->filename);
+        return false;
+    }
+
+    psMetadata *tableHeader = psFitsReadHeader(NULL, file->fits); // The FITS header
+    if (!tableHeader) psAbort("cannot read table header");
+
+    char *xtension = psMetadataLookupStr (NULL, tableHeader, "XTENSION");
+    if (!xtension) psAbort("cannot read table type");
+    if (strcmp (xtension, "BINTABLE")) {
+        psFree(tableHeader);
+        psWarning ("no binary table in extension %s, skipping\n", xgalname);
+        return false;
+    }
+
+# define PM_SOURCES_READ_XGAL(NAME,TYPE)				\
+    if (!strcmp (exttype, NAME)) {					\
+	status = pmSourcesRead_##TYPE##_XGAL(file->fits, readout, hduHeader, tableHeader, sources, sourceIndex); \
+    }									
+
+    bool status = false;
+    if (file->type == PM_FPA_FILE_CMF) {
+	PM_SOURCES_READ_XGAL("PS1_V1",    CMF_PS1_V1);
+	PM_SOURCES_READ_XGAL("PS1_V2",    CMF_PS1_V2);
+	PM_SOURCES_READ_XGAL("PS1_V3",    CMF_PS1_V3);
+	PM_SOURCES_READ_XGAL("PS1_V4",    CMF_PS1_V4);
+	PM_SOURCES_READ_XGAL("PS1_V5",    CMF_PS1_V5);
+	PM_SOURCES_READ_XGAL("PS1_SV1",   CMF_PS1_SV1);
+	PM_SOURCES_READ_XGAL("PS1_SV2",   CMF_PS1_SV2);
+	PM_SOURCES_READ_XGAL("PS1_SV3",   CMF_PS1_SV3);
+	PM_SOURCES_READ_XGAL("PS1_SV4",   CMF_PS1_SV4);
+	PM_SOURCES_READ_XGAL("PS1_DV1",   CMF_PS1_DV1);
+	PM_SOURCES_READ_XGAL("PS1_DV2",   CMF_PS1_DV2);
+	PM_SOURCES_READ_XGAL("PS1_DV3",   CMF_PS1_DV3);
+	PM_SOURCES_READ_XGAL("PS1_DV4",   CMF_PS1_DV4);
+	PM_SOURCES_READ_XGAL("PS1_DV5",   CMF_PS1_DV5);
+    }
+    psFree(tableHeader);
+    return status;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO.h	(revision 42651)
@@ -0,0 +1,98 @@
+/* @file  pmSourceIO.h
+ * @brief functions to read and write object files
+ *
+ * @author EAM, IfA; GLG, MHPCC
+ *
+ * @version $Revision: 1.20 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-16 22:30:50 $
+ * 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
+/// @{
+
+# define MK_PROTO(TYPE) \
+  bool pmSourcesWrite_##TYPE(psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, psMetadata *tableHeader, char *extname, psMetadata *recipe); \
+  bool pmSourcesWrite_##TYPE##_XSRC(psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname, psMetadata *recipe); \
+  bool pmSourcesWrite_##TYPE##_XFIT(psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname); \
+  bool pmSourcesWrite_##TYPE##_XRAD(psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname, psMetadata *recipe); \
+  bool pmSourcesWrite_##TYPE##_XGAL(psFits *fits, pmReadout *readout, psArray *sources, char *extname, psMetadata *recipe); \
+  psArray *pmSourcesRead_##TYPE (psFits *fits, psMetadata *header); \
+  bool pmSourcesRead_##TYPE##_XSRC (psFits *fits, pmReadout *readout, psMetadata *header, psMetadata *tableHeader, psArray *sources, long *index); \
+  bool pmSourcesRead_##TYPE##_XFIT (psFits *fits, pmReadout *readout, psMetadata *header, psMetadata *tableHeader, psArray *sources, long *index); \
+  bool pmSourcesRead_##TYPE##_XRAD (psFits *fits, pmReadout *readout, psMetadata *header, psMetadata *tableHeader, psArray *sources, long *index);\
+  bool pmSourcesRead_##TYPE##_XGAL (psFits *fits, pmReadout *readout, psMetadata *header, psMetadata *tableHeader, psArray *sources, long *index);\
+  
+// All of these functions need to use the same API, even if not all elements are used in a specific case
+MK_PROTO(SMPDATA);
+MK_PROTO(PS1_DEV_0);
+MK_PROTO(PS1_DEV_1);
+MK_PROTO(PS1_CAL_0);
+MK_PROTO(CMF_PS1_V1);
+MK_PROTO(CMF_PS1_V2);
+MK_PROTO(CMF_PS1_V3);
+MK_PROTO(CMF_PS1_V4);
+MK_PROTO(CMF_PS1_V5);
+MK_PROTO(CMF_PS1_SV1);
+MK_PROTO(CMF_PS1_SV2);
+MK_PROTO(CMF_PS1_SV3);
+MK_PROTO(CMF_PS1_SV4);
+MK_PROTO(CMF_PS1_DV1);
+MK_PROTO(CMF_PS1_DV2);
+MK_PROTO(CMF_PS1_DV3);
+MK_PROTO(CMF_PS1_DV4);
+MK_PROTO(CMF_PS1_DV5);
+
+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 pmSource_CMF_WritePHU (const pmFPAview *view, pmFPAfile *file, pmConfig *config);
+
+psArray *pmSourcesReadCMP (char *filename, psMetadata *header);
+psArray *pmSourcesRead_CFF (psFits *fits, psMetadata *header, psMetadata *recipe);
+bool pmSourcesWrite_CFF (pmReadout *readout, psFits *fits, psArray *sources, psMetadata *header, psMetadata *recipe);
+
+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, 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);
+
+bool pmSourceLocalAstrometry (psSphere *ptSky, float *posAngle, float *pltScale, pmChip *chip, float xPos, float yPos);
+
+bool pmSourceIO_WriteMatchedRefs (psFits *fits, pmFPA *fpa, pmConfig *config);
+bool pmSourceIO_ReadMatchedRefs (psFits *fits, pmFPA *fpa, const pmConfig *config);
+
+bool pmSourceZeroPointFromRecipeGlint (float *zeropt, float *exptime, float *ghostMaxMag, double *glintMaxMag, pmFPA *fpa, psMetadata *recipe);
+bool pmSourceIO_WriteGlints (psFits *fits, pmFPA *fpa, pmConfig *config);
+bool pmSourceIO_WriteGhosts (psFits *fits, pmFPA *fpa, pmConfig *config);
+
+/// @}
+# endif /* PM_SOURCE_IO_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_CFF.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_CFF.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_CFF.c	(revision 42651)
@@ -0,0 +1,549 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-18 02:44:19 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+
+#include "pmSourceIO.h"
+#include "pmSourceOutputs.h"
+
+// read in sources readout from a cff fits file
+psArray *pmSourcesRead_CFF (psFits *fits, psMetadata *header, psMetadata *recipe)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(header, false);
+
+    bool status;
+    psEllipseAxes axes;
+
+    // define PSF model type
+    // XXX need to carry the extra model parameters
+    int modelType = pmModelClassGetType ("PS_MODEL_GAUSS");
+
+    // Read lookup table for model classes (if defined)
+    pmModelClassReadHeader(header);
+
+    char *PSF_NAME = psMetadataLookupStr (&status, header, "PSFMODEL");
+    if (PSF_NAME != NULL) {
+        modelType = pmModelClassGetType (PSF_NAME);
+    }
+    PS_ASSERT_INT_NONNEGATIVE(modelType, NULL);
+
+    pmModelType sersicModelType = pmModelClassGetType("PS_MODEL_SERSIC");
+    pmModelType devModelType    = pmModelClassGetType("PS_MODEL_DEV");
+
+    psString modelForce = psMetadataLookupStr(&status, recipe, "EXT_MODEL_TYPE_FORCE");
+    psF32 forceDevSersicMin = NAN;
+    bool forceAll = false;
+    int modelTypeForce = 0;
+    if (!strcmp(modelForce, "ALL")) {
+        forceAll = true;
+    } else if (!strcmp(modelForce, "PS_MODEL_SERSIC")) {
+        modelTypeForce = sersicModelType;
+        forceDevSersicMin = psMetadataLookupF32(&status, recipe, "EXT_MODEL_FORCE_DEV_SERSIC_MIN");
+    } else {
+        modelTypeForce = pmModelClassGetType(modelForce);
+        PS_ASSERT_INT_NONNEGATIVE(modelTypeForce, NULL);
+    }
+
+    // skip extended model types for likely stars
+    // max value of KronMag - psfMag to keep ...
+    psF32 starCut = psMetadataLookupF32(&status, recipe, "EXT_MODEL_FORCE_MAGDIFF_MAX");
+    if (!status) {
+        starCut = 0;
+    }
+    // ... unless SN is less than this value
+    psF32 SNMinForCut = psMetadataLookupF32(&status, recipe, "EXT_MODEL_FORCE_CUT_SN_MIN");
+    if (!status) {
+        SNMinForCut = 10;
+    }
+
+    // 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 blocks the free.  In fact, it's better to read the table row by row.
+    long numRows = psFitsTableSize(fits); // Number of rows in table
+    psArray *sources = psArrayAllocEmpty(numRows); // Array of sources, to return
+
+    // convert the table to the pmSource entries
+    pmSource *source = NULL;
+    for (int i = 0; i < numRows; i++) {
+        psMetadata *row = psFitsReadTableRow(fits, i); // Table row
+        if (!row) {
+            psError(psErrorCodeLast(), false, "Unable to read row %d of sources", i);
+            psFree(sources);
+            return NULL;
+        }
+
+	// here are the things we read from the FITS table (XXX modify names if needed)
+
+	unsigned int ID  = psMetadataLookupU32 (&status, row, "ID");
+	float X          = psMetadataLookupF32 (&status, row, "X");
+        assert(status);  // poorly constructed cff
+	float Y          = psMetadataLookupF32 (&status, row, "Y");
+        assert(status);  // poorly constructed cff
+        float flux       = psMetadataLookupF32 (&status, row, "FLUX"); // guess at the instrumental flux
+        if (!isfinite(X) || !isfinite(Y) || !isfinite(flux)) {
+            psError(PS_ERR_IO, true, "source ID %d is invalid x: %f y: %f flux: %f", ID, X, Y, flux);
+            psFree(sources);
+            return NULL;
+        }
+
+
+        float apRadius   = psMetadataLookupF32 (&status, row, "AP_RADIUS");
+        float kronRadius = psMetadataLookupF32 (&status, row, "KRON_RADIUS");
+        float petRadius  = psMetadataLookupF32 (&status, row, "PETRO_RADIUS");
+        float SN         = psMetadataLookupF32 (&status, row, "SN");
+        bool fitGalaxy   = psMetadataLookupU8 (&status, row, "FIT_GALAXY");
+        bool psfStar     = psMetadataLookupU8 (&status, row, "PSF_STAR");
+
+        float Rmajor     = psMetadataLookupF32 (&status, row, "R_MAJOR");
+        float Rminor     = psMetadataLookupF32 (&status, row, "R_MINOR");
+        float theta      = psMetadataLookupF32 (&status, row, "THETA");
+        float chisq      = psMetadataLookupF32 (&status, row, "CHISQ");
+        float nDOF       = psMetadataLookupF32 (&status, row, "NDOF");
+        float magDiff    = psMetadataLookupF32 (&status, row, "MAG_DIFF");
+        psS16 modelFlags = psMetadataLookupS32 (&status, row, "MODEL_FLAGS");
+
+        int   galaxyModelType = psMetadataLookupS32(&status, row, "MODEL_TYPE");
+        if (status && galaxyModelType >= 0) {
+            galaxyModelType = pmModelClassGetLocalType(galaxyModelType);
+        } else {
+            galaxyModelType = -1;
+        }
+        float Sindex     = psMetadataLookupF32 (&status, row, "INDEX"); // Should this be PAR_07 not sersic index
+
+        if (!source || ID != source->seq) {
+            if (source) {
+                psArrayAdd (sources, 1, source);
+                psFree(source);
+            }
+            source = pmSourceAlloc ();
+            pmModel *model = pmModelAlloc (modelType);
+            source->modelPSF  = model;
+            //        RoughClass wants source type to be unknown
+            //        source->type = PM_SOURCE_TYPE_STAR; // XXX this should be added to the flags
+            source->type = PM_SOURCE_TYPE_UNKNOWN;
+
+            // XXX we can set this in general, but for a specific image, we need to weed out SATSTARS and
+            // stars that are masked
+            if (psfStar) {
+                source->tmpFlags |= PM_SOURCE_TMPF_CANDIDATE_PSFSTAR;
+            }
+
+            // NOTE: A SEGV here because "model" is NULL is probably caused by not initialising the models.
+            psF32 *PAR = model->params->data.F32;
+            psF32 *dPAR = model->dparams->data.F32;
+
+            source->seq       = ID;
+
+            PAR[PM_PAR_XPOS]  = X;
+            PAR[PM_PAR_YPOS]  = Y;
+
+            dPAR[PM_PAR_XPOS] = 0.0;
+            dPAR[PM_PAR_YPOS] = 0.0;
+
+            PAR[PM_PAR_SKY]   = 0.0;
+            dPAR[PM_PAR_SKY]  = 0.0;
+
+            PAR[PM_PAR_I0]    = 1.0;
+            dPAR[PM_PAR_I0]   = 0.0;
+
+            source->sky       = PAR[PM_PAR_SKY];
+            source->skyErr    = dPAR[PM_PAR_SKY];
+
+            source->psfMag    = 0.0;
+            source->psfMagErr = 0.0;
+            source->apMag     = 0.0;
+            source->apRadius  = apRadius;
+
+            // we generate a somewhat fake PSF model here -- 
+            // in most (all?) contexts, we will replace this with a measured psf model
+            // elsewhere
+            axes.major        = 1.0;
+            axes.minor        = 1.0;
+            axes.theta        = 0.0;
+            pmPSF_AxesToModel (PAR, axes, model->class->useReff);
+
+            // peak->detValue, rawFlux, smoothFlux are all set to the flux argument which is counts per second
+            source->peak      = pmPeakAlloc(X, Y, flux, PM_PEAK_LONE);
+            source->peak->xf  = X; // pmPeakAlloc converts X,Y to int, so reset here
+            source->peak->yf  = Y; // pmPeakAlloc converts X,Y to int, so reset here
+            source->peak->dx  = 0.0;
+            source->peak->dy  = 0.0;
+
+            source->extSN     = SN;
+
+            source->moments = pmMomentsAlloc ();
+            source->moments->Mx = X;
+            source->moments->My = Y;
+            source->moments->Mrf = kronRadius * 0.4; // kronRadius is 2.5 * first radial moment
+
+            // Don't mark the moments as measured because that causes many fields to be left blank.
+            // The moments code knows not to change the position or the Mrf for external sources
+            // source->tmpFlags |= PM_SOURCE_TMPF_MOMENTS_MEASURED;
+
+            if (isfinite(petRadius)) {
+                source->extpars = pmSourceExtendedParsAlloc ();
+                source->extpars->petrosianRadius = petRadius;
+            }
+
+        }
+        bool saveExtModelParams = false;
+        if (fitGalaxy && galaxyModelType >= 0) {
+            // skip likely stars 
+            if (magDiff < starCut || SN < SNMinForCut) {
+                if (forceAll) {
+                    saveExtModelParams = true;
+                } else if (galaxyModelType == modelTypeForce) {
+                    // This is the model type that we are looking for
+                    // proceed
+                    saveExtModelParams = true;
+                } else if (modelTypeForce == sersicModelType && galaxyModelType == devModelType) {
+                    // We're doing sersic models, if sersic index is greater than the recipe's minimum index
+                    // do dev model as well
+                    if (isfinite(forceDevSersicMin) || Sindex >= forceDevSersicMin) {
+                        saveExtModelParams = true;
+                    }
+                } else {
+                    // not interested in this model
+                }
+            }
+        }
+
+        if (saveExtModelParams) {
+            if (!source->modelFits) {
+                source->modelFits = psArrayAllocEmpty (1);
+            }
+            pmModel *model = pmModelAlloc(galaxyModelType);
+            psF32 *xPAR = model->params->data.F32;
+
+            xPAR[PM_PAR_SKY]  = 0.0;
+            xPAR[PM_PAR_I0]   = 1.0;
+            xPAR[PM_PAR_XPOS] = X;
+	    xPAR[PM_PAR_YPOS] = Y;
+	    
+	    psEllipseAxes galaxyAxes;
+	    galaxyAxes.major = Rmajor;
+	    galaxyAxes.minor = Rminor;
+	    galaxyAxes.theta = theta * PS_RAD_DEG;
+
+	    pmPSF_AxesToModel (xPAR, galaxyAxes, model->class->useReff);
+	    if (model->params->n > 7) {
+                xPAR[PM_PAR_7] = 0.5 / Sindex;
+	    }
+
+            model->chisq = chisq;
+            model->nDOF = nDOF;
+            model->flags = modelFlags;
+
+	    psArrayAdd (source->modelFits, 1, model);
+
+	    psFree (model);
+        }
+
+        psFree(row);
+    }
+    if (source) {
+        // close out last source
+        psArrayAdd (sources, 1, source);
+        psFree(source);
+    }
+
+    return sources;
+}
+
+bool pmSourcesWrite_CFF(pmReadout *readout, psFits *fits, psArray *sources, psMetadata *header, psMetadata *recipe) {
+    
+    char *extname = "SkyChip.cff";
+    
+    bool mdok;
+    psF32 exptime = psMetadataLookupF32(&mdok, readout->parent->concepts, "CELL.EXPOSURE");
+    PS_ASSERT(mdok, false);
+
+    // write the definition of the model class type values to the header
+    psMetadata *outputHeader = psMetadataAlloc();
+    pmModelClassWriteHeader(outputHeader);
+
+    psArray *table = psArrayAllocEmpty(sources->n);
+
+    pmModelType sersicModelType = pmModelClassGetType("PS_MODEL_SERSIC");
+    pmModelType devModelType    = pmModelClassGetType("PS_MODEL_DEV");
+    pmModelType selectedModelType = -1;
+    bool chooseBest = false;
+    bool chooseAll = false;
+
+    psString modelToChoose = psMetadataLookupStr(&mdok, recipe, "EXT_MODEL_TYPE_FOR_CFF");
+
+    if (mdok && modelToChoose != NULL) {
+        if (!strcmp(modelToChoose, "BEST")) {
+            chooseBest = true;
+        } else if (!strcmp(modelToChoose, "ALL")) {
+            chooseAll = true;
+        } else if (strcmp(modelToChoose, "PS_MODEL_SERSIC")) {
+            // We have selected a model type other than Sersic. 
+            // Save it's type for use below.  Sersic is handled specially
+            selectedModelType = pmModelClassGetType(modelToChoose);
+        }
+    }
+
+    // minimum sersic index to force devModel
+    psF32 sersicMinDev = psMetadataLookupF32(&mdok, recipe, "EXT_MODEL_FORCE_DEV_SERSIC_MIN");
+    if (!mdok) {
+        sersicMinDev = NAN;
+    }
+
+    sources = psArraySort (sources, pmSourceSortBySeq);
+
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *thisSource = sources->data[i];
+        pmSource *source = thisSource->parent ? thisSource->parent : thisSource;
+
+        #define MAX_ROWS_PER_SRC 10
+        psF32 xPos[MAX_ROWS_PER_SRC], yPos[MAX_ROWS_PER_SRC], flux[MAX_ROWS_PER_SRC];
+        psF32 rMajor[MAX_ROWS_PER_SRC], rMinor[MAX_ROWS_PER_SRC], theta[MAX_ROWS_PER_SRC];
+        psF32 chisq[MAX_ROWS_PER_SRC], nDOF[MAX_ROWS_PER_SRC];
+        psS32 modelFlags[MAX_ROWS_PER_SRC];
+        psS32 modelType[MAX_ROWS_PER_SRC];
+        psF32 sersicIndex = NAN;
+        bool fitGalaxy = false;
+        bool psfStar = (source->mode & PM_SOURCE_MODE_PSFSTAR) ? true : false;
+        int n_rows = 0;
+
+        psF32 kronFlux = source->moments->KronFlux;
+        psF32 SN = NAN;
+        psF32 magDiff = NAN;
+        if (isfinite(kronFlux) && isfinite(source->moments->KronFluxErr) && isfinite(source->psfMag)) {
+            SN = kronFlux/source->moments->KronFluxErr;
+            // kronMag - psfMag for use as star/glaxy separator
+            magDiff = -2.5 * log10(kronFlux) - source->psfMag ;
+        }
+
+        // start with psf model
+        pmModel *model = source->modelPSF;
+        if (model == NULL) continue;
+        psF32 *PAR = model->params->data.F32;
+        if (!isfinite(PAR[PM_PAR_SXX]) || !isfinite(PAR[PM_PAR_SYY])  || !isfinite(PAR[PM_PAR_SXY]) ||
+            !isfinite(source->psfFlux)) {
+            continue;
+        }
+
+	// save the PSF model parameters for each object as the first entry
+        xPos[0] = model->params->data.F32[PM_PAR_XPOS];
+        yPos[0] = model->params->data.F32[PM_PAR_YPOS];
+        flux[0] = source->psfFlux;
+        rMajor[0] = 0;
+        rMinor[0] = 0;
+        theta[0] = 0;
+        modelType[0] = -1;
+        sersicIndex = NAN;
+        chisq[0] = NAN;
+        nDOF[0] = NAN;
+        modelFlags[0] = 0;
+	n_rows ++;
+
+        if (source->modelFits != NULL) {
+            // figure out which models to use based on recipe paramters
+            if (chooseAll) {
+                // Save parameters for all valid extended models
+
+                // but make sure we aren't going to overflow our arrays
+                assert (source->modelFits->n < MAX_ROWS_PER_SRC);
+
+                for (int j=0; j<source->modelFits->n; j++) {
+                    pmModel *model = source->modelFits->data[j];
+                    psF32 *PAR = model->params->data.F32;
+
+                    if (isfinite(PAR[PM_PAR_SXX]) && isfinite(PAR[PM_PAR_SYY])  && isfinite(PAR[PM_PAR_SXY]) &&
+                        isfinite(model->mag)) {
+
+                        xPos[n_rows] = PAR[PM_PAR_XPOS];
+                        yPos[n_rows] = PAR[PM_PAR_YPOS];
+
+                        psEllipseAxes axes = pmPSF_ModelToAxes (PAR, model->class->useReff);
+                        rMajor[n_rows] = axes.major;
+                        rMinor[n_rows] = axes.minor;
+                        theta[n_rows]  = axes.theta*PS_DEG_RAD;
+                        flux[n_rows] = pow(10.0, -0.4*model->mag);
+                        modelType[n_rows] = model->type;
+                        chisq[n_rows] = model->chisq;
+                        nDOF[n_rows] = model->nDOF;
+                        modelFlags[n_rows] = model->flags;
+                        fitGalaxy = true;
+                        if (model->params->n == 8) {
+                            // this will save the sersic index in all models but that might
+                            // be useful
+                            sersicIndex = 0.5 / PAR[PM_PAR_7];
+                        } else {
+                            sersicIndex = NAN;
+                        }
+
+                        n_rows++;
+                    }
+                }
+            } else {
+                int jModelSersic = -1;
+                int jModelDev = -1;
+                int jModelSelected = -1;
+                psF32 minChisq = NAN;
+                for (int j=0; j<source->modelFits->n; j++) {
+                    pmModel *model = source->modelFits->data[j];
+                    if (chooseBest) {
+                        // choose the model with lowest chisq
+                        if (isfinite(model->chisq) && (!isfinite(minChisq) || model->chisq < minChisq)) {
+                            jModelSelected = j;
+                            minChisq = model->chisq;
+                        }
+                    } else {
+                        // find the index of models of interest
+                        if (model->type == selectedModelType) {
+                            jModelSelected = j;
+                        } else if (model->type == sersicModelType) {
+                            jModelSersic = j;
+                        } else if (model->type == devModelType) {
+                            jModelDev = j;
+                        }
+                    }
+                }
+                if (jModelSelected >= 0 || jModelSersic >= 0) {
+                    // If a specific non-sersic model we take paramers from that one.
+                    // Otherwise we do the sersic model.
+                    pmModel *model = jModelSelected >= 0 ? source->modelFits->data[jModelSelected] :
+                                                           source->modelFits->data[jModelSersic];
+                    psF32 *PAR = model->params->data.F32;
+
+                    if (isfinite(PAR[PM_PAR_SXX]) && isfinite(PAR[PM_PAR_SYY])  && isfinite(PAR[PM_PAR_SXY]) &&
+                        isfinite(model->mag)) {
+
+                        xPos[0] = PAR[PM_PAR_XPOS];
+                        yPos[0] = PAR[PM_PAR_YPOS];
+
+                        psEllipseAxes axes = pmPSF_ModelToAxes (PAR, model->class->useReff);
+                        rMajor[0] = axes.major;
+                        rMinor[0] = axes.minor;
+                        theta[0]  = axes.theta*PS_DEG_RAD;
+                        flux[0] = pow(10.0, -0.4*model->mag);
+                        modelType[0] = model->type;
+                        chisq[0] = model->chisq;
+                        nDOF[0] = model->nDOF;
+                        modelFlags[0] = model->flags;
+                        fitGalaxy = true;
+                        if (model->type == sersicModelType) {
+                            PS_ASSERT_FLOAT_LARGER_THAN(PAR[PM_PAR_7], 0.0, false);
+                            sersicIndex = 0.5 / PAR[PM_PAR_7];
+                        }
+
+                        n_rows = 1;
+
+                        // Unless a specific non-sersic model type was selected do dev model for sources with
+                        // sersic index above the recipe limit.
+                        if (jModelSelected == -1 && jModelDev >= 0 && isfinite(sersicMinDev) &&
+                            sersicIndex > sersicMinDev) {
+
+                            model = source->modelFits->data[jModelDev];
+                            if (isfinite(PAR[PM_PAR_SXX]) && isfinite(PAR[PM_PAR_SYY])  && isfinite(PAR[PM_PAR_SXY]) &&
+                                isfinite(model->mag)) { 
+
+                                xPos[1] = PAR[PM_PAR_XPOS];
+                                yPos[1] = PAR[PM_PAR_YPOS];
+
+                                psEllipseAxes axes = pmPSF_ModelToAxes (PAR, model->class->useReff);
+                                rMajor[1] = axes.major;
+                                rMinor[1] = axes.minor;
+                                theta[1]  = axes.theta*PS_DEG_RAD;
+                                flux[1] = pow(10.0, -0.4*model->mag);
+                                modelType[1] = model->type;
+                                sersicIndex = NAN;
+                                chisq[1] = model->chisq;
+                                nDOF[1] = model->nDOF;
+                                modelFlags[1] = model->flags;
+                                n_rows = 2;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        for (int j = 0; j < n_rows; j++) {
+            psMetadata *row = psMetadataAlloc();
+            psMetadataAddU32 (row, PS_LIST_TAIL, "ID",         0,   "IPP detection identifier",  source->seq);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "X",                0, "x coordinate",          xPos[j]);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "Y",                0, "y coordinate",          yPos[j]);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "FLUX",             0, "flux per second",       flux[j]/exptime);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "SN",               0, "kron flux signal to noise", SN);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "MAG_DIFF",         0, "psf mag - kron mag",    magDiff);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "AP_RADIUS",        0, "aperture radius",       source->apRadius);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "KRON_RADIUS",      0, "Kron radius",           source->moments->Mrf * 2.5);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "PETRO_RADIUS",     0, "Petrosian Radius",      source->extpars ? source->extpars->petrosianRadius : NAN);
+            psMetadataAddBool (row, PS_LIST_TAIL, "FIT_GALAXY",      0, "source has xfit",       fitGalaxy); 
+            psMetadataAddBool (row, PS_LIST_TAIL, "PSF_STAR",        0, "source was psf star",   psfStar);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "R_MAJOR",          0, "radius of major axis",  rMajor[j]);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "R_MINOR",          0, "radius of minor axis",  rMinor[j]);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "THETA",            0, "theta",                 theta[j]);
+            psMetadataAddS32 (row, PS_LIST_TAIL, "MODEL_TYPE",       0, "model type",            modelType[j]);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "INDEX",            0, "sersic index",          sersicIndex);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "CHISQ",            0, "chisq",                 chisq[j]);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "NDOF",             0, "n degrees of freedom",  nDOF[j]);
+            psMetadataAddS32 (row, PS_LIST_TAIL, "MODEL_FLAGS",      0, "model flags",           modelFlags[j]);
+
+            psArrayAdd(table, 100, row);
+            psFree(row);
+        }
+    }
+
+    if (!psFitsWriteTable(fits, outputHeader, table, extname)) {
+        psError(psErrorCodeLast(), false, "writing ext data %s\n", extname);
+        psFree(table);
+        psFree(outputHeader);
+        return false;
+    }
+    psFree(table);
+    psFree(outputHeader);
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_CMF.c.in
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_CMF.c.in	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_CMF.c.in	(revision 42651)
@@ -0,0 +1,2441 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-18 02:44:19 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+
+#include "pmSourceIO.h"
+#include "pmSourceOutputs.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 file is used to generate c-code for a number of different formats.  the perl program
+ ** 'mksource.pl' reads this template file and generates an output file for one of the named
+ ** formats, e.g., PS1_V1 or PS1_DV4 (see list in mksource.pl).  
+
+ ** Any instances of the word @CMFMODE@ are replaced by the named version of the format.
+
+ ** Any line which starts with a command of the form: @COMMAND[,COMMAND...]@ is kept or removed
+ ** from the output c-code depending on the commands.  These may be: ALL (lines are kept for
+ ** all formats) or the name of a format.  A line with just a plain format will be kept only
+ ** for that format.  The format name may be optionally proceded by >, <, <=, >=, ! or =.  In
+ ** these cases, the line is kept if the format is greater / lesser / not the same, etc as the
+ ** listed format.  The sequence of formats is defined for series (V, SV, DV) in mksource.pl so
+ ** that, e.g., >PS1_V3 would include e.g., PS1_V4 and PS1_V5, but not PS1_DV4.  The number in
+ ** a series may be replaced with ? to stand for the full set.  The format 
+ ** commands may be grouped on a line separated by commas.  Note that only ! (not) is
+ ** restrictive: only this rule will reduce the number of matches.  So for example,
+ ** @ALL,<PS1_DV3@ will have the effect of @ALL@ since any format >= PS1_DV3 will be matched by
+ ** ALL.  But @ALL,!PS1_DV3@ will generate a line for any format except PS1_DV3.
+
+ **/ 
+
+bool pmSourcesWrite_CMF_@CMFMODE@_New (psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, psMetadata *tableHeader, char *extname, psMetadata *recipe)
+{
+    // fprintf (stderr, "writing with %s\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(extname, false);
+
+    pmChip *chip = readout->parent->parent;
+
+    // if the sequence is defined, write these in seq order; otherwise
+    // write them in S/N order:
+    if (sources->n > 0) {
+        pmSource *source = (pmSource *) sources->data[0];
+        if (source->seq == -1) {
+            // let's write these out in S/N order
+            sources = psArraySort (sources, pmSourceSortByFlux);
+        } else {
+            sources = psArraySort (sources, pmSourceSortBySeq);
+        }
+    }
+
+    float magOffset; 
+    float zeroptErr; 
+    float fwhmMajor; 
+    float fwhmMinor;
+    pmSourceOutputsCommonValues (&magOffset, &zeroptErr, &fwhmMajor, &fwhmMinor, readout, imageHeader);
+
+    // before we can generate the table structure, we need to know if certain fields were measured.
+    // this information is not available a priori, so we need to check all sources
+    
+    bool haveLensingOBJsmear = false;
+    bool haveLensingOBJshear = false;
+    bool haveLensingPSFsmear = false;
+    bool haveLensingPSFshear = false;
+    bool haveLensingPSF      = false;
+
+    for (int i = 0; (i < sources->n) && !haveLensingOBJsmear && !haveLensingOBJshear && !haveLensingPSFsmear && !haveLensingPSFshear && !haveLensingPSF; i++) {
+        // this is the source associated with this image
+        pmSource *thisSource = sources->data[i];
+
+        // this is the "real" version of this source 
+        pmSource *source = thisSource->parent ? thisSource->parent : thisSource;
+
+	if (source->lensingOBJ && source->lensingOBJ->smear) haveLensingOBJsmear = true;
+	if (source->lensingOBJ && source->lensingOBJ->shear) haveLensingOBJshear = true;
+	if (source->lensingPSF && source->lensingPSF->smear) haveLensingPSFsmear = true;
+	if (source->lensingPSF && source->lensingPSF->shear) haveLensingPSFshear = true;
+	if (source->lensingPSF                             ) haveLensingPSF      = true;
+    }
+
+    /************ generate the table columns *****************/
+
+    // before we allocate the table, generate an empty array of table columns and generate them
+    psArray *tableColumns = psArrayAllocEmpty (100);
+
+    // add the named / typed columns to the collection of columns
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "IPP_IDET",         PS_DATA_U32); // "IPP detection identifier index"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "X_PSF",            PS_DATA_F32); // "PSF x coordinate"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "Y_PSF",            PS_DATA_F32); // "PSF y coordinate"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "X_PSF_SIG",        PS_DATA_F32); // "Sigma in PSF x coordinate"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "Y_PSF_SIG",        PS_DATA_F32); // "Sigma in PSF y coordinate"
+
+    // NOTE: pre-PS1_V2, we only reported RA & DEC in floats for reference, not precision
+    @PS1_V1@                    psFitsTableColumnAdd (tableColumns, "RA_PSF",           PS_DATA_F32); // "PSF RA coordinate (degrees)"
+    @PS1_V1@                    psFitsTableColumnAdd (tableColumns, "DEC_PSF",          PS_DATA_F32); // "PSF DEC coordinate (degrees)"
+
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "POSANGLE",         PS_DATA_F32); // "position angle at source (degrees)"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "PLTSCALE",         PS_DATA_F32); // "plate scale at source (arcsec/pixel)"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "PSF_INST_MAG",     PS_DATA_F32); // "PSF fit instrumental magnitude"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "PSF_INST_MAG_SIG", PS_DATA_F32); // "Sigma of PSF instrumental magnitude"
+
+    @ALL,!PS1_V1,!PS1_V2@       psFitsTableColumnAdd (tableColumns, "PSF_INST_FLUX",    PS_DATA_F32); // "PSF fit instrumental flux (counts)"
+    @ALL,!PS1_V1,!PS1_V2@       psFitsTableColumnAdd (tableColumns, "PSF_INST_FLUX_SIG", PS_DATA_F32); // "Sigma of PSF instrumental flux"
+
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "AP_MAG",           PS_DATA_F32); // "magnitude in standard aperture"
+    @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableColumnAdd (tableColumns, "AP_MAG_RAW",       PS_DATA_F32); // "magnitude in reported aperture"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "AP_MAG_RADIUS",    PS_DATA_F32); // "radius used for aperture mags"
+    @>PS1_DV1,>PS1_V3,>PS1_SV1@ psFitsTableColumnAdd (tableColumns, "AP_FLUX",          PS_DATA_F32); // "instrumental flux in standard aperture"
+    @>PS1_DV1,>PS1_V3,>PS1_SV1@ psFitsTableColumnAdd (tableColumns, "AP_FLUX_SIG",      PS_DATA_F32); // "aperture flux error"
+    @>PS1_V4,>PS1_SV2,>PS1_DV3@ psFitsTableColumnAdd (tableColumns, "AP_NPIX",          PS_DATA_S32); // "aperture unmasked pixels"
+
+    @<PS1_V3,PS1_SV1,PS1_DV?@   psFitsTableColumnAdd (tableColumns, "PEAK_FLUX_AS_MAG", PS_DATA_F32); // "Peak flux expressed as magnitude"
+
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "CAL_PSF_MAG",      PS_DATA_F32); // "PSF Magnitude using supplied calibration"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "CAL_PSF_MAG_SIG",  PS_DATA_F32); // "measured scatter of zero point calibration"
+
+    // NOTE: RA & DEC (both double) need to be on an 8-byte boundary...
+    @ALL,!PS1_V1@               psFitsTableColumnAdd (tableColumns, "RA_PSF",           PS_DATA_F64); // "PSF RA coordinate (degrees)"
+    @ALL,!PS1_V1@               psFitsTableColumnAdd (tableColumns, "DEC_PSF",          PS_DATA_F64); // "PSF DEC coordinate (degrees)"
+
+    @>=PS1_V3,>PS1_SV1@         psFitsTableColumnAdd (tableColumns, "PEAK_FLUX_AS_MAG", PS_DATA_F32); // "Peak flux expressed as magnitude"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "SKY",              PS_DATA_F32); // "Sky level"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "SKY_SIGMA",        PS_DATA_F32); // "Sigma of sky level"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "PSF_CHISQ",        PS_DATA_F32); // "Chisq of PSF-fit"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "CR_NSIGMA",        PS_DATA_F32); // "Nsigma deviations from PSF to CF"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "EXT_NSIGMA",       PS_DATA_F32); // "Nsigma deviations from PSF to EXT"
+
+    // PSF shape parameters:
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "PSF_MAJOR",        PS_DATA_F32); // "PSF width (major axis)"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "PSF_MINOR",        PS_DATA_F32); // "PSF width (minor axis)"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "PSF_THETA",        PS_DATA_F32); // "PSF orientation angle"
+    @>PS1_V4,>PS1_SV2,>PS1_DV3@ psFitsTableColumnAdd (tableColumns, "PSF_CORE",         PS_DATA_F32); // "k term if defined"
+    @>PS1_V4,>PS1_SV2,>PS1_DV3@ psFitsTableColumnAdd (tableColumns, "PSF_FWHM_MAJ",     PS_DATA_F32); // "PSF FWHM (major axis)"
+    @>PS1_V4,>PS1_SV2,>PS1_DV3@ psFitsTableColumnAdd (tableColumns, "PSF_FWHM_MIN",     PS_DATA_F32); // "PSF FWHM (minor axis)"
+
+    // psf data quality
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "PSF_QF",           PS_DATA_F32); // "PSF coverage/quality factor (bad)"
+    @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableColumnAdd (tableColumns, "PSF_QF_PERFECT",   PS_DATA_F32); // "PSF coverage/quality factor (poor)"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "PSF_NDOF",         PS_DATA_S32); // "degrees of freedom"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "PSF_NPIX",         PS_DATA_S32); // "number of pixels in fit"
+
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "MOMENTS_XX",       PS_DATA_F32); // "second moments (X^2)"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "MOMENTS_XY",       PS_DATA_F32); // "second moments (X*Y)"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "MOMENTS_YY",       PS_DATA_F32); // "second moments (Y*Y)"
+    @>PS1_V2,PS1_SV?@           psFitsTableColumnAdd (tableColumns, "MOMENTS_M3C",      PS_DATA_F32); // "third momemt cos theta"
+    @>PS1_V2,PS1_SV?@           psFitsTableColumnAdd (tableColumns, "MOMENTS_M3S",      PS_DATA_F32); // "third momemt sin theta"
+    @>PS1_V2,PS1_SV?@           psFitsTableColumnAdd (tableColumns, "MOMENTS_M4C",      PS_DATA_F32); // "fourth momemt cos theta"
+    @>PS1_V2,PS1_SV?@           psFitsTableColumnAdd (tableColumns, "MOMENTS_M4S",      PS_DATA_F32); // "fourth momemt sin theta"
+  
+    // Lensing parameters are only written if they are measured
+    if (haveLensingOBJsmear) {
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_X11_SM_OBJ",  PS_DATA_F32); // "smear polarizability element (objects)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_X12_SM_OBJ",  PS_DATA_F32); // "smear polarizability element (objects)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_X22_SM_OBJ",  PS_DATA_F32); // "smear polarizability element (objects)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_E1_SM_OBJ",   PS_DATA_F32); // "smear polarizability element (objects)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_E2_SM_OBJ",   PS_DATA_F32); // "smear polarizability element (objects)"
+    }
+    if (haveLensingOBJshear) {
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_X11_SH_OBJ",  PS_DATA_F32); // "shear polarizability element (objects)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_X12_SH_OBJ",  PS_DATA_F32); // "shear polarizability element (objects)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_X22_SH_OBJ",  PS_DATA_F32); // "shear polarizability element (objects)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_E1_SH_OBJ",   PS_DATA_F32); // "shear polarizability element (objects)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_E2_SH_OBJ",   PS_DATA_F32); // "shear polarizability element (objects)"
+    }
+    if (false) {
+        // do not bother to save these as they are equivalent to Mxx,Mxy,Myy above
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_E1_OBJ",      PS_DATA_F32); // "shear polarizability element (objects)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_E2_OBJ",      PS_DATA_F32); // "shear polarizability element (objects)"
+    }
+
+    if (haveLensingPSFsmear) {
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_X11_SM_PSF",  PS_DATA_F32); // "smear polarizability element (PSFs)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_X12_SM_PSF",  PS_DATA_F32); // "smear polarizability element (PSFs)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_X22_SM_PSF",  PS_DATA_F32); // "smear polarizability element (PSFs)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_E1_SM_PSF",   PS_DATA_F32); // "smear polarizability element (PSFs)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_E2_SM_PSF",   PS_DATA_F32); // "smear polarizability element (PSFs)"
+    }
+    if (haveLensingPSFshear) {
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_X11_SH_PSF",  PS_DATA_F32); // "shear polarizability element (PSFs)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_X12_SH_PSF",  PS_DATA_F32); // "shear polarizability element (PSFs)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_X22_SH_PSF",  PS_DATA_F32); // "shear polarizability element (PSFs)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_E1_SH_PSF",   PS_DATA_F32); // "shear polarizability element (PSFs)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_E2_SH_PSF",   PS_DATA_F32); // "shear polarizability element (PSFs)"
+    }
+    if (haveLensingPSF) {
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_E1_PSF",      PS_DATA_F32); // "shear polarizability element (PSFs)"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "LENS_E2_PSF",      PS_DATA_F32); // "shear polarizability element (PSFs)"
+    }
+
+    // if lensing params exist also include the backmapped chipID and chip coordinates
+    if (haveLensingPSF) {
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "SRC_CHIP_NUM",     PS_DATA_S16); // "id of warp input chip"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "SRC_CHIP_X",       PS_DATA_S16); // "x coord in warp input chip"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "SRC_CHIP_Y",       PS_DATA_S16); // "y coord in warp input chip"
+        @>PS1_V4@               psFitsTableColumnAdd (tableColumns, "PADDING3",         PS_DATA_S16); // "more padding"
+    }
+
+    @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableColumnAdd (tableColumns, "MOMENTS_R1",       PS_DATA_F32); // "first radial moment"
+    @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableColumnAdd (tableColumns, "MOMENTS_RH",       PS_DATA_F32); // "half radial moment"
+    @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableColumnAdd (tableColumns, "KRON_FLUX",        PS_DATA_F32); // "Kron Flux (in 2.5 R1)"
+    @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableColumnAdd (tableColumns, "KRON_FLUX_ERR",    PS_DATA_F32); // "Kron Flux Error"
+    @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableColumnAdd (tableColumns, "KRON_FLUX_INNER",  PS_DATA_F32); // "Kron Flux (in 1.0 R1)"
+    @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableColumnAdd (tableColumns, "KRON_FLUX_OUTER",  PS_DATA_F32); // "Kron Flux (in 4.0 R1)"
+
+    @>PS1_V3@                   psFitsTableColumnAdd (tableColumns, "SKY_LIMIT_RAD",    PS_DATA_F32); // "Radius where object hits sky"
+    @>PS1_V3@                   psFitsTableColumnAdd (tableColumns, "SKY_LIMIT_FLUX",   PS_DATA_F32); // "Flux / pix where object hits sky"
+    @>PS1_V3@                   psFitsTableColumnAdd (tableColumns, "SKY_LIMIT_SLOPE",  PS_DATA_F32); // "d(Flux/pix)/dRadius where object hits sky"
+
+    @>PS1_DV4@                  psFitsTableColumnAdd (tableColumns, "SRC_CHIP_NUM",     PS_DATA_S16); // "id of warp input chip"
+    @>PS1_DV4@                  psFitsTableColumnAdd (tableColumns, "SRC_CHIP_X",       PS_DATA_S16); // "x coord in warp input chip"
+    @>PS1_DV4@                  psFitsTableColumnAdd (tableColumns, "SRC_CHIP_Y",       PS_DATA_S16); // "y coord in warp input chip"
+    @>PS1_DV4@                  psFitsTableColumnAdd (tableColumns, "PADDING3",         PS_DATA_S16); // "more padding"
+
+    @PS1_DV?@                   psFitsTableColumnAdd (tableColumns, "DIFF_NPOS",        PS_DATA_S32); // "nPos (n pix > 3 sigma)"
+    @PS1_DV?@                   psFitsTableColumnAdd (tableColumns, "DIFF_FRATIO",      PS_DATA_F32); // "fPos / (fPos + fNeg)"
+    @PS1_DV?@                   psFitsTableColumnAdd (tableColumns, "DIFF_NRATIO_BAD",  PS_DATA_F32); // "nPos / (nPos + nNeg)"
+    @PS1_DV?@                   psFitsTableColumnAdd (tableColumns, "DIFF_NRATIO_MASK", PS_DATA_F32); // "nPos / (nPos + nMask)"
+    @PS1_DV?@                   psFitsTableColumnAdd (tableColumns, "DIFF_NRATIO_ALL",  PS_DATA_F32); // "nPos / (nGood + nMask + nBad)"
+
+    @>PS1_DV1@                  psFitsTableColumnAdd (tableColumns, "DIFF_R_P",         PS_DATA_F32); // "distance to positive match source"
+    @>PS1_DV1@                  psFitsTableColumnAdd (tableColumns, "DIFF_SN_P",        PS_DATA_F32); // "signal-to-noise of pos match src"
+    @>PS1_DV1@                  psFitsTableColumnAdd (tableColumns, "DIFF_R_M",         PS_DATA_F32); // "distance to negative match source"
+    @>PS1_DV1@                  psFitsTableColumnAdd (tableColumns, "DIFF_SN_M",        PS_DATA_F32); // "signal-to-noise of neg match src"
+
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "FLAGS",            PS_DATA_U32); // "psphot analysis flags"
+    @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableColumnAdd (tableColumns, "FLAGS2",           PS_DATA_U32); // "psphot analysis flags"
+    @PS1_V3,PS1_SV2@            psFitsTableColumnAdd (tableColumns, "PADDING2",         PS_DATA_S32); // "more padding"
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "N_FRAMES",         PS_DATA_U16); // "Number of frames overlapping source center"
+
+    @ALL@                       psFitsTableColumnAdd (tableColumns, "PADDING",          PS_DATA_S16); // "more padding"
+
+    // note that this definition is inconsistent with the definition in
+    // Ohana/src/libautocode/def.  This version creates a table with data not
+    // properly aligned with the 8-byte boundaries. The structure defined by
+    // libautocode does this, but has a different order of elements (and adds
+    // padding2 to fix things). We have generated may files with PS1_SV1 as is, so
+    // I'll leave it. But in future a PS1_SV2 should be forced to match
+    // libautocode. Note that addstar knows to detect the alternate version of
+    // PS1_SV1 and correctly interpret its fields.
+
+    // EXT_NSIGMA will be NAN if: 1) contour ellipse is imaginary; 2) source is not
+    // subtracted
+
+    // CR_NSIGMA will be NAN if: 1) source is not subtracted; 2) source is on the image
+    // edge; 3) any pixels in the 3x3 peak region are masked;
+
+    // generate an FITS table using the array of columns defined above, with space for all rows
+    psFitsTable *table = psFitsTableCreate (tableColumns, sources->n);
+    psFree (tableColumns); // we are now done with the definition of the table columns
+
+    /************ write the data to the table *****************/
+
+    // we write out PSF-fits for all sources, regardless of quality.  the source flags tell us the state
+    // by the time we call this function, all values should be assigned.  let's use asserts to be sure in some cases.
+    for (int i = 0; i < sources->n; i++) {
+        // this is the source associated with this image
+        pmSource *thisSource = sources->data[i];
+
+        // this is the "real" version of this source 
+        pmSource *source = thisSource->parent ? thisSource->parent : thisSource;
+
+        // If source->seq is -1, source was generated in this analysis.  If source->seq is
+        // not -1, source was read from elsewhere: in the latter case, preserve the source
+        // ID.  source.seq is used instead of source.id since the latter is a const
+        // generated on Alloc, and would thus be wrong for read in sources.
+        if (source->seq == -1) {
+            source->seq = i;
+        }
+
+        // set the 'best' values for various output fields:
+        pmSourceOutputs outputs;
+        pmSourceOutputsSetValues (&outputs, source, chip, fwhmMajor, fwhmMinor, magOffset);
+
+        pmSourceOutputsMoments moments;
+        pmSourceOutputsSetMoments (&moments, source);
+
+        @PS1_DV?@ pmSourceDiffStats diffStats;
+        @PS1_DV?@ pmSourceDiffStatsInit(&diffStats);
+        @PS1_DV?@ if (source->diffStats) { diffStats = *source->diffStats;}
+
+        // the psMetadataAdd entry and the double quotes are used by grep to select the output fields for automatic documentation
+        // This set of psMetadataAdd Entries marks the "----" "Start of the PSF segment"
+        @ALL@                       psFitsTableSetU32 (table, i, "IPP_IDET",          source->seq);                     // "IPP detection identifier index",            
+        @ALL@                       psFitsTableSetF32 (table, i, "X_PSF",             outputs.xPos);                    // "PSF x coordinate",                          
+        @ALL@                       psFitsTableSetF32 (table, i, "Y_PSF",             outputs.yPos);                    // "PSF y coordinate",                          
+        @ALL@                       psFitsTableSetF32 (table, i, "X_PSF_SIG",         outputs.xErr);                    // "Sigma in PSF x coordinate",                 
+        @ALL@                       psFitsTableSetF32 (table, i, "Y_PSF_SIG",         outputs.yErr);                    // "Sigma in PSF y coordinate",                 
+
+        // NOTE: pre-PS1_V2, we only reported RA & DEC in floats for reference, not precision		                
+        @PS1_V1@                    psFitsTableSetF32 (table, i, "RA_PSF",            outputs.ra);                      // "PSF RA coordinate (degrees)",                
+        @PS1_V1@                    psFitsTableSetF32 (table, i, "DEC_PSF",           outputs.dec);                     // "PSF DEC coordinate (degrees)",               
+
+        @ALL@                       psFitsTableSetF32 (table, i, "POSANGLE",          outputs.posAngle);                // "position angle at source (degrees)",        
+        @ALL@                       psFitsTableSetF32 (table, i, "PLTSCALE",          outputs.pltScale);                // "plate scale at source (arcsec/pixel)",      
+        @ALL@                       psFitsTableSetF32 (table, i, "PSF_INST_MAG",      source->psfMag);                  // "PSF fit instrumental magnitude",            
+        @ALL@                       psFitsTableSetF32 (table, i, "PSF_INST_MAG_SIG",  source->psfMagErr);               // "Sigma of PSF instrumental magnitude",       
+
+        @ALL,!PS1_V1,!PS1_V2@       psFitsTableSetF32 (table, i, "PSF_INST_FLUX",     source->psfFlux);                 // "PSF fit instrumental flux (counts)",        
+        @ALL,!PS1_V1,!PS1_V2@       psFitsTableSetF32 (table, i, "PSF_INST_FLUX_SIG", source->psfFluxErr);              // "Sigma of PSF instrumental flux",            
+
+        @ALL@                       psFitsTableSetF32 (table, i, "AP_MAG",            source->apMag);                   // "magnitude in standard aperture",            
+        @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableSetF32 (table, i, "AP_MAG_RAW",        source->apMagRaw);                // "magnitude in reported aperture",            
+        @ALL@                       psFitsTableSetF32 (table, i, "AP_MAG_RADIUS",     source->apRadius);                // "radius used for aperture mags",             
+        @>PS1_DV1,>PS1_V3,>PS1_SV1@ psFitsTableSetF32 (table, i, "AP_FLUX",           source->apFlux);                  // "instrumental flux in standard aperture",    
+        @>PS1_DV1,>PS1_V3,>PS1_SV1@ psFitsTableSetF32 (table, i, "AP_FLUX_SIG",       source->apFluxErr);               // "aperture flux error",                       
+        @>PS1_V4,>PS1_SV2,>PS1_DV3@ psFitsTableSetS32 (table, i, "AP_NPIX",           source->apNpixels);               // "aperture unmasked pixels",                  
+
+        @<PS1_V3,PS1_SV1,PS1_DV?@   psFitsTableSetF32 (table, i, "PEAK_FLUX_AS_MAG",  outputs.peakMag);                 // "Peak flux expressed as magnitude"
+
+        @ALL@                       psFitsTableSetF32 (table, i, "CAL_PSF_MAG",       outputs.calMag);                  // "PSF Magnitude using supplied calibration",  
+        @ALL@                       psFitsTableSetF32 (table, i, "CAL_PSF_MAG_SIG",   zeroptErr);                       // "measured scatter of zero point calibration",
+
+        // NOTE: RA & DEC (both double) need to be on an 8-byte boundary...				         
+        @ALL,!PS1_V1@               psFitsTableSetF64 (table, i, "RA_PSF",            outputs.ra);                      // "PSF RA coordinate (degrees)",               
+        @ALL,!PS1_V1@               psFitsTableSetF64 (table, i, "DEC_PSF",           outputs.dec);                     // "PSF DEC coordinate (degrees)",              
+
+        @>=PS1_V3,>PS1_SV1@         psFitsTableSetF32 (table, i, "PEAK_FLUX_AS_MAG",  outputs.peakMag);                 // "Peak flux expressed as magnitude",          
+        @ALL@                       psFitsTableSetF32 (table, i, "SKY",               source->sky);                     // "Sky level",                                 
+        @ALL@                       psFitsTableSetF32 (table, i, "SKY_SIGMA",         source->skyErr);                  // "Sigma of sky level",                        
+
+        @ALL@                       psFitsTableSetF32 (table, i, "PSF_CHISQ",         outputs.chisq);                   // "Chisq of PSF-fit",                          
+        @ALL@                       psFitsTableSetF32 (table, i, "CR_NSIGMA",         source->crNsigma);                // "Nsigma deviations from PSF to CF",          
+        @ALL@                       psFitsTableSetF32 (table, i, "EXT_NSIGMA",        source->extNsigma);               // "Nsigma deviations from PSF to EXT",         
+
+        // PSF shape parameters:
+        @ALL@                       psFitsTableSetF32 (table, i, "PSF_MAJOR",         outputs.psfMajor);                // "PSF width (major axis)",                    
+        @ALL@                       psFitsTableSetF32 (table, i, "PSF_MINOR",         outputs.psfMinor);                // "PSF width (minor axis)",                    
+        @ALL@                       psFitsTableSetF32 (table, i, "PSF_THETA",         outputs.psfTheta);                // "PSF orientation angle",                     
+        @>PS1_V4,>PS1_SV2,>PS1_DV3@ psFitsTableSetF32 (table, i, "PSF_CORE",          outputs.psfCore);                 // "k term if defined",                         
+
+	// I use a look-up table and linear interpolation to map PSF_MAJOR,PSF_MINOR + PSF_CORE to FWHM values
+        @>PS1_V4,>PS1_SV2,>PS1_DV3@ psFitsTableSetF32 (table, i, "PSF_FWHM_MAJ",      outputs.psfMajorFWHM);            // "PSF FWHM (major axis)",                     
+        @>PS1_V4,>PS1_SV2,>PS1_DV3@ psFitsTableSetF32 (table, i, "PSF_FWHM_MIN",      outputs.psfMinorFWHM);            // "PSF FWHM (minor axis)",                     
+
+        // psf data quality
+        @ALL@                       psFitsTableSetF32 (table, i, "PSF_QF",            source->pixWeightNotBad);         // "PSF coverage/quality factor (bad)",         
+        @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableSetF32 (table, i, "PSF_QF_PERFECT",    source->pixWeightNotPoor);        // "PSF coverage/quality factor (poor)",        
+        @ALL@                       psFitsTableSetS32 (table, i, "PSF_NDOF",          outputs.nDOF);                    // "degrees of freedom",                        
+        @ALL@                       psFitsTableSetS32 (table, i, "PSF_NPIX",          outputs.nPix);                    // "number of pixels in fit",                   
+
+        @ALL@                       psFitsTableSetF32 (table, i, "MOMENTS_XX",        moments.Mxx);                     // "second moments (X^2)",                      
+        @ALL@                       psFitsTableSetF32 (table, i, "MOMENTS_XY",        moments.Mxy);                     // "second moments (X*Y)",                      
+        @ALL@                       psFitsTableSetF32 (table, i, "MOMENTS_YY",        moments.Myy);                     // "second moments (Y*Y)",                      
+
+        @>PS1_V2,PS1_SV?@           psFitsTableSetF32 (table, i, "MOMENTS_M3C",       moments.M_c3);                    // "third momemt cos theta",                    
+        @>PS1_V2,PS1_SV?@           psFitsTableSetF32 (table, i, "MOMENTS_M3S",       moments.M_s3);                    // "third momemt sin theta",                    
+        @>PS1_V2,PS1_SV?@           psFitsTableSetF32 (table, i, "MOMENTS_M4C",       moments.M_c4);                    // "fourth momemt cos theta",                   
+        @>PS1_V2,PS1_SV?@           psFitsTableSetF32 (table, i, "MOMENTS_M4S",       moments.M_s4);                    // "fourth momemt sin theta",                   
+
+        // Lensing parameters:
+        if (source->lensingOBJ && source->lensingOBJ->smear) {
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_X11_SM_OBJ",   source->lensingOBJ->smear->X11);  // "smear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_X12_SM_OBJ",   source->lensingOBJ->smear->X12);  // "smear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_X22_SM_OBJ",   source->lensingOBJ->smear->X22);  // "smear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_E1_SM_OBJ",    source->lensingOBJ->smear->e1);   // "smear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_E2_SM_OBJ",    source->lensingOBJ->smear->e2);   // "smear polarizability element (objects)",     
+        }
+        if (source->lensingOBJ && source->lensingOBJ->shear) {
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_X11_SH_OBJ",   source->lensingOBJ->shear->X11);  // "shear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_X12_SH_OBJ",   source->lensingOBJ->shear->X12);  // "shear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_X22_SH_OBJ",   source->lensingOBJ->shear->X22);  // "shear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_E1_SH_OBJ",    source->lensingOBJ->shear->e1);   // "shear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_E2_SH_OBJ",    source->lensingOBJ->shear->e2);   // "shear polarizability element (objects)",     
+        }
+        if (false && source->lensingOBJ) {
+            // do not bother to save these as they are equivalent to Mxx,Mxy,Myy above
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_E1_PSF",       source->lensingOBJ->e1);          // "shear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_E2_PSF",       source->lensingOBJ->e2);          // "shear polarizability element (objects)",     
+        }
+
+        if (source->lensingPSF && source->lensingPSF->smear) {
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_X11_SM_PSF",   source->lensingPSF->smear->X11);  // "smear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_X12_SM_PSF",   source->lensingPSF->smear->X12);  // "smear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_X22_SM_PSF",   source->lensingPSF->smear->X22);  // "smear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_E1_SM_PSF",    source->lensingPSF->smear->e1);   // "smear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_E2_SM_PSF",    source->lensingPSF->smear->e2);   // "smear polarizability element (objects)",     
+        }
+        if (source->lensingPSF && source->lensingPSF->shear) {
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_X11_SH_PSF",   source->lensingPSF->shear->X11);  // "shear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_X12_SH_PSF",   source->lensingPSF->shear->X12);  // "shear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_X22_SH_PSF",   source->lensingPSF->shear->X22);  // "shear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_E1_SH_PSF",    source->lensingPSF->shear->e1);   // "shear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_E2_SH_PSF",    source->lensingPSF->shear->e2);   // "shear polarizability element (objects)",     
+        }
+        if (source->lensingPSF) {
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_E1_PSF",       source->lensingPSF->e1);          // "shear polarizability element (objects)",     
+            @>PS1_V4@               psFitsTableSetF32 (table, i, "LENS_E2_PSF",       source->lensingPSF->e2);          // "shear polarizability element (objects)",     
+        }
+
+        // if lensing params exist also include the backmapped chipID and chip coordinates
+        if (source->lensingPSF && source->lensingPSF->shear) {
+            @>PS1_V4@               psFitsTableSetS16 (table, i, "SRC_CHIP_NUM",      source->chipNum);                 // "id of warp input chip",       
+            @>PS1_V4@               psFitsTableSetS16 (table, i, "SRC_CHIP_X",        source->chipX);                   // "x coord in warp input chip",  
+            @>PS1_V4@               psFitsTableSetS16 (table, i, "SRC_CHIP_Y",        source->chipY);                   // "y coord in warp input chip",  
+            @>PS1_V4@               psFitsTableSetS16 (table, i, "PADDING3",          0);                               // "more padding",                
+        }
+
+        @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableSetF32 (table, i, "MOMENTS_R1",        moments.Mrf);                     // "first radial moment",                        
+        @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableSetF32 (table, i, "MOMENTS_RH",        moments.Mrh);                     // "half radial moment",                         
+        @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableSetF32 (table, i, "KRON_FLUX",         moments.Krf);                     // "Kron Flux (in 2.5 R1)",                      
+        @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableSetF32 (table, i, "KRON_FLUX_ERR",     moments.dKrf);                    // "Kron Flux Error",                            
+        @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableSetF32 (table, i, "KRON_FLUX_INNER",   moments.Kinner);                  // "Kron Flux (in 1.0 R1)",                      
+        @>PS1_V2,PS1_SV?,>PS1_DV1@  psFitsTableSetF32 (table, i, "KRON_FLUX_OUTER",   moments.Kouter);                  // "Kron Flux (in 4.0 R1)",                      
+        @>PS1_V3@                   psFitsTableSetF32 (table, i, "SKY_LIMIT_RAD",     source->skyRadius);               // "Radius where object hits sky",               
+        @>PS1_V3@                   psFitsTableSetF32 (table, i, "SKY_LIMIT_FLUX",    source->skyFlux);                 // "Flux / pix where object hits sky",           
+        @>PS1_V3@                   psFitsTableSetF32 (table, i, "SKY_LIMIT_SLOPE",   source->skySlope);                // "d(Flux/pix)/dRadius where object hits sky",  
+
+        @>PS1_DV4@                  psFitsTableSetS16 (table, i, "SRC_CHIP_NUM",      source->chipNum);                 // "id of warp input chip",       
+        @>PS1_DV4@                  psFitsTableSetS16 (table, i, "SRC_CHIP_X",        source->chipX);                   // "x coord in warp input chip",  
+        @>PS1_DV4@                  psFitsTableSetS16 (table, i, "SRC_CHIP_Y",        source->chipY);                   // "y coord in warp input chip",  
+        @>PS1_DV4@                  psFitsTableSetS16 (table, i, "PADDING3",          0);                               // "more padding",                
+
+        @PS1_DV?@                   psFitsTableSetS32 (table, i, "DIFF_NPOS",         diffStats.nGood);                 // "nPos (n pix > 3 sigma)",                     
+        @PS1_DV?@                   psFitsTableSetF32 (table, i, "DIFF_FRATIO",       diffStats.fRatio);                // "fPos / (fPos + fNeg)",                       
+        @PS1_DV?@                   psFitsTableSetF32 (table, i, "DIFF_NRATIO_BAD",   diffStats.nRatioBad);             // "nPos / (nPos + nNeg)",                       
+        @PS1_DV?@                   psFitsTableSetF32 (table, i, "DIFF_NRATIO_MASK",  diffStats.nRatioMask);            // "nPos / (nPos + nMask)",                      
+        @PS1_DV?@                   psFitsTableSetF32 (table, i, "DIFF_NRATIO_ALL",   diffStats.nRatioAll);             // "nPos / (nGood + nMask + nBad)",              
+
+        @>PS1_DV1@                  psFitsTableSetF32 (table, i, "DIFF_R_P",          diffStats.Rp);                    // "distance to positive match source",  
+        @>PS1_DV1@                  psFitsTableSetF32 (table, i, "DIFF_SN_P",         diffStats.SNp);                   // "signal-to-noise of pos match src",   
+        @>PS1_DV1@                  psFitsTableSetF32 (table, i, "DIFF_R_M",          diffStats.Rm);                    // "distance to negative match source",  
+        @>PS1_DV1@                  psFitsTableSetF32 (table, i, "DIFF_SN_M",         diffStats.SNm);                   // "signal-to-noise of neg match src",   
+
+        @ALL@                      psFitsTableSetU32 (table, i, "FLAGS",              source->mode);                    // "psphot analysis flags",                      
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ psFitsTableSetU32 (table, i, "FLAGS2",             source->mode2);                   // "psphot analysis flags",                      
+        @PS1_V3,PS1_SV2@           psFitsTableSetS32 (table, i, "PADDING2",           0);                               // "padding",                                    
+
+        @ALL@                      psFitsTableSetU16 (table, i, "N_FRAMES",           source->nFrames);                 // "Number of frames overlapping source center", 
+        @ALL@                      psFitsTableSetS16 (table, i, "PADDING",            0);                               // "padding",                                    
+    }
+
+    // XXX why do we make a copy here to be supplemented with the masks?  why not do this in the calling function?
+    psMetadata *header = psMetadataCopy(NULL, tableHeader);
+    pmSourceMasksHeader(header);
+
+    psTrace ("pmFPAfile", 5, "writing ext data %s\n", extname);
+    if (!psFitsWriteTableNew(fits, header, table, extname)) {
+        psError(psErrorCodeLast(), false, "writing ext data %s\n", extname);
+        psFree(table);
+        psFree(header);
+        return false;
+    }
+    psFree(table);
+    psFree(header);
+
+    return true;
+}
+
+bool pmSourcesWrite_CMF_@CMFMODE@_Old (psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, psMetadata *tableHeader, char *extname, psMetadata *recipe)
+{
+    // fprintf (stderr, "writing with %s\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(extname, false);
+
+    psArray *table;
+    psMetadata *row;
+
+    pmChip *chip = readout->parent->parent;
+
+    // if the sequence is defined, write these in seq order; otherwise
+    // write them in S/N order:
+    if (sources->n > 0) {
+        pmSource *source = (pmSource *) sources->data[0];
+        if (source->seq == -1) {
+            // let's write these out in S/N order
+            sources = psArraySort (sources, pmSourceSortByFlux);
+        } else {
+            sources = psArraySort (sources, pmSourceSortBySeq);
+        }
+    }
+
+    table = psArrayAllocEmpty (sources->n);
+
+    float magOffset; 
+    float zeroptErr; 
+    float fwhmMajor; 
+    float fwhmMinor;
+    pmSourceOutputsCommonValues (&magOffset, &zeroptErr, &fwhmMajor, &fwhmMinor, readout, imageHeader);
+
+    // we write out PSF-fits for all sources, regardless of quality.  the source flags tell us the state
+    // by the time we call this function, all values should be assigned.  let's use asserts to be sure in some cases.
+    for (int i = 0; i < sources->n; i++) {
+        // this is the source associated with this image
+        pmSource *thisSource = sources->data[i];
+
+        // this is the "real" version of this source 
+        pmSource *source = thisSource->parent ? thisSource->parent : thisSource;
+
+        // If source->seq is -1, source was generated in this analysis.  If source->seq is
+        // not -1, source was read from elsewhere: in the latter case, preserve the source
+        // ID.  source.seq is used instead of source.id since the latter is a const
+        // generated on Alloc, and would thus be wrong for read in sources.
+        if (source->seq == -1) {
+            source->seq = i;
+        }
+
+        // set the 'best' values for various output fields:
+        pmSourceOutputs outputs;
+        pmSourceOutputsSetValues (&outputs, source, chip, fwhmMajor, fwhmMinor, magOffset);
+
+        pmSourceOutputsMoments moments;
+        pmSourceOutputsSetMoments (&moments, source);
+
+        @PS1_DV?@ pmSourceDiffStats diffStats;
+        @PS1_DV?@ pmSourceDiffStatsInit(&diffStats);
+        @PS1_DV?@ if (source->diffStats) {
+        @PS1_DV?@     diffStats = *source->diffStats;
+        @PS1_DV?@ }
+
+        row = psMetadataAlloc ();
+
+        // the psMetadataAdd entry and the double quotes are used by grep to select the output fields for automatic documentation
+        // This set of psMetadataAdd Entries marks the "----" "Start of the PSF segment"
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "IPP_IDET",         PS_DATA_U32, "IPP detection identifier index",             source->seq);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "X_PSF",            PS_DATA_F32, "PSF x coordinate",                           outputs.xPos);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "Y_PSF",            PS_DATA_F32, "PSF y coordinate",                           outputs.yPos);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "X_PSF_SIG",        PS_DATA_F32, "Sigma in PSF x coordinate",                  outputs.xErr);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "Y_PSF_SIG",        PS_DATA_F32, "Sigma in PSF y coordinate",                  outputs.yErr);
+
+        // NOTE: pre-PS1_V2, we only reported RA & DEC in floats for reference, not precision
+        @PS1_V1@                  psMetadataAdd (row, PS_LIST_TAIL, "RA_PSF",           PS_DATA_F32, "PSF RA coordinate (degrees)",                outputs.ra);
+        @PS1_V1@                  psMetadataAdd (row, PS_LIST_TAIL, "DEC_PSF",          PS_DATA_F32, "PSF DEC coordinate (degrees)",               outputs.dec);
+
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "POSANGLE",         PS_DATA_F32, "position angle at source (degrees)",         outputs.posAngle);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "PLTSCALE",         PS_DATA_F32, "plate scale at source (arcsec/pixel)",       outputs.pltScale);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "PSF_INST_MAG",     PS_DATA_F32, "PSF fit instrumental magnitude",             source->psfMag);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "PSF_INST_MAG_SIG", PS_DATA_F32, "Sigma of PSF instrumental magnitude",        source->psfMagErr);
+
+        @ALL,!PS1_V1,!PS1_V2@     psMetadataAdd (row, PS_LIST_TAIL, "PSF_INST_FLUX",    PS_DATA_F32, "PSF fit instrumental flux (counts)",         source->psfFlux);
+        @ALL,!PS1_V1,!PS1_V2@     psMetadataAdd (row, PS_LIST_TAIL, "PSF_INST_FLUX_SIG",PS_DATA_F32, "Sigma of PSF instrumental flux",             source->psfFluxErr);
+
+        @ALL@                       psMetadataAdd (row, PS_LIST_TAIL, "AP_MAG",           PS_DATA_F32, "magnitude in standard aperture",             source->apMag);
+        @>PS1_V2,PS1_SV?,>PS1_DV1@  psMetadataAdd (row, PS_LIST_TAIL, "AP_MAG_RAW",       PS_DATA_F32, "magnitude in reported aperture",             source->apMagRaw);
+        @ALL@                       psMetadataAdd (row, PS_LIST_TAIL, "AP_MAG_RADIUS",    PS_DATA_F32, "radius used for aperture mags",              source->apRadius);
+        @>PS1_DV1,>PS1_V3,>PS1_SV1@ psMetadataAdd (row, PS_LIST_TAIL, "AP_FLUX",          PS_DATA_F32, "instrumental flux in standard aperture",     source->apFlux);
+        @>PS1_DV1,>PS1_V3,>PS1_SV1@ psMetadataAdd (row, PS_LIST_TAIL, "AP_FLUX_SIG",      PS_DATA_F32, "aperture flux error",                        source->apFluxErr);
+        @>PS1_V4,>PS1_SV2,>PS1_DV3@ psMetadataAdd (row, PS_LIST_TAIL, "AP_NPIX",          PS_DATA_S32, "aperture unmasked pixels",                   source->apNpixels);
+
+        @<PS1_V3,PS1_SV1,PS1_DV?@ psMetadataAdd (row, PS_LIST_TAIL, "PEAK_FLUX_AS_MAG", PS_DATA_F32, "Peak flux expressed as magnitude",           outputs.peakMag);
+
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "CAL_PSF_MAG",      PS_DATA_F32, "PSF Magnitude using supplied calibration",   outputs.calMag);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "CAL_PSF_MAG_SIG",  PS_DATA_F32, "measured scatter of zero point calibration", zeroptErr);
+        
+        // NOTE: RA & DEC (both double) need to be on an 8-byte boundary...
+        @ALL,!PS1_V1@             psMetadataAdd (row, PS_LIST_TAIL, "RA_PSF",           PS_DATA_F64, "PSF RA coordinate (degrees)",                outputs.ra);
+        @ALL,!PS1_V1@             psMetadataAdd (row, PS_LIST_TAIL, "DEC_PSF",          PS_DATA_F64, "PSF DEC coordinate (degrees)",               outputs.dec);
+
+        @>=PS1_V3,>PS1_SV1@       psMetadataAdd (row, PS_LIST_TAIL, "PEAK_FLUX_AS_MAG", PS_DATA_F32, "Peak flux expressed as magnitude",           outputs.peakMag);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "SKY",              PS_DATA_F32, "Sky level",                                  source->sky);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "SKY_SIGMA",        PS_DATA_F32, "Sigma of sky level",                         source->skyErr);
+
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "PSF_CHISQ",        PS_DATA_F32, "Chisq of PSF-fit",                           outputs.chisq);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "CR_NSIGMA",        PS_DATA_F32, "Nsigma deviations from PSF to CF",           source->crNsigma);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "EXT_NSIGMA",       PS_DATA_F32, "Nsigma deviations from PSF to EXT",          source->extNsigma);
+
+        // PSF shape parameters:
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "PSF_MAJOR",        PS_DATA_F32, "PSF width (major axis)",                     outputs.psfMajor);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "PSF_MINOR",        PS_DATA_F32, "PSF width (minor axis)",                     outputs.psfMinor);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "PSF_THETA",        PS_DATA_F32, "PSF orientation angle",                      outputs.psfTheta);
+        @>PS1_V4,>PS1_SV2,>PS1_DV3@ psMetadataAdd (row, PS_LIST_TAIL, "PSF_CORE",         PS_DATA_F32, "k term if defined",                          outputs.psfCore);
+
+        // I use a look-up table and linear interpolation to map PSF_MAJOR,PSF_MINOR + PSF_CORE to FWHM values
+        @>PS1_V4,>PS1_SV2,>PS1_DV3@ psMetadataAdd (row, PS_LIST_TAIL, "PSF_FWHM_MAJ",        PS_DATA_F32, "PSF FWHM (major axis)",                   outputs.psfMajorFWHM);
+        @>PS1_V4,>PS1_SV2,>PS1_DV3@ psMetadataAdd (row, PS_LIST_TAIL, "PSF_FWHM_MIN",        PS_DATA_F32, "PSF FWHM (minor axis)",                   outputs.psfMinorFWHM);
+
+        // psf data quality
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "PSF_QF",           PS_DATA_F32, "PSF coverage/quality factor (bad)",          source->pixWeightNotBad);
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ psMetadataAdd (row, PS_LIST_TAIL, "PSF_QF_PERFECT",   PS_DATA_F32, "PSF coverage/quality factor (poor)",         source->pixWeightNotPoor);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "PSF_NDOF",         PS_DATA_S32, "degrees of freedom",                         outputs.nDOF);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "PSF_NPIX",         PS_DATA_S32, "number of pixels in fit",                    outputs.nPix);
+
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "MOMENTS_XX",       PS_DATA_F32, "second moments (X^2)",                       moments.Mxx);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "MOMENTS_XY",       PS_DATA_F32, "second moments (X*Y)",                       moments.Mxy);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "MOMENTS_YY",       PS_DATA_F32, "second moments (Y*Y)",                       moments.Myy);
+
+        @>PS1_V2,PS1_SV?@         psMetadataAdd (row, PS_LIST_TAIL, "MOMENTS_M3C",      PS_DATA_F32, "third momemt cos theta",                     moments.M_c3);
+        @>PS1_V2,PS1_SV?@         psMetadataAdd (row, PS_LIST_TAIL, "MOMENTS_M3S",      PS_DATA_F32, "third momemt sin theta",                     moments.M_s3);
+        @>PS1_V2,PS1_SV?@         psMetadataAdd (row, PS_LIST_TAIL, "MOMENTS_M4C",      PS_DATA_F32, "fourth momemt cos theta",                    moments.M_c4);
+        @>PS1_V2,PS1_SV?@         psMetadataAdd (row, PS_LIST_TAIL, "MOMENTS_M4S",      PS_DATA_F32, "fourth momemt sin theta",                    moments.M_s4);
+
+        // Lensing parameters:
+        if (source->lensingOBJ && source->lensingOBJ->smear) {
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_X11_SM_OBJ",  PS_DATA_F32, "smear polarizability element (objects)",     source->lensingOBJ->smear->X11);
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_X12_SM_OBJ",  PS_DATA_F32, "smear polarizability element (objects)",     source->lensingOBJ->smear->X12);
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_X22_SM_OBJ",  PS_DATA_F32, "smear polarizability element (objects)",     source->lensingOBJ->smear->X22);
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_E1_SM_OBJ",   PS_DATA_F32, "smear polarizability element (objects)",     source->lensingOBJ->smear->e1);
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_E2_SM_OBJ",   PS_DATA_F32, "smear polarizability element (objects)",     source->lensingOBJ->smear->e2);
+        }
+        if (source->lensingOBJ && source->lensingOBJ->shear) {
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_X11_SH_OBJ",  PS_DATA_F32, "shear polarizability element (objects)",     source->lensingOBJ->shear->X11);
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_X12_SH_OBJ",  PS_DATA_F32, "shear polarizability element (objects)",     source->lensingOBJ->shear->X12);
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_X22_SH_OBJ",  PS_DATA_F32, "shear polarizability element (objects)",     source->lensingOBJ->shear->X22);
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_E1_SH_OBJ",   PS_DATA_F32, "shear polarizability element (objects)",     source->lensingOBJ->shear->e1); 
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_E2_SH_OBJ",   PS_DATA_F32, "shear polarizability element (objects)",     source->lensingOBJ->shear->e2); 
+        }
+        if (false && source->lensingOBJ) {
+          // do not bother to save these as they are equivalent to Mxx,Mxy,Myy above
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_E1_PSF",      PS_DATA_F32, "shear polarizability element (objects)",     source->lensingOBJ->e1); 
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_E2_PSF",      PS_DATA_F32, "shear polarizability element (objects)",     source->lensingOBJ->e2); 
+        }
+
+        if (source->lensingPSF && source->lensingPSF->smear) {
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_X11_SM_PSF",  PS_DATA_F32, "smear polarizability element (objects)",     source->lensingPSF->smear->X11);
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_X12_SM_PSF",  PS_DATA_F32, "smear polarizability element (objects)",     source->lensingPSF->smear->X12);
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_X22_SM_PSF",  PS_DATA_F32, "smear polarizability element (objects)",     source->lensingPSF->smear->X22);
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_E1_SM_PSF",   PS_DATA_F32, "smear polarizability element (objects)",     source->lensingPSF->smear->e1); 
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_E2_SM_PSF",   PS_DATA_F32, "smear polarizability element (objects)",     source->lensingPSF->smear->e2); 
+        }
+        if (source->lensingPSF && source->lensingPSF->shear) {
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_X11_SH_PSF",  PS_DATA_F32, "shear polarizability element (objects)",     source->lensingPSF->shear->X11);
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_X12_SH_PSF",  PS_DATA_F32, "shear polarizability element (objects)",     source->lensingPSF->shear->X12);
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_X22_SH_PSF",  PS_DATA_F32, "shear polarizability element (objects)",     source->lensingPSF->shear->X22);
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_E1_SH_PSF",   PS_DATA_F32, "shear polarizability element (objects)",     source->lensingPSF->shear->e1); 
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_E2_SH_PSF",   PS_DATA_F32, "shear polarizability element (objects)",     source->lensingPSF->shear->e2); 
+        }
+        if (source->lensingPSF) {
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_E1_PSF",      PS_DATA_F32, "shear polarizability element (objects)",     source->lensingPSF->e1); 
+          @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "LENS_E2_PSF",      PS_DATA_F32, "shear polarizability element (objects)",     source->lensingPSF->e2); 
+        }
+
+        // if lensing params exist also include the backmapped chipID and chip coordinates
+        if (source->lensingPSF && source->lensingPSF->shear) {
+            @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "SRC_CHIP_NUM",   PS_DATA_S16, "id of warp input chip",     source->chipNum); 
+            @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "SRC_CHIP_X",    PS_DATA_S16, "x coord in warp input chip",     source->chipX); 
+            @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "SRC_CHIP_Y",    PS_DATA_S16, "y coord in warp input chip",     source->chipY); 
+            @>PS1_V4@               psMetadataAdd (row, PS_LIST_TAIL, "PADDING3",      PS_DATA_S16, "more padding", 0);
+        }
+
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ psMetadataAdd (row, PS_LIST_TAIL, "MOMENTS_R1",       PS_DATA_F32, "first radial moment",                        moments.Mrf);
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ psMetadataAdd (row, PS_LIST_TAIL, "MOMENTS_RH",       PS_DATA_F32, "half radial moment",                         moments.Mrh);
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ psMetadataAdd (row, PS_LIST_TAIL, "KRON_FLUX",        PS_DATA_F32, "Kron Flux (in 2.5 R1)",                      moments.Krf);
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ psMetadataAdd (row, PS_LIST_TAIL, "KRON_FLUX_ERR",    PS_DATA_F32, "Kron Flux Error",                            moments.dKrf);
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ psMetadataAdd (row, PS_LIST_TAIL, "KRON_FLUX_INNER",  PS_DATA_F32, "Kron Flux (in 1.0 R1)",                      moments.Kinner);
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ psMetadataAdd (row, PS_LIST_TAIL, "KRON_FLUX_OUTER",  PS_DATA_F32, "Kron Flux (in 4.0 R1)",                      moments.Kouter);
+
+        @>PS1_V3@                 psMetadataAdd (row, PS_LIST_TAIL, "SKY_LIMIT_RAD",    PS_DATA_F32, "Radius where object hits sky",               source->skyRadius);
+        @>PS1_V3@                 psMetadataAdd (row, PS_LIST_TAIL, "SKY_LIMIT_FLUX",   PS_DATA_F32, "Flux / pix where object hits sky",           source->skyFlux);
+        @>PS1_V3@                 psMetadataAdd (row, PS_LIST_TAIL, "SKY_LIMIT_SLOPE",  PS_DATA_F32, "d(Flux/pix)/dRadius where object hits sky",  source->skySlope);
+
+        @>PS1_DV4@                psMetadataAdd (row, PS_LIST_TAIL, "SRC_CHIP_NUM",     PS_DATA_S16, "id of warp input chip",                      source->chipNum); 
+        @>PS1_DV4@                psMetadataAdd (row, PS_LIST_TAIL, "SRC_CHIP_X",       PS_DATA_S16, "x coord in warp input chip",                 source->chipX); 
+        @>PS1_DV4@                psMetadataAdd (row, PS_LIST_TAIL, "SRC_CHIP_Y",       PS_DATA_S16, "y coord in warp input chip",                 source->chipY); 
+        @>PS1_DV4@                psMetadataAdd (row, PS_LIST_TAIL, "PADDING3",         PS_DATA_S16, "more padding", 0);
+
+        @PS1_DV?@                 psMetadataAdd (row, PS_LIST_TAIL, "DIFF_NPOS",        PS_DATA_S32, "nPos (n pix > 3 sigma)",                     diffStats.nGood);
+        @PS1_DV?@                 psMetadataAdd (row, PS_LIST_TAIL, "DIFF_FRATIO",      PS_DATA_F32, "fPos / (fPos + fNeg)",                       diffStats.fRatio);
+        @PS1_DV?@                 psMetadataAdd (row, PS_LIST_TAIL, "DIFF_NRATIO_BAD",  PS_DATA_F32, "nPos / (nPos + nNeg)",                       diffStats.nRatioBad);
+        @PS1_DV?@                 psMetadataAdd (row, PS_LIST_TAIL, "DIFF_NRATIO_MASK", PS_DATA_F32, "nPos / (nPos + nMask)",                      diffStats.nRatioMask);
+        @PS1_DV?@                 psMetadataAdd (row, PS_LIST_TAIL, "DIFF_NRATIO_ALL",  PS_DATA_F32, "nPos / (nGood + nMask + nBad)",              diffStats.nRatioAll);
+
+        @>PS1_DV1@                        psMetadataAdd (row, PS_LIST_TAIL, "DIFF_R_P",         PS_DATA_F32, "distance to positive match source",          diffStats.Rp);
+        @>PS1_DV1@                        psMetadataAdd (row, PS_LIST_TAIL, "DIFF_SN_P",        PS_DATA_F32, "signal-to-noise of pos match src",           diffStats.SNp);
+        @>PS1_DV1@                        psMetadataAdd (row, PS_LIST_TAIL, "DIFF_R_M",         PS_DATA_F32, "distance to negative match source",          diffStats.Rm);
+        @>PS1_DV1@                        psMetadataAdd (row, PS_LIST_TAIL, "DIFF_SN_M",        PS_DATA_F32, "signal-to-noise of neg match src",           diffStats.SNm);
+
+        @ALL@                      psMetadataAdd (row, PS_LIST_TAIL, "FLAGS",            PS_DATA_U32, "psphot analysis flags",                      source->mode);
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ psMetadataAdd (row, PS_LIST_TAIL, "FLAGS2",           PS_DATA_U32, "psphot analysis flags",                      source->mode2);
+        @PS1_V3,PS1_SV2@           psMetadataAdd (row, PS_LIST_TAIL, "PADDING2",         PS_DATA_S32, "more padding", 0);
+        @PS1_SV?@
+
+          // note that this definition is inconsistent with the definition in
+          // Ohana/src/libautocode/def.  This version creates a table with data not
+          // properly aligned with the 8-byte boundaries. The structure defined by
+          // libautocode does this, but has a different order of elements (and adds
+          // padding2 to fix things). We have generated may files with PS1_SV1 as is, so
+          // I'll leave it. But in future a PS1_SV2 should be forced to match
+          // libautocode. Note that addstar knows to detect the alternate version of
+          // PS1_SV1 and correctly interpret its fields.
+
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "N_FRAMES",         PS_DATA_U16, "Number of frames overlapping source center", source->nFrames);
+        @ALL@                     psMetadataAdd (row, PS_LIST_TAIL, "PADDING",          PS_DATA_S16, "padding", 0);
+
+        psArrayAdd (table, 100, row);
+        psFree (row);
+
+        // EXT_NSIGMA will be NAN if: 1) contour ellipse is imaginary; 2) source is not
+        // subtracted
+
+        // CR_NSIGMA will be NAN if: 1) source is not subtracted; 2) source is on the image
+        // edge; 3) any pixels in the 3x3 peak region are masked;
+    }
+
+    // XXX why do we make a copy here to be supplemented with the masks?  why not do this in the calling function?
+    psMetadata *header = psMetadataCopy(NULL, tableHeader);
+    pmSourceMasksHeader(header);
+
+    if (table->n == 0) {
+        if (!psFitsWriteBlank(fits, header, extname)) {
+            psError(psErrorCodeLast(), false, "Unable to write blank sources file.");
+            psFree(table);
+            psFree(header);
+            return false;
+        }
+        psFree(table);
+        psFree(header);
+        return true;
+    }
+
+    psTrace ("pmFPAfile", 5, "writing ext data %s\n", extname);
+    if (!psFitsWriteTable(fits, header, table, extname)) {
+        psError(psErrorCodeLast(), false, "writing ext data %s\n", extname);
+        psFree(table);
+        psFree(header);
+        return false;
+    }
+    psFree(table);
+    psFree(header);
+
+    return true;
+}
+
+bool pmSourcesWrite_CMF_@CMFMODE@ (psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, psMetadata *tableHeader, char *extname, psMetadata *recipe)
+{
+  // bool status = pmSourcesWrite_CMF_@CMFMODE@_Old (fits, readout, sources, imageHeader, tableHeader, extname, recipe);
+     bool status = pmSourcesWrite_CMF_@CMFMODE@_New (fits, readout, sources, imageHeader, tableHeader, extname, recipe);
+    return status;
+}
+
+// read in a readout from the fits file
+psArray *pmSourcesRead_CMF_@CMFMODE@_New (psFits *fits, psMetadata *header)
+{
+    // fprintf (stderr, "reading with %s\n", __func__);
+
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(header, false);
+
+    bool status;
+    psF32 *PAR, *dPAR;
+    psEllipseAxes axes;
+
+    // define PSF model type
+    int defaultModelType = pmModelClassGetType ("PS_MODEL_GAUSS");
+    int modelType = -1;
+
+    // if header does not define the model, default to a gaussian
+    char *PSF_NAME = psMetadataLookupStr (&status, header, "PSFMODEL");
+    if (PSF_NAME != NULL) {
+        modelType = pmModelClassGetType (PSF_NAME);
+    }
+    // work around bug in psphotFullForce
+    if (modelType < 0) {
+    	modelType = defaultModelType;
+    }
+    // assert (modelType > -1);
+
+    // do we expect to find lensing parameters?
+    bool haveLensOBJ = psMetadataLookupBool (&status, header, "LENS_OBJ");
+    bool haveLensPSF = psMetadataLookupBool (&status, header, "LENS_PSF");
+
+    // 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 blocks the free.  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
+
+    // reads full table into memory
+    psFitsTable *table = psFitsReadTableNew (fits);
+
+    // convert the table to the pmSource entries
+    for (int i = 0; i < numSources; i++) {
+        pmSource *source = pmSourceAlloc ();
+        pmModel *model = pmModelAlloc (modelType);
+        source->modelPSF  = model;
+        source->type = PM_SOURCE_TYPE_STAR; // XXX this should be added to the flags
+
+        // 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;
+
+	@ALL@                       source->seq                 = psFitsTableGetU32 (&status, table, i, "IPP_IDET");
+	@ALL@                       PAR[PM_PAR_XPOS]            = psFitsTableGetF32 (&status, table, i, "X_PSF");
+	@ALL@                       PAR[PM_PAR_YPOS]            = psFitsTableGetF32 (&status, table, i, "Y_PSF");
+	@ALL@                       dPAR[PM_PAR_XPOS]           = psFitsTableGetF32 (&status, table, i, "X_PSF_SIG");
+	@ALL@                       dPAR[PM_PAR_YPOS]           = psFitsTableGetF32 (&status, table, i, "Y_PSF_SIG");
+	@ALL@                       axes.major                  = psFitsTableGetF32 (&status, table, i, "PSF_MAJOR");
+	@ALL@                       axes.minor                  = psFitsTableGetF32 (&status, table, i, "PSF_MINOR");
+	@ALL@                       axes.theta                  = psFitsTableGetF32 (&status, table, i, "PSF_THETA");
+	@ALL@                       axes.theta                  = axes.theta * PS_RAD_DEG;
+	
+	@>PS1_V4,>PS1_SV2,>PS1_DV3@ if (model->params->n > PM_PAR_7) {
+	@>PS1_V4,>PS1_SV2,>PS1_DV3@     PAR[PM_PAR_7]           = psFitsTableGetF32 (&status, table, i, "PSF_CORE");
+	@>PS1_V4,>PS1_SV2,>PS1_DV3@ } 
+
+	@ALL@                       PAR[PM_PAR_SKY]             = psFitsTableGetF32 (&status, table, i, "SKY");
+	@ALL@                       dPAR[PM_PAR_SKY]            = psFitsTableGetF32 (&status, table, i, "SKY_SIGMA");
+	@ALL@                       source->sky                 = PAR[PM_PAR_SKY];
+	@ALL@                       source->skyErr              = dPAR[PM_PAR_SKY];
+
+        // XXX use these to determine PAR[PM_PAR_I0]?
+	@ALL@                       source->psfMag              = psFitsTableGetF32 (&status, table, i, "PSF_INST_MAG");
+	@ALL@                       source->psfMagErr           = psFitsTableGetF32 (&status, table, i, "PSF_INST_MAG_SIG");
+	@ALL@                       source->apMag               = psFitsTableGetF32 (&status, table, i, "AP_MAG");
+	@>PS1_V2,PS1_SV?,>PS1_DV1@  source->apMagRaw            = psFitsTableGetF32 (&status, table, i, "AP_MAG_RAW");
+	@>PS1_DV1,>PS1_V3,>PS1_SV1@ source->apFlux              = psFitsTableGetF32 (&status, table, i, "AP_FLUX");
+	@>PS1_DV1,>PS1_V3,>PS1_SV1@ source->apFluxErr           = psFitsTableGetF32 (&status, table, i, "AP_FLUX_SIG");
+
+        // XXX use these to determine PAR[PM_PAR_I0] if they exist?
+	// XXX add these to PS1_SV1?
+	@>PS1_V2,PS1_SV?,PS1_DV?@   source->psfFlux             = psFitsTableGetF32 (&status, table, i, "PSF_INST_FLUX");
+	@>PS1_V2,PS1_SV?,PS1_DV?@   source->psfFluxErr          = psFitsTableGetF32 (&status, table, i, "PSF_INST_FLUX_SIG");
+
+        // XXX this scaling is incorrect: does not include the 2 \pi AREA factor
+	@ALL@     PAR[PM_PAR_I0]                                = (isfinite(source->psfMag)) ? pow(10.0, -0.4*source->psfMag) : NAN;
+	@ALL@     dPAR[PM_PAR_I0]                               = (isfinite(source->psfMag)) ? PAR[PM_PAR_I0] * source->psfMagErr : NAN;
+
+        pmPSF_AxesToModel (PAR, axes, model->class->useReff);
+
+	@ALL@     float peakMag                                 = psFitsTableGetF32 (&status, table, i, "PEAK_FLUX_AS_MAG");
+	@ALL@     float peakFlux                                = (isfinite(peakMag)) ? pow(10.0, -0.4*peakMag) : NAN;
+
+        // recreate the peak to match (xPos, yPos) +/- (xErr, yErr)
+	@ALL@     source->peak = pmPeakAlloc(PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], peakFlux, PM_PEAK_LONE);
+	@ALL@     source->peak->rawFlux = peakFlux;
+	@ALL@     source->peak->smoothFlux = peakFlux;
+	@ALL@     source->peak->xf                              = PAR[PM_PAR_XPOS]; // more accurate position
+	@ALL@     source->peak->yf                              = PAR[PM_PAR_YPOS]; // more accurate position
+	@ALL@     source->peak->dx                              = dPAR[PM_PAR_XPOS];
+	@ALL@     source->peak->dy                              = dPAR[PM_PAR_YPOS];
+
+	@ALL@     source->pixWeightNotBad                       = psFitsTableGetF32 (&status, table, i, "PSF_QF");
+	@>PS1_V2,PS1_SV?,>PS1_DV1@ source->pixWeightNotPoor     = psFitsTableGetF32 (&status, table, i, "PSF_QF_PERFECT");
+	@ALL@     source->crNsigma                              = psFitsTableGetF32 (&status, table, i, "CR_NSIGMA");
+	@ALL@     source->extNsigma                             = psFitsTableGetF32 (&status, table, i, "EXT_NSIGMA");
+	@ALL@     source->apRadius                              = psFitsTableGetF32 (&status, table, i, "AP_MAG_RADIUS");
+	@>PS1_V4,>PS1_SV2,>PS1_DV3@ source->apNpixels           = psFitsTableGetS32 (&status, table, i, "AP_NPIX");
+
+        // note that some older versions used PSF_PROBABILITY: this was not well defined.
+	@ALL@     model->chisq                                  = psFitsTableGetF32 (&status, table, i, "PSF_CHISQ");
+	@ALL@     model->nDOF                                   = psFitsTableGetS32 (&status, table, i, "PSF_NDOF");
+	@ALL@     model->nPix                                   = psFitsTableGetS32 (&status, table, i, "PSF_NPIX");
+
+	@ALL@     source->moments = pmMomentsAlloc ();
+	@ALL@     source->moments->Mx = source->peak->xf; // we don't have both Mx,My and xf,yf in the cmf
+	@ALL@     source->moments->My = source->peak->yf; // we don't have both Mx,My and xf,yf in the cmf
+
+	@ALL@     source->moments->Mxx                          = psFitsTableGetF32 (&status, table, i, "MOMENTS_XX");
+	@ALL@     source->moments->Mxy                          = psFitsTableGetF32 (&status, table, i, "MOMENTS_XY");
+	@ALL@     source->moments->Myy                          = psFitsTableGetF32 (&status, table, i, "MOMENTS_YY");
+
+	// XXX we do not save all of the 3rd and 4th moment parameters. when we load in data,
+	// we are storing enough information so the output will be consistent with the input
+	@>PS1_V2,PS1_SV?@ source->moments->Mxxx         = +1.00 * psFitsTableGetF32 (&status, table, i, "MOMENTS_M3C");
+	@>PS1_V2,PS1_SV?@ source->moments->Mxxy         =  0.00;
+	@>PS1_V2,PS1_SV?@ source->moments->Mxyy         =  0.00;
+	@>PS1_V2,PS1_SV?@ source->moments->Myyy         = -1.00 * psFitsTableGetF32 (&status, table, i, "MOMENTS_M3S");
+	@>PS1_V2,PS1_SV?@ source->moments->Mxxxx        = +1.00 * psFitsTableGetF32 (&status, table, i, "MOMENTS_M4C");
+	@>PS1_V2,PS1_SV?@ source->moments->Mxxxy        =  0.00;
+	@>PS1_V2,PS1_SV?@ source->moments->Mxxyy        =  0.00;
+	@>PS1_V2,PS1_SV?@ source->moments->Mxyyy        = -0.25 * psFitsTableGetF32 (&status, table, i, "MOMENTS_M4S");
+	@>PS1_V2,PS1_SV?@ source->moments->Myyyy        =  0.00;
+
+	// Lensing parameters (on read if PS1_V5+)
+	if (haveLensOBJ) {
+	    source->lensingOBJ = pmSourceLensingAlloc ();
+	    source->lensingOBJ->smear = pmLensingParsAlloc();
+	    source->lensingOBJ->shear = pmLensingParsAlloc();
+
+	    @>PS1_V4@ source->lensingOBJ->smear->X11            = psFitsTableGetF32 (&status, table, i, "LENS_X11_SM_OBJ");
+	    @>PS1_V4@ source->lensingOBJ->smear->X12            = psFitsTableGetF32 (&status, table, i, "LENS_X12_SM_OBJ");
+	    @>PS1_V4@ source->lensingOBJ->smear->X22            = psFitsTableGetF32 (&status, table, i, "LENS_X22_SM_OBJ");
+	    @>PS1_V4@ source->lensingOBJ->smear->e1             = psFitsTableGetF32 (&status, table, i, "LENS_E1_SM_OBJ");
+	    @>PS1_V4@ source->lensingOBJ->smear->e2             = psFitsTableGetF32 (&status, table, i, "LENS_E2_SM_OBJ");
+	    @>PS1_V4@ source->lensingOBJ->shear->X11            = psFitsTableGetF32 (&status, table, i, "LENS_X11_SH_OBJ");
+	    @>PS1_V4@ source->lensingOBJ->shear->X12            = psFitsTableGetF32 (&status, table, i, "LENS_X12_SH_OBJ");
+	    @>PS1_V4@ source->lensingOBJ->shear->X22            = psFitsTableGetF32 (&status, table, i, "LENS_X22_SH_OBJ");
+	    @>PS1_V4@ source->lensingOBJ->shear->e1             = psFitsTableGetF32 (&status, table, i, "LENS_E1_SH_OBJ");
+	    @>PS1_V4@ source->lensingOBJ->shear->e2             = psFitsTableGetF32 (&status, table, i, "LENS_E2_SH_OBJ");
+	}
+
+	@>PS1_V4@ source->chipNum                               = psFitsTableGetS16 (&status, table, i, "SRC_CHIP_NUM");
+	@>PS1_V4@ source->chipX                                 = psFitsTableGetS16 (&status, table, i, "SRC_CHIP_X");
+	@>PS1_V4@ source->chipY                                 = psFitsTableGetS16 (&status, table, i, "SRC_CHIP_Y");
+
+	if (haveLensPSF) {
+	    source->lensingPSF = pmSourceLensingAlloc ();
+	    source->lensingPSF->smear = pmLensingParsAlloc();
+	    source->lensingPSF->shear = pmLensingParsAlloc();
+	    
+	    @>PS1_V4@            source->lensingPSF->smear->X11 = psFitsTableGetF32 (&status, table, i, "LENS_X11_SM_PSF");
+	    @>PS1_V4@            source->lensingPSF->smear->X12 = psFitsTableGetF32 (&status, table, i, "LENS_X12_SM_PSF");
+	    @>PS1_V4@            source->lensingPSF->smear->X22 = psFitsTableGetF32 (&status, table, i, "LENS_X22_SM_PSF");
+	    @>PS1_V4@            source->lensingPSF->smear->e1  = psFitsTableGetF32 (&status, table, i, "LENS_E1_SM_PSF");
+	    @>PS1_V4@            source->lensingPSF->smear->e2  = psFitsTableGetF32 (&status, table, i, "LENS_E2_SM_PSF");
+	    @>PS1_V4@            source->lensingPSF->shear->X11 = psFitsTableGetF32 (&status, table, i, "LENS_X11_SH_PSF");
+	    @>PS1_V4@            source->lensingPSF->shear->X12 = psFitsTableGetF32 (&status, table, i, "LENS_X12_SH_PSF");
+	    @>PS1_V4@            source->lensingPSF->shear->X22 = psFitsTableGetF32 (&status, table, i, "LENS_X22_SH_PSF");
+	    @>PS1_V4@            source->lensingPSF->shear->e1  = psFitsTableGetF32 (&status, table, i, "LENS_E1_SH_PSF");
+	    @>PS1_V4@            source->lensingPSF->shear->e2  = psFitsTableGetF32 (&status, table, i, "LENS_E2_SH_PSF");
+	    @>PS1_V4@            source->lensingPSF->e1         = psFitsTableGetF32 (&status, table, i, "LENS_E1_PSF");
+	    @>PS1_V4@            source->lensingPSF->e2         = psFitsTableGetF32 (&status, table, i, "LENS_E2_PSF");
+	}
+
+	@>PS1_V2,PS1_SV?,>PS1_DV1@ source->moments->Mrf         = psFitsTableGetF32 (&status, table, i, "MOMENTS_R1");
+	@>PS1_V2,PS1_SV?,>PS1_DV1@ source->moments->Mrh         = psFitsTableGetF32 (&status, table, i, "MOMENTS_RH");
+	@>PS1_V2,PS1_SV?,>PS1_DV1@ source->moments->KronFlux    = psFitsTableGetF32 (&status, table, i, "KRON_FLUX");
+	@>PS1_V2,PS1_SV?,>PS1_DV1@ source->moments->KronFluxErr = psFitsTableGetF32 (&status, table, i, "KRON_FLUX_ERR");
+	@>PS1_V2,PS1_SV?,>PS1_DV1@ source->moments->KronFinner  = psFitsTableGetF32 (&status, table, i, "KRON_FLUX_INNER");
+	@>PS1_V2,PS1_SV?,>PS1_DV1@ source->moments->KronFouter  = psFitsTableGetF32 (&status, table, i, "KRON_FLUX_OUTER");
+
+	@>PS1_V3@                  source->skyRadius            = psFitsTableGetF32 (&status, table, i, "SKY_LIMIT_RAD");
+	@>PS1_V3@                  source->skyFlux              = psFitsTableGetF32 (&status, table, i, "SKY_LIMIT_FLUX");
+	@>PS1_V3@                  source->skySlope             = psFitsTableGetF32 (&status, table, i, "SKY_LIMIT_SLOPE");
+
+        @PS1_DV?@                  int nPos                     = psFitsTableGetS32 (&status, table, i, "DIFF_NPOS");
+        @PS1_DV?@  if (nPos) {
+        @PS1_DV?@      source->diffStats                        = pmSourceDiffStatsAlloc();
+        @PS1_DV?@      source->diffStats->nGood                 = nPos;
+        @PS1_DV?@      source->diffStats->fRatio                = psFitsTableGetF32 (&status, table, i, "DIFF_FRATIO");
+        @PS1_DV?@      source->diffStats->nRatioBad             = psFitsTableGetF32 (&status, table, i, "DIFF_NRATIO_BAD");
+        @PS1_DV?@      source->diffStats->nRatioMask            = psFitsTableGetF32 (&status, table, i, "DIFF_NRATIO_MASK");
+        @PS1_DV?@      source->diffStats->nRatioAll             = psFitsTableGetF32 (&status, table, i, "DIFF_NRATIO_ALL");
+        
+        @>PS1_DV1@      source->diffStats->Rp                   = psFitsTableGetF32 (&status, table, i, "DIFF_R_P");
+        @>PS1_DV1@      source->diffStats->SNp                  = psFitsTableGetF32 (&status, table, i, "DIFF_SN_P");
+        @>PS1_DV1@      source->diffStats->Rm                   = psFitsTableGetF32 (&status, table, i, "DIFF_R_M");
+        @>PS1_DV1@      source->diffStats->SNm                  = psFitsTableGetF32 (&status, table, i, "DIFF_SN_M");
+        @PS1_DV?@  }
+
+	@ALL@                      source->mode                 = psFitsTableGetU32 (&status, table, i, "FLAGS");
+	@>PS1_V2,PS1_SV?,>PS1_DV1@ source->mode2                = psFitsTableGetU32 (&status, table, i, "FLAGS2");
+	@ALL@                      source->nFrames              = psFitsTableGetU16 (&status, table, i, "N_FRAMES");
+        assert (status);
+
+        sources->data[i] = source;
+    }
+    psFree (table);
+    return sources;
+}
+
+// read in a readout from the fits file
+psArray *pmSourcesRead_CMF_@CMFMODE@_Old (psFits *fits, psMetadata *header)
+{
+    // fprintf (stderr, "reading with %s\n", __func__);
+
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(header, false);
+
+    bool status;
+    psF32 *PAR, *dPAR;
+    psEllipseAxes axes;
+
+    // define PSF model type
+    int defaultModelType = pmModelClassGetType ("PS_MODEL_GAUSS");
+    int modelType = -1;
+
+    // if header does not define the model, default to a gaussian
+    char *PSF_NAME = psMetadataLookupStr (&status, header, "PSFMODEL");
+    if (PSF_NAME != NULL) {
+        modelType = pmModelClassGetType (PSF_NAME);
+    }
+    // work around bug in psphotFullForce
+    if (modelType < 0) {
+        modelType = defaultModelType;
+    }
+    // assert (modelType > -1);
+
+    // do we expect to find lensing parameters?
+    bool haveLensOBJ = psMetadataLookupBool (&status, header, "LENS_OBJ");
+    bool haveLensPSF = psMetadataLookupBool (&status, header, "LENS_PSF");
+
+    // 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 blocks the free.  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
+    for (int i = 0; i < numSources; i++) {
+        psMetadata *row = psFitsReadTableRow(fits, i); // Table row
+        if (!row) {
+            psError(psErrorCodeLast(), false, "Unable to read row %d of sources", i);
+            psFree(sources);
+            return NULL;
+        }
+
+        pmSource *source = pmSourceAlloc ();
+        pmModel *model = pmModelAlloc (modelType);
+        source->modelPSF  = model;
+        source->type = PM_SOURCE_TYPE_STAR; // XXX this should be added to the flags
+
+        // 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;
+
+        @ALL@     source->seq       = psMetadataLookupU32 (&status, row, "IPP_IDET");
+        @ALL@     PAR[PM_PAR_XPOS]  = psMetadataLookupF32 (&status, row, "X_PSF");
+        @ALL@     PAR[PM_PAR_YPOS]  = psMetadataLookupF32 (&status, row, "Y_PSF");
+        @ALL@     dPAR[PM_PAR_XPOS] = psMetadataLookupF32 (&status, row, "X_PSF_SIG");
+        @ALL@     dPAR[PM_PAR_YPOS] = psMetadataLookupF32 (&status, row, "Y_PSF_SIG");
+        @ALL@     axes.major        = psMetadataLookupF32 (&status, row, "PSF_MAJOR");
+        @ALL@     axes.minor        = psMetadataLookupF32 (&status, row, "PSF_MINOR");
+        @ALL@     axes.theta        = psMetadataLookupF32 (&status, row, "PSF_THETA");
+        @ALL@     axes.theta        = axes.theta * PS_RAD_DEG;
+        
+        @>PS1_V4,>PS1_SV2,>PS1_DV3@ if (model->params->n > PM_PAR_7) {
+        @>PS1_V4,>PS1_SV2,>PS1_DV3@     PAR[PM_PAR_7] = psMetadataLookupF32 (&status, row, "PSF_CORE");
+        @>PS1_V4,>PS1_SV2,>PS1_DV3@ } 
+
+        @ALL@     PAR[PM_PAR_SKY]   = psMetadataLookupF32 (&status, row, "SKY");
+        @ALL@     dPAR[PM_PAR_SKY]  = psMetadataLookupF32 (&status, row, "SKY_SIGMA");
+        @ALL@     source->sky       = PAR[PM_PAR_SKY];
+        @ALL@     source->skyErr    = dPAR[PM_PAR_SKY];
+
+        // XXX use these to determine PAR[PM_PAR_I0]?
+        @ALL@     source->psfMag    = psMetadataLookupF32 (&status, row, "PSF_INST_MAG");
+        @ALL@     source->psfMagErr = psMetadataLookupF32 (&status, row, "PSF_INST_MAG_SIG");
+        @ALL@     source->apMag     = psMetadataLookupF32 (&status, row, "AP_MAG");
+        @>PS1_V2,PS1_SV?,>PS1_DV1@  source->apMagRaw  = psMetadataLookupF32 (&status, row, "AP_MAG_RAW");
+        @>PS1_DV1,>PS1_V3,>PS1_SV1@ source->apFlux    = psMetadataLookupF32 (&status, row, "AP_FLUX");
+        @>PS1_DV1,>PS1_V3,>PS1_SV1@ source->apFluxErr = psMetadataLookupF32 (&status, row, "AP_FLUX_SIG");
+
+        // XXX use these to determine PAR[PM_PAR_I0] if they exist?
+        // XXX add these to PS1_SV1?
+        @>PS1_V2,PS1_SV?,PS1_DV?@ source->psfFlux   = psMetadataLookupF32 (&status, row, "PSF_INST_FLUX");
+        @>PS1_V2,PS1_SV?,PS1_DV?@ source->psfFluxErr= psMetadataLookupF32 (&status, row, "PSF_INST_FLUX_SIG");
+
+        // XXX this scaling is incorrect: does not include the 2 \pi AREA factor
+        @ALL@     PAR[PM_PAR_I0]    = (isfinite(source->psfMag)) ? pow(10.0, -0.4*source->psfMag) : NAN;
+        @ALL@     dPAR[PM_PAR_I0]   = (isfinite(source->psfMag)) ? PAR[PM_PAR_I0] * source->psfMagErr : NAN;
+
+        pmPSF_AxesToModel (PAR, axes, model->class->useReff);
+
+        @ALL@     float peakMag     = psMetadataLookupF32 (&status, row, "PEAK_FLUX_AS_MAG");
+        @ALL@     float peakFlux    = (isfinite(peakMag)) ? pow(10.0, -0.4*peakMag) : NAN;
+
+        // recreate the peak to match (xPos, yPos) +/- (xErr, yErr)
+        @ALL@     source->peak = pmPeakAlloc(PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], peakFlux, PM_PEAK_LONE);
+        @ALL@     source->peak->rawFlux = peakFlux;
+        @ALL@     source->peak->smoothFlux = peakFlux;
+        @ALL@     source->peak->xf   = PAR[PM_PAR_XPOS]; // more accurate position
+        @ALL@     source->peak->yf   = PAR[PM_PAR_YPOS]; // more accurate position
+        @ALL@     source->peak->dx   = dPAR[PM_PAR_XPOS];
+        @ALL@     source->peak->dy   = dPAR[PM_PAR_YPOS];
+
+        @ALL@     source->pixWeightNotBad = psMetadataLookupF32 (&status, row, "PSF_QF");
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ source->pixWeightNotPoor = psMetadataLookupF32 (&status, row, "PSF_QF_PERFECT");
+        @ALL@     source->crNsigma  = psMetadataLookupF32 (&status, row, "CR_NSIGMA");
+        @ALL@     source->extNsigma = psMetadataLookupF32 (&status, row, "EXT_NSIGMA");
+        @ALL@     source->apRadius  = psMetadataLookupF32 (&status, row, "AP_MAG_RADIUS");
+        @>PS1_V4,>PS1_SV2,>PS1_DV3@ source->apNpixels = psMetadataLookupS32 (&status, row, "AP_NPIX");
+
+        // note that some older versions used PSF_PROBABILITY: this was not well defined.
+        @ALL@     model->chisq      = psMetadataLookupF32 (&status, row, "PSF_CHISQ");
+        @ALL@     model->nDOF       = psMetadataLookupS32 (&status, row, "PSF_NDOF");
+        @ALL@     model->nPix       = psMetadataLookupS32 (&status, row, "PSF_NPIX");
+
+        @ALL@     source->moments = pmMomentsAlloc ();
+        @ALL@     source->moments->Mx = source->peak->xf; // we don't have both Mx,My and xf,yf in the cmf
+        @ALL@     source->moments->My = source->peak->yf; // we don't have both Mx,My and xf,yf in the cmf
+
+        @ALL@     source->moments->Mxx = psMetadataLookupF32 (&status, row, "MOMENTS_XX");
+        @ALL@     source->moments->Mxy = psMetadataLookupF32 (&status, row, "MOMENTS_XY");
+        @ALL@     source->moments->Myy = psMetadataLookupF32 (&status, row, "MOMENTS_YY");
+
+        // XXX we do not save all of the 3rd and 4th moment parameters. when we load in data,
+        // we are storing enough information so the output will be consistent with the input
+        @>PS1_V2,PS1_SV?@ source->moments->Mxxx = +1.0 * psMetadataLookupF32 (&status, row, "MOMENTS_M3C");
+        @>PS1_V2,PS1_SV?@ source->moments->Mxxy = 0.0;
+        @>PS1_V2,PS1_SV?@ source->moments->Mxyy = 0.0;
+        @>PS1_V2,PS1_SV?@ source->moments->Myyy = -1.0 * psMetadataLookupF32 (&status, row, "MOMENTS_M3S");
+
+        @>PS1_V2,PS1_SV?@ source->moments->Mxxxx = +1.00 * psMetadataLookupF32 (&status, row, "MOMENTS_M4C");
+        @>PS1_V2,PS1_SV?@ source->moments->Mxxxy = 0.0;
+        @>PS1_V2,PS1_SV?@ source->moments->Mxxyy = 0.0;
+        @>PS1_V2,PS1_SV?@ source->moments->Mxyyy = -0.25 * psMetadataLookupF32 (&status, row, "MOMENTS_M4S");
+        @>PS1_V2,PS1_SV?@ source->moments->Myyyy = 0.0;
+
+        // Lensing parameters (on read if PS1_V5+)
+        if (haveLensOBJ) {
+          source->lensingOBJ = pmSourceLensingAlloc ();
+          source->lensingOBJ->smear = pmLensingParsAlloc();
+          source->lensingOBJ->shear = pmLensingParsAlloc();
+
+          @>PS1_V4@ source->lensingOBJ->smear->X11 = psMetadataLookupF32 (&status, row, "LENS_X11_SM_OBJ");
+          @>PS1_V4@ source->lensingOBJ->smear->X12 = psMetadataLookupF32 (&status, row, "LENS_X12_SM_OBJ");
+          @>PS1_V4@ source->lensingOBJ->smear->X22 = psMetadataLookupF32 (&status, row, "LENS_X22_SM_OBJ");
+          @>PS1_V4@ source->lensingOBJ->smear->e1  = psMetadataLookupF32 (&status, row, "LENS_E1_SM_OBJ");
+          @>PS1_V4@ source->lensingOBJ->smear->e2  = psMetadataLookupF32 (&status, row, "LENS_E2_SM_OBJ");
+          @>PS1_V4@ source->lensingOBJ->shear->X11 = psMetadataLookupF32 (&status, row, "LENS_X11_SH_OBJ");
+          @>PS1_V4@ source->lensingOBJ->shear->X12 = psMetadataLookupF32 (&status, row, "LENS_X12_SH_OBJ");
+          @>PS1_V4@ source->lensingOBJ->shear->X22 = psMetadataLookupF32 (&status, row, "LENS_X22_SH_OBJ");
+          @>PS1_V4@ source->lensingOBJ->shear->e1  = psMetadataLookupF32 (&status, row, "LENS_E1_SH_OBJ");
+          @>PS1_V4@ source->lensingOBJ->shear->e2  = psMetadataLookupF32 (&status, row, "LENS_E2_SH_OBJ");
+        }
+
+        @>PS1_V4@ source->chipNum = psMetadataLookupS16 (&status, row, "SRC_CHIP_NUM");
+        @>PS1_V4@ source->chipX = psMetadataLookupS16 (&status, row, "SRC_CHIP_X");
+        @>PS1_V4@ source->chipY = psMetadataLookupS16 (&status, row, "SRC_CHIP_Y");
+
+        if (haveLensPSF) {
+          source->lensingPSF = pmSourceLensingAlloc ();
+          source->lensingPSF->smear = pmLensingParsAlloc();
+          source->lensingPSF->shear = pmLensingParsAlloc();
+
+          @>PS1_V4@ source->lensingPSF->smear->X11 = psMetadataLookupF32 (&status, row, "LENS_X11_SM_PSF");
+          @>PS1_V4@ source->lensingPSF->smear->X12 = psMetadataLookupF32 (&status, row, "LENS_X12_SM_PSF");
+          @>PS1_V4@ source->lensingPSF->smear->X22 = psMetadataLookupF32 (&status, row, "LENS_X22_SM_PSF");
+          @>PS1_V4@ source->lensingPSF->smear->e1  = psMetadataLookupF32 (&status, row, "LENS_E1_SM_PSF");
+          @>PS1_V4@ source->lensingPSF->smear->e2  = psMetadataLookupF32 (&status, row, "LENS_E2_SM_PSF");
+          @>PS1_V4@ source->lensingPSF->shear->X11 = psMetadataLookupF32 (&status, row, "LENS_X11_SH_PSF");
+          @>PS1_V4@ source->lensingPSF->shear->X12 = psMetadataLookupF32 (&status, row, "LENS_X12_SH_PSF");
+          @>PS1_V4@ source->lensingPSF->shear->X22 = psMetadataLookupF32 (&status, row, "LENS_X22_SH_PSF");
+          @>PS1_V4@ source->lensingPSF->shear->e1  = psMetadataLookupF32 (&status, row, "LENS_E1_SH_PSF");
+          @>PS1_V4@ source->lensingPSF->shear->e2  = psMetadataLookupF32 (&status, row, "LENS_E2_SH_PSF");
+          @>PS1_V4@ source->lensingPSF->e1         = psMetadataLookupF32 (&status, row, "LENS_E1_PSF");
+          @>PS1_V4@ source->lensingPSF->e2         = psMetadataLookupF32 (&status, row, "LENS_E2_PSF");
+        }
+
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ source->moments->Mrf         = psMetadataLookupF32 (&status, row, "MOMENTS_R1");
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ source->moments->Mrh         = psMetadataLookupF32 (&status, row, "MOMENTS_RH");
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ source->moments->KronFlux    = psMetadataLookupF32 (&status, row, "KRON_FLUX");
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ source->moments->KronFluxErr = psMetadataLookupF32 (&status, row, "KRON_FLUX_ERR");
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ source->moments->KronFinner  = psMetadataLookupF32 (&status, row, "KRON_FLUX_INNER");
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ source->moments->KronFouter  = psMetadataLookupF32 (&status, row, "KRON_FLUX_OUTER");
+
+        @>PS1_V3@ source->skyRadius            = psMetadataLookupF32 (&status, row, "SKY_LIMIT_RAD");
+        @>PS1_V3@ source->skyFlux              = psMetadataLookupF32 (&status, row, "SKY_LIMIT_FLUX");
+        @>PS1_V3@ source->skySlope             = psMetadataLookupF32 (&status, row, "SKY_LIMIT_SLOPE");
+
+        @PS1_DV?@  int nPos = psMetadataLookupS32 (&status, row, "DIFF_NPOS");
+        @PS1_DV?@  if (nPos) {
+        @PS1_DV?@      source->diffStats = pmSourceDiffStatsAlloc();
+        @PS1_DV?@      source->diffStats->nGood      = nPos;
+        @PS1_DV?@      source->diffStats->fRatio     = psMetadataLookupF32 (&status, row, "DIFF_FRATIO");
+        @PS1_DV?@      source->diffStats->nRatioBad  = psMetadataLookupF32 (&status, row, "DIFF_NRATIO_BAD");
+        @PS1_DV?@      source->diffStats->nRatioMask = psMetadataLookupF32 (&status, row, "DIFF_NRATIO_MASK");
+        @PS1_DV?@      source->diffStats->nRatioAll  = psMetadataLookupF32 (&status, row, "DIFF_NRATIO_ALL");
+        
+        @>PS1_DV1@      source->diffStats->Rp         = psMetadataLookupF32 (&status, row, "DIFF_R_P");
+        @>PS1_DV1@      source->diffStats->SNp        = psMetadataLookupF32 (&status, row, "DIFF_SN_P");
+        @>PS1_DV1@      source->diffStats->Rm         = psMetadataLookupF32 (&status, row, "DIFF_R_M");
+        @>PS1_DV1@      source->diffStats->SNm        = psMetadataLookupF32 (&status, row, "DIFF_SN_M");
+        @PS1_DV?@  }
+
+        @ALL@                      source->mode       = psMetadataLookupU32 (&status, row, "FLAGS");
+        @>PS1_V2,PS1_SV?,>PS1_DV1@ source->mode2      = psMetadataLookupU32 (&status, row, "FLAGS2");
+        @ALL@                      source->nFrames    = psMetadataLookupU16 (&status, row, "N_FRAMES");
+        assert (status);
+
+        sources->data[i] = source;
+        psFree(row);
+    }
+
+    return sources;
+}
+
+psArray *pmSourcesRead_CMF_@CMFMODE@ (psFits *fits, psMetadata *header) {
+  // psArray *array = pmSourcesRead_CMF_@CMFMODE@_Old (fits, header);
+      psArray *array = pmSourcesRead_CMF_@CMFMODE@_New (fits, header);
+    return array;
+}
+
+bool pmSourcesWrite_CMF_@CMFMODE@_XSRC (psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname, psMetadata *recipe)
+{
+    bool status;
+    psArray *table;
+    psMetadata *row;
+    psF32 *PAR, *dPAR;
+    psF32 xPos, yPos;
+    psF32 xErr, yErr;
+    int nRow = -1;
+    char keyword1[80], keyword2[80];
+
+    // create a header to hold the output data
+    psMetadata *outhead = psMetadataAlloc ();
+
+    // write the links to the image header
+    psMetadataAddStr (outhead, PS_LIST_TAIL, "EXTNAME", PS_META_REPLACE, "xsrc table extension", extname);
+
+    // we can either write out the petrosian mag errors correctly or inverted (by default, psphot writes them out inverted)
+    bool repairMagErrors = psMetadataLookupBool (&status, recipe, "REPAIR.PETROSIAN.MAG.ERRORS");
+    if (repairMagErrors) {
+      psMetadataAddBool (outhead, PS_LIST_TAIL, "REPAIR.PETROSIAN.MAG.ERRORS", PS_META_REPLACE, "", true);
+    }
+
+    pmChip *chip = readout->parent->parent;
+    pmFPA  *fpa  = chip->parent;
+
+    // zero point corrections
+    bool status1 = false;
+    bool status2 = false;
+    float magOffset = 0.0;
+    float exptime   = psMetadataLookupF32(&status1, fpa->concepts, "FPA.EXPOSURE");
+    float zeropt    = psMetadataLookupF32(&status2, fpa->concepts, "FPA.ZP");
+    if (!isfinite(zeropt)) {
+        zeropt    = psMetadataLookupF32 (&status2, imageHeader, "ZPT_OBS");
+    }
+    if (status1 && status2 && (exptime > 0.0)) {
+        magOffset = zeropt + 2.5*log10(exptime);
+    }
+
+#ifdef SORT_EXTENSIONS_BY_FLUX
+    // let's write these out in S/N order
+    sources = psArraySort (sources, pmSourceSortByFlux);
+#endif
+
+    table = psArrayAllocEmpty (sources->n);
+
+    // which extended source analyses should we perform?
+    bool doAnnuli       = psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_ANNULI");
+    bool doPetrosian    = psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_PETROSIAN");
+
+    // First look for radial bin definition in readout->analysis ...
+    psVector *radMin = psMetadataLookupPtr (&status, readout->analysis, "RADIAL.ANNULAR.BINS.LOWER");
+    psVector *radMax = psMetadataLookupPtr (&status, readout->analysis, "RADIAL.ANNULAR.BINS.UPPER");
+    if (!radMin) {
+        // .. if not found use the recipe values
+        radMin = psMetadataLookupPtr (&status, recipe, "RADIAL.ANNULAR.BINS.LOWER");
+        radMax = psMetadataLookupPtr (&status, recipe, "RADIAL.ANNULAR.BINS.UPPER");
+    }
+    psAssert(radMin != NULL, "unable to find RADIAL.ANNULAR.BINS.LOWER");
+    psAssert(radMax != NULL, "unable to find RADIAL.ANNULAR.BINS.LOWER");
+    psAssert (radMin->n == radMax->n, "inconsistent annular bins");
+
+    // write the radial profile apertures to header
+    for (int i = 0; i < radMax->n; i++) {
+        sprintf (keyword1, "RMIN_%02d", i);
+        sprintf (keyword2, "RMAX_%02d", i);
+        psMetadataAddF32 (outhead, PS_LIST_TAIL, keyword1, PS_META_REPLACE, "min radius for SB profile", radMin->data.F32[i]);
+        psMetadataAddF32 (outhead, PS_LIST_TAIL, keyword2, PS_META_REPLACE, "max radius for SB profile", radMax->data.F32[i]);
+    }
+
+    // we write out all sources, regardless of quality.  the source flags tell us the state
+    for (int i = 0; i < sources->n; i++) {
+        // this is the source associated with this image
+        pmSource *thisSource = sources->data[i];
+
+        // this is the "real" version of this source 
+        pmSource *source = thisSource->parent ? thisSource->parent : thisSource;
+
+        // skip sources without measurements
+        if (source->extpars == NULL) continue;
+
+        // we require a PSF model fit (ignore the real crud)
+        pmModel *model = source->modelPSF;
+        if (model == NULL) continue;
+
+        // XXX I need to split the extended models from the extended aperture measurements
+        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];
+
+        row = psMetadataAlloc ();
+
+        // XXX we are not writing out the mode (flags) or the type (psf, ext, etc)
+        // the psMetadataAdd entry and the double quotes are used by grep to select the output fields for automatic documentation
+        // This set of psMetadataAdd Entries marks the "----" "Start of the XSRC segment"
+        psMetadataAdd (row, PS_LIST_TAIL, "IPP_IDET",         PS_DATA_U32, "IPP detection identifier index",             source->seq);
+        psMetadataAdd (row, PS_LIST_TAIL, "X_EXT",            PS_DATA_F32, "EXT model x coordinate",                     xPos);
+        psMetadataAdd (row, PS_LIST_TAIL, "Y_EXT",            PS_DATA_F32, "EXT model y coordinate",                     yPos);
+        psMetadataAdd (row, PS_LIST_TAIL, "X_EXT_SIG",        PS_DATA_F32, "Sigma in EXT x coordinate",                  xErr);
+        psMetadataAdd (row, PS_LIST_TAIL, "Y_EXT_SIG",        PS_DATA_F32, "Sigma in EXT y coordinate",                  yErr);
+
+        float AxialRatio = NAN;
+        float AxialTheta = NAN;
+        pmSourceExtendedPars *extpars = source->extpars;
+        if (extpars) {
+            AxialRatio = extpars->axes.minor / extpars->axes.major;
+            AxialTheta = extpars->axes.theta;
+        }
+        psMetadataAdd (row, PS_LIST_TAIL, "F25_ARATIO",       PS_DATA_F32, "Axial Ratio of radial profile",              AxialRatio);
+        psMetadataAdd (row, PS_LIST_TAIL, "F25_THETA",        PS_DATA_F32, "Angle of radial profile ellipse",                  AxialTheta);
+
+        // Petrosian measurements
+        // XXX insert header data: petrosian ref radius, flux ratio
+        // XXX check flags to see if Pet was measured
+        if (doPetrosian) {
+            pmSourceExtendedPars *extpars = source->extpars;
+            if (extpars) {
+                // XXX note that this mag is either calibrated or instrumental depending on existence of zero point 
+                float mag = (extpars->petrosianFlux > 0.0) ? -2.5*log10(extpars->petrosianFlux) + magOffset : NAN; // XXX zero point
+                // NOTE EAM 20140806 : PETRO_MAG_ERR was inverted!! this allows for it to be repaired
+                float magErr = (extpars->petrosianFlux > 0.0) ? extpars->petrosianFlux / extpars->petrosianFluxErr : NAN; // XXX zero point
+                if (repairMagErrors) {
+                  // I need to add the kron error in quadrature becasue pet_error ignores the object flux
+                  float Krf  = source->moments ? source->moments->KronFlux : NAN;
+                  float dKrf = source->moments ? source->moments->KronFluxErr : NAN;
+                  if (isfinite (Krf) && isfinite (dKrf)) {
+                    magErr = sqrt(PS_SQR(1.0 / magErr) + PS_SQR(dKrf / Krf));
+                  } else {
+                    magErr = 1.0 / magErr;
+                  }
+                }
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_MAG",        PS_DATA_F32, "Petrosian Magnitude", mag);
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_MAG_ERR",    PS_DATA_F32, "Petrosian Magnitude Error", magErr);
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS",     PS_DATA_F32, "Petrosian Radius (pix)", extpars->petrosianRadius);
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS_ERR", PS_DATA_F32, "Petrosian Radius Error (pix)", extpars->petrosianRadiusErr);
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS_50",     PS_DATA_F32, "Petrosian R50 (pix)", extpars->petrosianR50);
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS_50_ERR", PS_DATA_F32, "Petrosian R50 Error (pix)", extpars->petrosianR50Err);
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS_90",     PS_DATA_F32, "Petrosian R90 (pix)", extpars->petrosianR90);
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS_90_ERR", PS_DATA_F32, "Petrosian R90 Error (pix)", extpars->petrosianR90Err);
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_FILL",          PS_DATA_F32, "Petrosian Fill Factor", extpars->petrosianFill);
+                @>PS1_SV3@ psMetadataAddF32 (row, PS_LIST_TAIL, "HALF_LIGHT_RADIUS", 0, "Half Light Radius", extpars->ghalfLightRadius);
+                @>PS1_SV3@ psMetadataAddF32 (row, PS_LIST_TAIL, "G_RT", 0, "total residual Sersic Model", extpars->gRT);
+                @>PS1_SV3@ psMetadataAddF32 (row, PS_LIST_TAIL, "G_RA", 0, "assymetric residual Sersic Model", extpars->gRA);
+                @>PS1_SV3@ psMetadataAddF32 (row, PS_LIST_TAIL, "G_S2", 0, "(un)smoothness paramter residual Sersic Model", extpars->gS2);
+                @>PS1_SV3@ psMetadataAddF32 (row, PS_LIST_TAIL, "G_A", 0, "assymetry index", extpars->gA);
+                @>PS1_SV3@ psMetadataAddF32 (row, PS_LIST_TAIL, "G_BUMPY", 0, "bumpiness", extpars->gbumpy);
+            } else {
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_MAG",        PS_DATA_F32, "Petrosian Magnitude",       NAN);
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_MAG_ERR",    PS_DATA_F32, "Petrosian Magnitude Error", NAN);
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS",     PS_DATA_F32, "Petrosian Radius",          NAN);
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS_ERR", PS_DATA_F32, "Petrosian Radius Error",    NAN);
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS_50",     PS_DATA_F32, "Petrosian R50 (pix)", NAN);
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS_50_ERR", PS_DATA_F32, "Petrosian R50 Error (pix)",NAN); 
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS_90",     PS_DATA_F32, "Petrosian R90 (pix)", NAN);
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS_90_ERR", PS_DATA_F32, "Petrosian R90 Error (pix)",NAN); 
+                psMetadataAdd (row, PS_LIST_TAIL, "PETRO_FILL",          PS_DATA_F32, "Petrosian Fill Factor", NAN);
+                @>PS1_SV3@ psMetadataAddF32 (row, PS_LIST_TAIL, "HALF_LIGHT_RADIUS", 0, "Half Light Radius", NAN);
+                @>PS1_SV3@ psMetadataAddF32 (row, PS_LIST_TAIL, "G_RT", 0, "total residual Sersic Model", NAN);
+                @>PS1_SV3@ psMetadataAddF32 (row, PS_LIST_TAIL, "G_RA", 0, "assymetric residual Sersic Model", NAN);
+                @>PS1_SV3@ psMetadataAddF32 (row, PS_LIST_TAIL, "G_S2", 0, "(un)smoothness paramter residual Sersic Model", NAN);
+                @>PS1_SV3@ psMetadataAddF32 (row, PS_LIST_TAIL, "G_A", 0, "assymetry index", NAN);
+                @>PS1_SV3@ psMetadataAddF32 (row, PS_LIST_TAIL, "G_BUMPY", 0, "bumpiness", NAN);
+            }
+        }
+
+        // Flux Annuli (if we have extended source measurements, we have these.  only optionally save them)
+        if (doAnnuli) {
+            psVector *radSB   = psVectorAlloc(radMin->n, PS_TYPE_F32);
+            psVector *radFlux = psVectorAlloc(radMin->n, PS_TYPE_F32);
+            psVector *radFill = psVectorAlloc(radMin->n, PS_TYPE_F32);
+            psVectorInit (radSB, NAN);
+            psVectorInit (radFlux, NAN);
+            psVectorInit (radFill, NAN);
+            if (!source->extpars) goto empty_annuli;
+            if (!source->extpars->radProfile) goto empty_annuli;
+            if (!source->extpars->radProfile->binSB) goto empty_annuli;
+            psAssert (source->extpars->radProfile->binSum, "programming error");
+            psAssert (source->extpars->radProfile->binFill, "programming error");
+            psAssert (source->extpars->radProfile->binSB->n <= radFlux->n, "inconsistent vector lengths");
+            psAssert (source->extpars->radProfile->binSum->n <= radFlux->n, "inconsistent vector lengths");
+            psAssert (source->extpars->radProfile->binFill->n <= radFlux->n, "inconsistent vector lengths");
+
+            // copy the data from fluxVal (which is not guaranteed to be the full length) to radFlux
+            for (int j = 0; j < source->extpars->radProfile->binSB->n; j++) {
+                radSB->data.F32[j]   = source->extpars->radProfile->binSB->data.F32[j];
+                radFlux->data.F32[j] = source->extpars->radProfile->binSum->data.F32[j];
+                radFill->data.F32[j] = source->extpars->radProfile->binFill->data.F32[j];
+            }
+
+        empty_annuli:
+            psMetadataAdd (row, PS_LIST_TAIL, "PROF_SB", PS_DATA_VECTOR, "mean surface brightness annuli", radSB);
+            psMetadataAdd (row, PS_LIST_TAIL, "PROF_FLUX", PS_DATA_VECTOR, "flux within annuli", radFlux);
+            psMetadataAdd (row, PS_LIST_TAIL, "PROF_FILL", PS_DATA_VECTOR, "fill factor of annuli", radFill);
+            psFree (radSB);
+            psFree (radFlux);
+            psFree (radFill);
+        }
+        if (nRow < 0) {
+            nRow = row->list->n;
+        } else {
+            psAssert (nRow == row->list->n, "inconsistent row lengths");
+        }
+        psArrayAdd (table, 100, row);
+        psFree (row);
+    }
+    
+    if (table->n == 0) {
+        if (!psFitsWriteBlank (fits, outhead, extname)) {
+            psError(psErrorCodeLast(), false, "Unable to write empty sources file.");
+            psFree(outhead);
+            psFree(table);
+            return false;
+        }
+        psFree (outhead);
+        psFree (table);
+        return true;
+    }
+    
+    psTrace ("pmFPAfile", 5, "writing ext data %s\n", extname);
+    if (!psFitsWriteTable (fits, outhead, table, extname)) {
+        psError(psErrorCodeLast(), false, "writing ext data %s\n", extname);
+        psFree (outhead);
+        psFree(table);
+        return false;
+    }
+    psFree (outhead);
+    psFree (table);
+    
+    return true;
+}
+
+static bool setRadialBinsInAnalysis(pmReadout *readout, psMetadata *tableHeader)
+{
+    if (!readout->analysis) {
+        readout->analysis = psMetadataAlloc();
+    }
+
+    bool status;
+    psVector *oldRMin = psMetadataLookupVector(&status, readout->analysis, "RADIAL.ANNULAR.BINS.LOWER");
+    psVector *oldRMax = psMetadataLookupVector(&status, readout->analysis, "RADIAL.ANNULAR.BINS.UPPER");
+
+    psVector *rMin = psVectorAllocEmpty(20, PS_TYPE_F32);
+    psVector *rMax = psVectorAllocEmpty(20, PS_TYPE_F32);
+
+    for (int i = 0; ; i++) {
+        char key [24];
+        sprintf(key, "RMIN_%02d", i);
+        psF32 rMinVal = psMetadataLookupF32(&status, tableHeader, key);
+        if (!status) {
+            break;
+        }
+        psVectorAppend(rMin, rMinVal);
+
+        sprintf(key, "RMAX_%02d", i);
+        psF32 rMaxVal = psMetadataLookupF32(&status, tableHeader, key);
+        if (!status) {
+            break;
+        }
+        psVectorAppend(rMax, rMaxVal);
+    }
+
+    if (rMin->n != rMax->n) {
+        psError(PS_ERR_UNKNOWN, true, "number of RMIN entries %ld does not equal number of RMAX entries %ld", 
+            rMin->n, rMax->n);
+        return false;
+    }
+
+    if (oldRMin) {
+        if (oldRMin->n != rMin->n) {
+            psError(PS_ERR_UNKNOWN, true, "number of RMIN entries in header: %ld does not equal number of RMAX entries in analysis: %ld", 
+                rMin->n, oldRMin->n);
+            return false;
+        }
+    } else {
+        psMetadataAddVector(readout->analysis, PS_LIST_TAIL, "RADIAL.ANNULAR.BINS.LOWER", PS_META_REPLACE, "", rMin);
+    }
+    psFree(rMin);
+
+    if (oldRMax) {
+        if (oldRMax->n != rMax->n) {
+            psError(PS_ERR_UNKNOWN, true, "number of RMIN entries in header: %ld does not equal number of RMAX entries in analysis: %ld", 
+                rMax->n, oldRMax->n);
+            return false;
+        }
+    } else {
+        psMetadataAddVector(readout->analysis, PS_LIST_TAIL, "RADIAL.ANNULAR.BINS.UPPER", PS_META_REPLACE, "", rMax);
+    }
+    psFree(rMax);
+
+    return true;
+}
+
+bool pmSourcesRead_CMF_@CMFMODE@_XSRC(psFits *fits, pmReadout *readout, psMetadata *hduHeader, psMetadata *tableHeader, psArray *sources, long *sourceIndex)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+
+    bool status;
+    long numSources = psFitsTableSize(fits); // Number of sources in table
+    if (numSources == 0) {
+        psError(psErrorCodeLast(), false, "XSRC Table contains no entries\n");
+        return false;
+    }
+
+    // petrosian mags are not saved, we need to calculate fluxes. For this we need exptime and zero point
+    float zeropt = psMetadataLookupF32(&status, hduHeader, "FPA.ZP");
+    float exptime = psMetadataLookupF32(&status, hduHeader, "EXPTIME");
+    float magOffset = zeropt + 2.5*log10(exptime);
+
+    if (!setRadialBinsInAnalysis(readout, tableHeader)) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to save radial bins in analysis");
+        return false;
+    }
+
+    bool repairMagErrors = psMetadataLookupBool (&status, tableHeader, "REPAIR.PETROSIAN.MAG.ERRORS");
+
+    for (long i = 0; i < numSources; i++) {
+        psMetadata *row = psFitsReadTableRow(fits, i); // Table row
+        if (!row) {
+            psError(psErrorCodeLast(), false, "Unable to read row %ld of sources", i);
+            psFree(row);
+            return false;
+        }
+        // Find the source with this sequence number using the sourceIndex. 
+        long seq = psMetadataLookupU32 (&status, row, "IPP_IDET");
+        long j = sourceIndex[seq];
+        psAssert(j >= 0 && j < sources->n, "invalid sourceIndex");
+        pmSource *source = sources->data[j];
+        if (!source) {
+            psError(PS_ERR_UNKNOWN, false, "Failed to find source for row %ld sequence number %ld\n", i, seq);
+            psFree(row);
+            return false;
+        }
+
+        if (!source->extpars) {
+            source->extpars = pmSourceExtendedParsAlloc ();
+        }
+        pmSourceExtendedPars *extpars = source->extpars;
+
+        // Assume that X_EXT Y_EXT and sigmas match the psf src so skip
+
+        // We don't have enough information to calculate the major and minor axis. Set major to 1. Should we scale this by
+        // psf size or something?
+        extpars->axes.major = 1.0;
+        extpars->axes.minor = extpars->axes.major * psMetadataLookupF32(&status, row, "F25_ARATIO");
+        extpars->axes.theta = psMetadataLookupF32(&status, row, "F25_THETA");
+
+        // magErr may have been saved in inverted form
+        float mag = psMetadataLookupF32(&status, row, "PETRO_MAG");
+        float magErr = psMetadataLookupF32(&status, row, "PETRO_MAG_ERR");
+        if (isfinite(mag)) {
+          extpars->petrosianFlux    = pow(10., (magOffset - mag) / 2.5);
+          if (isfinite(magErr)) {
+            if (repairMagErrors) {
+              // I need to add the kron error in quadrature becasue pet_error ignores the object flux
+              float Krf  = source->moments ? source->moments->KronFlux : NAN;
+              float dKrf = source->moments ? source->moments->KronFluxErr : NAN;
+              if (isfinite (Krf) && isfinite (dKrf)) {
+                magErr = 1.0 / sqrt(PS_SQR(magErr) - PS_SQR(dKrf / Krf));
+              } else {
+                magErr = 1.0 / magErr;
+              }
+              extpars->petrosianFluxErr = extpars->petrosianFlux * magErr;
+            } else {
+              extpars->petrosianFluxErr = extpars->petrosianFlux / magErr;
+            }
+          }
+        }
+
+        extpars->petrosianRadius   = psMetadataLookupF32(&status, row, "PETRO_RADIUS");
+        extpars->petrosianRadiusErr= psMetadataLookupF32(&status, row, "PETRO_RADIUS_ERR");
+        extpars->petrosianR50      = psMetadataLookupF32(&status, row, "PETRO_RADIUS_50");
+        extpars->petrosianR50Err   = psMetadataLookupF32(&status, row, "PETRO_RADIUS_50_ERR");
+        extpars->petrosianR90      = psMetadataLookupF32(&status, row, "PETRO_RADIUS_90");
+        extpars->petrosianR90Err   = psMetadataLookupF32(&status, row, "PETRO_RADIUS_90_ERR");
+        extpars->petrosianFill     = psMetadataLookupF32(&status, row, "PETRO_FILL");
+        extpars->ghalfLightRadius  = psMetadataLookupF32(&status, row, "HALF_LIGHT_RADIUS");
+        extpars->gRT               = psMetadataLookupF32(&status, row, "G_RT");
+        extpars->gRA               = psMetadataLookupF32(&status, row, "G_RA");
+        extpars->gS2               = psMetadataLookupF32(&status, row, "G_S2");
+        extpars->gA                = psMetadataLookupF32(&status, row, "G_A");
+        extpars->gbumpy            = psMetadataLookupF32(&status, row, "G_BUMPY");
+
+        psVector *radSB   = psMetadataLookupVector(&status, row, "PROF_SB");
+        psVector *radFlux = psMetadataLookupVector(&status, row, "PROF_FLUX");
+        psVector *radFill = psMetadataLookupVector(&status, row, "PROF_FILL");
+
+        if (radSB && radSB->n > 0) {
+            extpars->radProfile = pmSourceRadialProfileAlloc();
+            extpars->radProfile->binSB   = psMemIncrRefCounter(radSB);
+            extpars->radProfile->binSum   = psMemIncrRefCounter(radFlux);
+            extpars->radProfile->binFill = psMemIncrRefCounter(radFill);
+        }
+
+        psFree(row);
+    }
+
+    return true;
+}
+
+// XXX this layout is still the same as PS1_DEV_1
+bool pmSourcesWrite_CMF_@CMFMODE@_XFIT (psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname)
+{
+
+    psArray *table;
+    psMetadata *row;
+    psF32 *PAR, *dPAR;
+    psF32 xPos, yPos;
+    psF32 xErr, yErr;
+    char name[64];
+
+    pmModelType modelTypeTrail = pmModelClassGetType("PS_MODEL_TRAIL");
+
+    // create a header to hold the output data
+    psMetadata *outhead = psMetadataAlloc ();
+
+    pmModelClassWriteHeader(outhead);
+
+    // write the links to the image header
+    psMetadataAddStr (outhead, PS_LIST_TAIL, "EXTNAME", PS_META_REPLACE, "xsrc table extension", extname);
+
+#ifdef SORT_EXTENSIONS_BY_FLUX
+    // let's write these out in S/N order
+    sources = psArraySort (sources, pmSourceSortByFlux);
+#endif
+
+    float magOffset; 
+    float zeroptErr; 
+    float fwhmMajor; 
+    float fwhmMinor;
+    pmSourceOutputsCommonValues (&magOffset, &zeroptErr, &fwhmMajor, &fwhmMinor, readout, imageHeader);
+
+    // we are writing one row per model; we need to write out same number of columns for each row: find the max Nparams
+    int nParamMax = 0;
+    for (int i = 0; i < sources->n; i++) {
+        // this is the source associated with this image
+        pmSource *thisSource = sources->data[i];
+
+        // this is the "real" version of this source 
+        pmSource *source = thisSource->parent ? thisSource->parent : thisSource;
+
+        if (source->modelFits == NULL) continue;
+        for (int j = 0; j < source->modelFits->n; j++) {
+            pmModel *model = source->modelFits->data[j];
+            assert (model);
+            nParamMax = PS_MAX (nParamMax, model->params->n);
+        }
+    }
+
+    @>PS1_DV2,>PS1_SV3@ pmChip *chip = readout->parent->parent;
+
+    pmModelStatus badModel = PM_MODEL_STATUS_NONE;
+    badModel |= PM_MODEL_STATUS_BADARGS;
+    badModel |= PM_MODEL_STATUS_OFFIMAGE;
+    badModel |= PM_MODEL_STATUS_NAN_CHISQ;
+    badModel |= PM_MODEL_SERSIC_PCM_FAIL_GUESS;
+    badModel |= PM_MODEL_SERSIC_PCM_FAIL_GRID;
+    badModel |= PM_MODEL_PCM_FAIL_GUESS;
+
+    table = psArrayAllocEmpty (sources->n);
+
+    // we write out all sources, regardless of quality.  the source flags tell us the state
+    for (int i = 0; i < sources->n; i++) {
+
+        pmSource *thisSource = sources->data[i];
+
+        // this is the "real" version of this source 
+        pmSource *source = thisSource->parent ? thisSource->parent : thisSource;
+
+        // XXX if no model fits are saved, write out modelEXT?
+        if (source->modelFits == NULL) continue;
+
+        // We have multiple sources : need to flag the one used to subtract the light (the 'best' model)
+        for (int j = 0; j < source->modelFits->n; j++) {
+
+            // choose the convolved EXT model, if available, otherwise the simple one
+            pmModel *model = source->modelFits->data[j];
+            assert (model);
+
+            // pmSourceExtFitPars *extPars = source->extFitPars->data[j];
+            // assert (extPars);
+
+            // skip models which were not actually fitted
+            // XXX 
+            if (model->flags & badModel) continue;
+
+            PAR = model->params->data.F32;
+            dPAR = model->dparams->data.F32;
+            xPos = PAR[PM_PAR_XPOS];
+            yPos = PAR[PM_PAR_YPOS];
+
+            // for the extended source models, we do not always fit the centroid in the non-linear fitting process
+            // current situation (hard-wired into psphotSourceFits.c:psphotFitPCM,
+            // SERSIC, DEV, EXP : X,Y not fitted (PCM and not PCM)
+            // TRAIL : X,Y are fitted
+            // 
+            
+            // XXX this should be based on what happened, not on the model type
+            if (model->type == modelTypeTrail) {
+                xErr = dPAR[PM_PAR_XPOS];
+                yErr = dPAR[PM_PAR_YPOS];
+            } else {
+                // this is definitely an underestimate since it does not
+                // account for the extent of the source
+                xErr = fwhmMajor * model->magErr / 2.35;
+                yErr = fwhmMinor * model->magErr / 2.35;
+            }
+
+            @>PS1_DV2,>PS1_SV3@ psSphere ptSky = {0.0, 0.0, 0.0, 0.0};
+            @>PS1_DV2,>PS1_SV3@ float posAngle = 0.0;
+            @>PS1_DV2,>PS1_SV3@ float pltScale = 0.0;
+            @>PS1_DV2,>PS1_SV3@ pmSourceLocalAstrometry (&ptSky, &posAngle, &pltScale, chip, xPos, yPos);
+            @>PS1_DV2,>PS1_SV3@ double raPos = ptSky.r*PS_DEG_RAD;
+            @>PS1_DV2,>PS1_SV3@ double decPos = ptSky.d*PS_DEG_RAD;
+            @>PS1_DV2,>PS1_SV3@ posAngle *= PS_DEG_RAD;
+            @>PS1_DV2,>PS1_SV3@ pltScale *= PS_DEG_RAD*3600.0;
+
+            float kronFlux = source->moments ? source->moments->KronFlux : NAN;
+            float kronMag = isfinite(kronFlux) ? -2.5*log10(kronFlux) : NAN;
+
+            row = psMetadataAlloc ();
+
+            // the psMetadataAdd entry and the double quotes are used by grep to select the output fields for automatic documentation
+            // This set of psMetadataAdd Entries marks the "----" "Start of the XFIT segment"
+            psMetadataAddU32 (row, PS_LIST_TAIL, "IPP_IDET",         0, "IPP detection identifier index",             source->seq);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "X_EXT",            0, "EXT model x coordinate",                     xPos);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "Y_EXT",            0, "EXT model y coordinate",                     yPos);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "X_EXT_SIG",        0, "Sigma in EXT x coordinate",                  xErr);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "Y_EXT_SIG",        0, "Sigma in EXT y coordinate",                  yErr);
+            @>PS1_SV3@ psMetadataAddF32 (row, PS_LIST_TAIL, "SKY_EXT",        0, "EXT fit sky value",                  PAR ? PAR[PM_PAR_SKY] : NAN);
+            @>PS1_DV2,>PS1_SV3@ psMetadataAddF32 (row, PS_LIST_TAIL, "RA_EXT",           0, "EXT model ra coordinate",                    raPos);
+            @>PS1_DV2,>PS1_SV3@ psMetadataAddF32 (row, PS_LIST_TAIL, "DEC_EXT",          0, "EXT model dec coordinate",                   decPos);
+            @>PS1_DV2@ float instFlux = isfinite(model->mag) ? pow(10.0, -0.4*model->mag) : NAN;
+            @>PS1_DV2@ psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_INST_FLUX",    0, "EXT fit instrumental counts",                instFlux);
+
+            psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_INST_MAG",     0, "EXT fit instrumental magnitude",             model->mag);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_INST_MAG_SIG", 0, "Sigma of PSF instrumental magnitude",        model->magErr);
+
+            @>PS1_DV2@ float calMag = isfinite(magOffset) ? model->mag + magOffset : NAN;
+            @>PS1_DV2@ psMetadataAdd (row, PS_LIST_TAIL, "EXT_CAL_MAG", PS_DATA_F32, "EXT Magnitude using supplied calibration",   calMag);
+            @>PS1_DV2,PS1_SV?@ psMetadataAdd (row, PS_LIST_TAIL, "EXT_CHISQ",   PS_DATA_F32, "EXT Model Chisq",   model->chisq);
+            @>PS1_DV2,PS1_SV?@ psMetadataAdd (row, PS_LIST_TAIL, "EXT_NDOF",    PS_DATA_S32, "EXT Model num degrees of freedom",   model->nDOF);
+            @>PS1_SV1,PS1_SV?@ psMetadataAdd (row, PS_LIST_TAIL, "EXT_MODEL_TYPE",    PS_DATA_S32, "type for chosen EXT_MODEL",   source->modelEXT ? source->modelEXT->type : -1);
+
+            // EAM : adding for PV2 outputs:
+            @>PS1_SV1@ psMetadataAdd (row, PS_LIST_TAIL, "EXT_FLAGS", PS_DATA_S16, "model fit flags (pmModelStatus)", model->flags);
+
+            @>PS1_DV2@ psMetadataAddF32 (row, PS_LIST_TAIL, "POSANGLE",   0, "position angle at source (degrees)",         posAngle);
+            @>PS1_DV2@ psMetadataAddF32 (row, PS_LIST_TAIL, "PLTSCALE",   0, "plate scale at source (arcsec/pixel)",       pltScale);
+
+            // psMetadataAddF32 (row, PS_LIST_TAIL, "MOMENTS_XX",       0, "second moment in x",                      extPars->Mxx);
+            // psMetadataAddF32 (row, PS_LIST_TAIL, "MOMENTS_XY",       0, "second moment in x,y",                    extPars->Mxy);
+            // psMetadataAddF32 (row, PS_LIST_TAIL, "MOMENTS_YY",       0, "second moment in y",                      extPars->Myy);
+            // psMetadataAddF32 (row, PS_LIST_TAIL, "MOMENTS_R1",       0, "first radial moment",                     extPars->Mrf);
+            // psMetadataAddF32 (row, PS_LIST_TAIL, "MOMENTS_RH",       0, "half radial moment",                      extPars->Mrh);
+
+            psMetadataAddF32 (row, PS_LIST_TAIL, "PSF_INST_MAG",     0, "PSF fit instrumental magnitude",             source->psfMag);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "AP_MAG",           0, "PSF-sized aperture magnitude",               source->apMag);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "KRON_MAG",         0, "Kron Mag",                                   kronMag);
+
+            @PS1_DV0,PS1_DV1@ psMetadataAddF32 (row, PS_LIST_TAIL, "NPARAMS",          0, "number of model parameters",                 model->params->n);
+            @ALL,!PS1_DV0,!PS1_DV1@ psMetadataAddS32 (row, PS_LIST_TAIL, "NPARAMS",          0, "number of model parameters",                 model->params->n);
+            psMetadataAddStr (row, PS_LIST_TAIL, "MODEL_TYPE",       0, "name of model",                              pmModelClassGetName (model->type));
+
+            // XXX these should be major and minor, not 'x' and 'y'
+            if (model->type == pmModelClassGetType("PS_MODEL_TRAIL")) {
+                psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MAJ",    0, "EXT width (major axis), length for trail", PAR[PM_PAR_LENGTH]);
+                psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MIN",    0, "EXT width (minor axis), sigma for trail",  PAR[PM_PAR_SIGMA]); // this is not fitted
+                psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_THETA",        0, "EXT orientation angle",                    PAR[PM_PAR_THETA]);
+                psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MAJ_ERR",0, "EXT width error (major axis)",            dPAR[PM_PAR_LENGTH]);
+                psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MIN_ERR",0, "EXT width error (minor axis)",            NAN); // this is not fitted, so error is NAN
+                psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_THETA_ERR",    0, "EXT orientation angle (error)",           dPAR[PM_PAR_THETA]);
+            } else {
+                if (!isfinite(PAR[PM_PAR_SXX]) || !isfinite(PAR[PM_PAR_SYY])  || !isfinite(PAR[PM_PAR_SXY])) {
+                    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MAJ",     0, "EXT width (SXX, isnan)", PAR[PM_PAR_SXX]);
+                    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MIN",     0, "EXT width (SYY, isnan)", PAR[PM_PAR_SYY]);
+                    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_THETA",         0, "EXT angle (SXY, isnan)", PAR[PM_PAR_SXY]);
+                    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MAJ_ERR", 0, "EXT width err (SXX, isnan)", dPAR[PM_PAR_SXX]);
+                    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MIN_ERR", 0, "EXT width err (SYY, isnan)", dPAR[PM_PAR_SYY]);
+                    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_THETA_ERR",     0, "EXT angle err (SXY, isnan)", dPAR[PM_PAR_SXY]);
+                } else {
+                    psEllipseAxes axes = pmPSF_ModelToAxes (PAR, model->class->useReff);
+                    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MAJ",    0, "EXT width (major axis), length for trail", axes.major);
+                    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MIN",    0, "EXT width (minor axis), sigma for trail",  axes.minor);
+                    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_THETA",        0, "EXT orientation angle",                    axes.theta);
+                    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MAJ_ERR",0, "EXT width error (major axis)",            dPAR[PM_PAR_SXX]);
+                    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MIN_ERR",0, "EXT width error (minor axis)",            dPAR[PM_PAR_SYY]);
+                    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_THETA_ERR",    0, "EXT orientation angle (error)",           dPAR[PM_PAR_SXY]);
+                }
+            }
+
+            // write out the other generic parameters
+            for (int k = 0; k < nParamMax; k++) {
+                if (k == PM_PAR_I0) continue;
+                if (k == PM_PAR_SKY) continue;
+                if (k == PM_PAR_XPOS) continue;
+                if (k == PM_PAR_YPOS) continue;
+                if (k == PM_PAR_SXX) continue;
+                if (k == PM_PAR_SXY) continue;
+                if (k == PM_PAR_SYY) continue;
+
+                snprintf (name, 64, "EXT_PAR_%02d", k);
+               
+                if (k < model->params->n) {
+                    psMetadataAddF32 (row, PS_LIST_TAIL, name, 0, "", model->params->data.F32[k]);
+                } else {
+                    psMetadataAddF32 (row, PS_LIST_TAIL, name, 0, "", NAN);
+                }
+            }
+
+            // optionally, write out the covariance matrix values
+            // XXX do I need to pad this to match the biggest covar matrix?
+            if (false && model->covar) {
+                for (int iy = 0; iy < model->covar->numCols; iy++) {
+                    for (int ix = iy; ix < model->covar->numCols; ix++) {
+                        snprintf (name, 64, "EXT_COVAR_%02d_%02d", iy, ix);
+                        psMetadataAddF32 (row, PS_LIST_TAIL, name, 0, "", model->covar->data.F32[iy][ix]);
+
+                    }
+                }                   
+            }
+            psArrayAdd (table, 100, row);
+            psFree (row);
+        }
+    }
+
+    if (table->n == 0) {
+        if (!psFitsWriteBlank (fits, outhead, extname)) {
+            psError(psErrorCodeLast(), false, "Unable to write empty sources file.");
+            psFree(outhead);
+            psFree(table);
+            return false;
+        }
+        psFree (outhead);
+        psFree (table);
+        return true;
+    }
+
+    psTrace ("pmFPAfile", 5, "writing ext data %s\n", extname);
+    if (!psFitsWriteTable (fits, outhead, table, extname)) {
+        psError(psErrorCodeLast(), false, "writing ext data %s\n", extname);
+        psFree (outhead);
+        psFree(table);
+        return false;
+    }
+    psFree (outhead);
+    psFree (table);
+    return true;
+}
+
+bool pmSourcesRead_CMF_@CMFMODE@_XFIT(psFits *fits, pmReadout *readout, psMetadata *hduHeader, psMetadata *tableHeader, psArray *sources, long *sourceIndex)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+
+    bool status;
+    long numSources = psFitsTableSize(fits); // Number of sources in table
+    if (numSources == 0) {
+        psError(psErrorCodeLast(), false, "XFIT Table contains no entries\n");
+        return false;
+    }
+    // set up the lookup table to translate between input model types and output model types
+    // if not defined it is assumed that the tables are the same
+    pmModelClassReadHeader(tableHeader);
+
+    for (long i = 0; i < numSources; i++) {
+        psMetadata *row = psFitsReadTableRow(fits, i); // Table row
+        if (!row) {
+            psError(psErrorCodeLast(), false, "Unable to read row %ld of sources", i);
+            psFree(row);
+            return false;
+        }
+        long seq = psMetadataLookupU32 (&status, row, "IPP_IDET");
+        long j = sourceIndex[seq];
+        psAssert(j >= 0 && j < sources->n, "invalid sourceIndex");
+        pmSource *source = sources->data[j];
+        if (!source) {
+            psError(PS_ERR_UNKNOWN, false, "Failed to find source for row %ld sequence number %ld\n", i, seq);
+            psFree(row);
+            return false;
+        }
+        if (!source->modelFits) {
+            // XXX: where to find the number of models to expect?
+            source->modelFits = psArrayAllocEmpty(5);
+        }
+        psString modelName = psMetadataLookupStr(&status, row, "MODEL_TYPE");
+        if (!modelName) {
+            psError(PS_ERR_UNKNOWN, true, "Failed to find model name for row %ld\n", i);
+            psFree(row);
+            return false;
+        }
+        pmModelType modelType = pmModelClassGetType(modelName);
+        if (modelType < 0) {
+            psError(PS_ERR_UNKNOWN, true, "Failed to find model type for %s\n", modelName);
+            psFree(row);
+            return false;
+        }
+        pmModel *model = pmModelAlloc(modelType);
+
+        psF32 *PAR = model->params->data.F32;
+        psF32 *dPAR = model->dparams->data.F32;
+
+        PAR[PM_PAR_XPOS] = psMetadataLookupF32(&status, row, "X_EXT");
+        PAR[PM_PAR_YPOS] = psMetadataLookupF32(&status, row, "Y_EXT");
+        dPAR[PM_PAR_XPOS] = psMetadataLookupF32(&status, row, "X_EXT_SIG");
+        dPAR[PM_PAR_YPOS] = psMetadataLookupF32(&status, row, "Y_EXT_SIG");
+
+        model->mag = psMetadataLookupF32(&status, row, "EXT_INST_MAG");
+        model->magErr = psMetadataLookupF32(&status, row, "EXT_INST_MAG_SIG");
+
+        model->chisq = psMetadataLookupF32(&status, row, "EXT_CHISQ");
+        model->nDOF = psMetadataLookupF32(&status, row, "EXT_NDOF");
+        model->flags = psMetadataLookupS16(&status, row, "EXT_FLAGS");
+
+        // EXT_MODEL_TYPE gives the model chosen by psphot as the best.
+        // Putting this into the XFIT table makes 3 copies of it (one for each model)
+        // but since we have many fewer XFIT rows than psf rows that is cheaper than putting it
+        // in the psf table.
+        psS32 extModelType = psMetadataLookupS32(&status, row, "EXT_MODEL_TYPE");
+        if (status) {
+            // translate between the type value in xfit and values used by this program
+            extModelType = pmModelClassGetLocalType(extModelType);
+        } else {
+            // older cmfs don't have this column
+            extModelType = -1;
+        } 
+
+        psEllipseAxes axes;
+        axes.major = psMetadataLookupF32(&status, row, "EXT_WIDTH_MAJ");
+        axes.minor = psMetadataLookupF32(&status, row, "EXT_WIDTH_MIN");
+        axes.theta = psMetadataLookupF32(&status, row, "EXT_THETA");
+        if (!pmPSF_AxesToModel(PAR, axes, model->class->useReff)) {
+            // Do we need to fail here or can this happen?
+            psError(PS_ERR_UNKNOWN, false, "Failed to convert psf axes to model");
+            psFree(model);
+            psFree(row);
+            return false;
+        }
+        // XXX: clean this up
+        if (model->params->n > 7) {
+            PAR[7] = psMetadataLookupF32(&status, row, "EXT_PAR_07");
+            // XXX add an error:
+            // dPAR[7] = psMetadataLookupF32(&status, row, "EXT_PAR_07_");
+        }
+
+        // NOTE: we no longer write out the covariance matrix
+        if (false) {
+            // read the covariance matrix
+            int nparams = model->params->n;
+            psImage *covar = psImageAlloc(nparams, nparams, PS_TYPE_F32);
+            for (int y = 0; y < nparams; y++) {
+                for (int x = 0; x < nparams; x++) {
+                    char name[64];
+                    snprintf(name, 64, "EXT_COVAR_%02d_%02d", y, x);
+                    covar->data.F32[y][x] = psMetadataLookupF32(&status, row, name);
+                }
+            }
+            model->covar = covar;
+        }
+
+        // we are only saving the values stored in dPAR[SXX,etc]
+        dPAR[PM_PAR_SXX] = psMetadataLookupF32(&status, row, "EXT_WIDTH_MAJ_ERR");
+        dPAR[PM_PAR_SYY] = psMetadataLookupF32(&status, row, "EXT_WIDTH_MIN_ERR");
+        dPAR[PM_PAR_SXY] = psMetadataLookupF32(&status, row, "EXT_THETA_ERR");
+
+        // other parameters that we need to read
+        PAR[PM_PAR_SKY] = psMetadataLookupF32(&status, row, "SKY_EXT");
+
+        if (modelType == extModelType) {
+            // The software that created this source picked this model as the best of the fits. 
+            // Set the extModel to point to it.
+            // This is important for programs like psastro (skycal) so that its output cmfs
+            // will have valid EXT_MODEL_TYPE
+            psFree(source->modelEXT);
+            source->modelEXT = psMemIncrRefCounter(model);
+            source->type = PM_SOURCE_TYPE_EXTENDED;
+            if (0) {
+                // since FLAGS were read we don't need to do this
+                source->mode |= PM_SOURCE_MODE_EXTMODEL;
+                source->mode |= PM_SOURCE_MODE_NONLINEAR_FIT;
+            }
+        }
+
+        psArrayAdd(source->modelFits, 1, model);
+        psFree(model);
+        psFree(row);
+    }
+
+    return true;
+}
+
+// **** write out the radial flux values for the sources for a given matched-PSF image
+// **** how do we distinguish the matched-PSF images from the non-matched version
+bool pmSourcesWrite_CMF_@CMFMODE@_XRAD(psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname, psMetadata *recipe)
+{
+    bool status = false;
+    psArray *table;
+    psMetadata *row;
+    psF32 xPos, yPos;
+    char keyword1[80], keyword2[80];
+
+    // perform full non-linear fits / extended source analysis?
+    if (!psMetadataLookupBool (&status, recipe, "RADIAL_APERTURES")) {
+        psLogMsg ("psphot", PS_LOG_INFO, "radial apertures were not measured, skipping\n");
+        return true;
+    }
+
+    // create a header to hold the output data
+    psMetadata *outhead = psMetadataAlloc ();
+
+    // write the links to the image header
+    psMetadataAddStr (outhead, PS_LIST_TAIL, "EXTNAME", PS_META_REPLACE, "radial flux table extension", extname);
+
+    // we use this just to define the output vectors (which must be present for all objects)
+    // First look for radial bin definition in readout->analysis ...
+    psVector *radMin = psMetadataLookupPtr (&status, readout->analysis, "RADIAL.ANNULAR.BINS.LOWER");
+    psVector *radMax = psMetadataLookupPtr (&status, readout->analysis, "RADIAL.ANNULAR.BINS.UPPER");
+    if (!radMin) {
+        // .. if not found use the recipe values
+        radMin = psMetadataLookupPtr (&status, recipe, "RADIAL.ANNULAR.BINS.LOWER");
+        radMax = psMetadataLookupPtr (&status, recipe, "RADIAL.ANNULAR.BINS.UPPER");
+    }
+    psAssert (radMin, "this must have been defined and tested earlier!");
+    psAssert (radMax, "this must have been defined and tested earlier!");
+    psAssert (radMin->n == radMax->n, "inconsistent annular bins");
+
+    // write the radial profile apertures to header
+    for (int i = 0; i < radMax->n; i++) {
+      sprintf (keyword1, "RMIN_%02d", i);
+      sprintf (keyword2, "RMAX_%02d", i);
+      psMetadataAddF32 (outhead, PS_LIST_TAIL, keyword1, PS_META_REPLACE, "min radius for SB profile", radMin->data.F32[i]);
+      psMetadataAddF32 (outhead, PS_LIST_TAIL, keyword2, PS_META_REPLACE, "min radius for SB profile", radMax->data.F32[i]);
+    }
+
+    // the FWHM values are available if we measured a psf-matched convolved set
+    psVector *fwhmValues = psMetadataLookupVector(&status, readout->analysis, "STACK.PSF.FWHM.VALUES");
+
+#ifdef SORT_EXTENSIONS_BY_FLUX
+    // let's write these out in S/N order
+    sources = psArraySort (sources, pmSourceSortByFlux);
+#endif
+
+    table = psArrayAllocEmpty (sources->n);
+
+    // we write out all sources, regardless of quality.  the source flags tell us the state
+    for (int i = 0; i < sources->n; i++) {
+
+        // this is the source associated with this image
+        pmSource *thisSource = sources->data[i];
+
+        // this is the "real" version of this source 
+        pmSource *source = thisSource->parent ? thisSource->parent : thisSource;
+
+        // skip sources without radial aper measurements (or insufficient)
+        if (source->radialAper == NULL) continue;
+
+        // psAssert (source->radialAper->n == fwhmValues->n, "inconsistent radial aperture set");
+
+        for (int entry = 0; entry < source->radialAper->n; entry++) {
+
+            // choose the convolved EXT model, if available, otherwise the simple one
+            pmSourceRadialApertures *radialAper = source->radialAper->data[entry];
+            assert (radialAper);
+
+            if (pmSourcePositionUseMoments(source)) {
+                xPos = source->moments->Mx;
+                yPos = source->moments->My;
+            } else {
+                xPos = source->peak->xf;
+                yPos = source->peak->yf;
+            }
+
+            row = psMetadataAlloc ();
+
+            // XXX we are not writing out the mode (flags) or the type (psf, ext, etc)
+            // the psMetadataAdd entry and the double quotes are used by grep to select the output fields for automatic documentation
+            // This set of psMetadataAdd Entries marks the "----" "Start of the XRAD segment"
+            psMetadataAddU32 (row, PS_LIST_TAIL, "IPP_IDET",         0, "IPP detection identifier index",             source->seq);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "X_APER",           0, "Center of aperture measurements",            xPos);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "Y_APER",           0, "Center of aperture measurements",            yPos);
+            if (fwhmValues) {
+                psMetadataAddF32 (row, PS_LIST_TAIL, "PSF_FWHM",         0, "FWHM of matched PSF",                    fwhmValues->data.F32[entry]);
+            } else {
+                psMetadataAddF32 (row, PS_LIST_TAIL, "PSF_FWHM",         0, "image is not FWHM-matched",              NAN);
+            }
+
+            // XXX if we have raw radial apertures, write them out here
+            psVector *radFlux      = psVectorAlloc(radMax->n, PS_TYPE_F32);
+            psVector *radFluxErr   = psVectorAlloc(radMax->n, PS_TYPE_F32);
+            psVector *radFill      = psVectorAlloc(radMax->n, PS_TYPE_F32);
+            psVector *radFluxStdev = psVectorAlloc(radMax->n, PS_TYPE_F32);
+            psVectorInit (radFlux,    NAN);
+            psVectorInit (radFluxErr, NAN);
+            psVectorInit (radFill,    NAN);
+            if (!radialAper->flux) goto write_annuli;
+            if (!radialAper->fill) goto write_annuli;
+            psAssert (radialAper->flux->n <= radFlux->n, "inconsistent vector lengths");
+            psAssert (radialAper->fill->n <= radFlux->n, "inconsistent vector lengths");
+
+            // copy the data from fluxVal (which is not guaranteed to be the full length) to radFlux
+            for (int j = 0; j < radialAper->flux->n; j++) {
+                radFlux->data.F32[j]      = radialAper->flux->data.F32[j];
+                radFluxErr->data.F32[j]   = radialAper->fluxErr->data.F32[j];
+                radFluxStdev->data.F32[j] = radialAper->fluxStdev->data.F32[j];
+                radFill->data.F32[j]      = radialAper->fill->data.F32[j];
+            }
+
+        write_annuli:
+            psMetadataAddVector (row, PS_LIST_TAIL, "APER_FLUX",       PS_META_REPLACE, "flux within annuli",       radFlux);
+            psMetadataAddVector (row, PS_LIST_TAIL, "APER_FLUX_ERR",   PS_META_REPLACE, "flux error in annuli",     radFluxErr);
+            psMetadataAddVector (row, PS_LIST_TAIL, "APER_FLUX_STDEV", PS_META_REPLACE, "flux standard deviation",  radFluxStdev);
+            psMetadataAddVector (row, PS_LIST_TAIL, "APER_FILL",       PS_META_REPLACE, "fill factor of annuli",    radFill);
+            psFree (radFlux);
+            psFree (radFluxErr);
+            psFree (radFluxStdev);
+            psFree (radFill);
+
+            psArrayAdd (table, 100, row);
+            psFree (row);
+        }
+    }
+
+    if (table->n == 0) {
+        if (!psFitsWriteBlank (fits, outhead, extname)) {
+            psError(psErrorCodeLast(), false, "Unable to write empty sources file.");
+            psFree(outhead);
+            psFree(table);
+            return false;
+        }
+        psFree (outhead);
+        psFree (table);
+        return true;
+    }
+
+    psTrace ("pmFPAfile", 5, "writing ext data %s\n", extname);
+    if (!psFitsWriteTable (fits, outhead, table, extname)) {
+        psError(psErrorCodeLast(), false, "writing ext data %s\n", extname);
+        psFree (outhead);
+        psFree(table);
+        return false;
+    }
+    psFree (outhead);
+    psFree (table);
+    return true;
+}
+
+bool pmSourcesRead_CMF_@CMFMODE@_XRAD(psFits *fits, pmReadout *readout, psMetadata *hduHeader, psMetadata *tableHeader, psArray *sources, long *sourceIndex)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+
+    bool status;
+    long numSources = psFitsTableSize(fits); // Number of sources in table
+    if (numSources == 0) {
+        psError(psErrorCodeLast(), false, "XRAD Table contains no entries\n");
+        return false;
+    }
+
+    if (!setRadialBinsInAnalysis(readout, tableHeader)) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to save radial bins in analysis");
+        return false;
+    }
+
+    long       seq_first = -1;
+    long       seq_last = -1;
+    psVector   *fwhmValues = psVectorAllocEmpty(10, PS_TYPE_F32);
+    long       max_entries = -1;
+    long       num_entries = -1;
+
+    for (long i = 0; i < numSources; i++) {
+        psMetadata *row = psFitsReadTableRow(fits, i); // Table row
+        if (!row) {
+            psError(psErrorCodeLast(), false, "Unable to read row %ld of sources", i);
+            psFree(row);
+            return false;
+        }
+        long seq = psMetadataLookupU32 (&status, row, "IPP_IDET");
+        long j = sourceIndex[seq];
+        psAssert(j >= 0 && j < sources->n, "invalid sourceIndex");
+        pmSource *source = sources->data[j];
+        if (!source) {
+            psError(PS_ERR_UNKNOWN, false, "Failed to find source for row %ld sequence number %ld\n", i, seq);
+            psFree(row);
+            return false;
+        }
+        if (seq_first == -1) {
+            seq_first = seq;
+        }
+        if (seq == seq_first) {
+            psF32 value = psMetadataLookupF32(&status, row, "PSF_FWHM");
+            psVectorAppend(fwhmValues, value);
+        }
+        if (seq == seq_last) {
+            num_entries++;
+        } else {
+            num_entries = 1;
+            seq_last = seq;
+        }
+        if (num_entries > max_entries) {
+            max_entries = num_entries;
+        }
+
+        if (!source->radialAper) {
+            // XXX: where to find the number of models to expect?
+            source->radialAper = psArrayAllocEmpty(5);
+        }
+        pmSourceRadialApertures *radialAper = pmSourceRadialAperturesAlloc();
+
+        radialAper->flux = psMemIncrRefCounter(psMetadataLookupVector(&status, row, "APER_FLUX"));
+        radialAper->fluxStdev = psMemIncrRefCounter(psMetadataLookupVector(&status, row, "APER_FLUX_STDEV"));
+        radialAper->fluxErr = psMemIncrRefCounter(psMetadataLookupVector(&status, row, "APER_FLUX_ERR"));
+        radialAper->fill = psMemIncrRefCounter(psMetadataLookupVector(&status, row, "APER_FILL"));
+
+        psArrayAdd(source->radialAper, 1, radialAper);
+
+        psFree(radialAper);
+        psFree(row);
+    }
+
+    // check for consistency between the length of fwhmValues and the maximum number of entries for each row
+    if (fwhmValues->n != max_entries) {
+        psError(PS_ERR_PROGRAMMING, true, "number of PSF_FWHM values found %ld does not match expected number: %ld\n",
+            fwhmValues->n, max_entries);
+        psAssert(0, "fixme");
+    }
+
+    if (!readout->analysis) {
+        readout->analysis = psMetadataAlloc();
+    }
+
+    psMetadataAddVector(readout->analysis, PS_LIST_TAIL, "STACK.PSF.FWHM.VALUES", PS_META_REPLACE, "PSF sizes", fwhmValues);
+    psFree(fwhmValues);
+
+    return true;
+}
+
+// XXX where should I record the number of columns??
+bool pmSourcesWrite_CMF_@CMFMODE@_XGAL (psFits *fits, pmReadout *readout, psArray *sources, char *extname, psMetadata *recipe)
+{
+    bool status = false;
+
+    // perform full non-linear fits / extended source analysis?
+    if (!psMetadataLookupBool (&status, recipe, "GALAXY_SHAPES")) {
+        psLogMsg ("psphot", PS_LOG_INFO, "galaxy shapes were not measured, skipping\n");
+        return true;
+    }
+
+    // create a header to hold the output data
+    psMetadata *outhead = psMetadataAlloc ();
+
+    // write the links to the image header
+    psMetadataAddStr (outhead, PS_LIST_TAIL, "EXTNAME", PS_META_REPLACE, "galaxy table extension", extname);
+
+    psF32 Q = psMetadataLookupF32(&status, recipe, "GALAXY_SHAPES_Q");
+    psMetadataAddF32 (outhead, PS_LIST_TAIL, "GALAXY_SHAPES_Q", PS_META_REPLACE, "", Q);
+
+    psF32 NSigma = psMetadataLookupF32(&status, recipe, "GALAXY_SHAPES_NSIGMA");
+    psMetadataAddF32 (outhead, PS_LIST_TAIL, "GALAXY_SHAPES_NSIGMA", PS_META_REPLACE, "", NSigma);
+
+    psF32 clampSN = psMetadataLookupF32(&status, recipe, "GALAXY_SHAPES_CLAMP_SN");
+    psMetadataAddF32 (outhead, PS_LIST_TAIL, "GALAXY_SHAPES_CLAMP_SN", PS_META_REPLACE, "", clampSN);
+
+    // They are probably already in this order but ...
+    sources = psArraySort (sources, pmSourceSortBySeq);
+
+    psArray *table = psArrayAllocEmpty (sources->n);
+
+    for (int i = 0; i < sources->n; i++) {
+
+        pmSource *thisSource = sources->data[i];
+
+        // this is the "real" version of this source 
+        pmSource *source = thisSource->parent ? thisSource->parent : thisSource;
+
+        // if we did not fit the galaxy model, modelFits will be NULL
+        if (source->modelFits == NULL) continue;
+
+        // if we did not fit the galaxy model, galaxyFits will also be NULL
+        if (source->galaxyFits == NULL) continue;
+
+        for (int iModel = 0; iModel < source->modelFits->n; iModel++) {
+            pmModel *model = source->modelFits->data[iModel];
+            if (!model) continue;
+
+            pmSourceGalaxyFits *galaxyFits = NULL;
+            for (int iFit = 0; iFit < source->galaxyFits->n; iFit++) {
+                galaxyFits = source->galaxyFits->data[iFit];
+                if (model->type == galaxyFits->modelType) break;
+                galaxyFits = NULL;
+            }
+
+            if (!galaxyFits) continue;
+
+            // X,Y coordinates are stored with the model parameters
+            psF32 *PAR = model->params->data.F32;
+
+            psMetadata *row = psMetadataAlloc ();
+
+            // we write out the x,y positions so people can link to the psf either way (position or ID)
+            psMetadataAddU32 (row, PS_LIST_TAIL, "IPP_IDET",         0, "IPP detection identifier index", source->seq);
+            psMetadataAddS32 (row, PS_LIST_TAIL, "MODEL_TYPE",       0, "model type",                     galaxyFits->modelType);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "X_FIT",            0, "model x coordinate",             PAR[PM_PAR_XPOS]);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "Y_FIT",            0, "model y coordinate",             PAR[PM_PAR_YPOS]);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "NPIX",             0, "number of pixels for fits",      galaxyFits->nPix);
+            // psMetadataAddS32 (row, PS_LIST_TAIL, "REDUCED_TRIALS",   0, "source has reduced number of trials",      galaxyFits->reducedTrials);
+
+            psVector *Flux = galaxyFits->Flux;
+            psVector *dFlux = galaxyFits->dFlux;
+            psVector *chisq = galaxyFits->chisq;
+
+            psMetadataAddVector (row, PS_LIST_TAIL, "GAL_FLUX",     PS_META_REPLACE, "normalization for galaxy flux", Flux);
+            psMetadataAddVector (row, PS_LIST_TAIL, "GAL_FLUX_ERR", PS_META_REPLACE, "error on normalization", dFlux);
+            psMetadataAddVector (row, PS_LIST_TAIL, "GAL_CHISQ",    PS_META_REPLACE, "galaxy fit chisq", chisq);
+
+            psMetadataAddF32 (row, PS_LIST_TAIL, "FR_MAJOR_MIN",    0, "fractional major axis min",      galaxyFits->fRmajorMin);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "FR_MAJOR_MAX",    0, "fractional major axis max",      galaxyFits->fRmajorMax);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "FR_MAJOR_DEL",    0, "fractional major axis max",      galaxyFits->fRmajorDel);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "FR_MINOR_MIN",    0, "fractional minor axis min",      galaxyFits->fRminorMin);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "FR_MINOR_MAX",    0, "fractional minor axis max",      galaxyFits->fRminorMax);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "FR_MINOR_DEL",    0, "fractional minor axis max",      galaxyFits->fRminorDel);
+
+            psArrayAdd (table, 100, row);
+            psFree (row);
+        }
+    }
+
+    if (table->n == 0) {
+        if (!psFitsWriteBlank (fits, outhead, extname)) {
+            psError(psErrorCodeLast(), false, "Unable to write empty sources file.");
+            psFree(outhead);
+            psFree(table);
+            return false;
+        }
+        psFree (outhead);
+        psFree (table);
+        return true;
+    }
+
+    psTrace ("pmFPAfile", 5, "writing galaxy data %s\n", extname);
+    if (!psFitsWriteTable (fits, outhead, table, extname)) {
+        psError(psErrorCodeLast(), false, "writing galaxy data %s\n", extname);
+        psFree (outhead);
+        psFree(table);
+        return false;
+    }
+    psFree (outhead);
+    psFree (table);
+    return true;
+}
+
+bool pmSourcesRead_CMF_@CMFMODE@_XGAL(psFits *fits, pmReadout *readout, psMetadata *hduHeader, psMetadata *tableHeader, psArray *sources, long *sourceIndex)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+
+    bool status;
+    long numSources = psFitsTableSize(fits); // Number of sources in table
+    if (numSources == 0) {
+        psError(psErrorCodeLast(), false, "XGAL Table contains no entries\n");
+        return false;
+    }
+    if (!readout->analysis) {
+        readout->analysis = psMetadataAlloc();
+    }
+    psF32 Q = psMetadataLookupF32(&status, tableHeader, "GALAXY_SHAPES_Q");
+    // XXX: turn this into an assert once we're done
+    if (status) {
+        psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "GALAXY_SHAPES_Q", PS_META_REPLACE, "", Q);
+
+        psF32 NSigma = psMetadataLookupF32(&status, tableHeader, "GALAXY_SHAPES_NSIGMA");
+        psAssert(status, "missing GALAXY_SHAPES_NSIGMA");
+        psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "GALAXY_SHAPES_NSIGMA", PS_META_REPLACE, "", NSigma);
+
+        psF32 clampSN = psMetadataLookupF32(&status, tableHeader, "GALAXY_SHAPES_CLAMP_SN");
+        psAssert(status, "missing GALAXY_SHAPES_CLAMP_SN");
+        psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "GALAXY_SHAPES_CLAMP_SN", PS_META_REPLACE, "", clampSN);
+    }
+
+    for (long i = 0; i < numSources; i++) {
+        psMetadata *row = psFitsReadTableRow(fits, i); // Table row
+        if (!row) {
+            psError(psErrorCodeLast(), false, "Unable to read row %ld of sources", i);
+            psFree(row);
+            return false;
+        }
+        // Find the source with this sequence number. 
+        // XXX: I am assuming that sources is sorted in order of seq
+        long seq = psMetadataLookupU32 (&status, row, "IPP_IDET");
+        long j = sourceIndex[seq];
+        psAssert(j >= 0 && j < sources->n, "invalid sourceIndex");
+
+        pmSource *source = sources->data[j];
+        if (!source) {
+            psError(PS_ERR_UNKNOWN, false, "Failed to find source for row %ld sequence number %ld\n", i, seq);
+            psFree(row);
+            return false;
+        }
+
+        int modelType = psMetadataLookupS32 (&status,    row, "MODEL_TYPE");
+        psVector *Flux  = psMetadataLookupVector(&status, row, "GAL_FLUX");
+        psVector *dFlux = psMetadataLookupVector(&status, row, "GAL_FLUX_ERR");
+        psVector *chisq = psMetadataLookupVector(&status, row, "GAL_CHISQ");
+
+        if (Flux && Flux->n > 0) {
+            if (!source->galaxyFits) {
+                source->galaxyFits = psArrayAllocEmpty(1);
+            }
+
+            pmSourceGalaxyFits *galaxyFits = pmSourceGalaxyFitsAlloc();
+
+            psArrayAdd(source->galaxyFits, 1, galaxyFits);
+
+            psFree(galaxyFits);
+            galaxyFits->modelType = modelType;
+            galaxyFits->nPix = psMetadataLookupF32(&status, row, "NPIX");
+
+            galaxyFits->fRmajorMin = psMetadataLookupF32(&status, row, "FR_MAJOR_MIN");
+            galaxyFits->fRmajorMax = psMetadataLookupF32(&status, row, "FR_MAJOR_MAX");
+            galaxyFits->fRmajorDel = psMetadataLookupF32(&status, row, "FR_MAJOR_DEL");
+            galaxyFits->fRminorMin = psMetadataLookupF32(&status, row, "FR_MINOR_MIN");
+            galaxyFits->fRminorMax = psMetadataLookupF32(&status, row, "FR_MINOR_MAX");
+            galaxyFits->fRminorDel = psMetadataLookupF32(&status, row, "FR_MINOR_DEL");
+
+            psFree(galaxyFits->Flux);
+            galaxyFits->Flux  = psMemIncrRefCounter(Flux);
+            psFree(galaxyFits->dFlux);
+            galaxyFits->dFlux = psMemIncrRefCounter(dFlux);
+            psFree(galaxyFits->chisq);
+            galaxyFits->chisq = psMemIncrRefCounter(chisq);
+        }
+
+        psFree(row);
+    }
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_CMF.txt
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_CMF.txt	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_CMF.txt	(revision 42651)
@@ -0,0 +1,155 @@
+                                
+                          ----  Start of the PSF segment
+                      IPP_IDET  IPP detection identifier index
+                         X_PSF  PSF x coordinate
+                         Y_PSF  PSF y coordinate
+                     X_PSF_SIG  Sigma in PSF x coordinate
+                     Y_PSF_SIG  Sigma in PSF y coordinate
+                        RA_PSF  PSF RA coordinate (degrees)
+                       DEC_PSF  PSF DEC coordinate (degrees)
+                      POSANGLE  position angle at source (degrees)
+                      PLTSCALE  plate scale at source (arcsec/pixel)
+                  PSF_INST_MAG  PSF fit instrumental magnitude
+              PSF_INST_MAG_SIG  Sigma of PSF instrumental magnitude
+                 PSF_INST_FLUX  PSF fit instrumental flux (counts)
+             PSF_INST_FLUX_SIG  Sigma of PSF instrumental flux
+                        AP_MAG  magnitude in standard aperture
+                    AP_MAG_RAW  magnitude in reported aperture
+                 AP_MAG_RADIUS  radius used for aperture mags
+                       AP_FLUX  instrumental flux in standard aperture
+                   AP_FLUX_SIG  aperture flux error
+              PEAK_FLUX_AS_MAG  Peak flux expressed as magnitude
+                   CAL_PSF_MAG  PSF Magnitude using supplied calibration
+               CAL_PSF_MAG_SIG  measured scatter of zero point calibration
+                        RA_PSF  PSF RA coordinate (degrees)
+                       DEC_PSF  PSF DEC coordinate (degrees)
+              PEAK_FLUX_AS_MAG  Peak flux expressed as magnitude
+                           SKY  Sky level
+                     SKY_SIGMA  Sigma of sky level
+                     PSF_CHISQ  Chisq of PSF-fit
+                     CR_NSIGMA  Nsigma deviations from PSF to CF
+                    EXT_NSIGMA  Nsigma deviations from PSF to EXT
+                     PSF_MAJOR  PSF width (major axis)
+                     PSF_MINOR  PSF width (minor axis)
+                     PSF_THETA  PSF orientation angle
+                        PSF_QF  PSF coverage/quality factor (bad)
+                PSF_QF_PERFECT  PSF coverage/quality factor (poor)
+                      PSF_NDOF  degrees of freedom
+                      PSF_NPIX  number of pixels in fit
+                    MOMENTS_XX  second moments (X^2)
+                    MOMENTS_XY  second moments (X*Y)
+                    MOMENTS_YY  second moments (Y*Y)
+                   MOMENTS_M3C  third momemt cos theta
+                   MOMENTS_M3S  third momemt sin theta
+                   MOMENTS_M4C  fourth momemt cos theta
+                   MOMENTS_M4S  fourth momemt sin theta
+                    MOMENTS_R1  first radial moment
+                    MOMENTS_RH  half radial moment
+                     KRON_FLUX  Kron Flux (in 2.5 R1)
+                 KRON_FLUX_ERR  Kron Flux Error
+               KRON_FLUX_INNER  Kron Flux (in 1.0 R1)
+               KRON_FLUX_OUTER  Kron Flux (in 4.0 R1)
+                 SKY_LIMIT_RAD  Radius where object hits sky
+                SKY_LIMIT_FLUX  Flux / pix where object hits sky
+               SKY_LIMIT_SLOPE  d(Flux/pix)/dRadius where object hits sky
+                     DIFF_NPOS  nPos (n pix > 3 sigma)
+                   DIFF_FRATIO  fPos / (fPos + fNeg)
+               DIFF_NRATIO_BAD  nPos / (nPos + nNeg)
+              DIFF_NRATIO_MASK  nPos / (nPos + nMask)
+               DIFF_NRATIO_ALL  nPos / (nGood + nMask + nBad)
+                      DIFF_R_P  distance to positive match source
+                     DIFF_SN_P  signal-to-noise of pos match src
+                      DIFF_R_M  distance to negative match source
+                     DIFF_SN_M  signal-to-noise of neg match src
+                         FLAGS  psphot analysis flags
+                        FLAGS2  psphot analysis flags
+                      PADDING2  more padding
+                      N_FRAMES  Number of frames overlapping source center
+                       PADDING  padding
+                                
+                          ----  Start of the XSRC segment
+                      IPP_IDET  IPP detection identifier index
+                         X_EXT  EXT model x coordinate
+                         Y_EXT  EXT model y coordinate
+                     X_EXT_SIG  Sigma in EXT x coordinate
+                     Y_EXT_SIG  Sigma in EXT y coordinate
+                    F25_ARATIO  Axial Ratio of radial profile
+                     F25_THETA  Angle of radial profile ellipse
+                     PETRO_MAG  Petrosian Magnitude
+                 PETRO_MAG_ERR  Petrosian Magnitude Error
+                  PETRO_RADIUS  Petrosian Radius (pix)
+              PETRO_RADIUS_ERR  Petrosian Radius Error (pix)
+               PETRO_RADIUS_50  Petrosian R50 (pix)
+           PETRO_RADIUS_50_ERR  Petrosian R50 Error (pix)
+               PETRO_RADIUS_90  Petrosian R90 (pix)
+           PETRO_RADIUS_90_ERR  Petrosian R90 Error (pix)
+                    PETRO_FILL  Petrosian Fill Factor
+                     PETRO_MAG  Petrosian Magnitude
+                 PETRO_MAG_ERR  Petrosian Magnitude Error
+                  PETRO_RADIUS  Petrosian Radius
+              PETRO_RADIUS_ERR  Petrosian Radius Error
+               PETRO_RADIUS_50  Petrosian R50 (pix)
+           PETRO_RADIUS_50_ERR  Petrosian R50 Error (pix)
+               PETRO_RADIUS_90  Petrosian R90 (pix)
+           PETRO_RADIUS_90_ERR  Petrosian R90 Error (pix)
+                    PETRO_FILL  Petrosian Fill Factor
+                       PROF_SB  mean surface brightness annuli
+                     PROF_FLUX  flux within annuli
+                     PROF_FILL  fill factor of annuli
+                                
+                          ----  Start of the XFIT segment
+                      IPP_IDET  IPP detection identifier index
+                         X_EXT  EXT model x coordinate
+                         Y_EXT  EXT model y coordinate
+                     X_EXT_SIG  Sigma in EXT x coordinate
+                     Y_EXT_SIG  Sigma in EXT y coordinate
+                        RA_EXT  EXT model ra coordinate
+                       DEC_EXT  EXT model dec coordinate
+                  EXT_INST_MAG  EXT fit instrumental magnitude
+              EXT_INST_MAG_SIG  Sigma of PSF instrumental magnitude
+                      POSANGLE  position angle at source (degrees)
+                      PLTSCALE  plate scale at source (arcsec/pixel)
+                    MOMENTS_XX  second moment in x
+                    MOMENTS_XY  second moment in x,y
+                    MOMENTS_YY  second moment in y
+                    MOMENTS_R1  first radial moment
+                    MOMENTS_RH  half radial moment
+                  PSF_INST_MAG  PSF fit instrumental magnitude
+                        AP_MAG  PSF-sized aperture magnitude
+                      KRON_MAG  Kron Mag
+                       NPARAMS  number of model parameters
+                       NPARAMS  number of model parameters
+                    MODEL_TYPE  name of model
+                 EXT_WIDTH_MAJ  EXT width (major axis), length for trail
+                 EXT_WIDTH_MIN  EXT width (minor axis), sigma for trail
+                     EXT_THETA  EXT orientation angle
+             EXT_WIDTH_MAJ_ERR  EXT width error (major axis)
+             EXT_WIDTH_MIN_ERR  EXT width error (minor axis)
+                 EXT_THETA_ERR  EXT orientation angle (error)
+                 EXT_WIDTH_MAJ  EXT width (SXX, isnan)
+                 EXT_WIDTH_MIN  EXT width (SYY, isnan)
+                     EXT_THETA  EXT angle (SXY, isnan)
+             EXT_WIDTH_MAJ_ERR  EXT width err (SXX, isnan)
+             EXT_WIDTH_MIN_ERR  EXT width err (SYY, isnan)
+                 EXT_THETA_ERR  EXT angle err (SXY, isnan)
+                 EXT_WIDTH_MAJ  EXT width (major axis), length for trail
+                 EXT_WIDTH_MIN  EXT width (minor axis), sigma for trail
+                     EXT_THETA  EXT orientation angle
+             EXT_WIDTH_MAJ_ERR  EXT width error (major axis)
+             EXT_WIDTH_MIN_ERR  EXT width error (minor axis)
+                 EXT_THETA_ERR  EXT orientation angle (error)
+                                
+                                
+                                
+                                
+                          ----  Start of the XRAD segment
+                      IPP_IDET  IPP detection identifier index
+                        X_APER  Center of aperture measurements
+                        Y_APER  Center of aperture measurements
+                      PSF_FWHM  FWHM of matched PSF
+                      PSF_FWHM  image is not FWHM-matched
+                     APER_FLUX  flux within annuli
+                 APER_FLUX_ERR  flux error in annuli
+               APER_FLUX_STDEV  flux standard deviation
+                     APER_FILL  fill factor of annuli
+         STACK.PSF.FWHM.VALUES  PSF sizes
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_CMP.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_CMP.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_CMP.c	(revision 42651)
@@ -0,0 +1,313 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.34 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-12-08 02:51:14 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.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)
+{
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(filename, false);
+    PS_ASSERT_PTR_NON_NULL(header, false);
+
+    int i, type;
+    // psMetadataItem *mdi;
+    psF32 *PAR;
+    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;
+
+        type = pmSourceGetDophotType (source);
+        lsky = (source->sky < 1.0) ? 0.0 : log10(source->sky);
+
+        axes = pmPSF_ModelToAxes (PAR, model->class->useReff);
+
+        float psfMagErr = isfinite(source->psfMagErr) ? source->psfMagErr : 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)psfMagErr));
+        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);
+        if (fwrite(line->line, 1, line->Nline, f) < line->Nline) {
+            psError(PS_ERR_IO, true, "Unable to write CMP sources file (%s)", filename);
+            fclose(f);
+            psFree(line);
+            return false;
+        }
+    }
+    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)
+{
+    PS_ASSERT_PTR_NON_NULL(filename, false);
+    PS_ASSERT_PTR_NON_NULL(header, false);
+
+    bool status;
+    int Ninstar;
+    psF32 *PAR;
+    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);
+    }
+
+    // XXX unused // find config information for output header
+    // XXX unused float ZERO_POINT = psMetadataLookupF32 (&status, header, "ZERO_PT");
+    // XXX unused if (!status)
+    // XXX unused     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;
+
+            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->psfMagErr = 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, source->modelPSF->class->useReff);
+
+            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_branches/psModules.20240412/src/objects/pmSourceIO_Ghosts.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_Ghosts.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_Ghosts.c	(revision 42651)
@@ -0,0 +1,525 @@
+/** @file  pmSourceIO_Ghosts.c
+ *
+ *  @author TdB, IfA
+ *
+ *  @version $Revision: 1.0 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 20020-03-30  $
+ *
+ *  Copyright 2020 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 "pmErrorCodes.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+
+#include "pmSourceIO.h"
+
+#include "pmAstrometryObjects.h"
+#include "pmAstrometryWCS.h"
+
+# include "pmSourceInternal.h"
+static psVector *chipXmin = NULL;
+static psVector *chipXmax = NULL;
+static psVector *chipYmin = NULL;
+static psVector *chipYmax = NULL;
+
+# define ESCAPE(MSG) { \
+  psLogMsg ("psastro", PS_LOG_INFO, MSG); \
+  return false; }
+
+# define GET_2D_POLY(NAME,OUT) \
+    md = psMetadataLookupMetadata (&status, ghostModel, NAME); \
+    if (!md) { \
+	psError(PM_ERR_CONFIG, true, "Missing %s in model file %s", NAME, ghostFile); \
+	goto escape; \
+    } \
+    OUT = psPolynomial2DfromMetadata(md); \
+    if (!OUT) { \
+	psError(PM_ERR_CONFIG, true, "Trouble interpretting %s in model file %s", NAME, ghostFile); \
+	goto escape; \
+    }
+
+# define GET_1D_POLY(NAME,OUT) \
+    md = psMetadataLookupMetadata (&status, ghostModel, NAME); \
+    if (!md) { \
+	psError(PM_ERR_CONFIG, true, "Missing %s in model file %s", NAME, ghostFile); \
+	goto escape; \
+    } \
+    OUT = psPolynomial1DfromMetadata(md);	\
+    if (!OUT) { \
+	psError(PM_ERR_CONFIG, true, "Trouble interpretting %s in model file %s", NAME, ghostFile); \
+	goto escape; \
+    }
+
+/**
+ * calculate ghost FPA and Chip positions for the stars loaded on the FPA
+ */
+
+
+bool pmSourceIO_WriteGhosts (psFits *fits, pmFPA *fpa, pmConfig *config) {
+
+    bool status;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    float zeropt, exptime, MAX_MAG;
+    psMetadata *md = NULL;
+    psPolynomial2D *centerX = NULL;
+    psPolynomial2D *centerY = NULL;
+    psPolynomial1D *mirrorRad = NULL;
+    psPolynomial1D *outerMajor = NULL;
+    psPolynomial1D *outerMinor = NULL;
+    psPolynomial1D *innerMajor = NULL;
+    psPolynomial1D *innerMinor = NULL;
+    psMetadata *ghostModel = NULL;
+
+    psLogMsg ("psastro", PS_LOG_INFO, "writing ghost positions");
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PM_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+        return false;
+    }
+
+    bool REFSTAR_MASK_GHOST = psMetadataLookupBool (&status, recipe, "REFSTAR_MASK_GHOST");
+    if (!REFSTAR_MASK_GHOST) return true;
+    bool GHOST_OUTPUT = psMetadataLookupBool(&status, recipe, "PSASTRO.SAVE.GHOSTS");
+    if (!GHOST_OUTPUT) return true;
+
+    char *ghostFile = psMetadataLookupStr (&status, recipe, "GHOST_MODEL");
+    if (!strcasecmp(ghostFile, "NONE")) return true;
+
+    if (!pmConfigFileRead (&ghostModel, ghostFile, "GHOST MODEL")) {
+	psError(PM_ERR_CONFIG, true, "Trouble loading ghost model");
+        return false;
+    }
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // We need to check whether we are dealing with an old style ghost_model, or a new style model. Check if the mirror_rad polynomial exists
+    float mirCheck = 0;
+    md = psMetadataLookupMetadata (&status, ghostModel, "GHOST.MIRROR.RAD"); 
+    if (!md) { psLogMsg ("psastro", PS_LOG_INFO, "No ghost mirror_rad polynomial found. Assuming old-style ghost masking"); } 
+    if (md) {
+        GET_1D_POLY ("GHOST.MIRROR.RAD", mirrorRad);
+        mirCheck = 1;
+    }  
+
+    GET_2D_POLY ("GHOST.CENTER.X", centerX);
+    GET_2D_POLY ("GHOST.CENTER.Y", centerY);
+
+    GET_1D_POLY ("GHOST.OUTER.MAJOR", outerMajor);
+    GET_1D_POLY ("GHOST.OUTER.MINOR", outerMinor);
+    GET_1D_POLY ("GHOST.INNER.MAJOR", innerMajor);
+    GET_1D_POLY ("GHOST.INNER.MINOR", innerMinor);
+
+    // select the input astrometry data (also carries the refstars)
+    pmFPAfile *astrom = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!astrom) {
+        psError(PM_ERR_CONFIG, true, "Can't find input data");
+	goto escape;
+    }
+    pmFPA *fpa_ast = astrom->fpa;
+
+    // really error-out here?  or just skip?
+    if (!pmSourceZeroPointFromRecipeGlint (&zeropt, &exptime, &MAX_MAG,NULL, fpa_ast, recipe)) {
+        psLogMsg ("psastro", PS_LOG_INFO, "failed to load zeropt data from recipe");
+	goto escape;
+    }
+
+    // recipe values are given in instrumental magnitudes
+    // use the zero point and exposure time to convert to apparent mags: M_ap = M_inst + C_0 + 2.5*log(exptime)
+    float MagOffset = zeropt + 2.5*log10(exptime);
+    MAX_MAG += MagOffset;
+
+    psArray *table = psMetadataLookupPtr (&status, fpa->analysis, "GHOST_POSITIONS");
+    psMemIncrRefCounter (table);
+    if (!table) {
+        table = psArrayAllocEmpty (0x1000);
+
+ 	// this loop selects the matched stars for all chips
+ 	while ((chip = pmFPAviewNextChip (view, fpa_ast, 1)) != NULL) {
+ 	    psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+ 	    if (!chip->process || !chip->file_exists) { continue; }
+ 	    if (!chip->fromFPA) { continue; }
+
+ 	    while ((cell = pmFPAviewNextCell (view, fpa_ast, 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_ast, 1)) != NULL) {
+ 		    if (! readout->data_exists) { continue; }
+
+ 		    // select the raw objects for this readout (loaded in psastroChooseRefstars.c)
+ 		    // XXX : note that we place limits on the refstar sample in psastroChooseRefstars.c:
+ 		    // 1) on chip and 2) < PSASTRO.MAX.NREF. magnitude limits and clump exclusion are only 
+ 		    psArray *refstars = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.REFSTARS");
+ 		    if (refstars == NULL) { continue; }
+
+ 		    // identify the bright stars of interest
+ 		    for (int i = 0; i < refstars->n; i++) {
+ 			pmAstromObj *ref = refstars->data[i];
+ 			if (ref->Mag > MAX_MAG) continue;
+
+ 			pmSourceGhost *ghost = pmSourceGhostAlloc ();
+ 			ghost->srcFP->x = ref->FP->x; 
+ 			ghost->srcFP->y = ref->FP->y;
+
+                        //TdB: first mirror the reference star positions (around the 0,0 pixel) using the radial offset coefficients and the ghost/star angle
+		        double rSrc = hypot (ref->FP->x, ref->FP->y);
+     	                double theta0 = atan2(ref->FP->y,ref->FP->x);
+
+			// EAM: XXX we should just use the existence of mirrorRad (!= NULL) instead of carrying a different bool
+                        if (mirCheck) {
+                            //TdB: first mirror the reference star positions (around the 0,0 pixel) using the radial offset coefficients and the ghost/star angle
+		            double ghost_offset_rad = psPolynomial1DEval (mirrorRad, rSrc);
+		            double ghost_x_fpa_mirror = ref->FP->x + ((ref->FP->x*-1.)/abs(ref->FP->x)*abs(cos(theta0)*ghost_offset_rad));
+		            double ghost_y_fpa_mirror = ref->FP->y + ((ref->FP->y*-1.)/abs(ref->FP->y)*abs(sin(theta0)*ghost_offset_rad));
+
+		            // Now use the mirrored position together with the 2D ghost center fitting to get the actual ghost position in FPA coords 
+		            ghost->FP->x = ghost_x_fpa_mirror + psPolynomial2DEval(centerX, ghost_x_fpa_mirror, ghost_y_fpa_mirror);
+		            ghost->FP->y = ghost_y_fpa_mirror + psPolynomial2DEval(centerY, ghost_x_fpa_mirror, ghost_y_fpa_mirror);
+                        } else {
+                            //Use the old-style ghost position determination
+                            ghost->FP->x = -ref->FP->x + psPolynomial2DEval(centerX, -ref->FP->x, -ref->FP->y);
+                            ghost->FP->y = -ref->FP->y + psPolynomial2DEval(centerY, -ref->FP->x, -ref->FP->y);
+                        }
+
+ 			ghost->inner.major = psPolynomial1DEval (innerMajor, rSrc);
+ 			ghost->inner.minor = psPolynomial1DEval (innerMinor, rSrc);
+ 			ghost->inner.theta = atan2(ref->FP->y, ref->FP->x);
+
+ 			ghost->outer.major = psPolynomial1DEval (outerMajor, rSrc);
+ 			ghost->outer.minor = psPolynomial1DEval (outerMinor, rSrc);
+ 			ghost->outer.theta = atan2(ref->FP->y, ref->FP->x);
+
+ 			// report instrumental ghost star mags
+ 			ghost->Mag = ref->Mag - MagOffset;
+ 			
+ 			// XXX this code yields a single chip: we need to provide results for any chips
+ 			// which encompass the full size of the ghost
+ 			pmChip *ghostChip = pmSourceFindChip (&ghost->chip->x, &ghost->chip->y, fpa, -ghost->srcFP->x, -ghost->srcFP->y);
+ 			ghostChip = pmSourceFindChip (&ghost->chip->x, &ghost->chip->y, fpa, ghost->FP->x, ghost->FP->y);
+
+                        //do a rudimentary check of whether the ghost is on the pixel FPA
+                        if (abs(ghost->FP->y) > 21000.)  continue;
+                        if (abs(ghost->FP->x) > 21000.)  continue;
+
+                        //save the ghost positions
+ 			if (ghostChip) {			
+ 			    psMetadata *row = psMetadataAlloc ();
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "X_STAR_FPA",     PS_DATA_F32,    "x coord on focal plane",	  ref->FP->x);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "Y_STAR_FPA",     PS_DATA_F32,    "y coord on focal plane",	  ref->FP->y);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "STAR_CHIP_NAME", PS_DATA_STRING, "star chip name",	 psMetadataLookupStr(NULL,chip->concepts,"CHIP.NAME"));
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "MAG_REF",        PS_DATA_F32,    "reference star magnitude",	  ghost->Mag);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "X_GHOST_FPA",    PS_DATA_F32,    "x coord on focal plane",	  ghost->FP->x);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "Y_GHOST_FPA",    PS_DATA_F32,    "y coord on focal plane",	  ghost->FP->y);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "X_GHOST_CHIP",   PS_DATA_F32,    "x coord on chip",	          ghost->chip->x);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "Y_GHOST_CHIP",   PS_DATA_F32,    "y coord on chip",	          ghost->chip->y);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "GHOST_CHIP_NAME",PS_DATA_STRING, "ghost chip name",	 psMetadataLookupStr(NULL,ghostChip->concepts,"CHIP.NAME"));
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "GHOST_INNER_MAJ",PS_DATA_F32,    "inner major axis (pixels)",	  ghost->inner.major);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "GHOST_INNER_MIN",PS_DATA_F32,    "inner minor axis (pixels)",	  ghost->inner.minor);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "GHOST_INNER_ANG",PS_DATA_F32,    "inner angle (degrees)",	  ghost->inner.theta/PM_RAD_DEG);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "GHOST_OUTER_MAJ",PS_DATA_F32,    "outer major axis (pixels)",	  ghost->outer.major);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "GHOST_OUTER_MIN",PS_DATA_F32,    "outer minor axis (pixels)",	  ghost->outer.minor);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "GHOST_OUTER_ANG",PS_DATA_F32,    "outer angle (degrees)",	  ghost->outer.theta/PM_RAD_DEG);
+			   
+ 			    psArrayAdd (table, 100, row);
+ 			    psFree (row);
+						
+ 			}
+ 			else {
+ 			    psMetadata *row = psMetadataAlloc ();
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "X_STAR_FPA",     PS_DATA_F32,    "x coord on focal plane",	  ref->FP->x);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "Y_STAR_FPA",     PS_DATA_F32,    "y coord on focal plane",	  ref->FP->y);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "STAR_CHIP_NAME", PS_DATA_STRING, "star chip name",	 psMetadataLookupStr(NULL,chip->concepts,"CHIP.NAME"));
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "MAG_REF",        PS_DATA_F32,    "reference star magnitude",	  ghost->Mag);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "X_GHOST_FPA",    PS_DATA_F32,    "x coord on focal plane",	  ghost->FP->x);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "Y_GHOST_FPA",    PS_DATA_F32,    "y coord on focal plane",	  ghost->FP->y);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "X_GHOST_CHIP",   PS_DATA_F32,    "x coord on chip",	          ghost->chip->x);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "Y_GHOST_CHIP",   PS_DATA_F32,    "y coord on chip",	          ghost->chip->y);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "GHOST_CHIP_NAME",PS_DATA_STRING, "ghost chip name",	 "NONE");
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "GHOST_INNER_MAJ",PS_DATA_F32,    "inner major axis (pixels)",	  ghost->inner.major);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "GHOST_INNER_MIN",PS_DATA_F32,    "inner minor axis (pixels)",	  ghost->inner.minor);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "GHOST_INNER_ANG",PS_DATA_F32,    "inner angle (degrees)",	  ghost->inner.theta/PM_RAD_DEG);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "GHOST_OUTER_MAJ",PS_DATA_F32,    "outer major axis (pixels)",	  ghost->outer.major);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "GHOST_OUTER_MIN",PS_DATA_F32,    "outer minor axis (pixels)",	  ghost->outer.minor);
+ 			    psMetadataAdd (row, PS_LIST_TAIL, "GHOST_OUTER_ANG",PS_DATA_F32,    "outer angle (degrees)",	  ghost->outer.theta/PM_RAD_DEG);
+			   
+ 			    psArrayAdd (table, 100, row);
+ 			    psFree (row);
+ 			}		  
+ 						
+ 		    }
+ 		}
+ 	    }
+ 	}
+
+    	psFree (centerX);
+    	psFree (centerY);
+    	psFree (innerMajor);
+    	psFree (innerMinor);
+    	psFree (outerMajor);
+    	psFree (outerMinor);
+    	psFree (mirrorRad);
+    	psFree (ghostModel);
+    	psFree (view);
+
+        if (table->n == 0) {
+            psFree(table);
+            return true;
+        }
+    }
+
+    if (!psFitsWriteTable(fits, NULL, table, "GHOST_POSITIONS")) {
+        psError(psErrorCodeLast(), false, "writing GHOST_POSITIONS\n");
+        psFree(table);
+        return false;
+    }
+    psFree(table);
+    return true;
+    
+escape:
+    psFree (centerX);
+    psFree (centerY);
+    psFree (innerMajor);
+    psFree (innerMinor);
+    psFree (outerMajor);
+    psFree (outerMinor);
+    psFree (mirrorRad);
+    psFree (ghostModel);
+    psFree (view);
+    return false;    
+}
+
+static void pmSourceGhostFree (pmSourceGhost *ghost) {
+
+    if (ghost == NULL) return;
+
+    psFree (ghost->srcFP);
+    psFree (ghost->FP);
+    psFree (ghost->chip);
+
+    return;
+}
+
+pmSourceGhost *pmSourceGhostAlloc (void) {
+
+    pmSourceGhost *ghost = (pmSourceGhost *) psAlloc(sizeof(pmSourceGhost));
+    psMemSetDeallocator(ghost, (psFreeFunc) pmSourceGhostFree);
+
+    ghost->srcFP = psPlaneAlloc();
+    ghost->FP    = psPlaneAlloc();
+    ghost->chip  = psPlaneAlloc();
+    
+    ghost->Mag   = 0.0;
+
+    ghost->inner.major = 0.0;
+    ghost->inner.minor = 0.0;
+    ghost->inner.theta = 0.0;
+
+    ghost->outer.major = 0.0;
+    ghost->outer.minor = 0.0;
+    ghost->outer.theta = 0.0;
+
+    return ghost;
+}
+
+
+bool pmSourceChipBounds (pmFPA *fpa) {
+
+    chipXmin = psVectorAlloc (fpa->chips->n, PS_TYPE_F32);
+    chipXmax = psVectorAlloc (fpa->chips->n, PS_TYPE_F32);
+    chipYmin = psVectorAlloc (fpa->chips->n, PS_TYPE_F32);
+    chipYmax = psVectorAlloc (fpa->chips->n, PS_TYPE_F32);
+
+    // this loop selects the matched stars for all chips
+    for (int i = 0; i < fpa->chips->n; i++) {
+
+      pmChip *chip = fpa->chips->data[i];
+      if (!chip->process || !chip->file_exists) { continue; }
+      if (!chip->fromFPA) { continue; }
+
+      // determine RA,DEC of 4 corners, use to find RA_MIN,MAX, DEC_MIN,MAX
+      psRegion *region = pmChipPixels (chip);
+      psPlane ptCH[4], ptFP;
+
+      ptCH[0].x = region->x0;
+      ptCH[0].y = region->y0;
+      ptCH[1].x = region->x1;
+      ptCH[1].y = region->y0;
+      ptCH[2].x = region->x1;
+      ptCH[2].y = region->y1;
+      ptCH[3].x = region->x0;
+      ptCH[3].y = region->y1;
+      psFree (region);
+      
+      double Xmin = +FLT_MAX;
+      double Xmax = -FLT_MAX;
+      double Ymin = +FLT_MAX;
+      double Ymax = -FLT_MAX;
+
+      for (int j = 0; j < 4; j++) {
+	psPlaneTransformApply (&ptFP, chip->toFPA, &ptCH[j]);
+	Xmin = PS_MIN (ptFP.x, Xmin);
+	Xmax = PS_MAX (ptFP.x, Xmax);
+	Ymin = PS_MIN (ptFP.y, Ymin);
+	Ymax = PS_MAX (ptFP.y, Ymax);
+      }
+
+      // fpa-range for the given chip
+      chipXmin->data.F32[i] = Xmin;
+      chipXmax->data.F32[i] = Xmax;
+      chipYmin->data.F32[i] = Ymin;
+      chipYmax->data.F32[i] = Ymax;
+    }
+
+    return true;
+}
+
+pmChip *pmSourceFindChip (double *xChip, double *yChip, pmFPA *fpa, double xFPA, double yFPA) {
+
+    *xChip = NAN;
+    *yChip = NAN;
+
+    if (!chipXmin) {
+	pmSourceChipBounds (fpa);
+    }
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+
+	if (xFPA <  chipXmin->data.F32[i]) continue;
+	if (xFPA >= chipXmax->data.F32[i]) continue;
+	if (yFPA <  chipYmin->data.F32[i]) continue;
+	if (yFPA >= chipYmax->data.F32[i]) continue;
+
+	pmChip *chip = fpa->chips->data[i];
+	psRegion *region = pmChipPixels (chip);
+
+	psPlane ptCH, ptFP;
+	ptFP.x = xFPA;
+	ptFP.y = yFPA;
+	psPlaneTransformApply (&ptCH, chip->fromFPA, &ptFP);
+
+	if (ptCH.x <  region->x0) goto next_chip;
+	if (ptCH.x >= region->x1) goto next_chip;
+	if (ptCH.y <  region->y0) goto next_chip;
+	if (ptCH.y >= region->y1) goto next_chip;
+	psFree (region);
+
+	*xChip = ptCH.x;
+	*yChip = ptCH.y;
+	return chip;
+
+    next_chip:
+	psFree (region);
+    }
+
+    return NULL;
+}
+
+// identify chips which land on this column (FP coords)
+bool pmSourceFindChipInXrange (pmFPA *fpa, int nChip, double xFPA, double yFPA) {
+
+    if (!chipXmin || !chipXmax) {
+	psAbort ("chip bounds not set");
+    }
+
+    if (xFPA <  chipXmin->data.F32[nChip]) return false;
+    if (xFPA >= chipXmax->data.F32[nChip]) return false;
+    return true;
+}
+
+// identify chips which land on this row (FP coords)
+bool pmSourceFindChipInYrange (pmFPA *fpa, int nChip, double xFPA, double yFPA) {
+
+    if (!chipYmin || !chipYmax) {
+	psAbort ("chip bounds not set");
+    }
+
+    if (yFPA <  chipYmin->data.F32[nChip]) return false;
+    if (yFPA >= chipYmax->data.F32[nChip]) return false;
+    return true;
+}
+
+// return the FPA coordinates of the Y edges of the chip
+bool pmSourceFindChipYedges (double *yFPAs, double *yFPAe, pmFPA *fpa, int nChip) {
+
+    *yFPAs = chipYmin->data.F32[nChip];
+    *yFPAe = chipYmax->data.F32[nChip];
+    return true;
+}
+
+// return the FPA coordinates of the X edges of the chip
+bool pmSourceFindChipXedges (double *yFPAs, double *yFPAe, pmFPA *fpa, int nChip) {
+
+    *yFPAs = chipXmin->data.F32[nChip];
+    *yFPAe = chipXmax->data.F32[nChip];
+    return true;
+}
+
+// convert FPA to Chip coordinates
+bool pmSourceFPAtoChip (double *xChip, double *yChip, pmFPA *fpa, int nChip, double xFPA, double yFPA) {
+
+    pmChip *chip = fpa->chips->data[nChip];
+
+    psPlane ptCH, ptFP;
+    ptFP.x = xFPA;
+    ptFP.y = yFPA;
+    psPlaneTransformApply (&ptCH, chip->fromFPA, &ptFP);
+
+    *xChip = ptCH.x;
+    *yChip = ptCH.y;
+    return true;
+}
+
+bool pmSourceExtractFreeChipBounds () {
+  
+  psFree (chipXmin);
+  psFree (chipXmax);
+  psFree (chipYmin);
+  psFree (chipYmax);
+  return true;
+}
+
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_Glints.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_Glints.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_Glints.c	(revision 42651)
@@ -0,0 +1,306 @@
+/** @file  pmSourceIO_Glints.c
+ *
+ *  @author TdB, IfA
+ *
+ *  @version $Revision: 1.0 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 20020-03-18  $
+ *
+ *  Copyright 2020 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 "pmErrorCodes.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+
+#include "pmSourceIO.h"
+
+#include "pmAstrometryObjects.h"
+#include "pmAstrometryWCS.h"
+
+# include "pmSourceInternal.h"
+
+# define ESCAPE(MSG) { \
+  psLogMsg ("psastro", PS_LOG_INFO, MSG); \
+  return false; }
+
+bool pmSourceIO_WriteGlints (psFits *fits, pmFPA *fpa, pmConfig *config) {
+
+    bool status;
+    float zeropt, exptime;
+
+    psLogMsg ("psastro", PS_LOG_INFO, "writing glint positions");
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PM_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+        return false;
+    }
+
+    bool REFSTAR_MASK_GLINTS = psMetadataLookupBool (&status, recipe, "REFSTAR_MASK_GLINTS");
+    if (!REFSTAR_MASK_GLINTS) return true;
+
+    // select relevant keywords
+    double GLINT_MAX_MAG = psMetadataLookupF32 (&status, recipe, "GLINT_MAX_MAG");
+    double GLINT_LENGTH_MAG_SLOPE = psMetadataLookupF32 (&status, recipe, "GLINT_LENGTH_MAG_SLOPE");
+    double GLINT_LENGTH_MAG_ZERO = psMetadataLookupF32 (&status, recipe, "GLINT_LENGTH_MAG_ZERO");
+    double glintWidth = psMetadataLookupF32 (&status, recipe, "GLINT_WIDTH");
+
+    //we will use one of the new keywords to differentiate between an old and new style glint treatment
+    float glintCheck = 0;
+    double GLINT_LENGTH_POS_SLOPE = psMetadataLookupF32 (&status, recipe, "GLINT_LENGTH_POS_SLOPE");
+    if (!status) { glintCheck = 1; }
+    double GLINT_LENGTH_POS_REF = psMetadataLookupF32 (&status, recipe, "GLINT_LENGTH_POS_REF");
+    double GLINT_LENGTH_POS_CUT = psMetadataLookupF32 (&status, recipe, "GLINT_LENGTH_POS_CUT");
+    double GLINT_ANGLE_POS_SLOPE = psMetadataLookupF32 (&status, recipe, "GLINT_ANGLE_POS_SLOPE");
+    double GLINT_ANGLE_POS_REF = psMetadataLookupF32 (&status, recipe, "GLINT_ANGLE_POS_REF");
+
+    bool GLINT_OUTPUT = psMetadataLookupBool(&status, recipe, "PSASTRO.SAVE.GLINTS");
+    if (!GLINT_OUTPUT) return true;
+
+    // select the set of glint regions (GLINT.REGION is a MULTI of METADATA items)
+    psMetadataItem *glintRegions = psMetadataLookup (recipe, "GLINT.REGION");
+    if (!status) {
+        psWarning ("glint masking requested but glint regions are missing (GLINT.REGION)\n");
+        return true;
+    }
+    if (glintRegions->type != PS_DATA_METADATA_MULTI) {
+        psWarning ("GLINT.REGION is not a MULTI\n");
+        return true;
+    }
+
+    // select the input astrometry data (also carries the glintStars)
+    pmFPAfile *astrom = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!astrom) {
+        psError(PM_ERR_CONFIG, true, "Can't find input data");
+        return false;
+    }
+
+    // recipe values are given in instrumental magnitudes
+    // use the zero point and exposure time to convert to apparent mags: M_ap = M_inst + C_0 + 2.5*log(exptime)
+    pmFPA *fpa_ast = astrom->fpa;
+    if (!pmSourceZeroPointFromRecipeGlint (&zeropt, &exptime, NULL,&GLINT_MAX_MAG, fpa_ast, recipe)) {
+        psLogMsg ("psastro", PS_LOG_INFO, "failed to load zeropt data from recipe");
+        return false;
+    }
+    float MagOffset = zeropt + 2.5*log10(exptime);
+    GLINT_MAX_MAG += MagOffset;
+    GLINT_LENGTH_MAG_ZERO += MagOffset;
+
+    // select the raw objects for this readout (loaded in psastroExtract.c)
+    psArray *glintStars = psMetadataLookupPtr (&status, fpa_ast->analysis, "PSASTRO.GLINT.STARS");
+    if (glintStars == NULL) { 
+        psLogMsg ("psastro", PS_LOG_INFO, "no glint stars found");
+        return false;
+    }
+
+    psArray *table = psMetadataLookupPtr (&status, fpa->analysis, "GLINT_POSITIONS");
+    psMemIncrRefCounter (table);
+    if (!table) {
+        table = psArrayAllocEmpty (0x1000);
+
+        for (int i = 0; i < glintStars->n; i++) {
+	    pmAstromObj *star = glintStars->data[i];
+	    if (star->Mag > GLINT_MAX_MAG) continue; // XXX should not be needed...
+
+	    // project glint star to the focal-plane
+	    psProject (star->TP, star->sky, fpa_ast->toSky);
+	    psPlaneTransformApply (star->FP, fpa_ast->fromTPA, star->TP);
+
+	    // find the GLINT.REGION this star lands in (if any)
+	    psListIterator *glintIter = psListIteratorAlloc(glintRegions->data.list, PS_LIST_HEAD, false);
+	    psMetadataItem *glintItem = NULL;
+	    while ((glintItem = psListGetAndIncrement (glintIter))) {
+	        if (glintItem->type != PS_DATA_METADATA) {
+		    psWarning ("GLINT.REGION entry is not a metadata folder");
+	    	    continue;
+	        }
+	    
+	        char *glintRegionString = psMetadataLookupStr (&status, glintItem->data.md, "REGION");
+	        if (!glintRegionString) {
+		    // psWarning ("GLINT.REGION entry is missing REGION entry");
+		    continue;
+	        }
+	        psRegion glintRegion = psRegionFromString (glintRegionString);
+
+	        // select stars that land in this region
+	        if (star->FP->x < glintRegion.x0) continue;
+	        if (star->FP->x > glintRegion.x1) continue;
+	        if (star->FP->y < glintRegion.y0) continue;
+	        if (star->FP->y > glintRegion.y1) continue;
+
+	        char *glintType = psMetadataLookupStr (&status, glintItem->data.md, "GLINT.TYPE");
+	        if (!status) {
+		    psWarning ("GLINT.REGION entry is missing TYPE entry");
+		    continue;
+	        }
+
+	        double glintLength = GLINT_LENGTH_MAG_SLOPE*(GLINT_LENGTH_MAG_ZERO - star->Mag);
+                double glintAngle = 0.;
+
+                //Besides brightness, the length of the glints also depends on the position of the star compared to the focal plane. But, seemingly only for stars closer than about 30k pixels
+                if(glintCheck) {
+	            if ((!strcasecmp(glintType, "TOP") || !strcasecmp(glintType, "BOTTOM")) && abs(star->FP->y) < GLINT_LENGTH_POS_CUT ){ 
+                      glintLength /= GLINT_LENGTH_POS_SLOPE*(GLINT_LENGTH_POS_REF - abs(star->FP->y));
+                    }
+	            if ((!strcasecmp(glintType, "LEFT") || !strcasecmp(glintType, "RIGHT")) && abs(star->FP->x) < GLINT_LENGTH_POS_CUT ) {
+                      glintLength /= GLINT_LENGTH_POS_SLOPE*(GLINT_LENGTH_POS_REF - abs(star->FP->x));
+                    }
+                    //also compute the angle of the glint, which depends on position parallel to the FPX
+	            if (!strcasecmp(glintType, "TOP") || !strcasecmp(glintType, "BOTTOM") ){ 
+	              glintAngle = PM_RAD_DEG * (GLINT_ANGLE_POS_SLOPE*((GLINT_ANGLE_POS_REF - star->FP->x)/1000.));
+                    }
+	            if (!strcasecmp(glintType, "LEFT") || !strcasecmp(glintType, "RIGHT") ) {
+ 	              glintAngle = PM_RAD_DEG * (GLINT_ANGLE_POS_SLOPE*((GLINT_ANGLE_POS_REF - star->FP->y)/1000.));
+                    }
+                }
+
+                //do a rudimentary check of whether the glint enters the pixel FPA, and exclude very short ones
+                if (!strcasecmp(glintType, "TOP")    && ((star->FP->y - glintLength) > 19500.))  continue;
+                if (!strcasecmp(glintType, "BOTTOM") && ((star->FP->y + glintLength) < -19500.))  continue;
+                if (!strcasecmp(glintType, "LEFT")   && ((star->FP->x + glintLength) < -19500.))  continue;
+                if (!strcasecmp(glintType, "RIGHT")  && ((star->FP->x - glintLength) > 19500.))  continue;
+            
+                //Set a glint type enum as well
+                double glintTypeEnum = 0;
+                if (!strcasecmp(glintType, "TOP"))  glintTypeEnum = 0;
+                if (!strcasecmp(glintType, "BOTTOM"))  glintTypeEnum = 1;
+                if (!strcasecmp(glintType, "LEFT"))  glintTypeEnum = 2;
+                if (!strcasecmp(glintType, "RIGHT"))  glintTypeEnum = 3;
+
+                //save the glint positions
+                psMetadata *row = psMetadataAlloc ();
+                psMetadataAdd (row, PS_LIST_TAIL, "X_FPA_START", PS_DATA_F32, "x coord on focal plane",       star->FP->x);
+                psMetadataAdd (row, PS_LIST_TAIL, "Y_FPA_START", PS_DATA_F32, "y coord on focal plane",       star->FP->y);
+                psMetadataAdd (row, PS_LIST_TAIL, "MAG_REF",     PS_DATA_F32, "reference star magnitude",     star->Mag);
+                psMetadataAdd (row, PS_LIST_TAIL, "GLINT_LENGTH",PS_DATA_F32, "glint length (pixels)",        glintLength);
+                psMetadataAdd (row, PS_LIST_TAIL, "GLINT_WIDTH", PS_DATA_F32, "glint width (pixels)",         glintWidth);
+                psMetadataAdd (row, PS_LIST_TAIL, "GLINT_ANGLE", PS_DATA_F32, "glint angle (degrees)",        glintAngle/PM_RAD_DEG);
+                psMetadataAdd (row, PS_LIST_TAIL, "GLINT_TYPE",  PS_DATA_STRING, "glint type",                glintType);
+                psMetadataAdd (row, PS_LIST_TAIL, "GLINT_TYPE_ENUM", PS_DATA_F32, "glint type enum",          glintTypeEnum);
+
+                psArrayAdd (table, 100, row);
+                psFree (row);
+            }
+        }
+
+        if (table->n == 0) {
+            psFree(table);
+            return true;
+        }
+    }
+
+    if (!psFitsWriteTable(fits, NULL, table, "GLINT_POSITIONS")) {
+        psError(psErrorCodeLast(), false, "writing GLINT_POSITIONS\n");
+        psFree(table);
+        return false;
+    }
+    psFree(table);
+    return true;
+}
+
+bool pmSourceZeroPointFromRecipeGlint (float *zeropt, float *exptime, float *ghostMaxMag, double *glintMaxMag, pmFPA *fpa, psMetadata *recipe) {
+
+    bool status;
+
+    // select the filter; default to fixed photcode and mag limit otherwise
+    char *filter = psMetadataLookupStr (&status, fpa->concepts, "FPA.FILTERID");
+    if (!status) ESCAPE ("missing FPA.FILTER in concepts");
+
+    *exptime = psMetadataLookupF32 (&status, fpa->concepts, "FPA.EXPOSURE");
+    if (!status) ESCAPE ("missing FPA.EXPOSURE in concepts");
+
+    // we need to select the PHOTCODE.DATA folder that matches our filter
+    psMetadataItem *item = psMetadataLookup (recipe, "PHOTCODE.DATA");
+    if (!item) ESCAPE ("PHOTCODE.DATA folders missing");
+    if (item->type != PS_DATA_METADATA_MULTI) ESCAPE ("PHOTCODE.DATA not a multi");
+
+    // PHOTCODE.DATA is a multi of metadata items
+    psListIterator *iter = psListIteratorAlloc(item->data.list, PS_LIST_HEAD, false);
+
+    psMetadataItem *refItem = NULL;
+    while ((refItem = psListGetAndIncrement (iter))) {
+        if (refItem->type != PS_DATA_METADATA) ESCAPE ("PHOTCODE.DATA entry is not a metadata folder");
+
+        char *refFilter = psMetadataLookupStr (&status, refItem->data.md, "FILTER");
+        if (!status) {
+            // psLogMsg ("psastro", PS_LOG_INFO, "a PHOTCODE.DATA recipe folder is missing FILTER");
+            continue;
+        }
+
+        // does this entry match the current filter?
+        if (strcmp (refFilter, filter)) continue;
+
+        psLogMsg ("psastro", PS_LOG_DETAIL, "PHOTCODE.DATA found for filter %s", filter);
+
+        *zeropt = psMetadataLookupF32 (&status, refItem->data.md, "ZEROPT");
+        if (!status) {
+            psLogMsg ("psastro", PS_LOG_INFO, "a PHOTCODE.DATA recipe folder is missing ZEROPT");
+            continue;
+        }
+        if (ghostMaxMag) {
+            *ghostMaxMag = psMetadataLookupF32 (&status, refItem->data.md, "GHOST_MAX_MAG");
+            if (!status) {
+                psLogMsg ("psastro", PS_LOG_INFO, "a PHOTCODE.DATA recipe folder is missing GHOST_MAX_MAG");
+                continue;
+            }
+	    //MEH null is a pain.. so only log if set
+            psLogMsg ("psastro", PS_LOG_INFO, "found GHOST_MAX_MAG %f",*ghostMaxMag);
+        }
+        if (glintMaxMag) {
+            float MaxMag = psMetadataLookupF32 (&status, refItem->data.md, "GLINT_MAX_MAG");
+            if (status) {
+                *glintMaxMag = MaxMag ;
+	        //MEH null is a pain.. so only log if set
+                psLogMsg ("psastro", PS_LOG_INFO, "found GLINT_MAX_MAG %f",*glintMaxMag);
+            }
+        }
+
+	//MEH what zpt is set to 
+        psLogMsg ("psastro", PS_LOG_INFO, "found ZEROPT  %f",*zeropt);
+        psFree (iter);
+        return true;
+    }
+    psFree (iter);
+    return false;
+}
+
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_MatchedRefs.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_MatchedRefs.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_MatchedRefs.c	(revision 42651)
@@ -0,0 +1,200 @@
+/** @file  pmSourceIO_MatchedRefs.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-18 02:44:19 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+
+#include "pmSourceIO.h"
+
+#include "pmAstrometryObjects.h"
+#include "pmAstrometryWCS.h"
+
+bool pmSourceIO_WriteMatchedRefs (psFits *fits, pmFPA *fpa, pmConfig *config) {
+
+    bool status = true;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    // first, check if there are any matches to be written
+
+    // determine the output table format
+    // XXX move this elsewhere? (psastro recipe? filerules?)
+    psMetadata *recipe = psMetadataLookupMetadata(&status, config->recipes, "PSASTRO");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "missing recipe PSASTRO in config data");
+        return false;
+    }
+
+    bool REFS_OUTPUT = psMetadataLookupBool(&status, recipe, "PSASTRO.SAVE.REFMATCH");
+    if (!REFS_OUTPUT) return true;
+
+    psArray *table = psMetadataLookupPtr (&status, fpa->analysis, "MATCHED_REFS");
+    psMemIncrRefCounter (table);
+
+    if (!table) {
+        table = psArrayAllocEmpty (0x1000);
+        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;
+
+            char *chipName = psMetadataLookupStr(NULL, chip->concepts, "CHIP.NAME");
+
+            while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+                psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+                if (!cell->process || !cell->file_exists) continue;
+
+                // process each of the readouts
+                // XXX there can only be one readout per chip, right?
+                while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                    if (! readout->data_exists) continue;
+
+                    // select the raw objects for this readout
+                    psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS.SUBSET");
+                    if (rawstars == NULL) continue;
+
+                    // select the raw objects for this readout
+                    psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS.SUBSET");
+                    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];
+                        pmAstromObj *ref = refstars->data[match->ref];
+
+                        psMetadata *row = psMetadataAlloc ();
+                        psMetadataAdd (row, PS_LIST_TAIL, "RA_REF",     PS_DATA_F64, "right ascension (deg, J2000)", PM_DEG_RAD*ref->sky->r);
+                        psMetadataAdd (row, PS_LIST_TAIL, "DEC_REF",    PS_DATA_F64, "declination (deg, J2000)",     PM_DEG_RAD*ref->sky->d);
+                        psMetadataAdd (row, PS_LIST_TAIL, "RA_RAW",     PS_DATA_F64, "right ascension (deg, J2000)", PM_DEG_RAD*raw->sky->r);
+                        psMetadataAdd (row, PS_LIST_TAIL, "DEC_RAW",    PS_DATA_F64, "declination (deg, J2000)",     PM_DEG_RAD*raw->sky->d);
+                        psMetadataAdd (row, PS_LIST_TAIL, "X_CHIP_REF", PS_DATA_F32, "x fitted coord on chip",       ref->chip->x);
+                        psMetadataAdd (row, PS_LIST_TAIL, "Y_CHIP_REF", PS_DATA_F32, "y fitted coord on chip",       ref->chip->y);
+                        psMetadataAdd (row, PS_LIST_TAIL, "X_CHIP_RAW", PS_DATA_F32, "x coord on chip",              raw->chip->x);
+                        psMetadataAdd (row, PS_LIST_TAIL, "Y_CHIP_RAW", PS_DATA_F32, "y coord on chip",              raw->chip->y);
+                        psMetadataAdd (row, PS_LIST_TAIL, "X_FPA_RAW",  PS_DATA_F32, "x coord on focal plane",       raw->FP->x);
+                        psMetadataAdd (row, PS_LIST_TAIL, "Y_FPA_RAW",  PS_DATA_F32, "y coord on focal plane",       raw->FP->y);
+                        psMetadataAdd (row, PS_LIST_TAIL, "X_TPA_RAW",  PS_DATA_F32, "x coord on focal plane",       raw->TP->x);
+                        psMetadataAdd (row, PS_LIST_TAIL, "Y_TPA_RAW",  PS_DATA_F32, "y coord on focal plane",       raw->TP->y);
+                        psMetadataAdd (row, PS_LIST_TAIL, "MAG_INST",   PS_DATA_F32, "instrumental magnitude",       raw->Mag);
+                        psMetadataAdd (row, PS_LIST_TAIL, "MAG_REF",    PS_DATA_F32, "reference star magnitude",     ref->Mag);
+                        psMetadataAdd (row, PS_LIST_TAIL, "COLOR_REF",  PS_DATA_F32, "reference star color",         ref->Color);
+                        psMetadataAdd (row, PS_LIST_TAIL, "CHIP_ID",    PS_DATA_STRING, "chip identifier",           chipName);
+                        // XXX need to add the reference color, but this needs getstar / dvo.photcodes for the reference to be refined.
+
+                        psArrayAdd (table, 100, row);
+                        psFree (row);
+                    }
+                }
+            }
+        }
+        psFree (view);
+
+        if (table->n == 0) {
+            psFree(table);
+            return true;
+        }
+    }
+
+    if (!psFitsWriteTable(fits, NULL, table, "MATCHED_REFS")) {
+        psError(psErrorCodeLast(), false, "writing MATCHED_REFS\n");
+        psFree(table);
+        return false;
+    }
+
+    psFree(table);
+    return true;
+}
+
+bool pmSourceIO_ReadMatchedRefs (psFits *fits, pmFPA *fpa, const pmConfig *config) {
+
+    bool status = true;
+
+    // check if we've already read (attempted to read) REFMATCH
+    bool readMatchedRefs = psMetadataLookupBool (&status, fpa->analysis, "READ.REFMATCH");
+    if (readMatchedRefs) return true;
+
+    // try find the MATCHED_REFS extension.  if non-existent, note that we tried, and move on.
+    // It is not an error to lack this entry -- psFitsMoveExtNameClean does not raise an error
+    if (!psFitsMoveExtNameClean (fits, "MATCHED_REFS")) {
+        psMetadataAddBool (fpa->analysis, PS_LIST_TAIL, "READ.REFMATCH", PS_META_REPLACE, "attempted to read MATCHED_REFS", true);
+        return true;
+    }
+
+    // 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 blocks the free.  In fact, it's better to read the table row by row.
+    long numRows = psFitsTableSize(fits); // Number of sources in table
+    psArray *rows = psArrayAlloc(numRows); // Array of sources, to return
+
+    // first, check if there are any matches to be written
+    for (int i = 0; i < numRows; i++) {
+        psMetadata *row = psFitsReadTableRow(fits, i); // Table row
+        if (!row) {
+            psError(psErrorCodeLast(), false, "Unable to read row %d of matched references.", i);
+            psFree(rows);
+            return false;
+        }
+        rows->data[i] = row;
+    }
+
+    psMetadataAddArray (fpa->analysis, PS_LIST_TAIL, "MATCHED_REFS", PS_META_REPLACE, "MATCHED_REFS", rows);
+    psFree (rows);
+
+    // note that we have already read this dataa
+    psMetadataAddBool (fpa->analysis, PS_LIST_TAIL, "READ.REFMATCH", PS_META_REPLACE, "attempted to read MATCHED_REFS", true);
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_OBJ.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_OBJ.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_OBJ.c	(revision 42651)
@@ -0,0 +1,121 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.19 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-12-08 02:51:14 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+
+#include "pmSourceIO.h"
+
+// dophot-style output list with fixed line width
+bool pmSourcesWriteOBJ (psArray *sources, char *filename)
+{
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(filename, false);
+
+    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, model->class->useReff);
+
+        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);
+        if (fwrite (line->line, 1, line->Nline, f) < line->Nline) {
+            psError(PS_ERR_IO, true, "Unable to write OBJ sources file (%s)", filename);
+            fclose(f);
+            psFree(line);
+            return false;
+        }
+    }
+    fclose (f);
+    psFree (line);
+    fprintf (stderr, "%f seconds for %d objects\n", psTimerMark ("string"), (int)sources->n);
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_PS1_CAL_0.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_PS1_CAL_0.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_PS1_CAL_0.c	(revision 42651)
@@ -0,0 +1,720 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-16 22:30:50 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.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_CAL_0 (psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, psMetadata *tableHeader, char *extname, psMetadata *recipe)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(extname, false);
+
+    psArray *table;
+    psMetadata *row;
+    int i;
+    psF32 *PAR, *dPAR;
+    psEllipseAxes axes;
+    psF32 xPos, yPos;
+    psF32 xErr, yErr;
+    psF32 psfMagErr, chisq;
+
+    pmChip *chip = readout->parent->parent;
+    pmFPA  *fpa  = chip->parent;
+    if (!chip->toFPA || !fpa->toTPA || !fpa->toSky) {
+	psWarning ("astrometry calibration is missing, no calibrated coords");
+    }
+
+    bool status1 = false;
+    bool status2 = false;
+    float magOffset = NAN;
+    float exptime = psMetadataLookupF32 (&status1, fpa->concepts, "FPA.EXPOSURE");
+    float zeropt  = psMetadataLookupF32 (&status2, imageHeader, "ZPT_OBS");
+    if (!status1 || !status2 || (exptime == 0.0)) {
+	psWarning ("exposure time or measured zero point not found for a readout, no calibrated mags");
+    } else {
+	magOffset = zeropt + 2.5*log10(exptime);
+    }
+
+    // let's write these out in S/N order
+    sources = psArraySort (sources, pmSourceSortByFlux);
+
+    table = psArrayAllocEmpty (sources->n);
+
+    // we write out PSF-fits for 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];
+	if (source->seq == -1) {
+	    source->seq = i;
+	}
+
+        // no difference between PSF and non-PSF model
+        pmModel *model = source->modelPSF;
+
+        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];
+	    if (isfinite(PAR[PM_PAR_SXX]) && isfinite(PAR[PM_PAR_SXX]) && isfinite(PAR[PM_PAR_SXX])) {
+		axes = pmPSF_ModelToAxes (PAR, model->class->useReff);
+	    } else {
+		axes.major = NAN;
+		axes.minor = NAN;
+		axes.theta = NAN;
+	    }
+	    chisq = model->chisq;
+
+	    // need to determine the PSF photometry error: source->psfMagErr is the error on the 'best' model mag.
+	    psfMagErr = model->dparams->data.F32[PM_PAR_I0] / model->params->data.F32[PM_PAR_I0];
+        } else {
+            xPos = source->peak->xf;
+            yPos = source->peak->yf;
+            xErr = source->peak->dx;
+            yErr = source->peak->dy;
+            axes.major = NAN;
+            axes.minor = NAN;
+            axes.theta = NAN;
+	    chisq = NAN;
+	    psfMagErr = NAN;
+        }
+
+	float calMag = source->psfMag + magOffset;
+        float peakMag = (source->peak->rawFlux > 0) ? -2.5*log10(source->peak->rawFlux) : NAN;
+        psS16 nImageOverlap = 1;
+
+	// generate RA,DEC
+	psPlane ptCH, ptFP, ptTP;
+	psSphere ptSky;
+
+	ptCH.x = xPos;
+	ptCH.y = yPos;
+	if (chip->toFPA && fpa->toTPA && fpa->toSky) {
+	    psPlaneTransformApply (&ptFP, chip->toFPA, &ptCH);
+	    psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+	    psDeproject (&ptSky, &ptTP, fpa->toSky);
+	} else {
+	    ptSky.r = NAN;
+	    ptSky.d = NAN;
+	}
+
+        row = psMetadataAlloc ();
+        psMetadataAdd (row, PS_LIST_TAIL, "IPP_IDET",         PS_DATA_U32, "IPP detection identifier index",             source->seq);
+        psMetadataAdd (row, PS_LIST_TAIL, "RA_PSF",           PS_DATA_F32, "PSF RA coordinate (degrees)",                ptSky.r*PS_DEG_RAD);
+        psMetadataAdd (row, PS_LIST_TAIL, "DEC_PSF",          PS_DATA_F32, "PSF DEC coordinate (degrees)",               ptSky.d*PS_DEG_RAD);
+	// XXX need to do the error propagation correctly..
+        // 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, "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, "CAL_PSF_MAG",      PS_DATA_F32, "Calibrated Magnitude from PSF Fit",          calMag);
+        psMetadataAdd (row, PS_LIST_TAIL, "CAL_PSF_MAG_SIG",  PS_DATA_F32, "Calibrated Magnitude Error",                 psfMagErr);
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_INST_MAG",     PS_DATA_F32, "PSF fit instrumental magnitude",             source->psfMag);
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_INST_MAG_SIG", PS_DATA_F32, "Sigma of PSF instrumental magnitude",        psfMagErr);
+        psMetadataAdd (row, PS_LIST_TAIL, "PEAK_FLUX_AS_MAG", PS_DATA_F32, "Peak flux expressed as magnitude",           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_CHISQ",        PS_DATA_F32,  "Chisq of PSF-fit",                          chisq);
+        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);
+
+	// EXT_NSIGMA will be NAN if: 1) contour ellipse is imaginary; 2) source is not
+	// subtracted
+
+	// CR_NSIGMA will be NAN if: 1) source is not subtracted; 2) source is on the image
+	// edge; 3) any pixels in the 3x3 peak region are masked; 
+
+        // 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->pixWeightNotBad);
+
+        // 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);
+
+        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_CAL_0 (psFits *fits, psMetadata *header)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(header, false);
+
+    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);
+
+    // XXX need to look up the XSRCNAME entries
+
+    // 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;
+
+        source->seq       = psMetadataLookupU32 (&status, row, "IPP_IDET");
+        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->psfMagErr    = psMetadataLookupF32 (&status, row, "PSF_INST_MAG_SIG");
+	PAR[PM_PAR_I0]    = (isfinite(source->psfMag)) ? pow(10.0, -0.4*source->psfMag) : NAN;
+	dPAR[PM_PAR_I0]   = (isfinite(source->psfMag)) ? PAR[PM_PAR_I0] * source->psfMagErr : NAN;
+
+        pmPSF_AxesToModel (PAR, axes, model->class->useReff);
+
+        float peakMag     = psMetadataLookupF32 (&status, row, "PEAK_FLUX_AS_MAG");
+        float peakFlux    = (isfinite(peakMag)) ? pow(10.0, -0.4*peakMag) : NAN;
+
+        source->peak       = pmPeakAlloc(PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], peakFlux, PM_PEAK_LONE);
+        source->peak->rawFlux = peakFlux;
+        source->peak->smoothFlux = peakFlux;
+        source->peak->dx   = dPAR[PM_PAR_XPOS];
+        source->peak->dy   = dPAR[PM_PAR_YPOS];
+        source->peak->xf   = PAR[PM_PAR_XPOS]; // more accurate position
+        source->peak->yf   = PAR[PM_PAR_YPOS]; // more accurate position
+
+        source->pixWeightNotBad = psMetadataLookupF32 (&status, row, "PSF_QF");
+
+	// note that some older versions used PSF_PROBABILITY: this was not well defined.
+        model->chisq      = psMetadataLookupF32 (&status, row, "PSF_CHISQ");
+        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;
+}
+
+# define WRITE_AP_DATA 0
+
+bool pmSourcesWrite_PS1_CAL_0_XSRC (psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname, psMetadata *recipe)
+{
+
+    bool status;
+    psArray *table;
+    psMetadata *row;
+    psF32 *PAR, *dPAR;
+    psF32 xPos, yPos;
+    psF32 xErr, yErr;
+
+    pmChip *chip = readout->parent->parent;
+    pmFPA  *fpa  = chip->parent;
+    if (!chip->toFPA || !fpa->toTPA || !fpa->toSky) {
+	psWarning ("astrometry calibration is missing, no calibrated coords");
+    }
+
+# if (WRITE_AP_DATA)
+    bool calMags = false;
+    bool status1 = false;
+    bool status2 = false;
+    float magOffset = NAN;
+    float exptime = psMetadataLookupF32 (&status1, fpa->concepts, "FPA.EXPOSURE");
+    float zeropt  = psMetadataLookupF32 (&status2, imageHeader, "ZPT_OBS");
+    if (!status1 || !status2 || (exptime == 0.0)) {
+	psWarning ("exposure time or measured zero point not found for a readout, no calibrated mags");
+    } else {
+	magOffset = zeropt + 2.5*log10(exptime);
+	calMags = true;
+    }
+# endif
+
+    // create a header to hold the output data
+    psMetadata *outhead = psMetadataAlloc ();
+
+    // write the links to the image header
+    psMetadataAddStr (outhead, PS_LIST_TAIL, "EXTNAME", PS_META_REPLACE, "xsrc table extension", extname);
+
+    // let's write these out in S/N order
+    sources = psArraySort (sources, pmSourceSortByFlux);
+
+    table = psArrayAllocEmpty (sources->n);
+
+    // which extended source analyses should we perform?
+    // 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");
+
+    psVector *radialBinsLower = psMetadataLookupPtr (&status, recipe, "RADIAL.ANNULAR.BINS.LOWER");
+    psVector *radialBinsUpper = psMetadataLookupPtr (&status, recipe, "RADIAL.ANNULAR.BINS.UPPER");
+    psAssert (radialBinsLower->n == radialBinsUpper->n, "upper and lower bins must match");
+
+    // we write out all sources, regardless of quality.  the source flags tell us the state
+    for (int i = 0; i < sources->n; i++) {
+	// skip source if it is not a ext sourc
+	// XXX we have two places that extended source parameters are measured:
+	// psphotExtendedSources, which measures the aperture-like parameters and (potentially) the psf-convolved extended source models,
+	// psphotFitEXT, which does the simple extended source model fit (not psf-convolved)
+	// should we require both?
+
+	pmSource *source = sources->data[i];
+
+	// skip sources without measurements
+	if (source->extpars == NULL) continue;
+
+	// we require a PSF model fit (ignore the real crud)
+	pmModel *model = source->modelPSF;
+	if (model == NULL) continue;
+
+	// XXX I need to split the extended models from the extended aperture measurements
+	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];
+
+	// generate RA,DEC
+	psPlane ptCH, ptFP, ptTP;
+	psSphere ptSky;
+
+	ptCH.x = xPos;
+	ptCH.y = yPos;
+	if (chip->toFPA && fpa->toTPA && fpa->toSky) {
+	    psPlaneTransformApply (&ptFP, chip->toFPA, &ptCH);
+	    psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+	    psDeproject (&ptSky, &ptTP, fpa->toSky);
+	} else {
+	    ptSky.r = NAN;
+	    ptSky.d = NAN;
+	}
+
+        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, "RA_EXT",           PS_DATA_F32, "EXT model RA (degrees)",                     ptSky.r*PS_DEG_RAD);
+        psMetadataAdd (row, PS_LIST_TAIL, "DEC_EXT",          PS_DATA_F32, "EXT model DEC (degrees)",                    ptSky.d*PS_DEG_RAD);
+
+        psMetadataAdd (row, PS_LIST_TAIL, "X_EXT",            PS_DATA_F32, "EXT model x coordinate",                     xPos);
+        psMetadataAdd (row, PS_LIST_TAIL, "Y_EXT",            PS_DATA_F32, "EXT model y coordinate",                     yPos);
+        psMetadataAdd (row, PS_LIST_TAIL, "X_EXT_SIG",        PS_DATA_F32, "Sigma in EXT x coordinate",                  xErr);
+        psMetadataAdd (row, PS_LIST_TAIL, "Y_EXT_SIG",        PS_DATA_F32, "Sigma in EXT y coordinate",                  yErr);
+
+# if (WRITE_AP_DATA)
+	// Petrosian measurements
+	// XXX insert header data: petrosian ref radius, flux ratio
+	if (doPetrosian) {
+	    pmSourcePetrosianValues *petrosian = source->extpars->petrosian;
+	    if (petrosian) {
+		if (calMags) {
+		    psMetadataAdd (row, PS_LIST_TAIL, "PETRO_MAG_CAL",    PS_DATA_F32, "Petrosian Magnitude (calibrated)", petrosian->mag + magOffset);
+		} else {
+		    psMetadataAdd (row, PS_LIST_TAIL, "PETRO_MAG_INST",   PS_DATA_F32, "Petrosian Magnitude (instrumental)", petrosian->mag);
+		}
+		psMetadataAdd (row, PS_LIST_TAIL, "PETRO_MAG_ERR",    PS_DATA_F32, "Petrosian Magnitude Error", petrosian->magErr);
+		psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS",     PS_DATA_F32, "Petrosian Radius",          petrosian->rad);
+		psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS_ERR", PS_DATA_F32, "Petrosian Radius Error",    petrosian->radErr);
+	    } else {
+		psMetadataAdd (row, PS_LIST_TAIL, "PETRO_MAG",        PS_DATA_F32, "Petrosian Magnitude",       NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "PETRO_MAG_ERR",    PS_DATA_F32, "Petrosian Magnitude Error", NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS",     PS_DATA_F32, "Petrosian Radius",          NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS_ERR", PS_DATA_F32, "Petrosian Radius Error",    NAN);
+	    }
+	} 
+
+	// Kron measurements
+	if (doKron) {
+	    pmSourceKronValues *kron = source->extpars->kron;
+	    if (kron) {
+		if (calMags) {
+		    psMetadataAdd (row, PS_LIST_TAIL, "KRON_MAG_CAL",  PS_DATA_F32, "Kron Magnitude",     kron->mag + magOffset);
+		} else {
+		    psMetadataAdd (row, PS_LIST_TAIL, "KRON_MAG_INST", PS_DATA_F32, "Kron Magnitude",     kron->mag);
+		}
+		psMetadataAdd (row, PS_LIST_TAIL, "KRON_MAG_ERR",    PS_DATA_F32, "Kron Magnitude Error", kron->magErr);
+		psMetadataAdd (row, PS_LIST_TAIL, "KRON_RADIUS",     PS_DATA_F32, "Kron Radius",          kron->rad);
+		psMetadataAdd (row, PS_LIST_TAIL, "KRON_RADIUS_ERR", PS_DATA_F32, "Kron Radius Error",    kron->radErr);
+	    } else {
+		psMetadataAdd (row, PS_LIST_TAIL, "KRON_MAG",        PS_DATA_F32, "Kron Magnitude",       NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "KRON_MAG_ERR",    PS_DATA_F32, "Kron Magnitude Error", NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "KRON_RADIUS",     PS_DATA_F32, "Kron Radius",          NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "KRON_RADIUS_ERR", PS_DATA_F32, "Kron Radius Error",    NAN);
+	    }
+	}
+
+	// Isophot measurements
+	// XXX insert header data: isophotal level
+	if (doIsophotal) {
+	    pmSourceIsophotalValues *isophot = source->extpars->isophot;
+	    if (isophot) {
+		if (calMags) {
+		    psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_MAG_CAL",    PS_DATA_F32, "Isophot Magnitude (calibrated)",   isophot->mag + magOffset);
+		} else {
+		    psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_MAG_INST",   PS_DATA_F32, "Isophot Magnitude (uncalibrated)", isophot->mag);
+		}
+		psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_MAG_ERR",    PS_DATA_F32, "Isophot Magnitude Error", isophot->magErr);
+		psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_RADIUS",     PS_DATA_F32, "Isophot Radius",          isophot->rad);
+		psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_RADIUS_ERR", PS_DATA_F32, "Isophot Radius Error",    isophot->radErr);
+	    } else {
+		psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_MAG",        PS_DATA_F32, "Isophot Magnitude",       NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_MAG_ERR",    PS_DATA_F32, "Isophot Magnitude Error", NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_RADIUS",     PS_DATA_F32, "Isophot Radius",          NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_RADIUS_ERR", PS_DATA_F32, "Isophot Radius Error",    NAN);
+	    }
+	}
+
+	// Flux Annuli
+	if (doAnnuli) {
+	    pmSourceAnnuli *annuli = source->extpars->annuli;
+	    if (annuli) {
+		psVector *fluxVal = annuli->flux;
+		psVector *fluxErr = annuli->fluxErr;
+		psVector *fluxVar = annuli->fluxVar;
+
+		for (int j = 0; j < fluxVal->n; j++) {
+		    char name[32];
+		    sprintf (name, "FLUX_VAL_R_%02d", j);
+		    psMetadataAdd (row, PS_LIST_TAIL, name, PS_DATA_F32, "flux value in annulus", fluxVal->data.F32[j]);
+		    sprintf (name, "FLUX_ERR_R_%02d", j);
+		    psMetadataAdd (row, PS_LIST_TAIL, name, PS_DATA_F32, "flux error in annulus", fluxErr->data.F32[j]);
+		    sprintf (name, "FLUX_VAR_R_%02d", j);
+		    psMetadataAdd (row, PS_LIST_TAIL, name, PS_DATA_F32, "flux stdev in annulus", fluxVar->data.F32[j]);
+		} 
+	    } else {
+		for (int j = 0; j < radialBinsLower->n; j++) {
+		    char name[32];
+		    sprintf (name, "FLUX_VAL_R_%02d", j);
+		    psMetadataAdd (row, PS_LIST_TAIL, name, PS_DATA_F32, "flux value in annulus", NAN);
+		    sprintf (name, "FLUX_ERR_R_%02d", j);
+		    psMetadataAdd (row, PS_LIST_TAIL, name, PS_DATA_F32, "flux error in annulus", NAN);
+		    sprintf (name, "FLUX_VAR_R_%02d", j);
+		    psMetadataAdd (row, PS_LIST_TAIL, name, PS_DATA_F32, "flux stdev in annulus", NAN);
+		} 
+	    }
+	}
+# endif
+
+	psArrayAdd (table, 100, row);
+	psFree (row);
+    }
+
+    if (table->n == 0) {
+	psFitsWriteBlank (fits, outhead, extname);
+	psFree (outhead);
+	psFree (table);
+	return true;
+    }
+
+    psTrace ("pmFPAfile", 5, "writing ext data %s\n", extname);
+    if (!psFitsWriteTable (fits, outhead, table, extname)) {
+	psError(PS_ERR_IO, false, "writing ext data %s\n", extname);
+	psFree (outhead);
+	psFree(table);
+	return false;
+    }
+    psFree (outhead);
+    psFree (table);
+
+    return true;
+}
+
+bool pmSourcesWrite_PS1_CAL_0_XFIT (psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname)
+{
+
+    psArray *table;
+    psMetadata *row;
+    psF32 *PAR, *dPAR;
+    psEllipseAxes axes;
+    psF32 xPos, yPos;
+    psF32 xErr, yErr;
+    char name[64];
+
+    pmChip *chip = readout->parent->parent;
+    pmFPA  *fpa  = chip->parent;
+    if (!chip->toFPA || !fpa->toTPA || !fpa->toSky) {
+	psWarning ("astrometry calibration is missing, no calibrated coords");
+    }
+
+    bool status1 = false;
+    bool status2 = false;
+    float magOffset = NAN;
+    float exptime = psMetadataLookupF32 (&status1, fpa->concepts, "FPA.EXPOSURE");
+    float zeropt  = psMetadataLookupF32 (&status2, imageHeader, "ZPT_OBS");
+    if (!status1 || !status2 || (exptime == 0.0)) {
+	psWarning ("exposure time or measured zero point not found for a readout, no calibrated mags");
+    } else {
+	magOffset = zeropt + 2.5*log10(exptime);
+    }
+
+    // create a header to hold the output data
+    psMetadata *outhead = psMetadataAlloc ();
+
+    // write the links to the image header
+    psMetadataAddStr (outhead, PS_LIST_TAIL, "EXTNAME", PS_META_REPLACE, "xsrc table extension", extname);
+
+    // let's write these out in S/N order
+    sources = psArraySort (sources, pmSourceSortByFlux);
+
+    // we are writing one row per model; we need to write out same number of columns for each row: find the max Nparams
+    int nParamMax = 0;
+    for (int i = 0; i < sources->n; i++) {
+	pmSource *source = sources->data[i];
+	if (source->modelFits == NULL) continue;
+	for (int j = 0; j < source->modelFits->n; j++) {
+	    pmModel *model = source->modelFits->data[j];
+	    assert (model);
+	    nParamMax = PS_MAX (nParamMax, model->params->n);
+	}
+    }
+
+    table = psArrayAllocEmpty (sources->n);
+
+    // we write out all sources, regardless of quality.  the source flags tell us the state
+    for (int i = 0; i < sources->n; i++) {
+
+	pmSource *source = sources->data[i];
+
+	// XXX if no model fits are saved, write out modelEXT?
+	if (source->modelFits == NULL) continue;
+
+	// We have multiple sources : need to flag the one used to subtract the light (the 'best' model)
+	for (int j = 0; j < source->modelFits->n; j++) {
+
+	    // choose the convolved EXT model, if available, otherwise the simple one
+	    pmModel *model = source->modelFits->data[j];
+	    assert (model);
+
+	    // skip models which were not actually fitted
+	    if (model->flags & PM_MODEL_STATUS_BADARGS) continue;
+
+	    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, model->class->useReff);
+
+	    // generate RA,DEC
+	    psPlane ptCH, ptFP, ptTP;
+	    psSphere ptSky;
+
+	    ptCH.x = xPos;
+	    ptCH.y = yPos;
+	    if (chip->toFPA && fpa->toTPA && fpa->toSky) {
+		psPlaneTransformApply (&ptFP, chip->toFPA, &ptCH);
+		psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+		psDeproject (&ptSky, &ptTP, fpa->toSky);
+	    } else {
+		ptSky.r = NAN;
+		ptSky.d = NAN;
+	    }
+
+	    row = psMetadataAlloc ();
+
+	    // XXX we are not writing out the mode (flags) or the type (psf, ext, etc)
+	    psMetadataAddU32 (row, PS_LIST_TAIL, "IPP_IDET",         0, "IPP detection identifier index",             source->seq);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "RA_EXT",           0, "EXT model RA (degrees)",                     ptSky.r);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "DEC_EXT",          0, "EXT model DEC (degrees)",                    ptSky.d);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "X_EXT",            0, "EXT model x coordinate",                     xPos);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "Y_EXT",            0, "EXT model y coordinate",                     yPos);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "X_EXT_SIG",        0, "Sigma in EXT x coordinate",                  xErr);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "Y_EXT_SIG",        0, "Sigma in EXT y coordinate",                  yErr);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_CAL_MAG",      0, "EXT fit calibrated magnitude",               model->mag + magOffset);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_INST_MAG",     0, "EXT fit instrumental magnitude",             model->mag);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_INST_MAG_SIG", 0, "Sigma of PSF instrumental magnitude",        model->magErr);
+
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "NPARAMS",          0, "number of model parameters",                 model->params->n);
+	    psMetadataAddStr (row, PS_LIST_TAIL, "MODEL_TYPE",       0, "name of model",                              pmModelClassGetName (model->type));
+
+	    // XXX these should be major and minor, not 'x' and 'y'
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MAJ",    0, "EXT width in x coordinate",                  axes.major);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MIN",    0, "EXT width in y coordinate",                  axes.minor);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_THETA",        0, "EXT orientation angle",                      axes.theta);
+
+	    // write out the other generic parameters
+	    for (int k = 0; k < nParamMax; k++) {
+		if (k == PM_PAR_I0) continue;
+		if (k == PM_PAR_SKY) continue;
+		if (k == PM_PAR_XPOS) continue;
+		if (k == PM_PAR_YPOS) continue;
+		if (k == PM_PAR_SXX) continue;
+		if (k == PM_PAR_SXY) continue;
+		if (k == PM_PAR_SYY) continue;
+
+		snprintf (name, 64, "EXT_PAR_%02d", k);
+
+		if (k < model->params->n) {
+		    psMetadataAdd (row, PS_LIST_TAIL, name, PS_DATA_F32, "", model->params->data.F32[k]);
+		} else {
+		    psMetadataAddF32 (row, PS_LIST_TAIL, name, PS_DATA_F32, "", NAN);
+		}
+	    }
+
+	    // XXX other parameters which may be set.
+	    // XXX flag / value to define the model
+	    // XXX write out the model type, fit status flags
+
+	    psArrayAdd (table, 100, row);
+	    psFree (row);
+	}
+    }
+
+    if (table->n == 0) {
+	psFitsWriteBlank (fits, outhead, extname);
+	psFree (outhead);
+	psFree (table);
+	return true;
+    }
+
+    psTrace ("pmFPAfile", 5, "writing ext data %s\n", extname);
+    if (!psFitsWriteTable (fits, outhead, table, extname)) {
+	psError(PS_ERR_IO, false, "writing ext data %s\n", extname);
+	psFree (outhead);
+	psFree(table);
+	return false;
+    }
+    psFree (outhead);
+    psFree (table);
+    return true;
+}
+
+bool pmSourcesWrite_PS1_CAL_0_XRAD (psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname, psMetadata *recipe)
+{
+    return true;
+}
+
+bool pmSourcesWrite_PS1_CAL_0_XGAL (psFits *fits, pmReadout *readout, psArray *sources, char *extname, psMetadata *recipe)
+{
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_PS1_DEV_0.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_PS1_DEV_0.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_PS1_DEV_0.c	(revision 42651)
@@ -0,0 +1,262 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.16 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-12-08 02:51:14 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.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?
+// XXX: input parameter imageHeader is never used.
+bool pmSourcesWrite_PS1_DEV_0 (psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, psMetadata *tableHeader, char *extname, psMetadata *recipe)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(extname, false);
+
+    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, model->class->useReff);
+        } else {
+            // XXX: This code seg faults if source->peak is NULL.
+            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->rawFlux > 0) ? -2.5*log10(source->peak->rawFlux) : 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->psfMagErr));
+        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->pixWeightNotBad);
+        // 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)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(header, false);
+
+    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)
+    // XXX: The following seg-faults if table returns NULL, so I added the ASSERT
+    PS_ASSERT_PTR_NON_NULL(table, NULL);
+    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->psfMagErr    = psMetadataLookupF32 (&status, row, "PSF_INST_MAG_SIG");
+
+        pmPSF_AxesToModel (PAR, axes, model->class->useReff);
+
+        float peakMag = psMetadataLookupF32 (&status, row, "PEAK_FLUX_AS_MAG");
+        float peakFlux = (isfinite(peakMag)) ? pow(10.0, -0.4*peakMag) : NAN;
+
+        source->peak = pmPeakAlloc(PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], peakFlux, PM_PEAK_LONE);
+        source->peak->rawFlux = peakFlux;
+        source->peak->smoothFlux = peakFlux;
+        source->peak->dx   = dPAR[PM_PAR_XPOS];
+        source->peak->dy   = dPAR[PM_PAR_YPOS];
+        source->peak->xf   = PAR[PM_PAR_XPOS]; // more accurate position
+        source->peak->yf   = PAR[PM_PAR_YPOS]; // more accurate position
+
+        source->pixWeightNotBad = 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);
+}
+
+bool pmSourcesWrite_PS1_DEV_0_XSRC (psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname, psMetadata *recipe)
+{
+    return true;
+}
+
+bool pmSourcesWrite_PS1_DEV_0_XFIT(psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname)
+{
+    return true;
+}
+
+bool pmSourcesWrite_PS1_DEV_0_XRAD(psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname, psMetadata *recipe)
+{
+    return true;
+}
+
+bool pmSourcesWrite_PS1_DEV_0_XGAL(psFits *fits, pmReadout *readout, psArray *sources, char *extname, psMetadata *recipe)
+{
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_PS1_DEV_1.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_PS1_DEV_1.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_PS1_DEV_1.c	(revision 42651)
@@ -0,0 +1,602 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.15 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-16 22:30:50 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.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.
+
+bool pmSourcesWrite_PS1_DEV_1(psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, psMetadata *tableHeader, char *extname, psMetadata *recipe)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(extname, false);
+
+    psArray *table;
+    psMetadata *row;
+    int i;
+    psF32 *PAR, *dPAR;
+    psEllipseAxes axes;
+    psF32 xPos, yPos;
+    psF32 xErr, yErr;
+    psF32 psfMagErr, chisq;
+
+    // let's write these out in S/N order
+    sources = psArraySort (sources, pmSourceSortByFlux);
+
+    table = psArrayAllocEmpty (sources->n);
+
+    // we write out PSF-fits for 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];
+	if (source->seq == -1) {
+	    source->seq = i;
+	}
+
+        // no difference between PSF and non-PSF model
+        pmModel *model = source->modelPSF;
+
+        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];
+	    if (isfinite(PAR[PM_PAR_SXX]) && isfinite(PAR[PM_PAR_SXX]) && isfinite(PAR[PM_PAR_SXX])) {
+		axes = pmPSF_ModelToAxes (PAR, model->class->useReff);
+	    } else {
+		axes.major = NAN;
+		axes.minor = NAN;
+		axes.theta = NAN;
+	    }
+	    chisq = model->chisq;
+
+	    // need to determine the PSF photometry error: source->psfMagErr is the error on the 'best' model mag.
+	    psfMagErr = model->dparams->data.F32[PM_PAR_I0] / model->params->data.F32[PM_PAR_I0];
+        } else {
+            xPos = source->peak->xf;
+            yPos = source->peak->yf;
+            xErr = source->peak->dx;
+            yErr = source->peak->dy;
+            axes.major = NAN;
+            axes.minor = NAN;
+            axes.theta = NAN;
+	    chisq = NAN;
+	    psfMagErr = NAN;
+        }
+
+        float peakMag = (source->peak->rawFlux > 0) ? -2.5*log10(source->peak->rawFlux) : 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",             source->psfMag);
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_INST_MAG_SIG", PS_DATA_F32, "Sigma of PSF instrumental magnitude",        psfMagErr);
+        psMetadataAdd (row, PS_LIST_TAIL, "PEAK_FLUX_AS_MAG", PS_DATA_F32, "Peak flux expressed as magnitude",           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_CHISQ",        PS_DATA_F32,  "Chisq of PSF-fit",                          chisq);
+        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);
+
+	// EXT_NSIGMA will be NAN if: 1) contour ellipse is imaginary; 2) source is not
+	// subtracted
+
+	// CR_NSIGMA will be NAN if: 1) source is not subtracted; 2) source is on the image
+	// edge; 3) any pixels in the 3x3 peak region are masked; 
+
+	// CR_NSIGMA and 
+
+        // 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->pixWeightNotBad);
+
+        // 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)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(header, false);
+
+    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);
+
+    // XXX need to look up the XSRCNAME entries
+
+    // 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;
+
+        source->seq       = psMetadataLookupU32 (&status, row, "IPP_IDET");
+        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->psfMagErr    = psMetadataLookupF32 (&status, row, "PSF_INST_MAG_SIG");
+	PAR[PM_PAR_I0]    = (isfinite(source->psfMag)) ? pow(10.0, -0.4*source->psfMag) : NAN;
+	dPAR[PM_PAR_I0]   = (isfinite(source->psfMag)) ? PAR[PM_PAR_I0] * source->psfMagErr : NAN;
+
+        pmPSF_AxesToModel (PAR, axes, model->class->useReff);
+
+        float peakMag     = psMetadataLookupF32 (&status, row, "PEAK_FLUX_AS_MAG");
+        float peakFlux    = (isfinite(peakMag)) ? pow(10.0, -0.4*peakMag) : NAN;
+
+        source->peak = pmPeakAlloc(PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], peakFlux, PM_PEAK_LONE);
+        source->peak->rawFlux = peakFlux;
+        source->peak->smoothFlux = peakFlux;
+        source->peak->dx   = dPAR[PM_PAR_XPOS];
+        source->peak->dy   = dPAR[PM_PAR_YPOS];
+        source->peak->xf   = PAR[PM_PAR_XPOS]; // more accurate position
+        source->peak->yf   = PAR[PM_PAR_YPOS]; // more accurate position
+
+        source->pixWeightNotBad = psMetadataLookupF32 (&status, row, "PSF_QF");
+
+	// note that some older versions used PSF_PROBABILITY: this was not well defined.
+        model->chisq      = psMetadataLookupF32 (&status, row, "PSF_CHISQ");
+        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;
+}
+
+bool pmSourcesWrite_PS1_DEV_1_XSRC (psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname, psMetadata *recipe)
+{
+
+    bool status;
+    psArray *table;
+    psMetadata *row;
+    psF32 *PAR, *dPAR;
+    psF32 xPos, yPos;
+    psF32 xErr, yErr;
+
+    // create a header to hold the output data
+    psMetadata *outhead = psMetadataAlloc ();
+
+    // write the links to the image header
+    psMetadataAddStr (outhead, PS_LIST_TAIL, "EXTNAME", PS_META_REPLACE, "xsrc table extension", extname);
+
+    // let's write these out in S/N order
+    sources = psArraySort (sources, pmSourceSortByFlux);
+
+    table = psArrayAllocEmpty (sources->n);
+
+    // which extended source analyses should we perform?
+    // 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");
+
+    psVector *radialBinsLower = psMetadataLookupPtr (&status, recipe, "RADIAL.ANNULAR.BINS.LOWER");
+    psVector *radialBinsUpper = psMetadataLookupPtr (&status, recipe, "RADIAL.ANNULAR.BINS.UPPER");
+    psAssert (radialBinsLower->n == radialBinsUpper->n, "upper and lower bins must match");
+
+    // we write out all sources, regardless of quality.  the source flags tell us the state
+    for (int i = 0; i < sources->n; i++) {
+	// skip source if it is not a ext sourc
+	// XXX we have two places that extended source parameters are measured:
+	// psphotExtendedSources, which measures the aperture-like parameters and (potentially) the psf-convolved extended source models,
+	// psphotFitEXT, which does the simple extended source model fit (not psf-convolved)
+	// should we require both?
+
+	pmSource *source = sources->data[i];
+
+	// skip sources without measurements
+	if (source->extpars == NULL) continue;
+
+	// we require a PSF model fit (ignore the real crud)
+	pmModel *model = source->modelPSF;
+	if (model == NULL) continue;
+
+	// XXX I need to split the extended models from the extended aperture measurements
+	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];
+
+        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_EXT",            PS_DATA_F32, "EXT model x coordinate",                     xPos);
+        psMetadataAdd (row, PS_LIST_TAIL, "Y_EXT",            PS_DATA_F32, "EXT model y coordinate",                     yPos);
+        psMetadataAdd (row, PS_LIST_TAIL, "X_EXT_SIG",        PS_DATA_F32, "Sigma in EXT x coordinate",                  xErr);
+        psMetadataAdd (row, PS_LIST_TAIL, "Y_EXT_SIG",        PS_DATA_F32, "Sigma in EXT y coordinate",                  yErr);
+
+// XXX disable these outputs until we clean up the names
+# if (0)
+	// Petrosian measurements
+	// XXX insert header data: petrosian ref radius, flux ratio
+	if (doPetrosian) {
+	    pmSourcePetrosianValues *petrosian = source->extpars->petrosian;
+	    if (petrosian) {
+		psMetadataAdd (row, PS_LIST_TAIL, "PETRO_MAG",        PS_DATA_F32, "Petrosian Magnitude",       petrosian->mag);
+		psMetadataAdd (row, PS_LIST_TAIL, "PETRO_MAG_ERR",    PS_DATA_F32, "Petrosian Magnitude Error", petrosian->magErr);
+		psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS",     PS_DATA_F32, "Petrosian Radius",          petrosian->rad);
+		psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS_ERR", PS_DATA_F32, "Petrosian Radius Error",    petrosian->radErr);
+	    } else {
+		psMetadataAdd (row, PS_LIST_TAIL, "PETRO_MAG",        PS_DATA_F32, "Petrosian Magnitude",       NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "PETRO_MAG_ERR",    PS_DATA_F32, "Petrosian Magnitude Error", NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS",     PS_DATA_F32, "Petrosian Radius",          NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "PETRO_RADIUS_ERR", PS_DATA_F32, "Petrosian Radius Error",    NAN);
+	    }
+	} 
+
+	// Kron measurements
+	if (doKron) {
+	    pmSourceKronValues *kron = source->extpars->kron;
+	    if (kron) {
+		psMetadataAdd (row, PS_LIST_TAIL, "KRON_MAG",        PS_DATA_F32, "Kron Magnitude",       kron->mag);
+		psMetadataAdd (row, PS_LIST_TAIL, "KRON_MAG_ERR",    PS_DATA_F32, "Kron Magnitude Error", kron->magErr);
+		psMetadataAdd (row, PS_LIST_TAIL, "KRON_RADIUS",     PS_DATA_F32, "Kron Radius",          kron->rad);
+		psMetadataAdd (row, PS_LIST_TAIL, "KRON_RADIUS_ERR", PS_DATA_F32, "Kron Radius Error",    kron->radErr);
+	    } else {
+		psMetadataAdd (row, PS_LIST_TAIL, "KRON_MAG",        PS_DATA_F32, "Kron Magnitude",       NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "KRON_MAG_ERR",    PS_DATA_F32, "Kron Magnitude Error", NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "KRON_RADIUS",     PS_DATA_F32, "Kron Radius",          NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "KRON_RADIUS_ERR", PS_DATA_F32, "Kron Radius Error",    NAN);
+	    }
+	}
+
+	// Isophot measurements
+	// XXX insert header data: isophotal level
+	if (doIsophotal) {
+	    pmSourceIsophotalValues *isophot = source->extpars->isophot;
+	    if (isophot) {
+		psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_MAG",        PS_DATA_F32, "Isophot Magnitude",       isophot->mag);
+		psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_MAG_ERR",    PS_DATA_F32, "Isophot Magnitude Error", isophot->magErr);
+		psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_RADIUS",     PS_DATA_F32, "Isophot Radius",          isophot->rad);
+		psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_RADIUS_ERR", PS_DATA_F32, "Isophot Radius Error",    isophot->radErr);
+	    } else {
+		psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_MAG",        PS_DATA_F32, "Isophot Magnitude",       NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_MAG_ERR",    PS_DATA_F32, "Isophot Magnitude Error", NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_RADIUS",     PS_DATA_F32, "Isophot Radius",          NAN);
+		psMetadataAdd (row, PS_LIST_TAIL, "ISOPHOT_RADIUS_ERR", PS_DATA_F32, "Isophot Radius Error",    NAN);
+	    }
+	}
+
+	// Flux Annuli
+	if (doAnnuli) {
+	    pmSourceAnnuli *annuli = source->extpars->annuli;
+	    if (annuli) {
+		psVector *fluxVal = annuli->flux;
+		psVector *fluxErr = annuli->fluxErr;
+		psVector *fluxVar = annuli->fluxVar;
+
+		for (int j = 0; j < fluxVal->n; j++) {
+		    char name[32];
+		    sprintf (name, "FLUX_VAL_R_%02d", j);
+		    psMetadataAdd (row, PS_LIST_TAIL, name, PS_DATA_F32, "flux value in annulus", fluxVal->data.F32[j]);
+		    sprintf (name, "FLUX_ERR_R_%02d", j);
+		    psMetadataAdd (row, PS_LIST_TAIL, name, PS_DATA_F32, "flux error in annulus", fluxErr->data.F32[j]);
+		    sprintf (name, "FLUX_VAR_R_%02d", j);
+		    psMetadataAdd (row, PS_LIST_TAIL, name, PS_DATA_F32, "flux stdev in annulus", fluxVar->data.F32[j]);
+		} 
+	    } else {
+		for (int j = 0; j < radialBinsLower->n; j++) {
+		    char name[32];
+		    sprintf (name, "FLUX_VAL_R_%02d", j);
+		    psMetadataAdd (row, PS_LIST_TAIL, name, PS_DATA_F32, "flux value in annulus", NAN);
+		    sprintf (name, "FLUX_ERR_R_%02d", j);
+		    psMetadataAdd (row, PS_LIST_TAIL, name, PS_DATA_F32, "flux error in annulus", NAN);
+		    sprintf (name, "FLUX_VAR_R_%02d", j);
+		    psMetadataAdd (row, PS_LIST_TAIL, name, PS_DATA_F32, "flux stdev in annulus", NAN);
+		} 
+	    }
+	}
+# endif
+
+	psArrayAdd (table, 100, row);
+	psFree (row);
+    }
+
+    if (table->n == 0) {
+	psFitsWriteBlank (fits, outhead, extname);
+	psFree (outhead);
+	psFree (table);
+	return true;
+    }
+
+    psTrace ("pmFPAfile", 5, "writing ext data %s\n", extname);
+    if (!psFitsWriteTable (fits, outhead, table, extname)) {
+	psError(PS_ERR_IO, false, "writing ext data %s\n", extname);
+	psFree (outhead);
+	psFree(table);
+	return false;
+    }
+    psFree (outhead);
+    psFree (table);
+
+    return true;
+}
+
+bool pmSourcesWrite_PS1_DEV_1_XFIT(psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname)
+{
+
+    psArray *table;
+    psMetadata *row;
+    psF32 *PAR, *dPAR;
+    psEllipseAxes axes;
+    psF32 xPos, yPos;
+    psF32 xErr, yErr;
+    char name[64];
+
+    // create a header to hold the output data
+    psMetadata *outhead = psMetadataAlloc ();
+
+    // write the links to the image header
+    psMetadataAddStr (outhead, PS_LIST_TAIL, "EXTNAME", PS_META_REPLACE, "xsrc table extension", extname);
+
+    // let's write these out in S/N order
+    sources = psArraySort (sources, pmSourceSortByFlux);
+
+    // we are writing one row per model; we need to write out same number of columns for each row: find the max Nparams
+    int nParamMax = 0;
+    for (int i = 0; i < sources->n; i++) {
+	pmSource *source = sources->data[i];
+	if (source->modelFits == NULL) continue;
+	for (int j = 0; j < source->modelFits->n; j++) {
+	    pmModel *model = source->modelFits->data[j];
+	    assert (model);
+	    nParamMax = PS_MAX (nParamMax, model->params->n);
+	}
+    }
+
+    table = psArrayAllocEmpty (sources->n);
+
+    // we write out all sources, regardless of quality.  the source flags tell us the state
+    for (int i = 0; i < sources->n; i++) {
+
+	pmSource *source = sources->data[i];
+
+	// XXX if no model fits are saved, write out modelEXT?
+	if (source->modelFits == NULL) continue;
+
+	// We have multiple sources : need to flag the one used to subtract the light (the 'best' model)
+	for (int j = 0; j < source->modelFits->n; j++) {
+
+	    // choose the convolved EXT model, if available, otherwise the simple one
+	    pmModel *model = source->modelFits->data[j];
+	    assert (model);
+
+	    // skip models which were not actually fitted
+	    if (model->flags & PM_MODEL_STATUS_BADARGS) continue;
+
+	    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, model->class->useReff);
+
+	    row = psMetadataAlloc ();
+
+	    // XXX we are not writing out the mode (flags) or the type (psf, ext, etc)
+	    psMetadataAddU32 (row, PS_LIST_TAIL, "IPP_IDET",         0, "IPP detection identifier index",             source->seq);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "X_EXT",            0, "EXT model x coordinate",                     xPos);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "Y_EXT",            0, "EXT model y coordinate",                     yPos);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "X_EXT_SIG",        0, "Sigma in EXT x coordinate",                  xErr);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "Y_EXT_SIG",        0, "Sigma in EXT y coordinate",                  yErr);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_INST_MAG",     0, "EXT fit instrumental magnitude",             model->mag);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_INST_MAG_SIG", 0, "Sigma of PSF instrumental magnitude",        model->magErr);
+
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "NPARAMS",          0, "number of model parameters",                 model->params->n);
+	    psMetadataAddStr (row, PS_LIST_TAIL, "MODEL_TYPE",       0, "name of model",                              pmModelClassGetName (model->type));
+
+	    // XXX these should be major and minor, not 'x' and 'y'
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MAJ",    0, "EXT width in x coordinate",                  axes.major);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_WIDTH_MIN",    0, "EXT width in y coordinate",                  axes.minor);
+	    psMetadataAddF32 (row, PS_LIST_TAIL, "EXT_THETA",        0, "EXT orientation angle",                      axes.theta);
+
+	    // write out the other generic parameters
+	    for (int k = 0; k < nParamMax; k++) {
+		if (k == PM_PAR_I0) continue;
+		if (k == PM_PAR_SKY) continue;
+		if (k == PM_PAR_XPOS) continue;
+		if (k == PM_PAR_YPOS) continue;
+		if (k == PM_PAR_SXX) continue;
+		if (k == PM_PAR_SXY) continue;
+		if (k == PM_PAR_SYY) continue;
+
+		snprintf (name, 64, "EXT_PAR_%02d", k);
+
+		if (k < model->params->n) {
+		    psMetadataAdd (row, PS_LIST_TAIL, name, PS_DATA_F32, "", model->params->data.F32[k]);
+		} else {
+		    psMetadataAddF32 (row, PS_LIST_TAIL, name, PS_DATA_F32, "", NAN);
+		}
+	    }
+
+	    // XXX other parameters which may be set.
+	    // XXX flag / value to define the model
+	    // XXX write out the model type, fit status flags
+
+	    psArrayAdd (table, 100, row);
+	    psFree (row);
+	}
+    }
+
+    if (table->n == 0) {
+	psFitsWriteBlank (fits, outhead, extname);
+	psFree (outhead);
+	psFree (table);
+	return true;
+    }
+
+    psTrace ("pmFPAfile", 5, "writing ext data %s\n", extname);
+    if (!psFitsWriteTable (fits, outhead, table, extname)) {
+	psError(PS_ERR_IO, false, "writing ext data %s\n", extname);
+	psFree (outhead);
+	psFree(table);
+	return false;
+    }
+    psFree (outhead);
+    psFree (table);
+    return true;
+}
+
+bool pmSourcesWrite_PS1_DEV_1_XRAD(psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname, psMetadata *recipe)
+{
+    return true;
+}
+
+bool pmSourcesWrite_PS1_DEV_1_XGAL(psFits *fits, pmReadout *readout, psArray *sources, char *extname, psMetadata *recipe)
+{
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_RAW.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_RAW.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_RAW.c	(revision 42651)
@@ -0,0 +1,308 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.20 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-12-08 02:51:14 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+
+#include "pmSourceIO.h"
+
+/***** Text Output Methods *****/
+bool pmSourcesWriteRAW (psArray *sources, char *filename)
+{
+
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(filename, false);
+
+    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)
+{
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(filename, false);
+
+    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->psfMagErr, 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->rawFlux,
+                 source[0].apRadius,
+                 source[0].pixWeightNotBad,
+                 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)
+{
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(filename, false);
+
+    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->psfMagErr, 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->rawFlux,
+                 source[0].apRadius,
+                 source[0].pixWeightNotBad,
+                 model[0].nDOF,
+                 model[0].nIter);
+    }
+    fclose (f);
+    return true;
+}
+
+// dump the sources to an output file
+bool pmSourcesWriteNULLs (psArray *sources, char *filename)
+{
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(filename, false);
+
+    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->detValue,
+                 moment->Mx, moment->My,
+                 moment->Mxx, moment->Myy,
+                 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)
+{
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(filename, false);
+
+    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;
+    }
+
+    fprintf (f, "# %5s %5s  %8s  %7s %7s  %6s %6s  %10s %7s %7s %7s  %4s %4s %5s\n",
+	     "x", "y", "peak", "Mx", "My", "Mxx", "Myy", "Sum", "Peak", "Sky", "SN", "nPix", "type", "mode");
+
+    for (i = 0; i < sources->n; i++) {
+        source = sources->data[i];
+        if (source->moments == NULL)
+            continue;
+        fprintf (f, "%5d %5d  %8.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->detValue,
+                 source->moments->Mx, source->moments->My,
+                 source->moments->Mxx, source->moments->Myy,
+                 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->detValue, peak->rawFlux, peak->xf, peak->yf);
+    }
+    fclose (f);
+    return true;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_SMPDATA.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_SMPDATA.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_SMPDATA.c	(revision 42651)
@@ -0,0 +1,232 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-12-08 02:51:14 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.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
+// XXX: input parameter imageHeader is never used
+bool pmSourcesWrite_SMPDATA (psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, psMetadata *tableHeader, char *extname, psMetadata *recipe)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(extname, false);
+
+    psArray *table;
+    psMetadata *row;
+    int i;
+    psF32 *PAR;
+    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;
+	    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, model->class->useReff);
+
+	} 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->psfMagErr));
+        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->pixWeightNotBad)));
+        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)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(header, false);
+
+    bool status;
+    psF32 *PAR;
+    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;
+
+        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, model->class->useReff);
+
+        source->psfMag = psMetadataLookupF32 (&status, row, "MAG_RAW") - ZERO_POINT;
+        source->extMag = psMetadataLookupF32 (&status, row, "MAG_GAL") - ZERO_POINT;
+        source->psfMagErr = psMetadataLookupF32 (&status, row, "MAG_ERR") * 0.001;
+        source->apMag  = psMetadataLookupF32 (&status, row, "MAG_AP")  - ZERO_POINT;
+
+        source->pixWeightNotBad = 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);
+}
+
+bool pmSourcesWrite_SMPDATA_XSRC (psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname, psMetadata *recipe)
+{
+    return true;
+}
+
+bool pmSourcesWrite_SMPDATA_XFIT(psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname)
+{
+    return true;
+} 
+
+bool pmSourcesWrite_SMPDATA_XRAD(psFits *fits, pmReadout *readout, psArray *sources, psMetadata *imageHeader, char *extname, psMetadata *recipe)
+{
+    return true;
+} 
+
+bool pmSourcesWrite_SMPDATA_XGAL(psFits *fits, pmReadout *readout, psArray *sources, char *extname, psMetadata *recipe)
+{
+    return true;
+} 
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_SX.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_SX.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceIO_SX.c	(revision 42651)
@@ -0,0 +1,112 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.16 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-12-08 02:51:14 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+
+#include "pmSourceIO.h"
+
+// elixir-mode / sextractor-style output list with fixed line width
+bool pmSourcesWriteSX (psArray *sources, char *filename)
+{
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(filename, false);
+
+    psF32 *PAR;
+    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;
+
+        // pmSourceSextractType (source, &type, &flags);
+
+        axes = pmPSF_ModelToAxes (PAR, model->class->useReff);
+
+        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->psfMagErr);
+        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
+        if (fwrite(line->line, 1, line->Nline, f) < line->Nline) {
+            psError(PS_ERR_IO, true, "Unable to write SX sources file (%s)", filename);
+            fclose(f);
+            psFree(line);
+            return false;
+        }
+    }
+    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_branches/psModules.20240412/src/objects/pmSourceInternal.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceInternal.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceInternal.h	(revision 42651)
@@ -0,0 +1,58 @@
+/** @file psastroInternal.h
+ *
+ *  @brief
+ *
+ *  @ingroup psastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# 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>
+
+# 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 defines the parameters to describe a ghost
+ */
+typedef struct {
+    psPlane *srcFP;			///< location in FPA coords of the source star
+    psPlane *FP;			///< location in FPA coords of the ghost center
+    psPlane *chip;			///< location in chip coords of the ghost center
+    double Mag;				///< instrumental magnitude of source star
+    psEllipseAxes inner;		///< inner elliptical annulus boundary
+    psEllipseAxes outer;		///< outer elliptical annulus boundary
+} pmSourceGhost;
+
+pmSourceGhost     *pmSourceGhostAlloc (void);
+
+pmChip           *pmSourceFindChip (double *xChip, double *yChip, pmFPA *fpa, double xFPA, double yFPA);
+bool 		  pmSourceChipBounds (pmFPA *fpa);
+bool              pmSourceFindChipInXrange (pmFPA *fpa, int nChip, double xFPA, double yFPA);
+bool              pmSourceFindChipInYrange (pmFPA *fpa, int nChip, double xFPA, double yFPA);
+bool 		  pmSourceFindChipYedges (double *yFPAs, double *yFPAe, pmFPA *fpa, int nChip);
+bool 		  pmSourceFindChipXedges (double *yFPAs, double *yFPAe, pmFPA *fpa, int nChip);
+bool 		  pmSourceFPAtoChip (double *xChip, double *yChip, pmFPA *fpa, int nChip, double xFPA, double yFPA);
+bool              pmSourceExtractFreeChipBounds(void);
+
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceLensing.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceLensing.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceLensing.c	(revision 42651)
@@ -0,0 +1,114 @@
+/** @file  pmSourceLensing.c
+ *
+ *  Functions to measure the local sky and sky variance for sources on images
+ *  @author EAM, IfA: 
+ *  @date $Date: 2014-03-20 $
+ *
+ *  Copyright 2014 Ifa, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmMoments.h"
+#include "pmSourceLensing.h"
+
+static void pmLensingParsFree (pmLensingPars *pars) {
+  if (!pars) return;
+  return;
+}
+
+pmLensingPars *pmLensingParsAlloc () {
+
+  pmLensingPars *tmp = (pmLensingPars *) psAlloc(sizeof(pmLensingPars));;
+  psMemSetDeallocator(tmp, (psFreeFunc) pmLensingParsFree);
+
+  tmp->X11 = NAN;
+  tmp->X12 = NAN;
+  tmp->X22 = NAN;
+
+  tmp->e1 = NAN;
+  tmp->e2 = NAN;
+
+  return tmp;
+}
+
+static void pmSourceLensingFree (pmSourceLensing *lensing) {
+  if (!lensing) return;
+  psFree (lensing->smear);
+  psFree (lensing->shear);
+  return;
+}
+
+pmSourceLensing *pmSourceLensingAlloc () {
+
+  pmSourceLensing *tmp = (pmSourceLensing *) psAlloc(sizeof(pmSourceLensing));;
+  psMemSetDeallocator(tmp, (psFreeFunc) pmSourceLensingFree);
+
+  tmp->smear = NULL;
+  tmp->shear = NULL;
+  tmp->e1 = NAN;
+  tmp->e2 = NAN;
+
+  return tmp;
+}
+
+// need to supply the moments and the window-function sigma
+bool pmSourceLensingShearFromMoments (pmSourceLensing *lensing, pmMoments *moments, float sigma) {
+  
+  if (!lensing->shear) {
+    lensing->shear = pmLensingParsAlloc();
+  }
+  
+  pmLensingPars *shear = lensing->shear;
+
+  float R = 1.0 / (moments->Mxx + moments->Myy);
+  float s2 = 1.0 / PS_SQR(sigma);
+  // NOTE : not used by shear : float s4 = PS_SQR(s2);
+
+  shear->X11 = R*(2.0*(moments->Mxx + moments->Myy) - s2 * (moments->Mxxxx - 2.0*moments->Mxxyy + moments->Myyyy));
+
+  shear->X22 = R*(2.0*(moments->Mxx + moments->Myy) - s2 * 4.0 * moments->Mxxyy);
+
+  shear->X12 = R*2.0*s2*(moments->Mxyyy - moments->Mxxxy);
+
+  shear->e1  = R*(2.0*(moments->Mxx - moments->Myy) + s2 * (moments->Myyyy - moments->Mxxxx));
+
+  shear->e2  = R*(4.0*moments->Mxy - 2.0*s2*(moments->Mxxxy + moments->Mxyyy));
+  
+  return true;
+}
+
+// need to supply the moments and the window-function sigma
+// NOTE: I'm using the coefficients from Hoekstra et al 1998, not KSB96
+bool pmSourceLensingSmearFromMoments (pmSourceLensing *lensing, pmMoments *moments, float sigma) {
+  
+  if (!lensing->smear) {
+    lensing->smear = pmLensingParsAlloc();
+  }
+  
+  pmLensingPars *smear = lensing->smear;
+
+  float R = 1.0 / (moments->Mxx + moments->Myy);
+  float s2 = 1.0 / PS_SQR(sigma);
+  float s4 = PS_SQR(s2);
+
+  smear->X11 = R*(1.0 - s2*(moments->Mxx + moments->Myy) + 0.25*s4 * (moments->Mxxxx - 2.0*moments->Mxxyy + moments->Myyyy));
+
+  smear->X22 = R*(1.0 - s2*(moments->Mxx + moments->Myy) + 1.00*s4 * (moments->Mxxyy));
+
+  smear->X12 = R*0.5*s4*(moments->Mxxxy - moments->Mxyyy);
+
+  smear->e1  = R*(s2*(moments->Myy - moments->Mxx) + 0.25*s4 * (moments->Mxxxx - moments->Myyyy));
+
+  smear->e2  = R*(0.5*s4*(moments->Mxxxy + moments->Mxyyy) - 2.0*s2*moments->Mxy);
+  
+  return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceLensing.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceLensing.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceLensing.h	(revision 42651)
@@ -0,0 +1,38 @@
+/* @file  pmSourceLensing.h
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2014-03-20 02:31:25 $
+ * Copyright 2014 IfA, University of Hawaii
+ */
+
+# ifndef PM_SOURCE_LENSING_H
+# define PM_SOURCE_LENSING_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+typedef struct {
+  float X11;
+  float X12;
+  float X22;
+  float e1;
+  float e2;
+} pmLensingPars;
+
+typedef struct {
+  pmLensingPars *smear;
+  pmLensingPars *shear;
+  float e1;
+  float e2;
+} pmSourceLensing; 
+
+pmLensingPars *pmLensingParsAlloc ();
+pmSourceLensing *pmSourceLensingAlloc ();
+
+bool pmSourceLensingShearFromMoments (pmSourceLensing *lensing, pmMoments *moments, float sigma);
+bool pmSourceLensingSmearFromMoments (pmSourceLensing *lensing, pmMoments *moments, float sigma);
+
+/// @}
+# endif /* PM_SOURCE_LENSING_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceMasks.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceMasks.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceMasks.c	(revision 42651)
@@ -0,0 +1,51 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmSourceMasks.h"
+
+#define ADD_MASK(HEADER, NAME, COMMENT) \
+    psMetadataAddU32(HEADER, PS_LIST_TAIL, "SOURCE.MASK." #NAME, \
+                     PS_META_REPLACE, COMMENT, (psU32)PM_SOURCE_MODE_##NAME);
+
+bool pmSourceMasksHeader(psMetadata *header)
+{
+    PS_ASSERT_METADATA_NON_NULL(header, false);
+
+    ADD_MASK(header, PSFMODEL        , "Fit with PSF model");
+    ADD_MASK(header, EXTMODEL        , "Fit with extended model");
+    ADD_MASK(header, FITTED          , "Fit with non-linear model");
+    ADD_MASK(header, FAIL            , "Non-linear fit failed");
+    ADD_MASK(header, POOR            , "Non-linear fit poor");
+    ADD_MASK(header, PAIR            , "Fit with double PSF");
+    ADD_MASK(header, PSFSTAR         , "Used to define PSF model");
+    ADD_MASK(header, SATSTAR         , "Model peak is above saturation");
+    ADD_MASK(header, BLEND           , "Blended with other sources");
+    ADD_MASK(header, EXTERNAL        , "Based on supplied input position");
+    ADD_MASK(header, BADPSF          , "Unable to estimate object PSF");
+    ADD_MASK(header, DEFECT          , "Suspected defect");
+    ADD_MASK(header, SATURATED       , "Suspected saturated (bleed trail)");
+    ADD_MASK(header, CR_LIMIT        , "Suspected cosmic ray");
+    ADD_MASK(header, EXT_LIMIT       , "Suspected extended");
+    ADD_MASK(header, MOMENTS_FAILURE , "Failed to measure moments");
+    ADD_MASK(header, SKY_FAILURE     , "Failed to measure local sky");
+    ADD_MASK(header, SKYVAR_FAILURE  , "Failed to measure sky variance");
+    ADD_MASK(header, BELOW_MOMENTS_SN, "Moments not measured due to low S/N");
+    ADD_MASK(header, BIG_RADIUS      , "Small radius has poor moments");
+    ADD_MASK(header, AP_MAGS         , "Measured aperture magnitude");
+    ADD_MASK(header, BLEND_FIT       , "Fit as a blend");
+    ADD_MASK(header, EXTENDED_FIT    , "Fit with full extended fit");
+    ADD_MASK(header, EXTENDED_STATS  , "Calculated extended aperture stats");
+    ADD_MASK(header, LINEAR_FIT      , "Fit with linear fit");
+    ADD_MASK(header, NONLINEAR_FIT   , "Fit with non-linear fit");
+    ADD_MASK(header, RADIAL_FLUX     , "Calculated radial flux measurements");
+    ADD_MASK(header, SIZE_SKIPPED    , "Could not be determine size");
+    ADD_MASK(header, ON_SPIKE        , "Source lands in bright star spike");
+    ADD_MASK(header, ON_GHOST        , "Source lands in bright star ghost / glint");
+    ADD_MASK(header, OFF_CHIP        , "Source centroid lands off chip edge");
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceMasks.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceMasks.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceMasks.h	(revision 42651)
@@ -0,0 +1,86 @@
+#ifndef PM_SOURCE_MASKS_H
+#define PM_SOURCE_MASKS_H
+
+// Bit flags to distinguish analysis results
+// When adding to or subtracting from this list, please also modify pmSourceMaskHeader
+typedef enum {
+    PM_SOURCE_MODE_DEFAULT          	  = 0x00000000, ///< Initial value: resets all bits
+    PM_SOURCE_MODE_PSFMODEL         	  = 0x00000001, ///< Source fitted with a psf model (linear or non-linear)
+    PM_SOURCE_MODE_EXTMODEL         	  = 0x00000002, ///< Source fitted with an extended-source model
+    PM_SOURCE_MODE_FITTED           	  = 0x00000004, ///< Source fitted with non-linear model (PSF or EXT; good or bad)
+    PM_SOURCE_MODE_FAIL             	  = 0x00000008, ///< Fit (non-linear) failed (non-converge, off-edge, run to zero)
+    PM_SOURCE_MODE_POOR             	  = 0x00000010, ///< Fit succeeds, but low-SN, high-Chisq, or large (for PSF -- drop?)
+    PM_SOURCE_MODE_PAIR             	  = 0x00000020, ///< Source fitted with a double psf
+    PM_SOURCE_MODE_PSFSTAR          	  = 0x00000040, ///< Source used to define PSF model
+    PM_SOURCE_MODE_SATSTAR          	  = 0x00000080, ///< Source model peak is above saturation
+    PM_SOURCE_MODE_BLEND            	  = 0x00000100, ///< Source is a blend with other sources
+    PM_SOURCE_MODE_EXTERNAL         	  = 0x00000200, ///< Source based on supplied input position
+    PM_SOURCE_MODE_BADPSF           	  = 0x00000400, ///< Failed to get good estimate of object's PSF
+    PM_SOURCE_MODE_DEFECT           	  = 0x00000800, ///< Source is thought to be a defect
+    PM_SOURCE_MODE_SATURATED        	  = 0x00001000, ///< Source is thought to be saturated pixels (bleed trail)
+    PM_SOURCE_MODE_CR_LIMIT         	  = 0x00002000, ///< Source has crNsigma above limit
+    PM_SOURCE_MODE_EXT_LIMIT        	  = 0x00004000, ///< Source has extNsigma above limit
+    PM_SOURCE_MODE_MOMENTS_FAILURE  	  = 0x00008000, ///< could not measure the moments
+    PM_SOURCE_MODE_SKY_FAILURE      	  = 0x00010000, ///< could not measure the local sky
+    PM_SOURCE_MODE_SKYVAR_FAILURE   	  = 0x00020000, ///< could not measure the local sky variance
+    PM_SOURCE_MODE_BELOW_MOMENTS_SN 	  = 0x00040000, ///< moments not measured due to low S/N
+    PM_SOURCE_MODE_BIG_RADIUS       	  = 0x00100000, ///< poor moments for small radius, try large radius
+    PM_SOURCE_MODE_AP_MAGS          	  = 0x00200000, ///< source has an aperture magnitude
+    PM_SOURCE_MODE_BLEND_FIT        	  = 0x00400000, ///< source was fitted as a blend
+    PM_SOURCE_MODE_EXTENDED_FIT     	  = 0x00800000, ///< full extended fit was used
+    PM_SOURCE_MODE_EXTENDED_STATS   	  = 0x01000000, ///< extended aperture stats calculated
+    PM_SOURCE_MODE_LINEAR_FIT       	  = 0x02000000, ///< source fitted with the linear fit
+    PM_SOURCE_MODE_NONLINEAR_FIT    	  = 0x04000000, ///< source fitted with the non-linear fit
+    PM_SOURCE_MODE_RADIAL_FLUX      	  = 0x08000000, ///< radial flux measurements calculated
+    PM_SOURCE_MODE_SIZE_SKIPPED     	  = 0x10000000, ///< size could not be determined
+    PM_SOURCE_MODE_ON_SPIKE         	  = 0x20000000, ///< peak lands on diffraction spike
+    PM_SOURCE_MODE_ON_GHOST         	  = 0x40000000, ///< peak lands on ghost or glint
+    PM_SOURCE_MODE_OFF_CHIP         	  = 0x80000000, ///< peak lands off edge of chip
+} pmSourceMode;
+
+// Bit flags to distinguish analysis results
+// When adding to or subtracting from this list, please also modify pmSourceMaskHeader
+typedef enum {
+    PM_SOURCE_MODE2_DEFAULT          	  = 0x00000000, ///< Initial value: resets all bits
+    PM_SOURCE_MODE2_DIFF_WITH_SINGLE 	  = 0x00000001, ///< diff source matched to a single positive detection
+    PM_SOURCE_MODE2_DIFF_WITH_DOUBLE 	  = 0x00000002, ///< diff source matched to positive detections in both images
+    PM_SOURCE_MODE2_MATCHED          	  = 0x00000004, ///< source generated based on another image
+    PM_SOURCE_MODE2_ON_SPIKE         	  = 0x00000008, ///< > 25% of (PSF-weighted) pixels land on diffraction spike
+    PM_SOURCE_MODE2_ON_STARCORE      	  = 0x00000010, ///< > 25% of (PSF-weighted) pixels land on starcore
+    PM_SOURCE_MODE2_ON_BURNTOOL      	  = 0x00000020, ///< > 25% of (PSF-weighted) pixels land on burntool
+    PM_SOURCE_MODE2_ON_CONVPOOR      	  = 0x00000040, ///< > 25% of (PSF-weighted) pixels land on convpoor
+    PM_SOURCE_MODE2_PASS1_SRC             = 0x00000080, ///< source detected in first pass analysis
+    PM_SOURCE_MODE2_HAS_BRIGHTER_NEIGHBOR = 0x00000100, ///< peak is not the brightest in its footprint
+    PM_SOURCE_MODE2_BRIGHT_NEIGHBOR_1     = 0x00000200, ///< flux_n / (r^2 flux_p) > 1
+    PM_SOURCE_MODE2_BRIGHT_NEIGHBOR_10    = 0x00000400, ///< flux_n / (r^2 flux_p) > 10
+    PM_SOURCE_MODE2_DIFF_SELF_MATCH  	  = 0x00000800, ///< positive detection match is probably this source 
+    PM_SOURCE_MODE2_SATSTAR_PROFILE       = 0x00001000, ///< saturated source is modeled with a radial profile
+
+    PM_SOURCE_MODE2_ECONTOUR_FEW_PTS      = 0x00002000, ///< too few points to measure the elliptical contour
+    PM_SOURCE_MODE2_RADBIN_NAN_CENTER     = 0x00004000, ///< radial bins failed with too many NaN center bin
+    PM_SOURCE_MODE2_PETRO_NAN_CENTER      = 0x00008000, ///< petrosian radial bins failed with too many NaN center bin
+    PM_SOURCE_MODE2_PETRO_NO_PROFILE      = 0x00010000, ///< petrosian not build because radial bins missing
+
+    PM_SOURCE_MODE2_PETRO_INSIG_RATIO     = 0x00020000, ///< insignificant measurement of petrosian ratio
+    PM_SOURCE_MODE2_PETRO_RATIO_ZEROBIN   = 0x00040000, ///< petrosian ratio in the 0th bin (likely bad)
+    
+    PM_SOURCE_MODE2_EXT_FITS_RUN          = 0x00080000, ///< we attempted to run extended fits on this source
+    PM_SOURCE_MODE2_EXT_FITS_FAIL         = 0x00100000, ///< at least one of the model fits failed
+    PM_SOURCE_MODE2_EXT_FITS_RETRY        = 0x00200000, ///< one of the model fits was re-tried with new window
+    PM_SOURCE_MODE2_EXT_FITS_NONE         = 0x00400000, ///< ALL of the model fits failed
+    
+    PM_SOURCE_MODE2_ON_GHOST      	  = 0x00800000, ///< > 25% of (PSF-weighted) pixels land on ghost
+    PM_SOURCE_MODE2_ON_CROSSTALK      	  = 0x01000000, ///< peaks land on electronic crostalk
+    PM_SOURCE_MODE2_ON_CTE      	  = 0x02000000, ///< peaks land on CTE region
+
+    
+} pmSourceMode2;
+
+/// Populate header with mask values
+bool pmSourceMasksHeader(
+    psMetadata *header                  ///< Header to populate
+    );
+
+
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceMatch.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceMatch.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceMatch.c	(revision 42651)
@@ -0,0 +1,983 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmErrorCodes.h"
+
+#include "pmSourceMatch.h"
+
+#define SOURCE_MASK (PM_SOURCE_MODE_FAIL | PM_SOURCE_MODE_SATSTAR | PM_SOURCE_MODE_BLEND | \
+                     PM_SOURCE_MODE_BADPSF | PM_SOURCE_MODE_DEFECT | PM_SOURCE_MODE_SATURATED | \
+                     PM_SOURCE_MODE_CR_LIMIT | PM_SOURCE_MODE_EXT_LIMIT) // Mask to apply to input sources
+#define SOURCE_FAINTEST 50.0            // Faintest magnitude to consider
+#define SOURCE_BRIGHTEST -30.0          // Brightest magnitude to consider
+#define SOURCES_MAX_LEAF 2              // Maximum number of points on a tree leaf
+#define ARRAY_BUFFER 16                 // Buffer for array
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Private (file-static) functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Extract coordinates from a source
+static inline void coordsFromSource(float *x, float *y, // Coordinates to return
+                                    pmSource *source // Source of interest
+    )
+{
+    if (!source) {
+        *x = NAN;
+        *y = NAN;
+    } else if (source->modelPSF) {
+        *x = source->modelPSF->params->data.F32[PM_PAR_XPOS];
+        *y = source->modelPSF->params->data.F32[PM_PAR_YPOS];
+    } else {
+        *x = source->peak->xf;
+        *y = source->peak->yf;
+    }
+    return;
+}
+
+// Parse the sources into vectors for each coordinate, and a bounding box
+// Returns number of valid sources
+static long sourcesParse(psRegion **bounds, // Region to update with bounding box
+                         psVector **x, psVector **y, // Coordinate vectors to return
+                         psVector **mag, psVector **magErr, // Magnitude and error vectors to return
+                         psVector **indices, // Indices for sources
+                         const psArray *sources // Input sources
+    )
+{
+    psAssert(bounds, "Must be given a region for bounding box");
+    psAssert(x && y, "Must be given position vectors");
+    psAssert(mag && magErr, "Must be given magnitude vectors");
+    psAssert(sources, "Must be given sources");
+
+    long numSources = sources->n;              // Number of sources
+    *x = psVectorRecycle(*x, numSources, PS_TYPE_F32);
+    *y = psVectorRecycle(*y, numSources, PS_TYPE_F32);
+    *mag = psVectorRecycle(*mag, numSources, PS_TYPE_F32);
+    *magErr = psVectorRecycle(*magErr, numSources, PS_TYPE_F32);
+    *indices = psVectorRecycle(*indices, numSources, PS_TYPE_S32);
+    float xMin = INFINITY, xMax = -INFINITY, yMin = INFINITY, yMax = -INFINITY; // Bounds of sources
+    long num = 0;                       // Number of valid sources
+    for (long i = 0; i < numSources; i++) {
+        pmSource *source = sources->data[i]; // Source of interest
+        if (!source || (source->mode & SOURCE_MASK) || !isfinite(source->psfMag) ||
+            !isfinite(source->psfMagErr) || source->psfMag > SOURCE_FAINTEST || source->psfMag < SOURCE_BRIGHTEST) {
+            continue;
+        }
+
+        float xSrc, ySrc;               // Coordinates of source
+        coordsFromSource(&xSrc, &ySrc, source);
+        if (xSrc < xMin) xMin = xSrc;
+        if (xSrc > xMax) xMax = xSrc;
+        if (ySrc < yMin) yMin = ySrc;
+        if (ySrc > yMax) yMax = ySrc;
+
+        (*x)->data.F32[num] = xSrc;
+        (*y)->data.F32[num] = ySrc;
+        (*mag)->data.F32[num] = source->psfMag;
+        (*magErr)->data.F32[num] = source->psfMagErr;
+        (*indices)->data.S32[num] = i;
+        num++;
+    }
+    (*x)->n = num;
+    (*y)->n = num;
+    (*mag)->n = num;
+    (*magErr)->n = num;
+    (*indices)->n = num;
+
+    if (*bounds) {
+        (*bounds)->x0 = xMin;
+        (*bounds)->x1 = xMax;
+        (*bounds)->y0 = yMin;
+        (*bounds)->y1 = yMax;
+    } else {
+        *bounds = psRegionAlloc(xMin, xMax, yMin, yMax);
+    }
+
+    psTrace("psModules.objects", 8, "%ld sources: bounds are [%.2f:%.2f,%.2f:%.2f]\n",
+            num, xMin, xMax, yMin, yMax);
+
+    return num;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// pmSourceMatch operations
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+static void sourceMatchFree(pmSourceMatch *match)
+{
+    psFree(match->mag);
+    psFree(match->magErr);
+    psFree(match->x);
+    psFree(match->y);
+    psFree(match->image);
+    psFree(match->index);
+    psFree(match->mask);
+}
+
+pmSourceMatch *pmSourceMatchAlloc(void)
+{
+    pmSourceMatch *match = psAlloc(sizeof(pmSourceMatch)); // Match data
+    psMemSetDeallocator(match, (psFreeFunc)sourceMatchFree);
+
+    match->num = 0;
+    match->mag = psVectorAllocEmpty(ARRAY_BUFFER, PS_TYPE_F32);
+    match->magErr = psVectorAllocEmpty(ARRAY_BUFFER, PS_TYPE_F32);
+    match->x = psVectorAllocEmpty(ARRAY_BUFFER, PS_TYPE_F32);
+    match->y = psVectorAllocEmpty(ARRAY_BUFFER, PS_TYPE_F32);
+    match->image = psVectorAllocEmpty(ARRAY_BUFFER, PS_TYPE_U32);
+    match->index = psVectorAllocEmpty(ARRAY_BUFFER, PS_TYPE_U32);
+    match->mask = psVectorAllocEmpty(ARRAY_BUFFER, PS_TYPE_VECTOR_MASK);
+
+    return match;
+}
+
+void pmSourceMatchAdd(pmSourceMatch *match, // Match data
+                      float mag, float magErr, // Magnitude and error
+                      float x, float y,        // Position
+                      int image, // Image index
+                      int index // Source index
+    )
+{
+    int num = match->num;               // Number of matches
+
+    match->mag = psVectorExtend(match->mag, match->mag->nalloc, 1);
+    match->magErr = psVectorExtend(match->magErr, match->magErr->nalloc, 1);
+    match->x = psVectorExtend(match->x, match->x->nalloc, 1);
+    match->y = psVectorExtend(match->y, match->y->nalloc, 1);
+    match->image = psVectorExtend(match->image, match->image->nalloc, 1);
+    match->index = psVectorExtend(match->index, match->index->nalloc, 1);
+    match->mask = psVectorExtend(match->mask, match->mask->nalloc, 1);
+
+    match->mag->data.F32[num] = mag;
+    match->magErr->data.F32[num] = magErr;
+    match->x->data.F32[num] = x;
+    match->y->data.F32[num] = y;
+    match->image->data.S32[num] = image;
+    match->index->data.S32[num] = index;
+    match->mask->data.PS_TYPE_VECTOR_MASK_DATA[num] = 0;
+    match->num++;
+
+    return;
+}
+
+
+psArray *pmSourceMatchSources(const psArray *sourceArrays, float radius, bool cullSingles)
+{
+    PS_ASSERT_ARRAY_NON_NULL(sourceArrays, NULL);
+    PS_ASSERT_FLOAT_LARGER_THAN(radius, 0.0, NULL);
+
+    int numImages = sourceArrays->n;    // Number of images
+
+    // First, merge the source lists, pulling out matches
+
+    psRegion *boundsMaster = NULL;       // Bounds of source list
+    psVector *xMaster = NULL, *yMaster = NULL; // Coordinates of sources
+    long numMaster = 0;                 // Number in master list
+    psArray *matches = NULL;            // Source matches (potential and actual matches)
+
+    for (int i = 0; i < numImages; i++) {
+        psArray *sources = sourceArrays->data[i]; // Sources in image
+        if (!sources || sources->n == 0) {
+            continue;
+        }
+        psRegion *boundsImage = NULL;   // Bounds of source list
+        psVector *xImage = NULL, *yImage = NULL; // Coordinates of sources
+        psVector *magImage = NULL, *magErrImage = NULL; // Magnitude and mag
+        psVector *indices = NULL;     // Indices for sources
+
+        int numSources = sourcesParse(&boundsImage, &xImage, &yImage, &magImage, &magErrImage, &indices,
+                                      sources); // Number of sources
+
+        // an input image with only poor detections can cause trouble, remove it
+        if (!numSources) continue;
+
+        if (!boundsMaster) {
+            // First run through --- can just copy
+            boundsMaster = psMemIncrRefCounter(boundsImage);
+            xMaster = psMemIncrRefCounter(xImage);
+            yMaster = psMemIncrRefCounter(yImage);
+            matches = psArrayAlloc(numSources);
+            for (int j = 0; j < numSources; j++) {
+                pmSourceMatch *match = pmSourceMatchAlloc(); // Match data
+                pmSourceMatchAdd(match, magImage->data.F32[j], magErrImage->data.F32[j],
+                                 xImage->data.F32[j], yImage->data.F32[j], i, indices->data.S32[j]);
+                matches->data[j] = match;
+            }
+            numMaster += numSources;
+        } else if (boundsImage->x0 > boundsMaster->x1 || boundsImage->x1 < boundsMaster->x0 ||
+                   boundsImage->y0 > boundsMaster->y1 || boundsImage->y1 < boundsMaster->y0) {
+            // Bounds don't overlap --- can just add everything in to the master list
+            psTrace("psModules.objects", 7, "Bounds don't overlap\n");
+            long size = numMaster + numSources; // New size
+            xMaster = psVectorRealloc(xMaster, size);
+            yMaster = psVectorRealloc(yMaster, size);
+            matches = psArrayRealloc(matches, size);
+
+            memcpy(&xMaster->data.F32[numMaster], xImage->data.F32,
+                   numSources * PSELEMTYPE_SIZEOF(PS_TYPE_F32));
+            memcpy(&yMaster->data.F32[numMaster], yImage->data.F32,
+                   numSources * PSELEMTYPE_SIZEOF(PS_TYPE_F32));
+            for (int j = 0, k = numMaster; j < numSources; j++, k++) {
+                pmSourceMatch *match = pmSourceMatchAlloc(); // Match data
+                pmSourceMatchAdd(match, magImage->data.F32[j], magErrImage->data.F32[j],
+                                 xImage->data.F32[j], yImage->data.F32[j], i, indices->data.S32[j]);
+                matches->data[k] = match;
+            }
+
+            numMaster = size;
+            xMaster->n = size;
+            yMaster->n = size;
+            matches->n = size;
+        } else {
+            // Match with the master list
+            psTree *tree = psTreePlant(2, SOURCES_MAX_LEAF, PS_TREE_EUCLIDEAN, xMaster, yMaster); // kd Tree
+            long numMatch = 0;          // Number of matches
+
+            long size = numMaster + numSources; // New size
+            xMaster = psVectorRealloc(xMaster, size);
+            yMaster = psVectorRealloc(yMaster, size);
+            matches = psArrayRealloc(matches, size);
+
+            psVector *coords = psVectorAlloc(2, PS_TYPE_F32); // Coordinates for tree lookup
+            for (int j = 0; j < numSources; j++) {
+                coords->data.F32[0] = xImage->data.F32[j];
+                coords->data.F32[1] = yImage->data.F32[j];
+                long index = psTreeNearestWithin(tree, coords, radius); // Match index
+                if (index >= 0) {
+                    // Record the match
+                    pmSourceMatch *match = matches->data[index]; // Match data
+                    pmSourceMatchAdd(match, magImage->data.F32[j], magErrImage->data.F32[j],
+                                     xImage->data.F32[j], yImage->data.F32[j], i, indices->data.S32[j]);
+                    numMatch++;
+                } else {
+                    // Add to the master list
+                    pmSourceMatch *match = pmSourceMatchAlloc(); // Match data
+                    pmSourceMatchAdd(match, magImage->data.F32[j], magErrImage->data.F32[j],
+                                     xImage->data.F32[j], yImage->data.F32[j], i, indices->data.S32[j]);
+                    xMaster->data.F32[numMaster] = xImage->data.F32[j];
+                    yMaster->data.F32[numMaster] = yImage->data.F32[j];
+                    matches->data[numMaster] = match;
+                    numMaster++;
+                    xMaster->n = yMaster->n = matches->n = numMaster;
+                }
+
+            }
+            psFree(coords);
+            psFree(tree);
+        }
+
+        psFree(boundsImage);
+        psFree(xImage);
+        psFree(yImage);
+        psFree(magImage);
+        psFree(magErrImage);
+        psFree(indices);
+    }
+
+    psFree(xMaster);
+    psFree(yMaster);
+    psFree(boundsMaster);
+
+    if (!matches) {
+        psError(PM_ERR_OBJECTS, true, "No matches made.");
+        return NULL;
+    }
+
+    if (cullSingles) {
+        // Now cull the matches that contain only a single star
+        int numGood = 0;                    // Number of good matches
+        for (int i = 0; i < matches->n; i++) {
+            pmSourceMatch *match = matches->data[i]; // Match of interest
+            if (match->num > 1) {
+                if (i != numGood) {
+                    psFree(matches->data[numGood]);
+                    matches->data[numGood] = psMemIncrRefCounter(match);
+                }
+                numGood++;
+            }
+        }
+        matches->n = numGood;
+        for (int i = numGood; i < numMaster; i++) {
+            psFree(matches->data[i]);
+            matches->data[i] = NULL;
+        }
+    }
+
+    if (matches->n == 0) {
+        psError(PM_ERR_OBJECTS, true, "No matches made.");
+        psFree(matches);
+        return NULL;
+    }
+
+    return matches;
+}
+
+
+psArray *pmSourceMatchMerge(psArray *sourceArrays, float radius, bool cullSingles)
+{
+    PS_ASSERT_ARRAY_NON_NULL(sourceArrays, NULL);
+    PS_ASSERT_FLOAT_LARGER_THAN(radius, 0.0, NULL);
+
+    // XXX MEH - ppSub should only get matched list so cull true. merge refers to the picking of the first entry data for match
+    //psArray *matches = pmSourceMatchSources(sourceArrays, radius, false); // Source matches
+    psArray *matches = pmSourceMatchSources(sourceArrays, radius, cullSingles); // Source matches
+    if (!matches) {
+        psError(psErrorCodeLast(), false, "Unable to match source lists.");
+        return NULL;
+    }
+
+    int index = 0;                      // Index to current position on list
+    for (int i = 0; i < matches->n; i++) {
+        pmSource *source = NULL;        // Source to put in merged list
+        pmSourceMatch *match = matches->data[i]; // Match of interest
+        for (int j = 0; j < match->num && !source; j++) {
+            if (!isfinite(match->mag->data.F32[j]) || !isfinite(match->magErr->data.F32[j]) ||
+                !isfinite(match->x->data.F32[j]) || !isfinite(match->y->data.F32[j])) {
+                continue;
+            }
+            int imgIndex = match->image->data.S32[j]; // Index of image
+            int srcIndex = match->index->data.S32[j]; // Index of source for image
+            psArray *list = sourceArrays->data[imgIndex]; // List of interest
+            source = list->data[srcIndex];
+            break;
+        }
+
+        if (source) {
+            psFree(matches->data[index]);
+            matches->data[index] = psMemIncrRefCounter(source);
+            index++;
+        }
+    }
+
+    // Clear out the rest of the list
+    int num = index;                    // Number of good sources
+    for (; index < matches->n; index++) {
+        psFree(matches->data[index]);
+        matches->data[index] = NULL;
+    }
+    matches->n = num;
+
+    return matches;
+}
+
+// Iterate on the star magnitudes and image transparencies
+// Returns the solution chi^2
+static float sourceMatchRelphotIterate(psVector *trans, // Transparencies
+                                       psVector *stars, // Star magnitudes
+                                       psVector *badImage, // Bad image mask
+                                       const psArray *matches, // Array of matches
+                                       const psVector *zp, // Zero points for each image (incl. airmass term)
+                                       const psVector *photo, // Photometric image?
+                                       float sysErr2 // Systematic error, squared
+    )
+{
+    psAssert(zp && zp->type.type == PS_TYPE_F32, "Need zero points");
+    psAssert(matches, "Need list of matches");
+
+    int numImages = zp->n;              // Number of images
+    int numStars = matches->n;          // Number of stars
+
+    psAssert(trans && trans->type.type == PS_TYPE_F32, "Need transparencies");
+    psAssert(trans->n == numImages, "Not enough transparencies: %ld\n", trans->n);
+    psAssert(stars && stars->type.type == PS_TYPE_F32, "Need star magnitudes");
+    psAssert(stars->n == numStars, "Not enough stars: %ld\n", stars->n);
+    psAssert(zp->n == numImages, "Not enough ZPs: %ld", zp->n);
+    psAssert(!photo || photo->type.type == PS_TYPE_U8, "Photometric determination is wrong type");
+    psAssert(!photo || photo->n == numImages, "Not enough photometric determinations: %ld", photo->n);
+
+    // Solve the star magnitudes
+    psVectorInit(stars, 0.0);
+    int numGoodStars = 0;               // Number of stars with good measurements
+    for (int i = 0; i < numStars; i++) {
+        pmSourceMatch *match = matches->data[i]; // Matched stars
+        int numMeasurements = 0;        // Number of unmasked measurements for star
+        double star = 0.0, starErr = 0.0; // Accumulators for star
+        for (int j = 0; j < match->num; j++) {
+            if (match->mask->data.PS_TYPE_VECTOR_MASK_DATA[j] & PM_SOURCE_MATCH_MASK_PHOT) {
+                continue;
+            }
+            numMeasurements++;
+            int index = match->image->data.U32[j]; // Image index
+            float mag = match->mag->data.F32[j]; // Measured magnitude
+            double magErr2 = PS_SQR(match->magErr->data.F32[j]) + sysErr2; // Error in measured magnitude
+            double invErr2 = 1.0 / magErr2; // Inverse square error
+            float cal = zp->data.F32[index]; // Calibration to apply to image
+            if (!photo || !photo->data.U8[index]) {
+                cal -= trans->data.F32[index];
+            }
+            star += (mag + cal) * invErr2;
+            starErr += invErr2;
+        }
+        if (numMeasurements > 1) {
+            // It's only a good star (contributing to the chi^2) if there's more than 1 measurement
+            numGoodStars++;
+        }
+        stars->data.F32[i] = star / starErr;
+    }
+
+    // Solve for the transparencies
+    // We solve even for the "photometric" images since they may jump out of that status upon iteration
+    psVector *accum = psVectorAlloc(numImages, PS_TYPE_F64); // Transparency accumulator
+    psVector *accumErr = psVectorAlloc(numImages, PS_TYPE_F64); // Transparency accumulator
+    psVectorInit(accum, 0.0);
+    psVectorInit(accumErr, 0.0);
+    for (int i = 0; i < numStars; i++) {
+        pmSourceMatch *match = matches->data[i]; // Matched stars
+        for (int j = 0; j < match->num; j++) {
+            if (match->mask->data.PS_TYPE_VECTOR_MASK_DATA[j] & PM_SOURCE_MATCH_MASK_PHOT) {
+                continue;
+            }
+            int index = match->image->data.U32[j]; // Image index
+            float mag = match->mag->data.F32[j]; // Measured magnitude
+            double magErr2 = PS_SQR(match->magErr->data.F32[j]) + sysErr2; // Error in measured magnitude
+            double invErr2 = 1.0 / magErr2; // Inverse square error
+            float cal = zp->data.F32[index]; // Calibration to apply to image
+            accum->data.F64[index] += (mag + cal - stars->data.F32[i]) * invErr2;
+            accumErr->data.F64[index] += invErr2;
+        }
+    }
+    for (int i = 0; i < numImages; i++) {
+        trans->data.F32[i] = accum->data.F64[i] / accumErr->data.F64[i];
+        if (!isfinite(trans->data.F32[i])) {
+            badImage->data.U8[i] = 0xFF;
+        }
+        psTrace("psModules.objects", 3, "Transparency for image %d: %f\n", i, trans->data.F32[i]);
+    }
+    psFree(accum);
+    psFree(accumErr);
+
+    // Once more through to evaluate chi^2
+    float chi2 = 0.0;                   // chi^2 for iteration
+    int dof = 0;                        // Degrees of freedom
+    for (int i = 0; i < numStars; i++) {
+        pmSourceMatch *match = matches->data[i]; // Matched stars
+        for (int j = 0; j < match->num; j++) {
+            if (match->mask->data.PS_TYPE_VECTOR_MASK_DATA[j] & PM_SOURCE_MATCH_MASK_PHOT) {
+                continue;
+            }
+            int index = match->image->data.U32[j]; // Image index
+            if (badImage->data.U8[index]) {
+                continue;
+            }
+            float mag = match->mag->data.F32[j]; // Measured magnitude
+            float magErr2 = PS_SQR(match->magErr->data.F32[j]) + sysErr2; // Error in measured magnitude
+            float cal = zp->data.F32[index]; // Calibration to apply to image
+            if (!photo || !photo->data.U8[index]) {
+                cal -= trans->data.F32[index];
+            }
+            float dev2 = mag + cal - stars->data.F32[index]; // Deviation from fit
+            if (isfinite(dev2)) {
+                chi2 += PS_SQR(dev2) / magErr2;
+                dof++;
+            }
+        }
+    }
+    dof -= numGoodStars + numImages;
+    chi2 /= dof;
+
+    return chi2;
+}
+
+// Determine which images are photometric, based on estimated transparencies
+// Returns number of photometric images, or -1 on error
+static int sourceMatchRelphotPhotometric(psVector *photo, // Photometric determination
+                                         const psVector *trans, // Estimated transparencies
+                                         const psVector *badImage, // Bad image?
+                                         int transIter, // Iterations for transparency
+                                         float transClip, // Clipping level for transparency
+                                         float photoLevel // Level below which we declare photometric
+                                         )
+{
+    psAssert(photo && photo->type.type == PS_TYPE_U8, "Need photometric determination");
+    psAssert(trans && trans->type.type == PS_TYPE_F32, "Need transparencies");
+    psAssert(badImage && badImage->type.type == PS_TYPE_U8, "Need bad image determination");
+
+    int numImages = photo->n;              // Number of images
+
+    psAssert(trans->n == numImages, "Not enough transparencies: %ld", trans->n);
+    psAssert(badImage->n == numImages, "Not enough bad image determinations: %ld", badImage->n);
+    psAssert(transIter >= 0, "Iterations for transparency must be non-negative: %d", transIter);
+    psAssert(transClip > 0, "Clipping level for transparency must be positive: %f", transClip);
+    psAssert(photoLevel > 0, "Photometric level must be positive: %f", photoLevel);
+
+    psStats *stats = psStatsAlloc(PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV); // Statistics
+    stats->clipIter = transIter;
+    stats->clipSigma = transClip;
+
+    if (!psVectorStats(stats, trans, NULL, badImage, 0xFF)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to perform statistics on transparencies.");
+        psFree(stats);
+        return -1;
+    }
+    // XXX handle this case better:
+    if (isnan(stats->clippedMean))  {
+        psError(PS_ERR_UNKNOWN, false, "Unable to perform statistics on transparencies.");
+        psFree(stats);
+        return -1;
+    }
+
+    float thresh = stats->clippedMean + photoLevel * stats->clippedStdev; // Threshold for clouds
+    psFree(stats);
+
+    int numPhoto = 0;                   // Number of photometric images
+    for (int i = 0; i < numImages; i++) {
+        if (badImage->data.U8[i]) {
+            continue;
+        }
+        if (trans->data.F32[i] < thresh) {
+            photo->data.U8[i] = 0xFF;
+            numPhoto++;
+        } else {
+            photo->data.U8[i] = 0;
+        }
+    }
+
+    return numPhoto;
+}
+
+
+// Reject star measurements
+// Returns the fraction of measurements that were rejected
+static float sourceMatchRelphotReject(psVector *trans, // Transparencies
+                                      psVector *stars, // Star magnitudes
+                                      const psArray *matches, // Array of matches
+                                      const psVector *zp, // Zero points for each image
+                                      const psVector *photo, // Photometric image?
+                                      const psVector *badImage, // Bad image?
+                                      float starClip, // Clipping for stars
+                                      float sysErr2 // Systematic error squared
+                                )
+{
+    psAssert(zp && zp->type.type == PS_TYPE_F32, "Need zero points");
+    psAssert(matches, "Need list of matches");
+
+    int numImages = zp->n;              // Number of images
+    int numStars = matches->n;          // Number of stars
+
+    psAssert(trans && trans->type.type == PS_TYPE_F32, "Need transparencies");
+    psAssert(trans->n == numImages, "Not enough transparencies: %ld\n", trans->n);
+    psAssert(stars && stars->type.type == PS_TYPE_F32, "Need star magnitudes");
+    psAssert(stars->n == numStars, "Not enough stars: %ld\n", stars->n);
+    psAssert(zp->n == numImages, "Not enough ZPs: %ld", zp->n);
+    psAssert(!photo || photo->type.type == PS_TYPE_U8, "Photometric determination is wrong type");
+    psAssert(!photo || photo->n == numImages, "Not enough photometric determinations: %ld", photo->n);
+    psAssert(!badImage || badImage->type.type == PS_TYPE_U8, "Photometric determination is wrong type");
+    psAssert(!badImage || badImage->n == numImages, "Not enough bad determinations: %ld", badImage->n);
+
+    starClip = PS_SQR(starClip);
+
+    int numRejected = 0;                // Number rejected
+    int numMeasurements = 0;            // Number of measurements
+    for (int i = 0; i < numStars; i++) {
+        pmSourceMatch *match = matches->data[i]; // Matched stars
+        for (int j = 0; j < match->num; j++) {
+            if (match->mask->data.PS_TYPE_VECTOR_MASK_DATA[j] & PM_SOURCE_MATCH_MASK_PHOT) {
+                continue;
+            }
+            numMeasurements++;
+            int index = match->image->data.U32[j]; // Image index
+            if (badImage->data.U8[index]) {
+                continue;
+            }
+            float mag = match->mag->data.F32[j]; // Measured magnitude
+            float magErr = match->magErr->data.F32[j]; // Error in measured magnitude
+            float cal = zp->data.F32[index]; // Calibration to apply to image
+            if (!photo || !photo->data.U8[index]) {
+                cal -= trans->data.F32[index];
+            }
+            float dev = mag + cal - stars->data.F32[i]; // Deviation
+
+            // only reject detections from photometric images (non-photometric images can
+            // have large errors.  XXX Or: allow a much higher rejection threshold
+            if (photo->data.U8[index]) {
+                if (PS_SQR(dev) > starClip * (PS_SQR(magErr) + sysErr2)) {
+                    numRejected++;
+                    match->mask->data.PS_TYPE_VECTOR_MASK_DATA[j] |= PM_SOURCE_MATCH_MASK_PHOT;
+                }
+            }
+        }
+    }
+
+    return (float)numRejected / (float)numMeasurements;
+}
+
+
+psVector *pmSourceMatchRelphot(const psArray *matches, // Array of matches
+			       psArray *matchedSources, // Array of averaged sources
+                               const psVector *zp, // Zero points for each image (including airmass term)
+                               float tol, // Relative tolerance for convergence
+                               int iter1, // Number of iterations for pass 1
+                               float rej1, // Limit on rejection between iterations for pass 1
+                               float sys1, // Systematic error in measurements for pass 1
+                               int iter2, // Number of iterations for pass 2
+                               float rej2, // Limit on rejection between iterations for pass 2
+                               float sys2, // Systematic error in measurements for pass 2
+                               float rejLimit, // Limit on rejection between iterations
+                               int transIter, // Clipping iterations for transparency
+                               float transClip, // Clipping level for transparency
+                               float photoLevel // Level at which we declare image is photometric
+                               )
+{
+    PS_ASSERT_ARRAY_NON_NULL(matches, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(zp, NULL);
+    PS_ASSERT_VECTOR_TYPE(zp, PS_TYPE_F32, NULL);
+    PS_ASSERT_FLOAT_LARGER_THAN(transClip, 0.0, NULL);
+    PS_ASSERT_ARRAY_NON_NULL(matchedSources, NULL);
+    
+    sys1 *= sys1;
+    sys2 *= sys2;
+
+    int numImages = zp->n;              // Number of images
+    int numStars = matches->n;          // Number of stars
+    psVector *badImage = psVectorAlloc(numImages, PS_TYPE_U8); // Bad image?
+    psVectorInit(badImage, 0);
+
+    // Check for data integrity
+    {
+        psVector *num = psVectorAlloc(numImages, PS_TYPE_S32); // Number of stars per image
+        psVectorInit(num, 0);
+        for (int i = 0; i < numStars; i++) {
+            pmSourceMatch *match = matches->data[i]; // Matched stars
+            for (int j = 0; j < match->num; j++) {
+                int index = match->image->data.U32[j]; // Image index
+                psAssert(index >= 0 && index < numImages, "Bad index: %d", index);
+                num->data.S32[index]++;
+            }
+        }
+        int numGood = 0;                // Number of good images
+        for (int i = 0; i < numImages; i++) {
+            if (num->data.S32[i] == 0 || !isfinite(zp->data.F32[i])) {
+                badImage->data.U8[i] = 0xFF;
+                continue;
+            }
+            numGood++;
+        }
+        psFree(num);
+        if (numGood == 0) {
+            psError(PM_ERR_DATA, true, "No images with good stars.");
+            psFree(badImage);
+            return false;
+        }
+    }
+
+    psVector *trans = psVectorAlloc(numImages, PS_TYPE_F32); // Transparencies for each image, magnitudes
+    psVectorInit(trans, 0.0);
+    psVector *photo = psVectorAlloc(numImages, PS_TYPE_U8); // Photometric determination for each image
+    psVectorInit(photo, 0);
+    psVector *stars = psVectorAlloc(numStars, PS_TYPE_F32); // Magnitudes for each star
+
+    float chi2 = sourceMatchRelphotIterate(trans, stars, badImage, matches, zp,
+                                           photo, sys1); // chi^2 for solution
+    psTrace("psModules.objects", 1, "Initial: chi^2 = %f\n", chi2);
+    float lastChi2 = INFINITY;          // chi^2 on last iteration
+    float fracRej = INFINITY;        // Fraction of measurements rejected
+
+    // In the first passes, the transparencies are not well deteremined: use high systematic error and
+    // rejection thresholds
+    for (int i = 0; i < iter1; i++) {
+
+        // Identify photometric nights
+        int numPhoto = sourceMatchRelphotPhotometric(photo, trans, badImage, transIter, transClip,
+                                                     photoLevel); // Number of photometric images
+        if (numPhoto < 0) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to perform photometric determination");
+            psFree(trans);
+            psFree(photo);
+            psFree(stars);
+            return NULL;
+        }
+        psTrace("psModules.objects", 3, "Pass 1: Determined %d/%d are photometric\n", numPhoto, numImages);
+
+        fracRej = sourceMatchRelphotReject(trans, stars, matches, zp, photo, badImage, rej1, sys1);
+        psTrace("psModules.objects", 3, "Pass 1: %f%% of measurements rejected\n", fracRej * 100);
+
+        chi2 = sourceMatchRelphotIterate(trans, stars, badImage, matches, zp, photo, sys1);
+        psTrace("psModules.objects", 1, "Pass 1: iter = %d: chi^2 = %f rejected = %f\n", i, chi2, fracRej);
+    }
+
+    for (int i = 0; i < iter2 && (fabsf(lastChi2 - chi2) > tol * chi2 || fracRej > rejLimit); i++) {
+        lastChi2 = chi2;
+
+        // Identify photometric nights
+        int numPhoto = sourceMatchRelphotPhotometric(photo, trans, badImage, transIter, transClip,
+                                                     photoLevel); // Number of photometric images
+        if (numPhoto < 0) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to perform photometric determination");
+            psFree(trans);
+            psFree(photo);
+            psFree(stars);
+            return NULL;
+        }
+        psTrace("psModules.objects", 3, "Pass 2: Determined %d/%d are photometric\n", numPhoto, numImages);
+
+        fracRej = sourceMatchRelphotReject(trans, stars, matches, zp, photo, badImage, rej2, sys2);
+        psTrace("psModules.objects", 3, "Pass 2: %f%% of measurements rejected\n", fracRej * 100);
+
+        chi2 = sourceMatchRelphotIterate(trans, stars, badImage, matches, zp, photo, sys2);
+        psTrace("psModules.objects", 1, "Pass 2: iter = %d: chi^2 = %f rejected = %f\n", i, chi2, fracRej);
+    }
+
+    for (int i = 0; i < matches->n; i++) {
+      pmSource *source = pmSourceAlloc();
+      source->psfMag = stars->data.F32[i];
+      psArrayAdd(matchedSources,matchedSources->n,source);
+    }
+    
+    psFree(photo);
+    psFree(badImage);
+    psFree(stars);
+
+    if (fabsf(lastChi2 - chi2) > tol * chi2 || fracRej > rejLimit) {
+        psWarning("Unable to converge to relphot solution (%f,%f)", (lastChi2 - chi2) / chi2, fracRej);
+    }
+
+    return trans;
+}
+
+
+// Iterate on the star positions and image shifts
+// Returns the solution chi^2
+static float sourceMatchRelastroIterate(psVector *xShift, psVector *yShift, // Shift for image
+                                        psVector *xStar, psVector *yStar,   // Position for star
+                                        const psArray *matches // Array of matches
+    )
+{
+    psAssert(matches, "Need list of matches");
+
+    int numImages = xShift->n;          // Number of images
+    int numStars = matches->n;          // Number of stars
+
+    psAssert(xShift && xShift->type.type == PS_TYPE_F32 && yShift && yShift->type.type == PS_TYPE_F32,
+             "Need shifts");
+    psAssert(yShift->n == numImages, "Not enough shifts: %ld\n", yShift->n);
+    psAssert(xStar && xStar->type.type == PS_TYPE_F32 && yStar && yStar->type.type == PS_TYPE_F32,
+             "Need star positions");
+    psAssert(xStar->n == numStars && yStar->n == numStars, "Not enough stars\n");
+
+    // Solve the star positions
+    psVectorInit(xStar, NAN);
+    psVectorInit(yStar, NAN);
+    psVector *starMask = psVectorAlloc(numStars, PS_TYPE_U8); // Mask for stars
+    psVectorInit(starMask, 0xFF);
+    int numGoodStars = 0;               // Number of stars with good measurements
+    for (int i = 0; i < numStars; i++) {
+        pmSourceMatch *match = matches->data[i]; // Matched stars
+        int numMeasurements = 0;        // Number of unmasked measurements for star
+        double xSum = 0.0, ySum = 0.0;  // Accumulators for star
+        for (int j = 0; j < match->num; j++) {
+            if (match->mask->data.PS_TYPE_VECTOR_MASK_DATA[j] & PM_SOURCE_MATCH_MASK_ASTRO) {
+                continue;
+            }
+            numMeasurements++;
+            int index = match->image->data.U32[j]; // Image index
+
+            xSum += match->x->data.F32[j] - xShift->data.F32[index];
+            ySum += match->y->data.F32[j] - yShift->data.F32[index];
+        }
+        if (numMeasurements > 1) {
+            // It's only a good star (contributing to the chi^2) if there's more than 1 measurement
+            numGoodStars++;
+            xStar->data.F32[i] = xSum / numMeasurements;
+            yStar->data.F32[i] = ySum / numMeasurements;
+            starMask->data.U8[i] = 0;
+        }
+    }
+
+    // Solve for the shifts
+    psVectorInit(xShift, 0.0);
+    psVectorInit(yShift, 0.0);
+    psVector *num = psVectorAlloc(numImages, PS_TYPE_S32);    // Number of stars
+    psVectorInit(num, 0);
+    for (int i = 0; i < numStars; i++) {
+        if (starMask->data.U8[i]) {
+            continue;
+        }
+        pmSourceMatch *match = matches->data[i]; // Matched stars
+        for (int j = 0; j < match->num; j++) {
+            if (match->mask->data.PS_TYPE_VECTOR_MASK_DATA[j] & PM_SOURCE_MATCH_MASK_ASTRO) {
+                continue;
+            }
+            int index = match->image->data.U32[j]; // Image index
+
+            xShift->data.F32[index] += match->x->data.F32[j] - xStar->data.F32[i];
+            yShift->data.F32[index] += match->y->data.F32[j] - yStar->data.F32[i];
+            num->data.S32[index]++;
+        }
+    }
+    for (int i = 0; i < numImages; i++) {
+        xShift->data.F32[i] /= num->data.S32[i];
+        yShift->data.F32[i] /= num->data.S32[i];
+        psTrace("psModules.objects", 3, "Shift for image %d: %f,%f\n",
+                i, xShift->data.F32[i], yShift->data.F32[i]);
+    }
+    psFree(num);
+
+    // Once more through to evaluate chi^2
+    float chi2 = 0.0;                   // chi^2 for iteration
+    int dof = 0;                        // Degrees of freedom
+    for (int i = 0; i < numStars; i++) {
+        pmSourceMatch *match = matches->data[i]; // Matched stars
+        if (starMask->data.U8[i]) {
+            continue;
+        }
+        for (int j = 0; j < match->num; j++) {
+            if (match->mask->data.PS_TYPE_VECTOR_MASK_DATA[j]) {
+                continue;
+            }
+
+            int index = match->image->data.U32[j]; // Image index
+            float dx = match->x->data.F32[j] - xShift->data.F32[index] - xStar->data.F32[i];
+            float dy = match->y->data.F32[j] - yShift->data.F32[index] - yStar->data.F32[i];
+
+            chi2 += PS_SQR(dx) + PS_SQR(dy);
+            dof++;
+        }
+    }
+    dof -= numGoodStars + numImages;
+    chi2 /= dof;
+
+    return chi2;
+}
+
+// Reject star measurements
+// Returns the fraction of measurements that were rejected
+static float sourceMatchRelastroReject(const psVector *xShift, const psVector *yShift, // Shifts for each image
+                                       const psVector *xStar, const psVector *yStar, // Positions for each star
+                                       const psArray *matches, // Array of matches
+                                       float chi2,             // chi^2 from fit
+                                       float rej               // Rejection threshold
+                                )
+{
+    psAssert(matches, "Need list of matches");
+
+    int numImages = xShift->n;          // Number of images
+    int numStars = matches->n;          // Number of stars
+
+    psAssert(xShift && xShift->type.type == PS_TYPE_F32 && yShift && yShift->type.type == PS_TYPE_F32,
+             "Need shifts");
+    psAssert(yShift->n == numImages, "Not enough shifts: %ld\n", yShift->n);
+    psAssert(xStar && xStar->type.type == PS_TYPE_F32 && yStar && yStar->type.type == PS_TYPE_F32,
+             "Need star positions");
+    psAssert(xStar->n == numStars && yStar->n == numStars, "Not enough stars\n");
+
+    int numRejected = 0;                // Number rejected
+    int numMeasurements = 0;            // Number of measurements
+
+    float thresh = PS_SQR(rej) * chi2;    // Threshold for rejection
+
+    for (int i = 0; i < numStars; i++) {
+        pmSourceMatch *match = matches->data[i]; // Matched stars
+        for (int j = 0; j < match->num; j++) {
+            if (match->mask->data.PS_TYPE_VECTOR_MASK_DATA[j] & PM_SOURCE_MATCH_MASK_ASTRO) {
+                continue;
+            }
+            numMeasurements++;
+            int index = match->image->data.U32[j]; // Image index
+
+            float dx = match->x->data.F32[j] - xShift->data.F32[index] - xStar->data.F32[i];
+            float dy = match->y->data.F32[j] - yShift->data.F32[index] - yStar->data.F32[i];
+
+            if (PS_SQR(dx) + PS_SQR(dy) > thresh) {
+                numRejected++;
+                match->mask->data.PS_TYPE_VECTOR_MASK_DATA[j] |= PM_SOURCE_MATCH_MASK_ASTRO;
+            }
+        }
+    }
+
+    return (float)numRejected / (float)numMeasurements;
+}
+
+psArray *pmSourceMatchRelastro(const psArray *matches, // Array of matches
+                               int numImages,          // Number of images
+                               float tol, // Relative tolerance for convergence
+                               int iter1, // Number of iterations for pass 1
+                               float rej1, // Limit on rejection between iterations for pass 1
+                               int iter2, // Number of iterations for pass 2
+                               float rej2, // Limit on rejection between iterations for pass 2
+                               float rejLimit // Limit on rejection between iterations
+    )
+{
+    PS_ASSERT_ARRAY_NON_NULL(matches, NULL);
+
+    int numStars = matches->n;          // Number of stars
+    psVector *xShift = psVectorAlloc(numImages, PS_TYPE_F32); // x shift for each image
+    psVector *yShift = psVectorAlloc(numImages, PS_TYPE_F32); // y shift for each image
+    psVectorInit(xShift, 0.0);
+    psVectorInit(yShift, 0.0);
+    psVector *xStar = psVectorAlloc(numStars, PS_TYPE_F32); // x position for each star
+    psVector *yStar = psVectorAlloc(numStars, PS_TYPE_F32); // y position for each star
+
+    float chi2 = sourceMatchRelastroIterate(xShift, yShift, xStar, yStar, matches); // chi^2 for solution
+    psTrace("psModules.objects", 1, "Initial: chi^2 = %f\n", chi2);
+    float lastChi2 = INFINITY;          // chi^2 on last iteration
+    float fracRej = INFINITY;           // Fraction of measurements rejected
+
+    // In the first passes, the shifts are not well deteremined: use high systematic error and
+    // rejection thresholds
+    for (int i = 0; i < iter1; i++) {
+        fracRej = sourceMatchRelastroReject(xShift, yShift, xStar, yStar, matches, chi2, rej1);
+        psTrace("psModules.objects", 3, "Pass 1: %f%% of measurements rejected\n", fracRej * 100);
+
+        chi2 = sourceMatchRelastroIterate(xShift, yShift, xStar, yStar, matches);
+        psTrace("psModules.objects", 1, "Pass 1: iter = %d: chi^2 = %f rejected = %f\n", i, chi2, fracRej);
+    }
+
+    for (int i = 0; i < iter2 && (fabsf(lastChi2 - chi2) > tol * chi2 || fracRej > rejLimit); i++) {
+        lastChi2 = chi2;
+
+        fracRej = sourceMatchRelastroReject(xShift, yShift, xStar, yStar, matches, chi2, rej2);
+        psTrace("psModules.objects", 3, "Pass 2: %f%% of measurements rejected\n", fracRej * 100);
+
+        chi2 = sourceMatchRelastroIterate(xShift, yShift, xStar, yStar, matches);
+        psTrace("psModules.objects", 1, "Pass 2: iter = %d: chi^2 = %f rejected = %f\n", i, chi2, fracRej);
+    }
+
+    psFree(xStar);
+    psFree(yStar);
+
+    if (fabsf(lastChi2 - chi2) > tol * chi2 || fracRej > rejLimit) {
+        psWarning("Unable to converge to relphot solution (%f,%f)", (lastChi2 - chi2) / chi2, fracRej);
+    }
+
+    psArray *results = psArrayAlloc(numImages); // Array of results
+    for (int i = 0; i < numImages; i++) {
+        psVector *offset = results->data[i] = psVectorAlloc(2, PS_TYPE_F32); // Offset for image
+        offset->data.F32[0] = xShift->data.F32[i];
+        offset->data.F32[1] = yShift->data.F32[i];
+    }
+    psFree(xShift);
+    psFree(yShift);
+
+    return results;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceMatch.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceMatch.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceMatch.h	(revision 42651)
@@ -0,0 +1,102 @@
+#ifndef PM_SOURCE_MATCH_H
+#define PM_SOURCE_MATCH_H
+
+/// Mask values for matched sources
+typedef enum {
+    PM_SOURCE_MATCH_MASK_PHOT = 0x01,   // Source was rejected from photometry fit
+    PM_SOURCE_MATCH_MASK_ASTRO = 0x02,     // Source was rejected from astrometry fit
+} pmSourceMatchMask;
+
+/// Matched sources
+///
+/// A match between sources on different images.  The index back to the image and source are provided.
+/// The magnitudes are pulled out, for convenience.
+typedef struct {
+    int num;                            // Number of matches
+    psVector *image;                    // Image index
+    psVector *index;                    // Source index for image
+    psVector *mag;                      // Magnitudes
+    psVector *magErr;                   // Magnitude errors
+    psVector *x, *y;                    // Positions
+    psVector *mask;                     // Mask for measurements
+} pmSourceMatch;
+
+
+// Assert that the source match and its components are non-NULL
+#define PM_ASSERT_SOURCE_MATCH_NON_NULL(MATCH, RVAL) { \
+    if (!MATCH) { \
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Source match %s is NULL.", #MATCH); \
+        return RVAL; \
+    } \
+    PS_ASSERT_VECTOR_NON_NULL((MATCH)->image, RVAL); \
+    PS_ASSERT_VECTOR_NON_NULL((MATCH)->index, RVAL); \
+    PS_ASSERT_VECTOR_NON_NULL((MATCH)->mag, RVAL); \
+    PS_ASSERT_VECTOR_NON_NULL((MATCH)->magErr, RVAL); \
+    PS_ASSERT_VECTOR_NON_NULL((MATCH)->x, RVAL); \
+    PS_ASSERT_VECTOR_NON_NULL((MATCH)->y, RVAL); \
+    PS_ASSERT_VECTOR_SIZE((MATCH)->image, (MATCH)->num, RVAL); \
+    PS_ASSERT_VECTOR_SIZE((MATCH)->index, (MATCH)->num, RVAL); \
+    PS_ASSERT_VECTOR_SIZE((MATCH)->mag, (MATCH)->num, RVAL); \
+    PS_ASSERT_VECTOR_SIZE((MATCH)->magErr, (MATCH)->num, RVAL); \
+    PS_ASSERT_VECTOR_SIZE((MATCH)->x, (MATCH)->num, RVAL); \
+    PS_ASSERT_VECTOR_SIZE((MATCH)->y, (MATCH)->num, RVAL); \
+}
+
+/// Allocator for pmSourceMatch
+pmSourceMatch *pmSourceMatchAlloc(void);
+
+/// Add a source to a match
+void pmSourceMatchAdd(pmSourceMatch *match, // Match data
+                      float mag, float magErr, // Magnitude and error
+                      float x, float y,        // Position
+                      int image, // Image index
+                      int index // Source index
+                      );
+
+
+
+/// Match sources between different images
+///
+/// Returns an array of psSourceMatch
+psArray *pmSourceMatchSources(const psArray *sourceArrays, ///< Array of arrays of sources on each image
+                              float radius, ///< Matching radius
+                              bool cullSingles ///< Cull "matches" with only a single source?
+                              );
+
+/// Merge two source lists
+///
+/// Sources are pulled from the lists into a new list, with no effort made to adjust them.
+psArray *pmSourceMatchMerge(psArray *sourceArrays, ///< Array of arrays of sources on each image
+                            float radius, ///< Matching radius
+                            bool cullSingles ///< Cull "matches" with only a single source?
+    );
+
+/// Perform relative photometry to calibrate images
+psVector *pmSourceMatchRelphot(const psArray *matches, // Array of matches
+			       psArray *matchedSources, // Array of average sources
+                               const psVector *zp, // Zero points for each image (including airmass term)
+                               float tol, // Relative tolerance for convergence
+                               int iter1, // Number of iterations for pass 1
+                               float rej1, // Limit on rejection between iterations for pass 1
+                               float sys1, // Systematic error in measurements for pass 1
+                               int iter2, // Number of iterations for pass 2
+                               float rej2, // Limit on rejection between iterations for pass 2
+                               float sys2, // Systematic error in measurements for pass 2
+                               float rejLimit, // Limit on rejection between iterations
+                               int transIter, // Clipping iterations for transparency
+                               float transClip, // Clipping level for transparency
+                               float photoLevel // Level at which we declare image is photometric
+    );
+
+/// Perform relative astrometry to calibrate images
+psArray *pmSourceMatchRelastro(const psArray *matches, // Array of matches
+                               int numImages,          // Number of images
+                               float tol, // Relative tolerance for convergence
+                               int iter1, // Number of iterations for pass 1
+                               float rej1, // Limit on rejection between iterations for pass 1
+                               int iter2, // Number of iterations for pass 2
+                               float rej2, // Limit on rejection between iterations for pass 2
+                               float rejLimit // Limit on rejection between iterations
+    );
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceMoments.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceMoments.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceMoments.c	(revision 42651)
@@ -0,0 +1,657 @@
+/** @file  pmSource.c
+ *
+ *  Functions to define and manipulate sources on images
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA: significant modifications.
+ *
+ *  @version $Revision: 1.8 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-06 02:31:25 $
+ *
+ *  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>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+
+/******************************************************************************
+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.
+
+Uses the following elements:
+    pmSource
+    pmSource->peak
+    pmSource->pixels
+    pmSource->variance (optional)
+    pmSource->mask (optional)
+
+The peak calculations are done in image coords, not subImage coords.
+
+this version optionally clips input pixels on S/N 
+*****************************************************************************/
+# define VALID_RADIUS(X,Y,RAD2) (((RAD2) >= (PS_SQR(X) + PS_SQR(Y))) ? 1 : 0)
+
+static bool beVerbose = false;
+void pmSourceMomentsSetVerbose(bool state){ beVerbose = state; }
+
+bool pmSourceMomentsHighOrder    (pmSource *source, float radius, float sigma, float minSN, psImageMaskType maskVal);
+bool pmSourceMomentsRadialMoment (pmSource *source, float radius, float minKronRadius, psImageMaskType maskVal);
+bool pmSourceMomentsKronFluxes   (pmSource *source, float sigma,  float minSN, psImageMaskType maskVal);
+
+// if mode & EXTERNAL or mode2 & MATCHED, do not re-calculate the centroid (use peak as centroid)
+bool pmSourceMoments(pmSource *source, float radius, float sigma, float minSN, float minKronRadius, psImageMaskType maskVal)
+{
+    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_FLOAT_LARGER_THAN(radius, 0.0, false);
+
+    if (source->moments == NULL) {
+      source->moments = pmMomentsAlloc();
+    }
+
+    // a note about coordinates: coordinates of objects throughout psphot refer to the primary
+    // image coordinates.  the source->pixels image has an offset relative to its parent of
+    // col0,row0: a pixel (x,y) in the primary image has coordinates of (x-col0, y-row0) in
+    // this subimage.  we subtract off the peak coordinates, adjusted to this subimage, to have
+    // minimal round-off error in the sums.  since these values are subtracted just to minimize
+    // the dynamic range and are added back below, the exact value does not matter. these are
+    // (int) so they can be used in the image index below.
+
+    // XXX // do 2 passes : the first pass should use a somewhat smaller radius and no sigma window to 
+    // XXX // get an unbiased (but probably noisy) centroid
+    // XXX if (!pmSourceMomentsGetCentroid (source, 0.75*radius, 0.0, minSN, maskVal, source->peak->xf, source->peak->yf)) {
+    // XXX 	return false;
+    // XXX }
+    // XXX // second pass applies the Gaussian window and uses the centroid from the first pass
+    // XXX if (!pmSourceMomentsGetCentroid (source, radius, sigma, minSN, maskVal, source->moments->Mx, source->moments->My)) {
+    // XXX 	return false;
+    // XXX }
+
+    // If we use a large radius for the centroid, it will be biased by any neighbors.  The flux
+    // of any object drops pretty quickly outside 1-2 sigmas.  (The exception is bright
+    // saturated stars, for which we need to use a very large radius here)
+    // NOTE: if (mode & EXTERNAL) or (mode2 & MATCHED), do not re-calculate the centroid (use peak as centroid)
+    // (we still call this function because it sets moments->Sum,SN,Peak,nPixels
+    if (!pmSourceMomentsGetCentroid (source, 1.5*sigma, 0.0, minSN, maskVal, source->peak->xf, source->peak->yf)) {
+	return false;
+    }
+
+    pmSourceMomentsHighOrder (source, radius, sigma, minSN, maskVal);
+
+    // now calculate the 1st radial moment (for kron flux) using symmetrical averaging
+    pmSourceMomentsRadialMoment (source, radius, minKronRadius, maskVal);
+
+    // now calculate the kron flux values using the 1st radial moment
+    pmSourceMomentsKronFluxes (source, sigma, minSN, maskVal);
+
+    psTrace ("psModules.objects", 4, "Mrf: %f  KronFlux: %f  Mxx: %f  Mxy: %f  Myy: %f  Mxxx: %f  Mxxy: %f  Mxyy: %f  Myyy: %f  Mxxxx: %f  Mxxxy: %f  Mxxyy: %f  Mxyyy: %f  Mxyyy: %f\n",
+	     source->moments->Mrf,   source->moments->KronFlux, 
+	     source->moments->Mxx,   source->moments->Mxy,   source->moments->Myy,
+	     source->moments->Mxxx,  source->moments->Mxxy,  source->moments->Mxyy,  source->moments->Myyy,
+	     source->moments->Mxxxx, source->moments->Mxxxy, source->moments->Mxxyy, source->moments->Mxyyy, source->moments->Myyyy);
+
+    psTrace ("psModules.objects", 3, "peak %f %f (%f = %f) Mx: %f  My: %f  Sum: %f  Mxx: %f  Mxy: %f  Myy: %f  Npix: %d\n",
+	     source->peak->xf, source->peak->yf, 
+	     source->peak->rawFlux, sqrt(source->peak->detValue), 
+	     source->moments->Mx, source->moments->My, 
+	     source->moments->Sum, 
+	     source->moments->Mxx, source->moments->Mxy, source->moments->Myy, 
+	     source->moments->nPixels);
+
+    return(true);
+}
+
+bool pmSourceMomentsGetCentroid(pmSource *source, float radius, float sigma, float minSN, psImageMaskType maskVal, float xGuess, float yGuess) { 
+
+    // First Pass: calculate the first moments (these are subtracted from the coordinates below)
+    // Sum = SUM (z - sky)
+    // X1  = SUM (x - xc)*(z - sky)
+    // .. etc
+
+    float sky = 0.0;
+
+    float peakPixel = -PS_MAX_F32;
+    psS32 numPixels = 0;
+    float Sum = 0.0;
+    float Var = 0.0;
+    float X1 = 0.0;
+    float Y1 = 0.0;
+    float R2 = PS_SQR(radius);
+    float minSN2 = PS_SQR(minSN);
+    float rsigma2 = 0.5 / PS_SQR(sigma);
+
+    float xPeak = xGuess - source->pixels->col0; // coord of peak in subimage
+    float yPeak = yGuess - source->pixels->row0; // coord of peak in subimage
+
+    // we are guaranteed to have a valid pixel and variance at this location (right? right?)
+    // float weightNorm = source->pixels->data.F32[yPeak][xPeak] / sqrt (source->variance->data.F32[yPeak][xPeak]);
+    // psAssert (isfinite(source->pixels->data.F32[yPeak][xPeak]), "peak must be on valid pixel");
+    // psAssert (isfinite(source->variance->data.F32[yPeak][xPeak]), "peak must be on valid pixel");
+    // psAssert (source->variance->data.F32[yPeak][xPeak] > 0, "peak must be on valid pixel");
+
+    // the moments [Sum(x*f) / Sum(f)] are calculated in pixel index values, and should
+    // not depend on the fractional pixel location of the source.  However, the aperture
+    // (radius) and the Gaussian window (sigma) depend subtly on the fractional pixel
+    // position of the expected centroid
+
+    for (psS32 row = 0; row < source->pixels->numRows ; row++) {
+
+	float yDiff = row + 0.5 - yPeak;
+	if (fabs(yDiff) > radius) continue;
+
+	float *vPix = source->pixels->data.F32[row];
+	float *vWgt = source->variance ? source->variance->data.F32[row] : source->pixels->data.F32[row];
+
+	psImageMaskType *vMsk = (source->maskObj == NULL) ? NULL : source->maskObj->data.PS_TYPE_IMAGE_MASK_DATA[row];
+	// psImageMaskType *vMsk = (source->maskView == NULL) ? NULL : source->maskView->data.PS_TYPE_IMAGE_MASK_DATA[row];
+
+	for (psS32 col = 0; col < source->pixels->numCols ; col++, vPix++, vWgt++) {
+	    if (vMsk) {
+		if (*vMsk & maskVal) {
+		    vMsk++;
+		    continue;
+		}
+		vMsk++;
+	    }
+	    if (isnan(*vPix)) continue;
+
+	    float xDiff = col + 0.5 - xPeak;
+	    if (fabs(xDiff) > radius) continue;
+
+	    // radius is just a function of (xDiff, yDiff)
+	    float r2  = PS_SQR(xDiff) + PS_SQR(yDiff);
+	    if (r2 > R2) continue;
+
+	    float pDiff = *vPix - sky;
+	    float wDiff = *vWgt;
+
+	    // skip pixels below specified significance level.  for a PSFs, this
+	    // over-weights the wings of bright stars compared to those of faint stars.
+	    // for the estimator used for extended source analysis (where the window
+	    // function is allowed to be arbitrarily large), we need to clip to avoid
+	    // negative second moments.
+	    if (PS_SQR(pDiff) < minSN2*wDiff) continue; // 
+	    if ((minSN > 0.0) && (pDiff < 0)) continue; // 
+
+	    // Apply a Gaussian window function.  Be careful with the window function.  S/N
+	    // weighting over weights the sky for faint sources
+	    if (sigma > 0.0) {
+		float z  = r2*rsigma2;
+		assert (z >= 0.0);
+		float weight  = exp(-z);
+
+		wDiff *= weight;
+		pDiff *= weight;
+	    } 
+
+	    Var += wDiff;
+	    Sum += pDiff;
+
+	    float xWght = xDiff * pDiff;
+	    float yWght = yDiff * pDiff;
+
+	    X1  += xWght;
+	    Y1  += yWght;
+
+	    peakPixel = PS_MAX (*vPix, peakPixel);
+	    numPixels++;
+	}
+    }
+
+    // we only update the centroid if the position is not supplied from elsewhere
+    bool skipCentroid = false;
+    skipCentroid |= (source->mode  & PM_SOURCE_MODE_EXTERNAL); // skip externally supplied positions
+    skipCentroid |= (source->mode2 & PM_SOURCE_MODE2_MATCHED); // skip sources defined by other image positions
+
+    // if we have less than (1/4) of the possible pixels (in circle or box), force a retry
+    int minPixels = PS_MIN(0.75*R2, source->pixels->numCols*source->pixels->numRows/4.0);
+
+    // XXX EAM - the limit is a bit arbitrary.  make it user defined?
+    if (numPixels < minPixels) {
+	psTrace ("psModules.objects", 3, "insufficient valid pixels (%d vs %d; %f) for source\n", numPixels, minPixels, Sum);
+	return (false);
+    }
+
+    // only skip negative sum sources if the sources are not forced
+    if (!skipCentroid && (Sum <= 0)) {
+	psTrace ("psModules.objects", 3, "insufficient significant pixels (%d vs %d; %f) for source\n", numPixels, minPixels, Sum);
+	return (false);
+    }
+
+    // calculate the first moment.
+    float Mx = X1/Sum;
+    float My = Y1/Sum;
+    if ((fabs(Mx) > 2.0) || (fabs(My) > 2.0)) {
+	psTrace ("psModules.objects", 3, " big centroid swing; ok peak? %d, %d\n", source->peak->x, source->peak->y);
+    }
+    psTrace ("psModules.objects", 5, "id: %d, sky: %f  Mx: %f  My: %f  Sum: %f  X1: %f  Y1: %f  Npix: %d\n", source->id, sky, Mx, My, Sum, X1, Y1, numPixels);
+
+    // add back offset of peak in primary image
+    // also offset from pixel index to pixel coordinate
+    // (the calculation above uses pixel index instead of coordinate)
+    // 0.5 PIX: moments are calculated using the pixel index and converted here to pixel coords
+
+    if (skipCentroid) {
+	source->moments->Mx = source->peak->xf;
+	source->moments->My = source->peak->yf;
+    } else {
+	if ((fabs(Mx) > radius) || (fabs(My) > radius)) {
+	    psTrace ("psModules.objects", 3, "extreme centroid swing; invalid peak %d, %d\n", source->peak->x, source->peak->y);
+	    return (false);
+	}
+	source->moments->Mx = Mx + xGuess;
+	source->moments->My = My + yGuess;
+    }
+
+    source->moments->Sum = Sum;
+    source->moments->SN  = Sum / sqrt(Var);
+    source->moments->Peak = peakPixel;
+    source->moments->nPixels = numPixels;
+
+    return true;
+}
+
+float pmSourceMinKronRadius(psArray *sources, float PSF_SN_LIM) {
+
+    psVector *radii = psVectorAllocEmpty(100, PS_TYPE_F32);
+
+    for (int i = 0; i < sources->n; i++) {
+	pmSource *src = sources->data[i]; // Source of interest
+	if (!src || !src->moments) {
+	    continue;
+	}
+
+	if (src->mode & PM_SOURCE_MODE_BLEND) {
+	    continue;
+	}
+
+	if (!src->moments->nPixels) continue;
+
+	if (src->moments->SN < PSF_SN_LIM) continue;
+
+	// XXX put in Mxx,Myy cut based on clump location
+
+	psVectorAppend(radii, src->moments->Mrf);
+    }
+
+    // find the peak in this image
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN);
+
+    if (!psVectorStats (stats, radii, NULL, NULL, 0)) {
+	psError(PS_ERR_UNKNOWN, false, "Unable to get image statistics.\n");
+	psFree(stats);
+	return NAN;
+    }
+
+    float minRadius = stats->sampleMedian;
+
+    psFree(radii);
+    psFree(stats);
+    return minRadius;
+}
+
+bool pmSourceMomentsHighOrder (pmSource *source, float radius, float sigma, float minSN, psImageMaskType maskVal) {
+
+    // this function assumes the sky has been well-subtracted for the image
+    float Sum = 0.0;
+    float R2 = PS_SQR(radius);
+    float minSN2 = PS_SQR(minSN);
+    float rsigma2 = 0.5 / PS_SQR(sigma);
+
+    // Now calculate higher-order moments, using the above-calculated first moments to adjust coordinates
+    // Xn  = SUM (x - xc)^n * (z - sky) -- note that sky is 0.0 by definition here
+    float XX = 0.0;
+    float XY = 0.0;
+    float YY = 0.0;
+    float XXX = 0.0;
+    float XXY = 0.0;
+    float XYY = 0.0;
+    float YYY = 0.0;
+    float XXXX = 0.0;
+    float XXXY = 0.0;
+    float XXYY = 0.0;
+    float XYYY = 0.0;
+    float YYYY = 0.0;
+
+    float Xo = source->moments->Mx;
+    float Yo = source->moments->My;
+
+    // center of mass in subimage.  Note: the calculation below uses pixel index, so we correct
+    // xCM, yCM from pixel coords to pixel index here.
+    float xCM = Xo - 0.5 - source->pixels->col0; // coord of peak in subimage
+    float yCM = Yo - 0.5 - source->pixels->row0; // coord of peak in subimage
+
+    // calculate the higher-order moments using Xo,Yo
+    for (psS32 row = 0; row < source->pixels->numRows ; row++) {
+
+	float yDiff = row - yCM;
+	if (fabs(yDiff) > radius) continue;
+
+	float *vPix = source->pixels->data.F32[row];
+	float *vWgt = source->variance ? source->variance->data.F32[row] : source->pixels->data.F32[row];
+
+	psImageMaskType  *vMsk = (source->maskObj == NULL) ? NULL : source->maskObj->data.PS_TYPE_IMAGE_MASK_DATA[row];
+
+	for (psS32 col = 0; col < source->pixels->numCols ; col++, vPix++, vWgt++) {
+	    if (vMsk) {
+		if (*vMsk & maskVal) {
+		    vMsk++;
+		    continue;
+		}
+		vMsk++;
+	    }
+	    if (isnan(*vPix)) continue;
+
+	    float xDiff = col - xCM;
+	    if (fabs(xDiff) > radius) continue;
+
+	    // radius is just a function of (xDiff, yDiff)
+	    float r2  = PS_SQR(xDiff) + PS_SQR(yDiff);
+	    if (r2 > R2) continue;
+
+	    float fDiff = *vPix;
+	    float pDiff = fDiff;
+	    float wDiff = *vWgt;
+
+	    // skip pixels below specified significance level.  this is allowed, but should be
+	    // avoided -- the over-weights the wings of bright stars compared to those of faint
+	    // stars.
+	    if (PS_SQR(pDiff) < minSN2*wDiff) continue;
+	    if ((minSN > 0.0) && (pDiff < 0)) continue; 
+
+	    // Apply a Gaussian window function.  Be careful with the window function.  S/N
+	    // weighting over-weights the sky for faint sources
+	    if (sigma > 0.0) {
+		float z = r2 * rsigma2;
+		assert (z >= 0.0);
+		float weight  = exp(-z);
+
+		wDiff *= weight;
+		pDiff *= weight;
+	    } 
+
+	    Sum += pDiff;
+
+	    float r = sqrt(r2);
+
+	    float x = xDiff * pDiff;
+	    float y = yDiff * pDiff;
+
+	    float xx = xDiff * x;
+	    float xy = xDiff * y;
+	    float yy = yDiff * y;
+
+	    // the factor of 1/r makes these appropriate for measuring
+	    // \sum r^2 cos(3 theta), sin(3 theta) (psphotImageQuality.c:107)
+	    float xxx = xDiff * xx / r;
+	    float xxy = xDiff * xy / r;
+	    float xyy = xDiff * yy / r;
+	    float yyy = yDiff * yy / r;
+
+	    // the factor of 1/r^2 makes these appropriate for measuring
+	    // \sum r^2 cos(4 theta), sin(4 theta) (psphotImageQuality.c:107)
+	    float xxxx = xDiff * xxx / r2;
+	    float xxxy = xDiff * xxy / r2;
+	    float xxyy = xDiff * xyy / r2;
+	    float xyyy = xDiff * yyy / r2;
+	    float yyyy = yDiff * yyy / r2;
+
+	    XX  += xx;
+	    XY  += xy;
+	    YY  += yy;
+
+	    XXX  += xxx;
+	    XXY  += xxy;
+	    XYY  += xyy;
+	    YYY  += yyy;
+
+	    XXXX  += xxxx;
+	    XXXY  += xxxy;
+	    XXYY  += xxyy;
+	    XYYY  += xyyy;
+	    YYYY  += yyyy;
+	}
+    }
+    // NOT needed : source->moments->wSum = Sum;
+
+    source->moments->Mxx = XX/Sum;
+    source->moments->Mxy = XY/Sum;
+    source->moments->Myy = YY/Sum;
+
+    source->moments->Mxxx = XXX/Sum;
+    source->moments->Mxxy = XXY/Sum;
+    source->moments->Mxyy = XYY/Sum;
+    source->moments->Myyy = YYY/Sum;
+
+    source->moments->Mxxxx = XXXX/Sum;
+    source->moments->Mxxxy = XXXY/Sum;
+    source->moments->Mxxyy = XXYY/Sum;
+    source->moments->Mxyyy = XYYY/Sum;
+    source->moments->Myyyy = YYYY/Sum;
+
+    return true;
+}
+
+bool pmSourceMomentsRadialMoment (pmSource *source, float radius, float minKronRadius, psImageMaskType maskVal) {
+
+
+    float **vPix = source->pixels->data.F32;
+    psImageMaskType **vMsk = (source->maskObj == NULL) ? NULL : source->maskObj->data.PS_TYPE_IMAGE_MASK_DATA;
+
+    float RF = 0.0;
+    float RH = 0.0;
+    float RS = 0.0;
+
+    // centroid around which to calculate the moments
+    float Xo = source->moments->Mx;
+    float Yo = source->moments->My;
+
+    // center of mass in subimage.  Note: the calculation below uses pixel index, so we correct
+    // xCM, yCM from pixel coords to pixel index here.
+    float xCM = Xo - 0.5 - source->pixels->col0; // coord of peak in subimage
+    float yCM = Yo - 0.5 - source->pixels->row0; // coord of peak in subimage
+
+    float R2 = PS_SQR(radius);
+
+    for (psS32 row = 0; row < source->pixels->numRows ; row++) {
+
+	float yDiff = row - yCM;
+	if (fabs(yDiff) > radius) continue;
+
+	// coordinate of mirror pixel
+	int yFlip = yCM - yDiff;
+	if (yFlip < 0) continue;
+	if (yFlip >= source->pixels->numRows) continue;
+
+	for (psS32 col = 0; col < source->pixels->numCols ; col++) {
+	    // check mask and value for this pixel
+	    if (vMsk && (vMsk[row][col] & maskVal)) continue;
+	    if (isnan(vPix[row][col])) continue;
+
+	    float xDiff = col - xCM;
+	    if (fabs(xDiff) > radius) continue;
+
+	    // coordinate of mirror pixel
+	    int xFlip = xCM - xDiff;
+	    if (xFlip < 0) continue;
+	    if (xFlip >= source->pixels->numCols) continue;
+
+	    // check mask and value for mirror pixel
+	    if (vMsk && (vMsk[yFlip][xFlip] & maskVal)) continue;
+	    if (isnan(vPix[yFlip][xFlip])) continue;
+
+	    // radius is just a function of (xDiff, yDiff)
+	    float r2  = PS_SQR(xDiff) + PS_SQR(yDiff);
+	    if (r2 > R2) continue;
+
+	    float fDiff1 = vPix[row][col];
+	    float fDiff2 = vPix[yFlip][xFlip];
+	    float pDiff = (fDiff1 > 0.0) ? sqrt(fabs(fDiff1*fDiff2)) : -sqrt(fabs(fDiff1*fDiff2));
+
+	    // Kron Flux uses the 1st radial moment (NOT Gaussian windowed?)
+	    float r = sqrt(r2);
+	    float rf = r * pDiff;
+	    float rh = sqrt(r) * pDiff;
+	    float rs = 0.5 * (fDiff1 + fDiff2);
+
+	    RF  += rf;
+	    RH  += rh;
+	    RS  += rs;
+	}
+    }
+
+    source->moments->Mrh = RH/RS;
+
+    // if Mrf = RF/RS (first radial moment) is very small, we are getting into low-significance
+    // territory.  saturate at minKronRadius.  conversely, if Mrf is >> radius for faint
+    // sources, we are clearly making an error.  saturate at radius.
+    float kronRefRadius = MAX(minKronRadius, RF/RS);
+    if (source->moments->SN < 10) {
+	kronRefRadius = MIN(radius, kronRefRadius);
+    }
+
+    // if source is externally supplied and it already has a finite Mrf do not change it
+    if (! ((source->mode & PM_SOURCE_MODE_EXTERNAL) && isfinite(source->moments->Mrf))) {
+        source->moments->Mrf = kronRefRadius;
+    }
+
+    return true;
+}
+
+bool pmSourceMomentsKronFluxes (pmSource *source, float sigma, float minSN, psImageMaskType maskVal) {
+
+    float **vPix = source->pixels->data.F32;
+    float **vWgt = source->variance ? source->variance->data.F32 : source->pixels->data.F32;
+    psImageMaskType **vMsk = (source->maskObj == NULL) ? NULL : source->maskObj->data.PS_TYPE_IMAGE_MASK_DATA;
+
+    float radKinner = 1.0*source->moments->Mrf;
+    float radKron   = 2.5*source->moments->Mrf;
+    float radKouter = 4.0*source->moments->Mrf;
+
+    int nKronPix = 0;
+    int nCorePix = 0;
+    int nInner = 0;
+    int nOuter = 0;
+    
+    float Sum = 0.0;
+    float Var = 0.0;
+    float SumCore = 0.0;
+    float VarCore = 0.0;
+    float SumInner = 0.0;
+    float SumOuter = 0.0;
+
+    // centroid around which to calculate the moments
+    float Xo = source->moments->Mx;
+    float Yo = source->moments->My;
+
+    // center of mass in subimage.  Note: the calculation below uses pixel index, so we correct
+    // xCM, yCM from pixel coords to pixel index here.
+    float xCM = Xo - 0.5 - source->pixels->col0; // coord of peak in subimage
+    float yCM = Yo - 0.5 - source->pixels->row0; // coord of peak in subimage
+
+    float minSN2 = PS_SQR(minSN);
+
+    // calculate the Kron flux, and related fluxes (NO symmetrical averaging)
+    for (psS32 row = 0; row < source->pixels->numRows ; row++) {
+	
+	float yDiff = row - yCM;
+	if (fabs(yDiff) > radKouter) continue;
+	
+	for (psS32 col = 0; col < source->pixels->numCols ; col++) {
+	    // check mask and value for this pixel
+	    if (vMsk && (vMsk[row][col] & maskVal)) continue;
+	    if (isnan(vPix[row][col])) continue;
+	    
+	    float xDiff = col - xCM;
+	    if (fabs(xDiff) > radKouter) continue;
+	    
+	    // radKron is just a function of (xDiff, yDiff)
+	    float r2  = PS_SQR(xDiff) + PS_SQR(yDiff);
+
+	    float fDiff1 = vPix[row][col];
+	    float pDiff = fDiff1;
+	    float wDiff = vWgt[row][col];
+				    
+	    // skip pixels below specified significance level.  this is allowed, but should be
+	    // avoided -- the over-weights the wings of bright stars compared to those of faint
+	    // stars.
+	    if (PS_SQR(pDiff) < minSN2*wDiff) continue;
+	    
+	    float r  = sqrt(r2);
+	    if (r < radKron) {
+		Sum += pDiff;
+		Var += wDiff;
+		nKronPix ++;
+	    }
+
+	    // use sigma (fixed by psf) not a radKron based value
+	    if (r < sigma) {
+		SumCore += pDiff;
+		VarCore += wDiff;
+		nCorePix ++;
+	    }
+
+	    if ((r > radKinner) && (r < radKron)) {
+		SumInner += pDiff;
+		nInner ++;
+	    }
+	    if ((r > radKron)  && (r < radKouter)) {
+		SumOuter += pDiff;
+		nOuter ++;
+	    }
+	}
+    }
+    // *** should I rescale these fluxes by pi R^2 / nNpix?
+    // XXX source->moments->KronCore    = SumCore       * M_PI *  PS_SQR(sigma) / nCorePix;
+    // XXX source->moments->KronCoreErr = sqrt(VarCore) * M_PI *  PS_SQR(sigma) / nCorePix;
+    // XXX source->moments->KronFlux    = Sum           * M_PI *  PS_SQR(radKron) / nKronPix;
+    // XXX source->moments->KronFluxErr = sqrt(Var)     * M_PI *  PS_SQR(radKron) / nKronPix;
+    // XXX source->moments->KronFinner  = SumInner      * M_PI * (PS_SQR(radKron)   - PS_SQR(radKinner)) / nInner;
+    // XXX source->moments->KronFouter  = SumOuter      * M_PI * (PS_SQR(radKouter) -   PS_SQR(radKron)) / nOuter;
+
+    source->moments->KronCore    = SumCore;
+    source->moments->KronCoreErr = sqrt(VarCore);
+    source->moments->KronFlux    = Sum;
+    source->moments->KronFluxErr = sqrt(Var);
+    source->moments->KronFinner  = SumInner;
+    source->moments->KronFouter  = SumOuter;
+
+    // XXX not sure I should save this here...
+    source->moments->KronFluxPSF    = source->moments->KronFlux;
+    source->moments->KronFluxPSFErr = source->moments->KronFluxErr;
+    source->moments->KronRadiusPSF  = source->moments->Mrf;
+
+    return true;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceOutputs.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceOutputs.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceOutputs.c	(revision 42651)
@@ -0,0 +1,290 @@
+#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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+
+#include "pmSourceIO.h"
+#include "pmSourceOutputs.h"
+
+bool pmSourceOutputsCommonValues (float *magOffset, float *zeroptErr, float *fwhmMajor, float *fwhmMinor, pmReadout *readout, psMetadata *header) {
+
+    pmFPA  *fpa  = readout->parent->parent->parent;
+
+    bool status1 = false;
+    bool status2 = false;
+    float exptime   = psMetadataLookupF32 (&status1, fpa->concepts, "FPA.EXPOSURE");
+    float zeropt    = psMetadataLookupF32(&status2, fpa->concepts, "FPA.ZP");
+    if (!isfinite(zeropt)) {
+        zeropt    = psMetadataLookupF32 (&status2, header, "ZPT_OBS");
+    }
+    if (status1 && status2 && (exptime > 0.0)) {
+        *magOffset = zeropt + 2.5*log10(exptime);
+    }
+    *zeroptErr = psMetadataLookupF32 (&status2, header, "ZPT_ERR");
+
+    // we need a measure of the image quality (FWHM) for this image, in order to get the positional errors
+    *fwhmMajor = psMetadataLookupF32(&status1, readout->analysis, "FWHM_MAJ");
+    if (status1) goto got_major;
+
+    *fwhmMajor = psMetadataLookupF32(&status1, header, "FWHM_MAJ");
+    if (status1) goto got_major;
+
+    *fwhmMajor = psMetadataLookupF32(&status1, readout->analysis, "IQ_FW1");
+    if (status1) goto got_major;
+
+    *fwhmMajor = psMetadataLookupF32(&status1, header, "IQ_FW1");
+    if (status1) goto got_major;
+
+    *fwhmMajor = 5.0; // XXX just a guess!
+
+    got_major:
+
+    *fwhmMinor = psMetadataLookupF32(&status1, readout->analysis, "FWHM_MIN");
+    if (status1) goto got_minor;
+    *fwhmMinor = psMetadataLookupF32(&status1, header, "FWHM_MIN");
+    if (status1) goto got_minor;
+
+    *fwhmMinor = psMetadataLookupF32(&status1, readout->analysis, "IQ_FW2");
+    if (status1) goto got_minor;
+    *fwhmMinor = psMetadataLookupF32(&status1, header, "IQ_FW2");
+    if (status1) goto got_minor;
+
+    *fwhmMinor = 5.0; // XXX just a guess!
+
+    got_minor:
+    return true;
+
+}
+
+// what is the correct postion?
+// * if we have a PSF model fit, use PM_PAR_XPOS,YPOS for X_PSF,Y_PSF 
+// * if we do not have a model:
+// ** if we have moments:
+// *** if the star is saturated, use the moments
+// *** if the moments and peak agree to < DR, use the moments
+// *** otherwise, use the peak
+
+bool pmSourceOutputsSetValues (pmSourceOutputs *outputs, pmSource *source, pmChip *chip, float fwhmMajor, float fwhmMinor, float magOffset) {
+
+    psF32 *PAR, *dPAR;
+    psEllipseAxes axes;
+
+    // no difference between PSF and non-PSF model
+    pmModel *model = source->modelPSF;
+
+    if (model != NULL) {
+	PAR = model->params->data.F32;
+	dPAR = model->dparams->data.F32;
+	outputs->xPos = PAR[PM_PAR_XPOS];
+	outputs->yPos = PAR[PM_PAR_YPOS];
+	if ((source->mode & PM_SOURCE_MODE_NONLINEAR_FIT) && !(source->mode & PM_SOURCE_MODE_EXTMODEL)) {
+	    // we only do non-linear PSF fits for non-extended objects
+	    outputs->xErr = dPAR[PM_PAR_XPOS];
+	    outputs->yErr = dPAR[PM_PAR_YPOS];
+	} else {
+	    outputs->xErr = fwhmMajor * source->psfMagErr / 2.35;
+	    outputs->yErr = fwhmMinor * source->psfMagErr / 2.35;
+	}
+	if (isfinite(PAR[PM_PAR_SXX]) && isfinite(PAR[PM_PAR_SXY]) && isfinite(PAR[PM_PAR_SYY])) {
+	    axes = pmPSF_ModelToAxes (PAR, model->class->useReff);
+	    outputs->psfMajor = axes.major;
+	    outputs->psfMinor = axes.minor;
+	    outputs->psfTheta = axes.theta*PS_DEG_RAD;
+
+	    // some models (PS1_V1, QGAUSS) have an extra 'core' parameter
+	    outputs->psfCore = NAN;
+	    if (model->type == pmModelClassGetType ("PS_MODEL_PS1_V1")) {
+		outputs->psfCore = PAR[PM_PAR_7];
+	    }
+	    if (model->type == pmModelClassGetType ("PS_MODEL_QGAUSS")) {
+		outputs->psfCore = PAR[PM_PAR_7];
+	    }
+	    if (model->type == pmModelClassGetType ("PS_MODEL_HSC_V1")) {
+		outputs->psfCore = PAR[PM_PAR_7];
+	    }
+
+	    outputs->psfMajorFWHM = model->class->modelSetFWHM(model->params, axes.major);
+	    outputs->psfMinorFWHM = model->class->modelSetFWHM(model->params, axes.minor);
+	} else {
+	    outputs->psfMajor = NAN;
+	    outputs->psfMinor = NAN;
+	    outputs->psfTheta = NAN;
+	    outputs->psfCore = NAN;
+	}
+	outputs->chisq = model->chisq;
+	outputs->nDOF = model->nDOF;
+	outputs->nPix = model->nPix;
+    } else {
+	bool useMoments = pmSourcePositionUseMoments(source);
+
+	if (useMoments) {
+	    outputs->xPos = source->moments->Mx;
+	    outputs->yPos = source->moments->My;
+	    outputs->xErr = fwhmMajor * source->psfMagErr / 2.35;
+	    outputs->yErr = fwhmMinor * source->psfMagErr / 2.35;
+	} else {
+	    outputs->xPos = source->peak->xf;
+	    outputs->yPos = source->peak->yf;
+	    outputs->xErr = source->peak->dx;
+	    outputs->yErr = source->peak->dy;
+	}
+	outputs->psfMajor = NAN;
+	outputs->psfMinor = NAN;
+	outputs->psfTheta = NAN;
+	outputs->psfCore = NAN;
+	outputs->chisq = NAN;
+	outputs->nDOF = 0;
+	outputs->nPix = 0;
+    }
+
+    outputs->calMag = isfinite(magOffset) ? source->psfMag + magOffset : NAN;
+    outputs->peakMag = (source->peak->rawFlux > 0) ? -2.5*log10(source->peak->rawFlux) : NAN;
+
+    psSphere ptSky = {0.0, 0.0, 0.0, 0.0};
+    float posAngle = 0.0;
+    float pltScale = 0.0;
+    pmSourceLocalAstrometry (&ptSky, &posAngle, &pltScale, chip, outputs->xPos, outputs->yPos);
+
+    outputs->posAngle = posAngle*PS_DEG_RAD;	   
+    outputs->pltScale = pltScale*PS_DEG_RAD*3600.0;
+
+    outputs->ra = ptSky.r*PS_DEG_RAD;
+    outputs->dec = ptSky.d*PS_DEG_RAD;
+
+    return true;
+}
+
+bool pmSourceOutputsSetMoments (pmSourceOutputsMoments *moments, pmSource *source) {
+
+    // distinguish moments measure from window vs S/N > XX ??
+    moments->Mxx = source->moments ? source->moments->Mxx : NAN;
+    moments->Mxy = source->moments ? source->moments->Mxy : NAN;
+    moments->Myy = source->moments ? source->moments->Myy : NAN;
+    moments->M_c3 = source->moments ? 1.0*source->moments->Mxxx - 3.0*source->moments->Mxyy : NAN;
+    moments->M_s3 = source->moments ? 3.0*source->moments->Mxxy - 1.0*source->moments->Myyy : NAN;
+    moments->M_c4 = source->moments ? 1.0*source->moments->Mxxxx - 6.0*source->moments->Mxxyy + 1.0*source->moments->Myyyy : NAN;
+    moments->M_s4 = source->moments ? 4.0*source->moments->Mxxxy - 4.0*source->moments->Mxyyy : NAN;
+    moments->Mrf  = source->moments ? source->moments->Mrf : NAN;
+    moments->Mrh  = source->moments ? source->moments->Mrh : NAN;
+    moments->Krf  = source->moments ? source->moments->KronFlux : NAN;
+    moments->dKrf = source->moments ? source->moments->KronFluxErr : NAN;
+    moments->Kinner = source->moments ? source->moments->KronFinner : NAN;
+    moments->Kouter = source->moments ? source->moments->KronFouter : NAN;
+    moments->KronCore    = source->moments ? source->moments->KronCore : NAN;
+    moments->KronCoreErr = source->moments ? source->moments->KronCoreErr : NAN;
+    moments->KronPSF    = source->moments ? source->moments->KronFluxPSF : NAN;
+    moments->KronPSFErr = source->moments ? source->moments->KronFluxPSFErr : NAN;
+
+    return true;
+}
+
+bool pmSourceLocalAstrometry (psSphere *ptSky, float *posAngle, float *pltScale, pmChip *chip, float xPos, float yPos) {
+
+    pmFPA *fpa = chip->parent;
+
+    if (!chip->toFPA) goto escape;
+    if (!fpa->toTPA) goto escape;
+    if (!fpa->toSky) goto escape;
+
+    // generate RA,DEC
+    psPlane ptCH, ptFP, ptTP_o, ptTP_x, ptTP_y;
+
+    // calculate the astrometry for the coordinate of interest
+    ptCH.x = xPos;
+    ptCH.y = yPos;
+    psPlaneTransformApply (&ptFP, chip->toFPA, &ptCH);
+    psPlaneTransformApply (&ptTP_o, fpa->toTPA, &ptFP);
+    psDeproject (ptSky, &ptTP_o, fpa->toSky);
+
+    // calculate the astrometry for the coordinate + 1pix in X
+    ptCH.x = xPos + 1.0;
+    ptCH.y = yPos;
+    psPlaneTransformApply (&ptFP, chip->toFPA, &ptCH);
+    psPlaneTransformApply (&ptTP_x, fpa->toTPA, &ptFP);
+
+    // calculate the astrometry for the coordinate + 1pix in Y
+    ptCH.x = xPos;
+    ptCH.y = yPos + 1.0;
+    psPlaneTransformApply (&ptFP, chip->toFPA, &ptCH);
+    psPlaneTransformApply (&ptTP_y, fpa->toTPA, &ptFP);
+
+    // the resulting Tangent Plane coordinates are in TP pixels; convert to local Tangent Plane
+    // degrees
+
+    float dTPx_dCHx = fpa->toSky->Xs * (ptTP_x.x - ptTP_o.x);
+    float dTPy_dCHx = fpa->toSky->Ys * (ptTP_x.y - ptTP_o.y);
+
+    float dTPx_dCHy = fpa->toSky->Xs * (ptTP_y.x - ptTP_o.x);
+    float dTPy_dCHy = fpa->toSky->Ys * (ptTP_y.y - ptTP_o.y);
+
+    float pltScale_x = hypot(dTPx_dCHx, dTPy_dCHx);
+    float pltScale_y = hypot(dTPx_dCHy, dTPy_dCHy);
+    *pltScale = 0.5*(pltScale_x + pltScale_y);
+
+    float posAngle_x, posAngle_y;
+    float crossProduct = dTPx_dCHx * dTPy_dCHy - dTPx_dCHy * dTPy_dCHx;
+    if  (crossProduct > 0.) {
+      *pltScale *= -1.0;
+      posAngle_x = atan2 (dTPy_dCHx, dTPx_dCHx);
+      posAngle_y = atan2 (dTPy_dCHy, dTPx_dCHy) - M_PI_2;
+    } else {
+      posAngle_x = atan2 (dTPy_dCHx, -dTPx_dCHx);
+      posAngle_y = atan2 (dTPy_dCHy,  dTPx_dCHy) - M_PI_2;
+    }
+
+    // with errors, these may end up on opposite sides of the M_PI boundary.  
+    if (posAngle_x - posAngle_y > M_PI) {
+      posAngle_y += 2.0 * M_PI;
+    }
+    if (posAngle_y - posAngle_x > M_PI) {
+      posAngle_x += 2.0 * M_PI;
+    }
+    *posAngle = 0.5*(posAngle_x + posAngle_y);
+
+    return true;
+
+escape:
+    // no astrometry calibration, give up
+    ptSky->r = NAN;
+    ptSky->d = NAN;
+    *posAngle = NAN;
+    *pltScale = NAN;
+
+    return false;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceOutputs.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceOutputs.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceOutputs.h	(revision 42651)
@@ -0,0 +1,66 @@
+/* @file  pmSourceOutputs.h
+ * @brief functions to perform common I/O conversions
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.20 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-16 22:30:50 $
+ * Copyright 2011 Institute for Astronomy, University of Hawaii
+ *
+ */
+
+# ifndef PM_SOURCE_OUTPUTS_H
+# define PM_SOURCE_OUTPUTS_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+typedef struct {
+    float xPos;
+    float yPos;
+    float xErr;
+    float yErr;
+    double ra;
+    double dec;
+    float psfMajor;
+    float psfMinor;
+    float psfTheta;
+    float psfCore;
+    float psfMajorFWHM;
+    float psfMinorFWHM;
+    float chisq;
+    int nPix;
+    int nDOF;
+    float calMag;
+    float peakMag;
+    float posAngle;
+    float pltScale;
+} pmSourceOutputs;
+
+typedef struct {
+    float Mxx;
+    float Mxy;
+    float Myy;
+    float M_c3;
+    float M_s3;
+    float M_c4;
+    float M_s4;
+    float Mrf;
+    float Mrh;
+    float Krf;
+    float dKrf;
+    float Kinner;
+    float Kouter;
+    float KronCore;
+    float KronCoreErr;
+    float KronPSF;
+    float KronPSFErr;
+} pmSourceOutputsMoments;
+
+bool pmSourceOutputsCommonValues (float *magOffset, float *zeroptErr, float *fwhmMajor, float *fwhmMinor, pmReadout *readout, psMetadata *header);
+
+bool pmSourceOutputsSetValues (pmSourceOutputs *outputs, pmSource *source, pmChip *chip, float fwhmMajor, float fwhmMinor, float magOffset);
+
+bool pmSourceOutputsSetMoments (pmSourceOutputsMoments *moments, pmSource *source);
+
+# endif
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourcePhotometry.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourcePhotometry.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourcePhotometry.c	(revision 42651)
@@ -0,0 +1,1344 @@
+/** @file  pmSourcePhotometry.c
+ *
+ *  @author EAM, IfA; GLG, MHPCC
+ *
+ *  @version $Revision: 1.50 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-16 22:28:54 $
+ *
+ *  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 "pmConfigMask.h"
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+
+#include "pmSourcePhotometry.h"
+
+# define DO_SKY 0
+
+static float AP_MIN_SN = 0.0;
+
+// make this a bit more clever and dynamic
+static psImageMaskType maskSuspect   = 0;
+static psImageMaskType maskSpike     = 0;
+static psImageMaskType maskStarCore  = 0;
+static psImageMaskType maskBurntool  = 0;
+static psImageMaskType maskConvPoor  = 0;
+static psImageMaskType maskGhost     = 0;
+static psImageMaskType maskGlint     = 0;
+static psImageMaskType maskCrosstalk = 0;
+static psImageMaskType maskCTE       = 0;
+
+bool pmSourceMagnitudesInit (pmConfig *config, psMetadata *recipe)
+{
+    PS_ASSERT_PTR_NON_NULL(recipe, false);
+    bool status;
+
+    // we are going to test specially against these poor values
+    if (config) {
+	maskSpike     = pmConfigMaskGet("SPIKE", config);
+	maskStarCore  = pmConfigMaskGet("STARCORE", config);
+	maskBurntool  = pmConfigMaskGet("BURNTOOL", config);
+	maskConvPoor  = pmConfigMaskGet("CONV.POOR", config);
+	maskGhost     = pmConfigMaskGet("GHOST", config);
+	maskGlint     = pmConfigMaskGet("GHOST", config);
+	maskCrosstalk = pmConfigMaskGet("CROSSTALK", config);
+	maskCTE       = pmConfigMaskGet("CTE", config);
+	maskSuspect   = maskSpike | maskStarCore | maskBurntool | maskConvPoor;
+    }
+
+    float limit = psMetadataLookupF32 (&status, recipe, "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 option is selected (mode & PM_SOURCE_PHOT_GROWTH)
+   - psfMag : all sources with non-NULL modelPSF
+   : is optionally corrected for aperture residual if:
+   - the option is selected (mode & PM_SOURCE_PHOT_APCORR)
+   - extMag : all sources with non-NULL modelEXT
+**/
+
+// XXX masked region should be (optionally) elliptical
+// if mode is PM_SOURCE_PHOT_PSFONLY, we skip all other magnitudes
+bool pmSourceMagnitudes (pmSource *source, pmPSF *psf, pmSourcePhotometryMode mode, psImageMaskType maskVal, psImageMaskType markVal, float radius)
+{
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    // PS_ASSERT_PTR_NON_NULL(psf, false);
+
+    int status = false;
+    float x, y;
+    float SN;
+
+    source->psfMag    = NAN;
+    source->extMag    = NAN;
+    source->psfMagErr = NAN;
+    source->apMag     = NAN;
+    source->apMagRaw  = NAN;
+    source->apFlux    = NAN;
+    source->apFluxErr = NAN;
+
+    pmModelStatus badModel = PM_MODEL_STATUS_NONE;
+    badModel |= PM_MODEL_STATUS_BADARGS;
+    badModel |= PM_MODEL_STATUS_OFFIMAGE;
+    badModel |= PM_MODEL_STATUS_NAN_CHISQ;
+    badModel |= PM_MODEL_SERSIC_PCM_FAIL_GUESS;
+    badModel |= PM_MODEL_SERSIC_PCM_FAIL_GRID;
+    badModel |= PM_MODEL_PCM_FAIL_GUESS;
+
+    // XXXXXX review:
+    // Select the 'best' model -- this is used for PSF_QF,_PERFECT & ???. isPSF is true if this
+    // object is a PSF (not extended).  We must have a valid model.  XXX NOTE: allow aperture
+    // magnitudes for sources without a model
+
+    // select the psf model
+    pmModel *modelPSF = source->modelPSF;
+    if (modelPSF == NULL) {
+        psTrace ("psModules.objects", 3, "fail mag : no valid PSF model");
+        return false;
+    }
+
+    // get the error on the PSF model magnitude
+    if (modelPSF->dparams->data.F32[PM_PAR_I0] > 0) {
+        SN = fabs(modelPSF->params->data.F32[PM_PAR_I0] / modelPSF->dparams->data.F32[PM_PAR_I0]);
+        source->psfMagErr = 1.0 / SN;
+    } else {
+        SN = NAN;
+        source->psfMagErr = NAN;
+    }
+    // the source position is used to recenter the aperture for ap photometry
+    x = modelPSF->params->data.F32[PM_PAR_XPOS];
+    y = modelPSF->params->data.F32[PM_PAR_YPOS];
+
+    // measure PSF model photometry
+    status = pmSourcePhotometryModel (&source->psfMag, &source->psfFlux, modelPSF);
+    source->psfFluxErr = fabs(source->psfFlux * source->psfMagErr);
+
+# if (0)
+    // XXX NOTE: old code to use the flux scale.  test & turn this back on?  if so, need to save with psf model
+    // 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);
+    psAssert (isfinite(fluxScale), "how can the flux scale be invalid? source at %d, %d\n", source->peak->x, source->peak->y);
+    psAssert (fluxScale > 0.0, "how can the flux scale be negative? source at %d, %d\n", source->peak->x, source->peak->y);
+    source->psfFlux = fluxScale * modelPSF->params->data.F32[PM_PAR_I0];
+    source->psfFluxErr = fluxScale * modelPSF->dparams->data.F32[PM_PAR_I0];
+    source->psfMag = -2.5*log10(source->psfFlux);
+# endif
+
+    if (mode == PM_SOURCE_PHOT_PSFONLY) {
+	return true;
+    }
+
+    // get the EXT model photometry (all EXT models)
+    // if we have a collection of model fits, check if one of them is a pointer to modelEXT
+    if (source->modelFits) {
+        bool foundEXT = false;
+        for (int i = 0; i < source->modelFits->n; i++) {
+            pmModel *model = source->modelFits->data[i];
+	    if (model->flags & badModel) continue;
+            status = pmSourcePhotometryModel (&model->mag, NULL, model);
+            if (model == source->modelEXT) foundEXT = true;
+	    float SN = fabs(model->params->data.F32[PM_PAR_I0] / model->dparams->data.F32[PM_PAR_I0]);
+	    model->magErr = 1.0 / SN;
+        }
+        if (foundEXT) {
+            source->extMag = source->modelEXT->mag;
+        } else {
+            status = pmSourcePhotometryModel (&source->extMag, NULL, source->modelEXT);
+        }
+    } else {
+        if (source->modelEXT) {
+            status = pmSourcePhotometryModel (&source->extMag, NULL, source->modelEXT);
+        }
+    }
+
+    // Correct psfMag to match aperture magnitude system (NOTE : Growth curve is already applied to ApTrend)
+    if ((mode & PM_SOURCE_PHOT_APCORR) && 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;
+	source->psfFlux *= pow(10.0, -0.4*apTrend);
+	source->psfFluxErr *= pow(10.0, -0.4*apTrend);
+    }
+
+    // measure the contribution of included pixels to the PSF model fit
+    if (mode & PM_SOURCE_PHOT_WEIGHT) {
+        pmSourcePixelWeight (source, modelPSF, source->maskObj, maskVal, radius);
+    }
+
+    // measure the contribution of included pixels
+    if (mode & PM_SOURCE_PHOT_DIFFSTATS) {
+        pmSourceMeasureDiffStats (source, maskVal, markVal);
+    }
+
+    pmSourceNeighborFlags (source);
+
+    // measure the aperture magnitude, if (SN > AP_MIN_SN)
+    if (!isfinite(SN)) {
+        psTrace ("psModules.objects", 3, "fail mag : bad SN: %f (limit: %f)", SN, AP_MIN_SN);
+        return false;
+    }
+
+    // measure the aperture magnitude, if (SN > AP_MIN_SN)
+    if (SN < AP_MIN_SN) {
+        psTrace ("psModules.objects", 3, "skip ap mag : SN < limit : %f vs %f)", SN, AP_MIN_SN);
+        return true;
+    }
+
+    // if we measure aperture magnitudes, the source must not currently be subtracted!
+    psAssert (!(source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED), "cannot measure ap mags if source is subtracted!");
+
+    // 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)
+    psImage *flux = NULL;
+    psImage *variance = NULL;
+    psImage *mask = NULL; // Star flux and mask images, to photometer
+    if (mode & PM_SOURCE_PHOT_INTERP) {
+        float dx = 0.5 - x + (int)x;
+        float dy = 0.5 - y + (int)y;
+        x += dx;
+        y += dy;
+
+        // if (!psImageShiftMask(&flux, &mask, source->pixels, source->maskObj, maskVal, dx, dy, NAN, 0xff, PS_INTERPOLATE_LANCZOS2)) {
+	// if (!psImageShiftMask(&flux, &mask, source->pixels, source->maskObj, maskVal, dx, dy, NAN, 0xff, PS_INTERPOLATE_BIQUADRATIC)) {
+	if (!psImageShiftMask(&flux, &mask, source->pixels, source->maskObj, maskVal, dx, dy, NAN, 0xff, PS_INTERPOLATE_BILINEAR)) {
+            // Not much we can do about it
+            psErrorClear();
+            psTrace ("psModules.objects", 3, "fail shift");
+            return false;
+        }
+	// NOTE : previous to r36860, we failed to pass a variance to pmSourcePhotometryAperSource, making apFluxErr wrong.
+	// I should interpolate the source variance to match the signal, but failing to do so only makes a tiny 
+	// (~1-2%) error in the variance, and thus only a small error in the error on the aperture flux.  
+        variance = source->variance;
+    } else {
+        flux = source->pixels;
+        variance = source->variance;
+        mask = source->maskObj;
+    }
+
+    // measure object aperture photometry
+    status = pmSourcePhotometryAperSource (source, modelPSF, flux, variance, mask, maskVal);
+    if (!status) {
+        psTrace ("psModules.objects", 3, "fail mag : bad Ap Mag");
+    }
+
+    // for PSFs, correct both apMag and psfMag to same system, consistent with infinite flux star in aperture RADIUS
+    // if the aper mag is NAN, the flux < 0.  this can happen for sources near the
+    // detection limits (esp near bright neighbors)
+    source->apMag = source->apMagRaw;
+    if (isfinite (source->apMag) && psf) {
+        if (psf->growth && (mode & PM_SOURCE_PHOT_GROWTH)) {
+	    float apOffset = pmGrowthCurveCorrect (psf->growth, source->apRadius);
+            source->apMag = source->apMagRaw + apOffset;
+	    source->apFlux *= pow(10.0, -0.4*apOffset);
+	    source->apFluxErr *= pow(10.0, -0.4*apOffset);
+        }
+    }
+    if (mode & PM_SOURCE_PHOT_INTERP) {
+        psFree(flux);
+        psFree(mask);
+    }
+
+    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)
+
+*/
+
+bool pmSourceNeighborFlags (pmSource *source) {
+
+    return false;
+
+    // source must have a peak to have a footprint
+    if (!source) return false;
+    if (!source->peak) return false;
+    if (!source->peak->footprint) return false;
+    if (!source->peak->footprint->peaks) return false;
+    if (!source->peak->footprint->peaks->n) return false;
+
+    // find the brightest peak (first peak)
+    pmPeak *brightPeak = source->peak->footprint->peaks->data[0];
+
+    // are we the brightest peak?
+    if (source->peak == brightPeak) return true;
+
+    // if not, raise a flag:
+    source->mode2 |= PM_SOURCE_MODE2_HAS_BRIGHTER_NEIGHBOR;
+
+    // but, this is a common situation.  more interesting is if the ratio flux_n / (r^2 flux_o) is large
+
+    float radius2 = PS_SQR(source->peak->xf - brightPeak->xf) + PS_SQR(source->peak->yf - brightPeak->yf);
+
+    float ratio = brightPeak->rawFlux / (source->peak->rawFlux * radius2);
+
+    if (ratio > 1) {
+	source->mode2 |= PM_SOURCE_MODE2_BRIGHT_NEIGHBOR_1;
+    }
+
+    if (ratio > 10) {
+	source->mode2 |= PM_SOURCE_MODE2_BRIGHT_NEIGHBOR_10;
+    }
+
+    return true;
+}
+
+// return source model magnitude
+bool pmSourcePhotometryModel (float *fitMag, float *fitFlux, pmModel *model)
+{
+    psAssert (fitMag || fitFlux, "at least one of magnitude or flux must be requested (not NULL)");
+    if (model == NULL) return false;
+
+    float mag  = NAN;
+    float flux = NAN;
+
+    // measure fitMag
+    flux = model->class->modelFlux (model->params);
+    if (flux > 0) {
+        mag = -2.5*log10(flux);
+    }
+    if (fitMag) {
+        *fitMag = mag;
+    }
+    if (fitFlux) {
+        *fitFlux = flux;
+    }
+
+    if (flux <= 0) return false;
+    if (!isfinite(flux)) return false;
+
+    return (true);
+}
+
+// return source aperture magnitude
+bool pmSourcePhotometryAperSource (pmSource *source, pmModel *model, psImage *image, psImage *variance, psImage *mask, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(image, false);
+    PS_ASSERT_PTR_NON_NULL(mask, false);
+
+    if (DO_SKY) {
+	PS_ASSERT_PTR_NON_NULL(model, false);
+    }
+
+    bool status;
+    status = pmSourcePhotometryAper(&source->apNpixels, &source->apMagRaw, &source->apFlux, &source->apFluxErr, model, image, variance, mask, maskVal);
+    if (status) {
+	source->mode |= PM_SOURCE_MODE_AP_MAGS;
+    }
+    return status;
+}
+
+// return source aperture magnitude
+bool pmSourcePhotometryAper (int *nPixOut, float *apMag, float *apFluxOut, float *apFluxErr, pmModel *model, psImage *image, psImage *variance, psImage *mask, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(apMag, false);
+    PS_ASSERT_PTR_NON_NULL(image, false);
+    PS_ASSERT_PTR_NON_NULL(mask, false);
+
+    if (DO_SKY) {
+	PS_ASSERT_PTR_NON_NULL(model, false);
+    }
+
+    float sky = 0;
+    float apFlux = 0;
+    float apFluxVar = 0;
+    int nPix = 0;
+
+    if (DO_SKY) {
+        sky = model->params->data.F32[PM_PAR_SKY];
+    }
+
+    psF32 **imData = image->data.F32;
+    psImageMaskType **mkData = mask->data.PS_TYPE_IMAGE_MASK_DATA;
+    psF32 **varData = (variance) ? variance->data.F32 : image->data.F32; // if variance is not supplied, assume gain of 1.0, no read noise
+
+    // measure apFlux and apFluxVar, save apMag if not NAN
+    // XXX note that these fluxes/mags are uncorrected for masked pixels
+    // XXX raise a bit if the aperture has a masked pixel (not marked)?
+    for (int iy = 0; iy < image->numRows; iy++) {
+	for (int ix = 0; ix < image->numCols; ix++) {
+            if (mkData[iy][ix] & maskVal) continue;
+            apFlux += imData[iy][ix] - sky;
+            apFluxVar += varData[iy][ix];
+	    nPix ++;
+        }
+    }
+    
+    if (apFluxOut) *apFluxOut = apFlux;
+    if (apFluxErr) *apFluxErr = sqrt(fabs(apFluxVar));
+    if (nPixOut) *nPixOut = nPix;
+
+    if (apFlux <= 0) {
+        *apMag = NAN;
+    } else {
+	*apMag = -2.5*log10(apFlux);
+    }
+    return true;
+}
+
+// return source aperture magnitude
+bool pmSourcePixelWeight (pmSource *source, pmModel *model, psImage *mask, psImageMaskType maskVal, float radius)
+{
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    source->pixWeightNotBad = NAN;
+    source->pixWeightNotPoor = NAN;
+
+    PS_ASSERT_PTR_NON_NULL(mask, false);
+    PS_ASSERT_PTR_NON_NULL(model, false);
+
+    float modelSum = 0;
+    float notBadSum = 0;
+    float notPoorSum = 0;
+    float sky = 0;
+    float value;
+
+    float spikeSum = 0;
+    float starcoreSum = 0;
+    float burntoolSum = 0;
+    float convpoorSum = 0;
+    float ghostSum = 0;
+    float cteSum = 0;
+
+    int Xo, Yo, dP;
+    int dX, DX, NX;
+    int dY, DY, NY;
+
+    float radius2 = PS_SQR(radius);
+
+    // 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 - mask->col0;
+    dP = mask->numCols - dX;
+    DX = PS_MAX(dX, dP);
+    NX = mask->numCols;
+
+    dY = Yo - mask->row0;
+    dP = mask->numRows - dY;
+    DY = PS_MAX(dY, dP);
+    NY = mask->numRows;
+
+    psImageMaskType maskBad = maskVal;
+    maskBad &= ~maskSuspect;
+
+    psImageMaskType maskPoor = maskVal | maskSuspect;
+
+    // measure modelSum and validSum.  this function is applied to a sources' subimage.  the
+    // value of DX is chosen (see above) to cover the full possible size of the subimage if it
+    // were not by an edge; ie, if the source is cut in half by an image edge, we correctly
+    // count the virtual pixels off the edge in normalizing the value of the pixWeight
+
+    // we skip any pixels [real or virtual] outside of the specified radius (nominally the aperture radius)
+    for (int ix = -DX; ix < DX + 1; ix++) {
+	if (ix > radius) continue;
+        int mx = ix + dX;
+        for (int iy = -DY; iy < DY + 1; iy++) {
+	    if (iy > radius) continue;
+	    if (ix*ix + iy*iy > radius2) continue;
+            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 = fabs(model->class->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;
+
+	    // count pixels which are masked only with bad pixels
+            if (!(mask->data.PS_TYPE_IMAGE_MASK_DATA[my][mx] & maskBad)) {
+		notBadSum += value;
+	    }
+
+	    // count pixels which are masked with an mask bit (bad or poor)
+            if (!(mask->data.PS_TYPE_IMAGE_MASK_DATA[my][mx] & maskPoor)) {
+		notPoorSum += value;
+	    }
+
+	    // count pixels which are masked with an mask bit (bad or poor)
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[my][mx] & maskSpike) {
+		spikeSum += value;
+	    }
+	    // count pixels which are masked with an mask bit (bad or poor)
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[my][mx] & maskStarCore) {
+		starcoreSum += value;
+	    }
+	    // count pixels which are masked with an mask bit (bad or poor)
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[my][mx] & maskGhost) {
+		ghostSum += value;
+	    }
+	    // count pixels which are masked with an mask bit (bad or poor)
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[my][mx] & maskBurntool) {
+		burntoolSum += value;
+	    }
+	    // count pixels which are masked with an mask bit (bad or poor)
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[my][mx] & maskConvPoor) {
+		convpoorSum += value;
+	    }
+	    // count pixels which are masked with an mask bit (bad or poor)
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[my][mx] & maskCTE) {
+		cteSum += value;
+	    }
+
+        }
+    }
+    psFree (coord);
+
+    source->pixWeightNotBad  = notBadSum  / modelSum;
+    source->pixWeightNotPoor = notPoorSum / modelSum;
+
+    if ((spikeSum/modelSum) > 0.25) {
+	source->mode2 |= PM_SOURCE_MODE2_ON_SPIKE;
+    }
+    if ((starcoreSum/modelSum) > 0.25) {
+	source->mode2 |= PM_SOURCE_MODE2_ON_STARCORE;
+    }
+    if ((ghostSum/modelSum) > 0.25) {
+	source->mode2 |= PM_SOURCE_MODE2_ON_GHOST;
+    }
+    if ((burntoolSum/modelSum) > 0.25) {
+	source->mode2 |= PM_SOURCE_MODE2_ON_BURNTOOL;
+    }
+    if ((convpoorSum/modelSum) > 0.25) {
+	source->mode2 |= PM_SOURCE_MODE2_ON_CONVPOOR;
+    }
+    if ((cteSum/modelSum) > 0.25) {
+	source->mode2 |= PM_SOURCE_MODE2_ON_CTE;
+    }
+
+    if (isfinite(source->pixWeightNotBad) && isfinite(source->pixWeightNotPoor)) {
+	psAssert (source->pixWeightNotPoor <= source->pixWeightNotBad, "error: all bad pixels should also be poor");
+    }
+
+    // Check that if the peak is on/off a ghost, glint, or diffraction spike.  In regular IPP
+    // processing, these values are only set in the image mask after the 'camera' stage
+
+    int xChip = source->peak->x;
+    int yChip = source->peak->y;
+
+    // need to access the parent if we are looking at a subimage (likely)
+    psImage *chipImage = (source->pixels == NULL) ? source->pixels : (psImage *) source->pixels->parent;
+
+    bool onChip = true;
+    onChip &= (xChip >= 0);
+    onChip &= (xChip < chipImage->numCols);
+    onChip &= (yChip >= 0);
+    onChip &= (yChip < chipImage->numRows);
+    if (!onChip) {
+	// if the source is off the edge of the chip, raise a different bit?
+	source->mode |= PM_SOURCE_MODE_OFF_CHIP;
+    } else {
+	int xMask = xChip - mask->col0;
+	int yMask = yChip - mask->row0;
+	psImageMaskType maskValue = mask->data.PS_TYPE_IMAGE_MASK_DATA[yMask][xMask];
+	if (maskValue & maskGhost) {
+	    source->mode |= PM_SOURCE_MODE_ON_GHOST;
+	}
+	pmSourceMode PM_SOURCE_MODE_ON_GLINT = PM_SOURCE_MODE_ON_GHOST;
+	if (maskValue & maskGlint) {
+	    source->mode |= PM_SOURCE_MODE_ON_GLINT;
+	}
+	if (maskValue & maskSpike) {
+	    source->mode |= PM_SOURCE_MODE_ON_SPIKE;
+	}
+	if (maskValue & maskCrosstalk) {
+	    source->mode2 |= PM_SOURCE_MODE2_ON_CROSSTALK;
+	}
+    }
+    return (true);
+}
+
+// return source aperture magnitude
+bool pmSourceMaskEval (pmSource *source, psImage *mask, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    source->pixWeightNotBad = NAN;
+    source->pixWeightNotPoor = NAN;
+
+    PS_ASSERT_PTR_NON_NULL(mask, false);
+
+    float modelSum = 0;
+    float spikeSum = 0;
+    float starcoreSum = 0;
+    float burntoolSum = 0;
+    float convpoorSum = 0;
+    float ghostSum = 0;
+    float cteSum = 0;
+
+    int Xo, Yo, dP;
+    int dX, DX, NX;
+    int dY, DY, NY;
+
+    float radius=10.;
+    float radius2 = PS_SQR(radius);
+
+    // the model function returns the source flux at a position
+    psVector *coord = psVectorAlloc(2, PS_TYPE_F32);
+
+    Xo = source->peak->x;
+    Yo = source->peak->y;
+
+    dX = Xo - mask->col0;
+    dP = mask->numCols - dX;
+    DX = PS_MAX(dX, dP);
+    NX = mask->numCols;
+
+    dY = Yo - mask->row0;
+    dP = mask->numRows - dY;
+    DY = PS_MAX(dY, dP);
+    NY = mask->numRows;
+
+    psImageMaskType maskBad = maskVal;
+    maskBad &= ~maskSuspect;
+
+    // were not by an edge; ie, if the source is cut in half by an image edge, we correctly
+    // count the virtual pixels off the edge in normalizing the value of the pixWeight
+
+    // we skip any pixels [real or virtual] outside of the specified radius (nominally the aperture radius)
+    for (int ix = -DX; ix < DX + 1; ix++) {
+	if (ix > radius) continue;
+        int mx = ix + dX;
+        for (int iy = -DY; iy < DY + 1; iy++) {
+	    if (iy > radius) continue;
+	    if (ix*ix + iy*iy > radius2) continue;
+            int my = iy + dY;
+
+            coord->data.F32[0] = (psF32) (ix + Xo);
+            coord->data.F32[1] = (psF32) (iy + Yo);
+
+            modelSum += 1.;
+            // 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;
+
+	    // count pixels which are masked with an mask bit (bad or poor)
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[my][mx] & maskSpike) {
+		spikeSum += 1.;
+	    }
+	    // count pixels which are masked with an mask bit (bad or poor)
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[my][mx] & maskStarCore) {
+		starcoreSum += 1.;
+	    }
+	    // count pixels which are masked with an mask bit (bad or poor)
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[my][mx] & maskGhost) {
+		ghostSum += 1.;
+	    }
+	    // count pixels which are masked with an mask bit (bad or poor)
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[my][mx] & maskBurntool) {
+		burntoolSum += 1.;
+	    }
+	    // count pixels which are masked with an mask bit (bad or poor)
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[my][mx] & maskConvPoor) {
+		convpoorSum += 1.;
+	    }
+	    // count pixels which are masked with an mask bit (bad or poor)
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[my][mx] & maskCTE) {
+                cteSum += 1.;
+            }
+        }
+    }
+    psFree (coord);
+
+    if ((spikeSum/modelSum) > 0.25) {
+	source->mode2 |= PM_SOURCE_MODE2_ON_SPIKE;
+    }
+    if ((starcoreSum/modelSum) > 0.25) {
+	source->mode2 |= PM_SOURCE_MODE2_ON_STARCORE;
+    }
+    if ((ghostSum/modelSum) > 0.25) {
+	source->mode2 |= PM_SOURCE_MODE2_ON_GHOST;
+    }
+    if ((burntoolSum/modelSum) > 0.25) {
+	source->mode2 |= PM_SOURCE_MODE2_ON_BURNTOOL;
+    }
+    if ((convpoorSum/modelSum) > 0.25) {
+	source->mode2 |= PM_SOURCE_MODE2_ON_CONVPOOR;
+    }
+    if ((cteSum/modelSum) > 0.25) {
+	source->mode2 |= PM_SOURCE_MODE2_ON_CTE;
+    }
+
+    // Check that if the peak is on/off a ghost, glint, or diffraction spike.  In regular IPP
+    // processing, these values are only set in the image mask after the 'camera' stage
+
+    // need to access the parent if we are looking at a subimage (likely)
+    psImage *chipImage = (source->pixels == NULL) ? source->pixels : (psImage *) source->pixels->parent;
+
+    bool onChip = true;
+    onChip &= (Xo >= 0);
+    onChip &= (Xo < chipImage->numCols);
+    onChip &= (Yo >= 0);
+    onChip &= (Yo < chipImage->numRows);
+    if (!onChip) {
+	// if the source is off the edge of the chip, raise a different bit?
+	source->mode |= PM_SOURCE_MODE_OFF_CHIP;
+    } else {
+	int xMask = Xo - mask->col0;
+	int yMask = Yo - mask->row0;
+	psImageMaskType maskValue = mask->data.PS_TYPE_IMAGE_MASK_DATA[yMask][xMask];
+	if (maskValue & maskGhost) {
+	    source->mode |= PM_SOURCE_MODE_ON_GHOST;
+	}
+	pmSourceMode PM_SOURCE_MODE_ON_GLINT = PM_SOURCE_MODE_ON_GHOST;
+	if (maskValue & maskGlint) {
+	    source->mode |= PM_SOURCE_MODE_ON_GLINT;
+	}
+	if (maskValue & maskCrosstalk) {
+	    source->mode2 |= PM_SOURCE_MODE2_ON_CROSSTALK;
+	}
+	if (maskValue & maskSpike) {
+	    source->mode |= PM_SOURCE_MODE_ON_SPIKE;
+	}
+    }
+    return (true);
+}
+
+# define FLUX_LIMIT 3.0
+
+// measure stats that may be used in difference images for distinguishing real sources from bad residuals
+bool pmSourceMeasureDiffStats (pmSource *source, psImageMaskType maskVal, psImageMaskType markVal)
+{
+    PS_ASSERT_PTR_NON_NULL(source, false);
+
+    if (source->diffStats == NULL) {
+        source->diffStats = pmSourceDiffStatsAlloc();
+    }
+
+    float fGood = 0.0;
+    float fBad  = 0.0;
+    int   nGood = 0;
+    int   nMask = 0;
+    int   nBad  = 0;
+
+    psImage *flux     = source->pixels;
+    psImage *variance = source->variance;
+    psImage *mask     = source->maskObj;
+
+    if (!flux || !variance || !mask) {
+        return false;
+    }
+
+    // NOTE: until 2010.10.01, these measurements included a 3sigma-per-pixel significance
+    // this followed what we understood as the definition given to us
+    // by Armin, but it always seemed a poor idea -- a faint source is unlikely to have any 3sigma pixels.
+    // changed to remove the per-pixel filter.
+
+    for (int iy = 0; iy < flux->numRows; iy++) {
+        for (int ix = 0; ix < flux->numCols; ix++) {
+	    // only count up the stats in the unmarked region (ie, the aperture)
+	    // skip the marked pixels; these are not relevant
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] & markVal) {
+                continue;
+            }
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] & maskVal) {
+                nMask ++;
+                continue;
+            }
+
+            float value = flux->data.F32[iy][ix];
+
+            if (value > 0.0) {
+                nGood ++;
+                fGood += fabs(value);
+            } else {
+                nBad ++;
+                fBad += fabs(value);
+            }
+        }
+    }
+
+    source->diffStats->nGood      = nGood;
+    source->diffStats->fRatio     = (fGood + fBad         == 0.0) ? NAN : fGood / (fGood + fBad);
+    source->diffStats->nRatioBad  = (nGood + nBad         == 0)   ? NAN : nGood / (float) (nGood + nBad);
+    source->diffStats->nRatioMask = (nGood + nMask        == 0)   ? NAN : nGood / (float) (nGood + nMask);
+    source->diffStats->nRatioAll  = (nGood + nMask + nBad == 0)   ? NAN : nGood / (float) (nGood + nMask + nBad);
+
+    return (true);
+}
+
+# if (0)
+double pmSourceCrossProduct (const pmSource *Mi,
+                             const pmSource *Mj,
+                             const bool unweighted_sum) // should the cross product be weighted?
+{
+    PS_ASSERT_PTR_NON_NULL(Mi, NAN);
+    PS_ASSERT_PTR_NON_NULL(Mj, NAN);
+
+    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->variance;
+    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.PS_TYPE_IMAGE_MASK_DATA[yi][xi])
+                continue;
+            if (Tj->data.PS_TYPE_IMAGE_MASK_DATA[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?
+{
+    PS_ASSERT_PTR_NON_NULL(Mi, NAN);
+    PS_ASSERT_PTR_NON_NULL(Mj, NAN);
+
+    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->variance;
+    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.PS_TYPE_IMAGE_MASK_DATA[yi][xi])
+                continue;
+            if (Tj->data.PS_TYPE_IMAGE_MASK_DATA[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?
+{
+    PS_ASSERT_PTR_NON_NULL(Mi, NAN);
+    double flux = 0, wt = 0, factor = 0;
+
+    const psImage *Pi = Mi->pixels;
+    assert (Pi != NULL);
+    const psImage *Wi = Mi->variance;
+    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.PS_TYPE_IMAGE_MASK_DATA[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
+
+// determine chisq, nPix, nDOF, chisqNorm : model->nPar must be set
+bool pmSourceChisq (pmModel *model, psImage *image, psImage *mask, psImage *variance, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(model, false);
+    PS_ASSERT_PTR_NON_NULL(image, false);
+    PS_ASSERT_PTR_NON_NULL(mask, false);
+    PS_ASSERT_PTR_NON_NULL(variance, false);
+
+    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.PS_TYPE_IMAGE_MASK_DATA[j][i] & maskVal)
+                continue;
+            if (variance->data.F32[j][i] <= 0)
+                continue;
+            dC += PS_SQR (image->data.F32[j][i]) / variance->data.F32[j][i];
+            Npix ++;
+        }
+    }
+    model->nPix = Npix;
+    model->nDOF = Npix - model->nPar;
+    model->chisq = dC;
+    model->chisqNorm = dC / model->nDOF;
+
+    return (true);
+}
+
+
+// return source aperture magnitude
+bool pmSourceChisqUnsubtracted (pmSource *source, pmModel *model, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(model, false);
+
+    float dC = 0.0;
+    int Npix = 0;
+
+    // the model function returns the source flux at a position
+    psVector *coord = psVectorAlloc(2, PS_TYPE_F32);
+
+    psVector *params = model->params;
+    psImage  *image = source->pixels;
+    psImage  *mask = source->maskObj;
+    psImage  *variance = source->variance;
+
+    int dX = image->col0;
+    int dY = image->row0;
+
+    for (int iy = 0; iy < image->numRows; iy++) {
+        for (int ix = 0; ix < image->numCols; ix++) {
+
+	    // skip pixels which are masked
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] & maskVal) continue;
+
+            if (variance->data.F32[iy][ix] <= 0) continue;
+
+            coord->data.F32[0] = (psF32) ix + dX + 0.5;
+            coord->data.F32[1] = (psF32) iy + dY + 0.5;
+
+            // for the full model, add all points
+            float value = model->class->modelFunc (NULL, params, coord);
+
+	    // fprintf (stderr, "%d, %d : %f, %f : %f - %f : %f\n", 
+	    // ix, iy, coord->data.F32[0], coord->data.F32[1], image->data.F32[iy][ix], value, dC);
+
+            dC += PS_SQR (image->data.F32[iy][ix] - value) / variance->data.F32[iy][ix];
+            Npix ++;
+        }
+    }
+    model->nPix = Npix;
+    model->nDOF = Npix - model->nPar;
+    model->chisq = dC;
+    model->chisqNorm = dC / model->nDOF;
+
+    psFree (coord);
+    return (true);
+}
+
+bool pmSourceChisqModelFlux (pmSource *source, pmModel *model, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(model, false);
+
+    float dC = 0.0;
+    int Npix = 0;
+
+    psVector *params = model->params;
+    psImage  *image = source->pixels;
+    psImage  *modelFlux = source->modelFlux;
+    psImage  *mask = source->maskObj;
+    psImage  *variance = source->variance;
+
+    float Io = params->data.F32[PM_PAR_I0];
+
+    for (int iy = 0; iy < image->numRows; iy++) {
+        for (int ix = 0; ix < image->numCols; ix++) {
+
+	    // skip pixels which are masked
+            if (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] & maskVal) continue;
+
+            if (variance->data.F32[iy][ix] <= 0) continue;
+
+            dC += PS_SQR (image->data.F32[iy][ix] - Io*modelFlux->data.F32[iy][ix]) / variance->data.F32[iy][ix];
+            Npix ++;
+        }
+    }
+    model->nPix = Npix;
+    model->nDOF = Npix - model->nPar;
+    model->chisq = dC;
+    model->chisqNorm = dC / model->nDOF;
+
+    return (true);
+}
+
+double pmSourceModelWeight(const pmSource *Mi, int term, const pmSourceFitVarMode fitVarMode, const float covarFactor, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(Mi, NAN);
+    double flux = 0;
+    double wt = 1.0;
+    double factor = 0;
+
+    const psImage *Pi = Mi->modelFlux;
+    assert (Pi != NULL);
+
+    const psImage *Wi = NULL;
+    switch (fitVarMode) {
+      case PM_SOURCE_PHOTFIT_CONST:
+	break;
+      case PM_SOURCE_PHOTFIT_IMAGE_VAR:
+      case PM_SOURCE_PHOTFIT_MODEL_SKY:
+	Wi = Mi->variance;
+	psAssert (Wi, "programming error");
+	break;
+      case PM_SOURCE_PHOTFIT_MODEL_VAR:
+	Wi = Mi->modelVar;
+	psAssert (Wi, "programming error");
+	break;
+      case PM_SOURCE_PHOTFIT_NONE:
+	psAbort("programming error");
+    }	
+    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.PS_TYPE_IMAGE_MASK_DATA[yi][xi] & maskVal)
+		continue;
+	    if (fitVarMode != PM_SOURCE_PHOTFIT_CONST) {
+		wt = covarFactor * 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");
+	    }
+
+	    // wt is 1.0 for CONST
+	    flux += (factor * Pi->data.F32[yi][xi]) / wt;
+	}
+    }
+    return flux;
+}
+
+double pmSourceModelDotModel (const pmSource *Mi, const pmSource *Mj, const pmSourceFitVarMode fitVarMode, const float covarFactor, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(Mi, NAN);
+    PS_ASSERT_PTR_NON_NULL(Mj, NAN);
+    int Xs, Xe, Ys, Ye;
+    int xi, xj, yi, yj;
+    int xIs, xJs, yIs, yJs;
+    int xIe, yIe;
+    double flux;
+    double wt = 1.0;
+
+    const psImage *Pi = Mi->modelFlux;
+    assert (Pi != NULL);
+    const psImage *Pj = Mj->modelFlux;
+    assert (Pj != NULL);
+
+    const psImage *Wi = NULL;
+    switch (fitVarMode) {
+      case PM_SOURCE_PHOTFIT_CONST:
+	break;
+      case PM_SOURCE_PHOTFIT_IMAGE_VAR:
+      case PM_SOURCE_PHOTFIT_MODEL_SKY:
+	Wi = Mi->variance;
+	psAssert (Wi, "programming error");
+	break;
+      case PM_SOURCE_PHOTFIT_MODEL_VAR:
+	Wi = Mi->modelVar;
+	psAssert (Wi, "programming error");
+	break;
+      case PM_SOURCE_PHOTFIT_NONE:
+	psAbort("programming error");
+    }	
+
+    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.PS_TYPE_IMAGE_MASK_DATA[yi][xi] & maskVal)
+                continue;
+            if (Tj->data.PS_TYPE_IMAGE_MASK_DATA[yj][xj] & maskVal)
+                continue;
+
+	    float value = (Pi->data.F32[yi][xi] * Pj->data.F32[yj][xj]);
+	    switch (fitVarMode) {
+	      case PM_SOURCE_PHOTFIT_CONST:
+		wt = 1.0;
+		break;
+	      case PM_SOURCE_PHOTFIT_IMAGE_VAR:
+	      case PM_SOURCE_PHOTFIT_MODEL_SKY:
+	      case PM_SOURCE_PHOTFIT_MODEL_VAR:
+		wt = covarFactor * Wi->data.F32[yi][xi];
+		break;
+	      case PM_SOURCE_PHOTFIT_NONE:
+		psAbort("programming error");
+	    }
+	    // skip pixels with nonsense weight values
+	    if (wt <= 0) continue;
+
+	    flux += value / wt;
+        }
+    }
+    return flux;
+}
+
+double pmSourceDataDotModel (const pmSource *Mi, const pmSource *Mj, const pmSourceFitVarMode fitVarMode, const float covarFactor, psImageMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(Mi, NAN);
+    PS_ASSERT_PTR_NON_NULL(Mj, NAN);
+    int Xs, Xe, Ys, Ye;
+    int xi, xj, yi, yj;
+    int xIs, xJs, yIs, yJs;
+    int xIe, yIe;
+    double flux;
+    double wt = 1.0;
+
+    const psImage *Pi = Mi->pixels;
+    assert (Pi != NULL);
+    const psImage *Pj = Mj->modelFlux;
+    assert (Pj != NULL);
+
+    const psImage *Wi = NULL;
+    switch (fitVarMode) {
+      case PM_SOURCE_PHOTFIT_CONST:
+	break;
+      case PM_SOURCE_PHOTFIT_IMAGE_VAR:
+      case PM_SOURCE_PHOTFIT_MODEL_SKY:
+	Wi = Mi->variance;
+        psAssert (Wi, "programming error");
+	break;
+      case PM_SOURCE_PHOTFIT_MODEL_VAR:
+	Wi = Mi->modelVar;
+        psAssert (Wi, "programming error");
+	break;
+      case PM_SOURCE_PHOTFIT_NONE:
+	psAbort("programming error");
+    }	
+
+    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.PS_TYPE_IMAGE_MASK_DATA[yi][xi] & maskVal)
+                continue;
+            if (Tj->data.PS_TYPE_IMAGE_MASK_DATA[yj][xj] & maskVal)
+                continue;
+
+	    float value = (Pi->data.F32[yi][xi] * Pj->data.F32[yj][xj]);
+	    switch (fitVarMode) {
+	      case PM_SOURCE_PHOTFIT_CONST:
+		wt = 1.0;
+		break;
+	      case PM_SOURCE_PHOTFIT_IMAGE_VAR:
+	      case PM_SOURCE_PHOTFIT_MODEL_SKY:
+	      case PM_SOURCE_PHOTFIT_MODEL_VAR:
+                wt = covarFactor * Wi->data.F32[yi][xi];
+		break;
+	      case PM_SOURCE_PHOTFIT_NONE:
+		psAbort("programming error");
+	    }
+            // skip pixels with nonsense weight values
+	    if (wt <= 0) continue;
+
+	    flux += value / wt;
+        }
+    }
+    return flux;
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourcePhotometry.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourcePhotometry.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourcePhotometry.h	(revision 42651)
@@ -0,0 +1,98 @@
+/* @file  pmSourcePhotometry.h
+ * @brief functions to measure source photometry
+ *
+ * @author EAM, IfA; GLG, MHPCC
+ *
+ * @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-16 22:28:54 $
+ * 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,
+    PM_SOURCE_PHOT_DIFFSTATS = 0x0010,
+    PM_SOURCE_PHOT_PSFONLY   = 0x0020,
+} pmSourcePhotometryMode;
+
+typedef enum {
+    PM_SOURCE_PHOTFIT_NONE       = 0,
+    PM_SOURCE_PHOTFIT_CONST      = 1,
+    PM_SOURCE_PHOTFIT_IMAGE_VAR  = 2,
+    PM_SOURCE_PHOTFIT_MODEL_VAR  = 3,
+    PM_SOURCE_PHOTFIT_MODEL_SKY  = 4,   // XXX bad name: set variance floor based on mean variance image (variance of sky)
+} pmSourceFitVarMode;
+
+bool pmSourcePhotometryModel(
+    float *fitMag,                      ///< integrated fit magnitude
+    float *fitFlux,                     ///< integrated fit magnitude
+    pmModel *model                      ///< model used for photometry
+);
+
+bool pmSourcePhotometryAper(
+    int *nPixOut,
+    float *apMag,
+    float *apFluxOut,
+    float *apFluxErr,
+    pmModel *model,                     ///< model used for photometry
+    psImage *image,                     ///< image pixels to be used
+    psImage *variance,                  ///< variance pixels to be used
+    psImage *mask,                      ///< mask of pixels to ignore
+    psImageMaskType maskVal             ///< Value to mask
+);
+
+bool pmSourcePhotometryAperSource(
+    pmSource *source,			///< aperture flux magnitude
+    pmModel *model,                     ///< model used for photometry
+    psImage *image,                     ///< image pixels to be used
+    psImage *variance,                  ///< variance pixels to be used
+    psImage *mask,                      ///< mask of pixels to ignore
+    psImageMaskType maskVal             ///< Value to mask
+);
+
+bool pmSourceMagnitudesInit (pmConfig *config, psMetadata *recipe);
+bool pmSourceMagnitudes (pmSource *source, pmPSF *psf, pmSourcePhotometryMode mode, psImageMaskType maskVal, psImageMaskType markVal, float radius);
+
+bool pmSourcePixelWeight (pmSource *source, pmModel *model, psImage *mask, psImageMaskType maskVal, float radius);
+bool pmSourceMaskEval (pmSource *source, psImage *mask, psImageMaskType maskVal);
+
+bool pmSourceChisq (pmModel *model, psImage *image, psImage *mask, psImage *weight, psImageMaskType maskVal);
+bool pmSourceChisqUnsubtracted (pmSource *source, pmModel *model, psImageMaskType maskVal);
+
+bool pmSourceMeasureDiffStats (pmSource *source, psImageMaskType maskVal, psImageMaskType markVal);
+
+double pmSourceDataDotModel (const pmSource *Mi, const pmSource *Mj, const pmSourceFitVarMode fitVarMode, const float covarFactor, psImageMaskType maskVal);
+double pmSourceModelDotModel (const pmSource *Mi, const pmSource *Mj, const pmSourceFitVarMode fitVarMode, const float covarFactor, psImageMaskType maskVal);
+double pmSourceModelWeight(const pmSource *Mi, int term, const pmSourceFitVarMode fitVarMode, const float covarFactor, psImageMaskType maskVal);
+
+bool pmSourceNeighborFlags (pmSource *source);
+
+// 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_branches/psModules.20240412/src/objects/pmSourcePlotApResid.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourcePlotApResid.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourcePlotApResid.c	(revision 42651)
@@ -0,0 +1,181 @@
+/** @file  pmSourcePlot.c
+ *
+ *  Plot the Aperture Mag - Fitted Mag residuals
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-12-08 02:51:14 $
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmDetections.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)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_PTR_NON_NULL(layout, false);
+
+    bool status;
+    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");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    if (detections == NULL) return false;
+
+    psArray *sources = detections->allSources;
+    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 *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 = KAPA_POINT_CROSS;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+    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_branches/psModules.20240412/src/objects/pmSourcePlotMoments.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourcePlotMoments.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourcePlotMoments.c	(revision 42651)
@@ -0,0 +1,193 @@
+/** @file  pmSourcePlot.c
+ *
+ * This file contains functions to write source plots
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-12-08 02:51:14 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmDetections.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)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_PTR_NON_NULL(layout, false);
+
+    bool status;
+    Graphdata graphdata;
+    KapaSection section;
+
+    psLogMsg ("psphot", 3, "creating moments plot");
+
+    // find the currently selected readout
+    pmReadout  *readout = pmFPAfileThisReadout (config->files, view, "PSPHOT.INPUT");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    if (detections == NULL) return false;
+
+    psArray *sources = detections->allSources;
+    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 = +4.05;
+    graphdata.ymax = +4.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->Mxx;
+        yFaint->data.F32[nF] = source->moments->Myy;
+        nF++;
+
+        // XXX make this a user-defined cutoff
+        if (source->moments->SN < 50)
+            continue;
+
+        xBright->data.F32[nB] = source->moments->Mxx;
+        yBright->data.F32[nB] = source->moments->Myy;
+        nB++;
+    }
+    xFaint->n = nF;
+    yFaint->n = nF;
+
+    xBright->n = nB;
+    yBright->n = nB;
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = KAPA_POINT_BOX_SOLID;
+    graphdata.size = 0.3;
+    graphdata.style = KAPA_PLOT_POINTS;
+    KapaPrepPlot (kapa, nF, &graphdata);
+    KapaPlotVector (kapa, nF, xFaint->data.F32, "x");
+    KapaPlotVector (kapa, nF, yFaint->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = KAPA_POINT_CROSS;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+    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_branches/psModules.20240412/src/objects/pmSourcePlotPSFModel.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourcePlotPSFModel.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourcePlotPSFModel.c	(revision 42651)
@@ -0,0 +1,255 @@
+/** @file  pmSourcePlot.c
+ *
+ * This file contains functions to write source plots
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.14 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-12-08 02:51:14 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmDetections.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)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_PTR_NON_NULL(layout, false);
+
+    bool status;
+    Graphdata graphdata;
+    KapaSection section;
+
+    psLogMsg ("psphot", 3, "creating psf model plot");
+
+    // find the currently selected readout
+    pmReadout  *readout = pmFPAfileThisReadout (config->files, view, "PSPHOT.INPUT");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    if (detections == NULL) return false;
+
+    psArray *sources = detections->allSources;
+    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->Mxx;
+        moments.xy = source->moments->Mxy;
+        moments.y2 = source->moments->Myy;
+
+        // force the axis ratio to be < 20.0
+        psEllipseAxes axes_mnt = psEllipseMomentsToAxes (moments, 20.0);
+        psEllipseAxes axes_psf = pmPSF_ModelToAxes (PAR, model->class->useReff);
+
+        // 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 = KAPA_POINT_PAIR_CONNECT;
+    graphdata.size = 0.3;
+    graphdata.style = KAPA_PLOT_POINTS;
+    KapaPrepPlot (kapa, nMNT, &graphdata);
+    KapaPlotVector (kapa, nMNT, xMNT->data.F32, "x");
+    KapaPlotVector (kapa, nMNT, yMNT->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = KAPA_POINT_PAIR_CONNECT;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+    KapaPrepPlot (kapa, nPSF, &graphdata);
+    KapaPlotVector (kapa, nPSF, xPSF->data.F32, "x");
+    KapaPlotVector (kapa, nPSF, yPSF->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = KAPA_POINT_PAIR_CONNECT;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+    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_branches/psModules.20240412/src/objects/pmSourcePlots.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourcePlots.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourcePlots.c	(revision 42651)
@@ -0,0 +1,178 @@
+/** @file  pmSourcePlot.c
+ *
+ * This file contains functions to write source plots
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-10-02 20:52:01 $
+ *
+ *  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);
+    PS_ASSERT_PTR_NON_NULL(config, 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 = 1;
+        layout->nY = 1;
+        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(void)
+{
+    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);
+}
+
+bool psMemCheckSourcePlotLayout(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmSourcePlotLayoutFree);
+}
+
+
+/* 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_branches/psModules.20240412/src/objects/pmSourcePlots.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourcePlots.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourcePlots.h	(revision 42651)
@@ -0,0 +1,42 @@
+/* @file  pmSourcePlots.h
+ * @brief functions to create plots illustrating source properties
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-11-10 01:09:20 $
+ * 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(void);
+bool psMemCheckSourcePlotLayout(psPtr ptr);
+
+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_branches/psModules.20240412/src/objects/pmSourceSatstar.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceSatstar.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceSatstar.c	(revision 42651)
@@ -0,0 +1,46 @@
+/** @file  pmSourceSatstar.c
+ *
+ *  Functions to manage saturated star profiles
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-06 02:31:25 $
+ *
+ *  Copyright 2012 University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+#include "pmSourceSatstar.h"
+
+// pmSourceSatstar defines the profile
+static void pmSourceSatstarFree(pmSourceSatstar *satstar)
+{
+    if (!satstar) return;
+    psFree(satstar->logRmodel);
+    psFree(satstar->logFmodel);
+}
+
+pmSourceSatstar *pmSourceSatstarAlloc()
+{
+    pmSourceSatstar *satstar = (pmSourceSatstar *)psAlloc(sizeof(pmSourceSatstar));
+    psMemSetDeallocator(satstar, (psFreeFunc) pmSourceSatstarFree);
+
+    satstar->logRmodel = NULL;
+    satstar->logFmodel = NULL;
+
+    satstar->Xo = NAN;
+    satstar->Yo = NAN;
+    satstar->Rmax = NAN;
+
+    return satstar;
+}
+
+bool psMemCheckSourceSatstar(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmSourceSatstarFree);
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceSatstar.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceSatstar.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceSatstar.h	(revision 42651)
@@ -0,0 +1,27 @@
+/* @file  pmSourceSatstar.h
+ *
+ * @author EAM, IfA
+ * @date $Date: 2012-08-30 $
+ * Copyright 2012 University of Hawaii
+ */
+
+# ifndef PM_SOURCE_SATSTAR_H
+# define PM_SOURCE_SATSTAR_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+typedef struct {
+    float Xo;
+    float Yo;
+    float Rmax;
+    psVector *logFmodel;
+    psVector *logRmodel;
+} pmSourceSatstar;
+
+
+pmSourceSatstar *pmSourceSatstarAlloc();
+bool psMemCheckSourceSatstar(psPtr ptr);
+
+/// @}
+# endif /* PM_SOURCE_SATSTAR_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceSky.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceSky.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceSky.c	(revision 42651)
@@ -0,0 +1,173 @@
+/** @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.20 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-06 02:31:25 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.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
+
+This function modifies the source mask; it should only be called before the object aperture is defined
+*****************************************************************************/
+
+bool pmSourceLocalSky(
+    pmSource *source,
+    psStatsOptions statsOptions,
+    psF32 Radius,
+    psImageMaskType maskVal,
+    psImageMaskType markVal)
+{
+    psTrace("psModules.objects", 10, "---- %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);
+
+    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;
+
+    // maskVal is used to test for rejected pixels, and must include markVal
+    maskVal |= markVal;
+
+    srcRegion = psRegionForSquare(peak->x, peak->y, Radius);
+    srcRegion = psRegionForImage(mask, srcRegion);
+
+    psImageMaskRegion(mask, srcRegion, "OR", markVal);
+    psStats *myStats = psStatsAlloc(statsOptions);
+    if (!psImageStats(myStats, image, mask, maskVal)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to get image statistics.\n");
+        psFree(myStats);
+        return false;
+    }
+    psImageMaskRegion(mask, srcRegion, "AND", PS_NOT_IMAGE_MASK(markVal));
+    double value = psStatsGetValue(myStats, statistic);
+    psFree(myStats);
+
+    if (isnan(value)) {
+        psTrace("psModules.objects", 10, "---- %s(false) end ----\n", __func__);
+        return(false);
+    }
+    if (source->moments == NULL) {
+        source->moments = pmMomentsAlloc();
+    }
+    source->moments->Sky = value;
+    psTrace("psModules.objects", 10, "---- %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,
+    psImageMaskType maskVal,
+    psImageMaskType markVal
+)
+{
+    psTrace("psModules.objects", 10, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_IMAGE_NON_NULL(source->maskObj, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+    PS_ASSERT_INT_POSITIVE(Radius, false);
+
+    if (source->variance == NULL) {
+      // XXX this is needed so psphotCheckRadiusPSF has a real value
+      source->moments->dSky = 1.0;
+      return true;
+    }
+
+    // maskVal is used to test for rejected pixels, and must include markVal
+    maskVal |= markVal;
+
+    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->variance;
+    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", markVal);
+    psStats *myStats = psStatsAlloc(statsOptions);
+    if (!psImageStats(myStats, image, mask, maskVal)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to get image statistics.\n");
+        psFree(myStats);
+        return false;
+    }
+    psImageMaskRegion(mask, srcRegion, "AND", PS_NOT_IMAGE_MASK(markVal));
+    double value = psStatsGetValue(myStats, statistic);
+    psFree(myStats);
+
+    if (isnan(value)) {
+        psTrace("psModules.objects", 10, "---- %s(false) end ----\n", __func__);
+        return(false);
+    }
+    if (source->moments == NULL) {
+        source->moments = pmMomentsAlloc();
+    }
+    source->moments->dSky = value;
+    psTrace("psModules.objects", 10, "---- %s(true) end ----\n", __func__);
+    return (true);
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceSky.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceSky.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceSky.h	(revision 42651)
@@ -0,0 +1,47 @@
+/* @file  pmSourceSky.h
+ * @author EAM, IfA; GLG, MHPCC
+ *
+ * @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-01-27 06:39:38 $
+ * 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
+    psImageMaskType maskVal,                 ///< Value to mask
+    psImageMaskType 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
+    psImageMaskType maskVal,                 ///< Value to mask
+    psImageMaskType mark                     ///< Mask value for marking
+);
+
+/// @}
+# endif /* PM_SOURCE_PHOTOMETRY_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceUtils.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceUtils.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceUtils.c	(revision 42651)
@@ -0,0 +1,139 @@
+/** @file  pmSource.c
+ *
+ *  Functions to define and manipulate sources on images
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA: significant modifications.
+ *
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-12-08 02:51:14 $
+ *
+ *  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 "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+
+#include "pmSourceUtils.h"
+
+/******************************************************************************
+    pmSourceModelGuess(source, model, maskVal, markVal): 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, psImageMaskType maskVal, psImageMaskType markVal)
+{
+    psTrace("psModules.objects", 10, "---- %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->class->modelGuess(model, source, maskVal, markVal)) {
+	psFree (model);
+	return NULL;
+    }
+
+    psTrace("psModules.objects", 10, "---- %s() end ----\n", __func__);
+    return(model);
+}
+
+pmSource *pmSourceFromModel (pmModel *model, pmReadout *readout, float radius, 
+                             pmSourceType type)
+{
+    PS_ASSERT_PTR_NON_NULL(model, NULL);
+    PS_ASSERT_PTR_NON_NULL(readout, NULL);
+
+    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);
+
+    float xReadout, yReadout;
+
+    // if we have information about the chip & cell, adjust the coordinates chip->cell->readout
+    // otherwise, assume 0,0 offset and 1,1 parity
+    if (cell) {
+      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);
+      xReadout = xCell;
+      yReadout = yCell;
+    } else {
+      xReadout = xChip;
+      yReadout = yChip;
+    }
+    
+    pmSourceDefinePixels (source, readout, xReadout, yReadout, radius);
+
+    return (source);
+}
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceUtils.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceUtils.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceUtils.h	(revision 42651)
@@ -0,0 +1,42 @@
+/* @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.
+    psImageMaskType maskVal, 
+    psImageMaskType markVal
+);
+
+pmSource *pmSourceFromModel (
+  pmModel *model, 
+  pmReadout *readout, 
+  float radius,
+  pmSourceType type
+  );
+
+/// @}
+# endif /* PM_SOURCE_UTILS_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceVisual.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceVisual.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceVisual.c	(revision 42651)
@@ -0,0 +1,680 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmTrend2D.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmSpan.h"
+#include "pmFootprintSpans.h"
+#include "pmFootprint.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModelFuncs.h"
+#include "pmModelClass.h"
+#include "pmModel.h"
+#include "pmModelUtils.h"
+#include "pmSourceMasks.h"
+#include "pmSourceExtendedPars.h"
+#include "pmSourceDiffStats.h"
+#include "pmSourceSatstar.h"
+#include "pmSourceLensing.h"
+#include "pmSource.h"
+#include "pmSourceFitModel.h"
+#include "pmPSF.h"
+#include "pmPSFtry.h"
+#include "pmDetections.h"
+
+#include "pmSourceVisual.h"
+
+#if (HAVE_KAPA)
+#include <kapa.h>
+#include "pmVisual.h"
+#include "pmVisualUtils.h"
+
+// functions used to visualize the analysis as it goes
+// these are invoked by the -visual options
+
+static int kapa1 = -1;
+static int kapa2 = -1;
+static bool plotPSF = true;
+// static int kapa3 = -1;
+
+bool pmSourceVisualClose() {
+    if (kapa1 != -1)
+        KapaClose(kapa1);
+    return true;
+}
+
+bool pmSourcePlotPoints3D (int myKapa, Graphdata *graphdata, psVector *xn, psVector *yn, psVector *zn, float theta, float phi);
+
+bool pmSourceVisualPlotPSFMetric (pmPSFtry *psfTry) {
+
+    Graphdata graphdata;
+
+    if (!pmVisualTestLevel("psphot.psf.metric", 2)) return true;
+    if (!pmVisualInitWindow (&kapa1, "pmSource:plots")) return false;
+
+    KapaClearSections (kapa1);
+    KapaInitGraph (&graphdata);
+
+    psVector *x = psVectorAllocEmpty (psfTry->sources->n, PS_TYPE_F32);
+    psVector *y = psVectorAllocEmpty (psfTry->sources->n, PS_TYPE_F32);
+    psVector *dy = psVectorAllocEmpty(psfTry->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 < psfTry->sources->n; i++) {
+	if (psfTry->mask->data.PS_TYPE_VECTOR_MASK_DATA[i] & PSFTRY_MASK_ALL) continue;
+        x->data.F32[n] = psfTry->fitMag->data.F32[i];
+	y->data.F32[n] = psfTry->metric->data.F32[i];
+        dy->data.F32[n] = psfTry->metricErr->data.F32[i];
+        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 = dy->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;
+
+    // better choice for range?
+    // graphdata.xmin = -17.0;
+    // graphdata.xmax =  -9.0;
+    graphdata.ymin = -0.51;
+    graphdata.ymax = +0.51;
+
+    KapaSetLimits (kapa1, &graphdata);
+
+    KapaSetFont (kapa1, "helvetica", 14);
+    KapaBox (kapa1, &graphdata);
+    KapaSendLabel (kapa1, "PSF Mag", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "Ap Mag - PSF Mag", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = KAPA_POINT_CROSS;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+    graphdata.etype |= 0x01;
+
+    KapaPrepPlot (kapa1, n, &graphdata);
+    KapaPlotVector (kapa1, n, x->data.F32, "x");
+    KapaPlotVector (kapa1, n, y->data.F32, "y");
+    KapaPlotVector (kapa1, n, dy->data.F32, "dym");
+    KapaPlotVector (kapa1, n, dy->data.F32, "dyp");
+
+    psFree (x);
+    psFree (y);
+    psFree (dy);
+
+    pmVisualAskUser(NULL);
+    return true;
+}
+
+bool pmSourceVisualPlotPSFMetricSubpix (pmPSFtry *psfTry) {
+
+    KapaSection section;  // put the positive profile in one and the residuals in another?
+    Graphdata graphdata;
+
+    if (!pmVisualTestLevel("psphot.psf.subpix", 3)) return true;
+    if (!pmVisualInitWindow (&kapa1, "pmSource:plots")) return false;
+
+    KapaClearSections (kapa1);
+    KapaInitGraph (&graphdata);
+    section.bg = KapaColorByName ("none"); // XXX probably should be 'none'
+
+    int n;
+    float range;
+    psVector *x = psVectorAllocEmpty (psfTry->sources->n, PS_TYPE_F32);
+    psVector *y = psVectorAllocEmpty (psfTry->sources->n, PS_TYPE_F32);
+    psVector *dy = psVectorAllocEmpty(psfTry->sources->n, PS_TYPE_F32);
+
+    // section a: fractional-x pixel
+    section.dx = 1.0;
+    section.dy = 0.5;
+    section.x = 0.0;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a1");
+    KapaSetSection (kapa1, &section);
+    psFree (section.name);
+
+    graphdata.xmin = +32.0;
+    graphdata.xmax = -32.0;
+    graphdata.ymin = +32.0;
+    graphdata.ymax = -32.0;
+
+    // construct the plot vectors
+    n = 0;
+    for (int i = 0; i < psfTry->sources->n; i++) {
+	if (psfTry->mask->data.PS_TYPE_VECTOR_MASK_DATA[i] & PSFTRY_MASK_ALL) continue;
+
+        pmSource *source = psfTry->sources->data[i];
+        x->data.F32[n] = source->modelEXT->params->data.F32[PM_PAR_XPOS] - (int)source->modelEXT->params->data.F32[PM_PAR_XPOS];
+
+	y->data.F32[n] = psfTry->metric->data.F32[i];
+        dy->data.F32[n] = psfTry->metricErr->data.F32[i];
+        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 = dy->n = n;
+
+    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;
+
+    // better choice for range?
+    // graphdata.xmin = -17.0;
+    // graphdata.xmax =  -9.0;
+    graphdata.ymin = -0.51;
+    graphdata.ymax = +0.51;
+
+    KapaSetLimits (kapa1, &graphdata);
+
+    KapaSetFont (kapa1, "helvetica", 14);
+    KapaBox (kapa1, &graphdata);
+    KapaSendLabel (kapa1, "PSF Mag", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "Ap Mag - PSF Mag", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = KAPA_POINT_CROSS;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+    graphdata.etype |= 0x01;
+
+    KapaPrepPlot (kapa1, n, &graphdata);
+    KapaPlotVector (kapa1, n, x->data.F32, "x");
+    KapaPlotVector (kapa1, n, y->data.F32, "y");
+    KapaPlotVector (kapa1, n, dy->data.F32, "dym");
+    KapaPlotVector (kapa1, n, dy->data.F32, "dyp");
+
+    // *** section b: fractional-x pixel
+    section.dx = 1.0;
+    section.dy = 0.5;
+    section.x = 0.0;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a2");
+    KapaSetSection (kapa1, &section);
+    psFree (section.name);
+
+    graphdata.xmin = +32.0;
+    graphdata.xmax = -32.0;
+    graphdata.ymin = +32.0;
+    graphdata.ymax = -32.0;
+
+    // construct the plot vectors
+    n = 0;
+    for (int i = 0; i < psfTry->sources->n; i++) {
+	if (psfTry->mask->data.PS_TYPE_VECTOR_MASK_DATA[i] & PSFTRY_MASK_ALL) continue;
+
+        pmSource *source = psfTry->sources->data[i];
+        x->data.F32[n] = source->modelEXT->params->data.F32[PM_PAR_YPOS] - (int)source->modelEXT->params->data.F32[PM_PAR_YPOS];
+
+	y->data.F32[n] = psfTry->metric->data.F32[i];
+        dy->data.F32[n] = psfTry->metricErr->data.F32[i];
+        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 = dy->n = n;
+
+    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;
+
+    // better choice for range?
+    // graphdata.xmin = -17.0;
+    // graphdata.xmax =  -9.0;
+    graphdata.ymin = -0.51;
+    graphdata.ymax = +0.51;
+
+    KapaSetLimits (kapa1, &graphdata);
+
+    KapaSetFont (kapa1, "helvetica", 14);
+    KapaBox (kapa1, &graphdata);
+    KapaSendLabel (kapa1, "PSF Mag", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "Ap Mag - PSF Mag", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = KAPA_POINT_CROSS;
+    graphdata.size = 0.5;
+    graphdata.style = KAPA_PLOT_POINTS;
+    graphdata.etype |= 0x01;
+
+    KapaPrepPlot (kapa1, n, &graphdata);
+    KapaPlotVector (kapa1, n, x->data.F32, "x");
+    KapaPlotVector (kapa1, n, y->data.F32, "y");
+    KapaPlotVector (kapa1, n, dy->data.F32, "dym");
+    KapaPlotVector (kapa1, n, dy->data.F32, "dyp");
+
+    psFree (x);
+    psFree (y);
+    psFree (dy);
+
+    pmVisualAskUser(NULL);
+    return true;
+}
+
+// to see the structure of the psf model, place the sources in a fake image 1/10th the size
+// at their appropriate relative location. later sources stomp on earlier sources
+bool pmSourceVisualShowModelFits (pmPSF *psf, psArray *sources, psImageMaskType maskVal) {
+
+    if (!pmVisualTestLevel("psphot.psf.fits", 2)) return true;
+    if (!pmVisualInitWindow (&kapa2, "pmSource:images")) return false;
+
+    // create images 1/10 scale:
+    psImage *image = psImageAlloc (0.1*psf->fieldNx, 0.1*psf->fieldNy, PS_TYPE_F32);
+    psImage *model = psImageAlloc (0.1*psf->fieldNx, 0.1*psf->fieldNy, PS_TYPE_F32);
+    psImage *resid = psImageAlloc (0.1*psf->fieldNx, 0.1*psf->fieldNy, PS_TYPE_F32);
+    psImageInit (image, 0.0);
+    psImageInit (model, 0.0);
+    psImageInit (resid, 0.0);
+
+    for (int i = sources->n - 1; i >= 0; i--) {
+	pmSource *source = sources->data[i];
+	if (!source) continue;
+	if (!source->pixels) continue;
+
+	pmSourceCacheModel (source, maskVal);
+	if (!source->modelFlux) continue;
+
+	pmModel *srcModel = pmSourceGetModel (NULL, source);
+	if (!model) continue;
+
+	float norm = srcModel->params->data.F32[PM_PAR_I0];
+
+	int Xo = 0.1*srcModel->params->data.F32[PM_PAR_XPOS];
+	int Yo = 0.1*srcModel->params->data.F32[PM_PAR_YPOS];
+
+	// insert source pixels in the image at 1/10th offset
+	for (int iy = 0; iy < source->pixels->numRows; iy++) {
+	    int jy = iy + Yo;
+	    if (jy >= image->numRows) continue;
+	    for (int ix = 0; ix < source->pixels->numCols; ix++) {
+		int jx = ix + Xo;
+		if (jx >= image->numCols) continue;
+		if (source->maskObj->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix]) continue;
+		if (source->modelFlux->data.F32[iy][ix] < 0.001) continue;
+		image->data.F32[jy][jx] = source->pixels->data.F32[iy][ix];
+		model->data.F32[jy][jx] = source->modelFlux->data.F32[iy][ix];
+		resid->data.F32[jy][jx] = source->pixels->data.F32[iy][ix] - norm*source->modelFlux->data.F32[iy][ix];
+	    }
+	}
+    }
+
+    // KapaClearSections (kapa2);
+    pmVisualScaleImage (kapa2, image, "image", 0, true);
+    pmVisualScaleImage (kapa2, model, "model", 1, true);
+    pmVisualScaleImage (kapa2, resid, "resid", 2, true);
+
+# ifdef DEBUG
+    { 
+	psFits *fits = psFitsOpen ("image.fits", "w");
+	psFitsWriteImage (fits, NULL, image, 0, NULL);
+	psFitsClose (fits);
+	fits = psFitsOpen ("model.fits", "w");
+	psFitsWriteImage (fits, NULL, model, 0, NULL);
+	psFitsClose (fits);
+	fits = psFitsOpen ("resid.fits", "w");
+	psFitsWriteImage (fits, NULL, resid, 0, NULL);
+	psFitsClose (fits);
+    }
+# endif
+
+    psFree (image);
+    psFree (model);
+    psFree (resid);
+
+    pmVisualAskUser(NULL);
+    return true;
+}
+
+bool pmSourceVisualShowModelFit (pmSource *source) {
+
+    if (!pmVisualTestLevel("psphot.psf.fitresid", 2)) return true;
+
+    if (!source->pixels) return false;
+    if (!source->modelFlux) return false;
+    if (!pmVisualInitWindow (&kapa2, "pmSource:images")) return false;
+
+    // KapaClearSections (kapa2);
+    pmVisualScaleImage (kapa2, source->pixels, "source", 0, false);
+    pmVisualScaleImage (kapa2, source->modelFlux, "model", 1, false);
+
+    pmModel *model = pmSourceGetModel (NULL, source);
+    float norm = model->params->data.F32[PM_PAR_I0];
+
+    psImage *resid = psImageAlloc (source->pixels->numCols, source->pixels->numRows, PS_TYPE_F32);
+    for (int iy = 0; iy < source->pixels->numRows; iy++) {
+	for (int ix = 0; ix < source->pixels->numCols; ix++) {
+	    if (source->maskObj->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix]) {
+		resid->data.F32[iy][ix] = NAN;
+		continue;
+	    }
+	    resid->data.F32[iy][ix] = source->pixels->data.F32[iy][ix] - norm*source->modelFlux->data.F32[iy][ix];
+	}
+    }
+    pmVisualScaleImage (kapa2, resid, "resid", 2, false);
+
+    psFree (resid);
+
+    pmVisualAskUser(NULL);
+    return true;
+}
+
+bool pmSourceVisualPSFModelResid (pmTrend2D *trend, psVector *x, psVector *y, psVector *param, psVector *mask) {
+
+    KapaSection section;  // put the positive profile in one and the residuals in another?
+
+    Graphdata graphdata;
+
+    if (!plotPSF) return true;
+    if (!pmVisualTestLevel("psphot.psf.resid", 2)) return true;
+    if (!pmVisualInitWindow (&kapa1, "pmSource:plots")) return false;
+
+    KapaClearPlots (kapa1);
+    KapaInitGraph (&graphdata);
+    section.bg = KapaColorByName ("none"); // XXX probably should be 'none'
+
+    float Xmin = +1e32;
+    float Xmax = -1e32;
+    float Ymin = +1e32;
+    float Ymax = -1e32;
+    float Fmin = +1e32;
+    float Fmax = -1e32;
+
+    psVector *resid = psVectorAlloc (x->n, PS_TYPE_F32);
+    psVector *model = psVectorAlloc (x->n, PS_TYPE_F32);
+
+    psVector *xm = psVectorAlloc (x->n, PS_TYPE_F32);
+    psVector *ym = psVectorAlloc (x->n, PS_TYPE_F32);
+    psVector *Fm = psVectorAlloc (x->n, PS_TYPE_F32);
+
+    int n = 0;
+    for (int i = 0; i < x->n; i++) {
+        model->data.F32[i] = pmTrend2DEval (trend, x->data.F32[i], y->data.F32[i]);
+        resid->data.F32[i] = param->data.F32[i] - model->data.F32[i];
+        if (mask->data.PS_TYPE_VECTOR_MASK_DATA[i]) continue;
+        Xmin = PS_MIN (Xmin, x->data.F32[i]);
+        Xmax = PS_MAX (Xmax, x->data.F32[i]);
+        Ymin = PS_MIN (Ymin, y->data.F32[i]);
+        Ymax = PS_MAX (Ymax, y->data.F32[i]);
+        Fmin = PS_MIN (Fmin, param->data.F32[i]);
+        Fmax = PS_MAX (Fmax, param->data.F32[i]);
+	xm->data.F32[n] = x->data.F32[i];
+	ym->data.F32[n] = y->data.F32[i];
+	Fm->data.F32[n] = param->data.F32[i];
+	n++;
+    }
+    xm->n = ym->n = Fm->n = n;
+
+    // view 1 on resid
+    section.dx = 1.0;
+    section.dy = 0.5;
+    section.x = 0.0;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a1");
+    KapaSetSection (kapa1, &section);
+    psFree (section.name);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.xmin = Xmin;
+    graphdata.xmax = Xmax;
+    graphdata.ymin = Fmin;
+    graphdata.ymax = Fmax;
+
+    { 
+	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 (kapa1, &graphdata);
+    KapaSetFont (kapa1, "helvetica", 14);
+    KapaBox (kapa1, &graphdata);
+    KapaSendLabel (kapa1, "X (pixels)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "Model Param", KAPA_LABEL_YM);
+
+    graphdata.ptype = KAPA_POINT_CROSS;
+    graphdata.size = 1.0;
+    graphdata.style = KAPA_PLOT_POINTS;
+    KapaPrepPlot (kapa1,   x->n, &graphdata);
+    KapaPlotVector (kapa1, x->n, x->data.F32, "x");
+    KapaPlotVector (kapa1, x->n, param->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = KAPA_POINT_BOX_OPEN;
+    KapaPrepPlot (kapa1,   xm->n, &graphdata);
+    KapaPlotVector (kapa1, xm->n, xm->data.F32, "x");
+    KapaPlotVector (kapa1, xm->n, Fm->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = KAPA_POINT_BOX_OPEN;
+    KapaPrepPlot (kapa1,   x->n, &graphdata);
+    KapaPlotVector (kapa1, x->n, x->data.F32, "x");
+    KapaPlotVector (kapa1, x->n, model->data.F32, "y");
+
+    // view 2 on resid
+    section.dx = 1.0;
+    section.dy = 0.5;
+    section.x = 0.0;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a2");
+    KapaSetSection (kapa1, &section);
+    psFree (section.name);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.xmin = Ymin;
+    graphdata.xmax = Ymax;
+    graphdata.ymin = Fmin;
+    graphdata.ymax = Fmax;
+    { 
+	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 (kapa1, &graphdata);
+    KapaSetFont (kapa1, "helvetica", 14);
+    KapaBox (kapa1, &graphdata);
+    KapaSendLabel (kapa1, "Y (pixels)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa1, "Model Param", KAPA_LABEL_YM);
+
+    graphdata.ptype = KAPA_POINT_CROSS;
+    graphdata.size = 1.0;
+    graphdata.style = KAPA_PLOT_POINTS;
+    KapaPrepPlot (kapa1,   y->n, &graphdata);
+    KapaPlotVector (kapa1, y->n, y->data.F32, "x");
+    KapaPlotVector (kapa1, y->n, param->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = KAPA_POINT_BOX_OPEN;
+    KapaPrepPlot (kapa1,   xm->n, &graphdata);
+    KapaPlotVector (kapa1, xm->n, ym->data.F32, "x");
+    KapaPlotVector (kapa1, xm->n, Fm->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = KAPA_POINT_BOX_OPEN;
+    KapaPrepPlot (kapa1,   y->n, &graphdata);
+    KapaPlotVector (kapa1, y->n, y->data.F32, "x");
+    KapaPlotVector (kapa1, y->n, model->data.F32, "y");
+
+    psFree (xm);
+    psFree (ym);
+    psFree (Fm);
+
+    psFree (resid);
+    psFree (model);
+
+    bool dumpData = false;
+
+    // pause and wait for user input:
+    // continue, save (provide name), ??
+retry:
+    pmVisualAskUserOrDump(&plotPSF, &dumpData);
+    if (dumpData) {
+      char name[128];
+      fprintf (stderr, "filename: ");
+      int status = fscanf (stdin, "%127s", name);
+      if (status != 1) {
+	fprintf (stderr, "odd response\n");
+	goto retry;
+      }
+
+      FILE *f = fopen (name, "w");
+      if (!f) {
+	fprintf (stderr, "cannot open %s for output\n", name);
+	goto retry;
+      }
+      for (int i = 0; i < x->n; i++) {
+        float vModel = pmTrend2DEval (trend, x->data.F32[i], y->data.F32[i]);
+	fprintf (f, "%f %f %f %f %d\n", x->data.F32[i], y->data.F32[i], param->data.F32[i], vModel, mask->data.PS_TYPE_VECTOR_MASK_DATA[i]);
+      }
+      fclose (f);
+      goto retry;
+    }
+
+    return true;
+}
+
+// Somewhat broken 3D plotting function (was used by pmSourceVisualPSFModelResid, but not anymore)
+bool pmSourcePlotPoints3D (int myKapa, Graphdata *graphdata, psVector *xn, psVector *yn, psVector *zn, float theta, float phi) {
+
+    return true;
+
+    psVector *xv = psVectorAlloc (PS_MAX(6, 2*xn->n), PS_TYPE_F32);
+    psVector *yv = psVectorAlloc (PS_MAX(6, 2*xn->n), PS_TYPE_F32);
+    psVector *zv = psVectorAlloc (PS_MAX(6, 2*xn->n), PS_TYPE_F32);
+
+    graphdata->xmin = +1e32;
+    graphdata->xmax = -1e32;
+    graphdata->ymin = +1e32;
+    graphdata->ymax = -1e32;
+
+    for (int i = 0; i < xn->n; i++) {
+        xv->data.F32[2*i+0] = +xn->data.F32[i]*cos(theta) + yn->data.F32[i]*sin(theta)*cos(phi) + zn->data.F32[i]*sin(theta)*sin(phi);
+        yv->data.F32[2*i+0] = -xn->data.F32[i]*sin(theta) + yn->data.F32[i]*cos(theta)*cos(phi) + zn->data.F32[i]*cos(theta)*sin(phi);
+        zv->data.F32[2*i+0] = -yn->data.F32[i]*sin(phi)   + zn->data.F32[i]*cos(phi);
+        xv->data.F32[2*i+1] = +xn->data.F32[i]*cos(theta) + yn->data.F32[i]*sin(theta)*cos(phi);
+        yv->data.F32[2*i+1] = -xn->data.F32[i]*sin(theta) + yn->data.F32[i]*cos(theta)*cos(phi);
+        zv->data.F32[2*i+1] = -yn->data.F32[i]*sin(phi);
+        graphdata->xmin = PS_MIN(graphdata->xmin, xv->data.F32[2*i+0]);
+        graphdata->xmax = PS_MAX(graphdata->xmax, xv->data.F32[2*i+0]);
+        graphdata->ymin = PS_MIN(graphdata->ymin, zv->data.F32[2*i+0]);
+        graphdata->ymax = PS_MAX(graphdata->ymax, zv->data.F32[2*i+0]);
+        graphdata->xmin = PS_MIN(graphdata->xmin, xv->data.F32[2*i+1]);
+        graphdata->xmax = PS_MAX(graphdata->xmax, xv->data.F32[2*i+1]);
+        graphdata->ymin = PS_MIN(graphdata->ymin, zv->data.F32[2*i+1]);
+        graphdata->ymax = PS_MAX(graphdata->ymax, zv->data.F32[2*i+1]);
+    }
+    xv->n = xn->n;
+
+    // examine sources to set data range
+    KapaSetLimits (myKapa, graphdata);
+
+    graphdata->color = KapaColorByName ("black");
+    graphdata->ptype = KAPA_POINT_PAIR_CONNECT;
+    graphdata->size = 0.5;
+    graphdata->style = KAPA_PLOT_POINTS;
+    KapaPrepPlot (myKapa, xv->n, graphdata);
+    KapaPlotVector (myKapa, xv->n, xv->data.F32, "x");
+    KapaPlotVector (myKapa, xv->n, zv->data.F32, "y");
+
+    graphdata->color = KapaColorByName ("blue");
+    graphdata->ptype = KAPA_POINT_BOX_OPEN;
+    graphdata->size = 1.5;
+    graphdata->style = KAPA_PLOT_POINTS;
+    KapaPrepPlot (myKapa, xv->n, graphdata);
+    KapaPlotVector (myKapa, xv->n, xv->data.F32, "x");
+    KapaPlotVector (myKapa, xv->n, zv->data.F32, "y");
+
+    xv->n = 6;
+
+    // set the three axis lines
+    xv->data.F32[0] = +0.0*cos(theta) + 0.0*sin(theta)*cos(phi) + 0.0*sin(theta)*sin(phi);
+    yv->data.F32[0] = -0.0*sin(theta) + 0.0*cos(theta)*cos(phi) + 0.0*cos(theta)*sin(phi);
+    zv->data.F32[0] =                 - 0.0*sin(phi)            + 0.0*cos(phi);
+    xv->data.F32[1] = +1.0*cos(theta) + 0.0*sin(theta)*cos(phi);
+    yv->data.F32[1] = -1.0*sin(theta) + 0.0*cos(theta)*cos(phi);
+    zv->data.F32[1] =                 - 0.0*sin(phi)            + 0.0*cos(phi);
+
+    xv->data.F32[2] = +0.0*cos(theta) + 0.0*sin(theta)*cos(phi) + 0.0*sin(theta)*sin(phi);
+    yv->data.F32[2] = -0.0*sin(theta) + 0.0*cos(theta)*cos(phi) + 0.0*cos(theta)*sin(phi);
+    zv->data.F32[2] =                 - 0.0*sin(phi)            + 0.0*cos(phi);
+    xv->data.F32[3] = +0.0*cos(theta) + 1.0*sin(theta)*cos(phi);
+    yv->data.F32[3] = -0.0*sin(theta) + 1.0*cos(theta)*cos(phi);
+    zv->data.F32[3] =                 - 1.0*sin(phi)            + 0.0*cos(phi);
+
+    xv->data.F32[4] = +0.0*cos(theta) + 0.0*sin(theta)*cos(phi) + 1.0*sin(theta)*sin(phi);
+    yv->data.F32[4] = -0.0*sin(theta) + 0.0*cos(theta)*cos(phi) + 1.0*cos(theta)*sin(phi);
+    zv->data.F32[4] =                 - 0.0*sin(phi)            + 1.0*cos(phi);
+    xv->data.F32[5] = +0.0*cos(theta) + 0.0*sin(theta)*cos(phi);
+    yv->data.F32[5] = -0.0*sin(theta) + 0.0*cos(theta)*cos(phi);
+    zv->data.F32[5] =                 - 0.0*sin(phi)            + 0.0*cos(phi);
+
+    graphdata->color = KapaColorByName ("red");
+    graphdata->ptype = KAPA_POINT_PAIR_CONNECT;
+    graphdata->size = 0.5;
+    graphdata->style = KAPA_PLOT_POINTS;
+    KapaPrepPlot (myKapa, xv->n, graphdata);
+    KapaPlotVector (myKapa, xv->n, xv->data.F32, "x");
+    KapaPlotVector (myKapa, xv->n, zv->data.F32, "y");
+
+    psFree (xv);
+    psFree (yv);
+    psFree (zv);
+
+    return true;
+}
+
+#else
+
+bool pmSourceSetVisual(bool mode)
+{
+    return true;
+}
+
+bool pmSourceVisualPSFModelResid(pmTrend2D *trend, psVector *x, psVector *y, psVector *param, psVector *mask)
+{
+    return true;
+}
+
+#endif
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSourceVisual.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSourceVisual.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSourceVisual.h	(revision 42651)
@@ -0,0 +1,27 @@
+/* @file  pmKapaPlots.h
+ * @brief functions to make plots with the external program 'kapa'
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.1.16.1 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-02-19 17:59:50 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_SOURCE_VISUAL_H
+#define PM_SOURCE_VISUAL_H
+
+bool pmSourceVisualClose(void);
+
+/// @addtogroup Extras Miscellaneous Funtions
+/// @{
+
+bool pmSourceVisualPSFModelResid (pmTrend2D *trend, psVector *x, psVector *y, psVector *param, psVector *mask);
+bool pmSourceVisualPlotPSFMetric (pmPSFtry *try);
+bool pmSourceVisualPlotPSFMetricSubpix (pmPSFtry *try);
+
+bool pmSourceVisualShowModelFit (pmSource *source);
+bool pmSourceVisualShowModelFits (pmPSF *psf, psArray *sources, psImageMaskType maskVal);
+
+/// @}
+#endif // PM_KAPA_PLOTS_H
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSpan.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSpan.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSpan.c	(revision 42651)
@@ -0,0 +1,73 @@
+/* @file  pmSpan.c
+ *
+ * @author RHL, Princeton & IfA; EAM, IfA
+ *
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-08-01 00:00:17 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>
+#include <pslib.h>
+#include "pmSpan.h"
+
+/*** functions to manipulate image spans ***/
+
+static void spanFree(pmSpan *tmp) {
+    return;
+}
+
+/*
+ * 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;
+    }
+}
Index: /branches/eam_branches/psModules.20240412/src/objects/pmSpan.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmSpan.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmSpan.h	(revision 42651)
@@ -0,0 +1,28 @@
+/* @file  pmSpan.h
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-12-09 21:16:09 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+# ifndef PM_SPAN_H
+# define PM_SPAN_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+// 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);
+
+/// @}
+# endif /* PM_SPAN_H */
Index: /branches/eam_branches/psModules.20240412/src/objects/pmTrend2D.c
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmTrend2D.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmTrend2D.c	(revision 42651)
@@ -0,0 +1,393 @@
+/** @file  pmTrend2D.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.12 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-01-27 06:39:38 $
+ *  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)
+{
+    psFree(trend->stats);
+    psFree(trend->poly);
+    psFree(trend->map);
+    return;
+}
+
+pmTrend2D *pmTrend2DAlloc(pmTrend2DMode mode, psImage *image, int nXtrend, int nYtrend, psStats *stats)
+{
+    if (mode == PM_TREND_MAP) {
+        psAssert(image, "Need an image for MAP trend mode");
+    }
+
+    pmTrend2D *trend = 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;
+      }
+      // XXX: Put a more graceful error here.
+      default:
+        psAbort("error");
+    }
+    return trend;
+}
+
+bool psMemCheckTrend2D(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmTrend2DFree);
+}
+
+pmTrend2D *pmTrend2DNoImageAlloc(pmTrend2DMode mode, psImageBinning *binning, psStats *stats)
+{
+    if (mode == PM_TREND_MAP) {
+        psAssert(binning, "Need binning for MAP mode");
+    }
+    pmTrend2D *trend = 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)
+{
+    psAssert(stats, "Require statistics");
+
+    pmTrend2D *trend = 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:
+        // XXX: Put a more graceful error here.
+        psAbort("error");
+    }
+    return trend;
+}
+
+bool pmTrend2DFit(bool *pGoodFit, pmTrend2D *trend, psVector *mask, psVectorMaskType maskVal, const psVector *x,
+                  const psVector *y, const psVector *f, const psVector *df)
+{
+    PM_ASSERT_TREND2D_NON_NULL(trend, false);
+    PM_ASSERT_TREND2D_STATS(trend, false);
+    PS_ASSERT_VECTOR_NON_NULL(x, false);
+    PS_ASSERT_VECTOR_NON_NULL(y, false);
+    PS_ASSERT_VECTOR_NON_NULL(f, false);
+
+    bool status = false;
+    *pGoodFit = false;
+    // for the psImageMap fit, it is possible to have valid data but no valid solution for
+    // example, an isolated cell may not be reached from other cells, making the solution
+    // degenerate.  psImageMapFit should probably handle this case, but until it does, we allow
+    // it to fail on the result, but not yield an error (pGoodFit = false).
+    // psVectorClipFitPolynomial2D can not fail in this way (really?), so pGoodFit is always
+    // true
+
+    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?
+	*pGoodFit = true;
+        break;
+
+      case PM_TREND_MAP:
+        // XXX supply fraction from trend elements
+        // XXX need to add the API which adjusts the scale
+        status = psImageMapClipFit(pGoodFit, trend->map, trend->stats, mask, maskVal, x, y, f, df);
+	if (!status) {
+	  psError(PS_ERR_UNKNOWN, true, "failed to build PSF model map");
+	  return false;
+	}
+
+	// the psf model map can have nan pixels: repair this by extrapolation / interpolation
+	// XXX TEST: p_psImagePrint(0, trend->map->map, "before");
+	status = psImageMapRepair (trend->map->map);
+	// XXX TEST: p_psImagePrint(0, trend->map->map, "after");
+	if (!status) {
+	  psError(PS_ERR_UNKNOWN, true, "failed to repair PSF model map");
+	  return false;
+	}
+        break;
+
+      default:
+        psAbort ("error");
+    }
+    return status;
+}
+
+bool psImageMapRepair (psImage *map) {
+
+
+  // XXX why is my repair not working??
+
+    // 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
+    for (int iy = 0; iy < map->numRows; iy++) {
+        for (int ix = 0; ix < map->numCols; ix++) {
+            if (isfinite(map->data.F32[iy][ix])) {
+                Value += map->data.F32[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 >= map->numRows) continue;
+                for (int jx = ix - 1; jx <= ix + 1; jx++) {
+                    if (!jx && !jy) continue;
+                    if (jx   <   0) continue;
+                    if (jx   >= map->numCols) continue;
+		    if (!isfinite(map->data.F32[jy][jx])) continue;
+                    value += map->data.F32[jy][jx];
+                    count += 1.0;
+                }
+            }
+            if (count > 0) {
+	      // psLogMsg ("psphot", PS_LOG_DETAIL, "patching image map %d, %d: %f (%d pts)\n", ix, iy, (value / count), (int) count);
+		map->data.F32[iy][ix] = value / count;
+	    }
+        }
+    }
+    if (Count == 0) {
+        psError(PS_ERR_UNKNOWN, true, "failed to repair PSF model map");
+        return false;
+    }
+    Value /= Count;
+
+    // patch over remaining bad regions (use global average)
+    for (int iy = 0; iy < map->numRows; iy++) {
+        for (int ix = 0; ix < map->numCols; ix++) {
+            if (!isnan(map->data.F32[iy][ix])) continue;
+            map->data.F32[iy][ix] = Value;
+        }
+    }
+    return true;
+}
+
+double pmTrend2DEval(const pmTrend2D *trend, float x, float y)
+{
+    // This might be in a tight loop, so no complicated assertions
+    if (!trend) {
+        return 0.0;
+    }
+
+    double result;
+    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(const pmTrend2D *trend, psVector *mask, psVectorMaskType maskValue, const psVector *x, const psVector *y)
+{
+    PM_ASSERT_TREND2D_NON_NULL(trend, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(x, false);
+    PS_ASSERT_VECTOR_NON_NULL(y, false);
+    psVector *result;
+
+    switch (trend->mode) {
+      case PM_TREND_POLY_ORD:
+      case PM_TREND_POLY_CHEB:
+	// XXX supply a mask here as well.
+        result = psPolynomial2DEvalVector (trend->poly, x, y);
+        break;
+
+      case PM_TREND_MAP:
+        result = psImageMapEvalVector (trend->map, mask, maskValue, x, y);
+        break;
+
+      default:
+        psAbort ("error");
+    }
+    return result;
+}
+
+psString pmTrend2DModeToString(pmTrend2DMode mode)
+{
+    switch (mode) {
+      case PM_TREND_NONE:
+        return psStringCopy("NONE");
+      case PM_TREND_POLY_ORD:
+        return psStringCopy("POLY_ORD");
+        break;
+      case PM_TREND_POLY_CHEB:
+        return psStringCopy("POLY_CHEB");
+      case PM_TREND_MAP:
+        return psStringCopy("MAP");
+        break;
+      default:
+        psError(PS_ERR_UNKNOWN, true, "Unknown pmTrend2D mode");
+    }
+    psAbort("invalid mode %d", mode);
+}
+
+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", name);
+    return PM_TREND_NONE;
+}
+
+bool pmTrend2DPrintMap (pmTrend2D *trend) {
+
+    if (!trend->map) return false;
+    if (!trend->map->map) return false;
+
+    for (int j = 0; j < trend->map->map->numRows; j++) {
+        for (int i = 0; i < trend->map->map->numCols; i++) {
+            fprintf (stderr, "%5.2f  ", trend->map->map->data.F32[j][i]);
+        }
+        fprintf (stderr, "\t\t\t");
+        for (int i = 0; i < trend->map->map->numCols; i++) {
+            fprintf (stderr, "%5.2f  ", trend->map->error->data.F32[j][i]);
+        }
+        fprintf (stderr, "\n");
+    }
+    return true;
+}
+
Index: /branches/eam_branches/psModules.20240412/src/objects/pmTrend2D.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/objects/pmTrend2D.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/objects/pmTrend2D.h	(revision 42651)
@@ -0,0 +1,101 @@
+/* @file  pmTrend2D.h
+ *
+ * functions to represent ways of modeling a 2D trend
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2009-01-27 06:39:38 $
+ * 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;                     // Statistics for clipped fitting
+    psStatsOptions singleMean, singleStdev; // Staistics for mean and stdev when there's a single pixel
+    pmTrend2DMode mode; // POLY_ORD, POLY_CHEB, MAP
+} pmTrend2D;
+
+// Assertion for pmTrend2D
+#define PM_ASSERT_TREND2D_NON_NULL(TREND, RVAL) \
+if (!(TREND)) { \
+    psError(PS_ERR_UNEXPECTED_NULL, true, "Trend %s is NULL", #TREND); \
+    return RVAL; \
+} \
+if ((TREND)->mode == PM_TREND_MAP) { \
+    PS_ASSERT_IMAGE_MAP_NON_NULL((TREND)->map, RVAL); \
+} else if ((TREND)->mode == PM_TREND_POLY_ORD || (TREND)->mode == PM_TREND_POLY_CHEB) { \
+    PS_ASSERT_POLY_NON_NULL((TREND)->poly, RVAL); \
+} else if ((TREND)->mode != PM_TREND_NONE) { \
+    psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unknown trend mode for %s: %x", #TREND, (TREND)->mode); \
+    return RVAL; \
+}
+
+#define PM_ASSERT_TREND2D_STATS(TREND, RVAL) \
+if (!(TREND)->stats) { \
+    psError(PS_ERR_UNEXPECTED_NULL, true, "Trend %s statistics is NULL", #TREND); \
+    return RVAL; \
+}
+
+
+// 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
+);
+
+bool psMemCheckTrend2D(psPtr ptr
+    );
+
+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(bool *goodFit,
+		  pmTrend2D *trend,
+                  psVector *mask,       // Warning: mask is modified!
+                  psVectorMaskType maskVal,
+                  const psVector *x,
+                  const psVector *y,
+                  const psVector *f,
+                  const psVector *df
+    );
+
+double pmTrend2DEval(const pmTrend2D *trend,
+                     float x, float y
+    );
+psVector *pmTrend2DEvalVector(const pmTrend2D *trend, psVector *mask, psVectorMaskType maskValue, 
+                              const psVector *x, const psVector *y
+    );
+
+psString pmTrend2DModeToString(pmTrend2DMode mode);
+pmTrend2DMode pmTrend2DModeFromString(psString name);
+
+bool pmTrend2DPrintMap (pmTrend2D *trend);
+
+/// @}
+# endif
Index: /branches/eam_branches/psModules.20240412/src/psmodules.h
===================================================================
--- /branches/eam_branches/psModules.20240412/src/psmodules.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/src/psmodules.h	(revision 42651)
@@ -0,0 +1,166 @@
+#ifndef PS_MODULES_H
+#define PS_MODULES_H
+
+#include <pslib.h>
+
+// the following headers are from psModule:extras
+#include <psPipe.h>
+#include <psIOBuffer.h>
+#include <psVectorBracket.h>
+#include <pmKapaPlots.h>
+#include <pmVisual.h>
+#include <pmVisualUtils.h>
+#include <ippStages.h>
+#include <ippDiffMode.h>
+#include <pmCensor.h>
+
+// XXX the following headers define constructs needed by the elements below
+#include <pmConfig.h>
+#include <pmDetrendDB.h>
+#include <pmDetrendThreads.h>
+#include <pmHDU.h>
+#include <pmFPA.h>
+#include <pmFPALevel.h>
+#include <pmFPAview.h>
+#include <pmFPAfile.h>
+
+// the following headers are from psModule:config
+#include <pmErrorCodes.h>
+#include <pmConfigRecipes.h>
+#include <pmConfigCamera.h>
+#include <pmConfigCommand.h>
+#include <pmConfigMask.h>
+#include <pmConfigDump.h>
+#include <pmConfigRun.h>
+#include <pmConfigRecipeValue.h>
+#include <pmVersion.h>
+
+// the following headers are from psModule:concepts
+#include <pmConcepts.h>
+#include <pmConceptsStandard.h>
+#include <pmConceptsRead.h>
+#include <pmConceptsWrite.h>
+#include <pmConceptsCopy.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 <pmFPAfileFringeIO.h>
+#include <pmFPAfileIO.h>
+#include <pmFPARead.h>
+#include <pmFPAConstruct.h>
+#include <pmFPACopy.h>
+#include <pmFPAHeader.h>
+#include <pmFPAMaskWeight.h>
+#include <pmFPAMosaic.h>
+#include <pmFPARead.h>
+#include <pmFPAWrite.h>
+#include <pmFPA_JPEG.h>
+#include <pmFPAExtent.h>
+#include <pmFPACalibration.h>
+#include <pmReadoutStack.h>
+#include <pmFPAUtils.h>
+#include <pmCellSquish.h>
+#include <pmFPABin.h>
+
+// the following headers are from psModule:detrend
+#include <pmFlatField.h>
+#include <pmFlatNormalize.h>
+#include <pmFringeStats.h>
+#include <pmMaskBadPixels.h>
+#include <pmMaskStats.h>
+#include <pmNonLinear.h>
+#include <pmNewNonLinear.h>
+#include <pmOverscan.h>
+#include <pmBias.h>
+#include <pmShutterCorrection.h>
+// #include <pmSkySubtract.h>
+#include <pmDark.h>
+#include <pmRemnance.h>
+#include <pmPattern.h>
+#include <pmPatternIO.h>
+
+// the following headers are from psModule:astrom
+#include <pmAstrometryWCS.h>
+#include <pmAstrometryUtils.h>
+#include <pmAstrometryRegions.h>
+#include <pmAstrometryObjects.h>
+#include <pmAstrometryModel.h>
+#include <pmAstrometryRefstars.h>
+#include <pmAstrometryDistortion.h>
+#include <pmAstrometryVisual.h>
+#include <pmKHcorrect.h>
+
+// the following headers are from psModule:imcombine
+#include <pmStack.h>
+#include <pmSubtractionTypes.h>
+#include <pmStackReject.h>
+#include <pmSubtraction.h>
+#include <pmSubtractionThreads.h>
+#include <pmSubtractionStamps.h>
+#include <pmSubtractionKernels.h>
+#include <pmSubtractionDeconvolve.h>
+#include <pmSubtractionAnalysis.h>
+#include <pmSubtractionMatch.h>
+#include <pmSubtractionIO.h>
+#include <pmSubtractionParams.h>
+#include <pmSubtractionMask.h>
+#include <pmSubtractionEquation.h>
+#include <pmReadoutCombine.h>
+#include <pmSubtractionVisual.h>
+#include <pmStackVisual.h>
+
+// the following headers are from psModule:objects
+#include <pmTrend2D.h>
+#include <pmResiduals.h>
+#include <pmGrowthCurve.h>
+
+#include <pmSpan.h>
+#include <pmFootprintSpans.h>
+#include <pmFootprint.h>
+#include <pmPeaks.h>
+#include <pmDetections.h>
+#include <pmMoments.h>
+
+#include <pmModelFuncs.h>
+#include <pmModelClass.h>
+#include <pmModel.h>
+#include <pmModel_CentralPixel.h>
+
+#include <pmSourceMasks.h>
+#include <pmSourceExtendedPars.h>
+#include <pmSourceSatstar.h>
+#include <pmSourceDiffStats.h>
+#include <pmSourceLensing.h>
+#include <pmSource.h>
+#include <pmSourceFitModel.h>
+#include <pmPSF.h>
+#include <pmPSFtry.h>
+#include <pmPhotObj.h>
+#include <pmSourceUtils.h>
+#include <pmSourceIO.h>
+#include <pmSourceSky.h>
+#include <pmSourceFitSet.h>
+#include <pmSourceContour.h>
+#include <pmSourcePlots.h>
+#include <pmPSF_IO.h>
+#include <pmModelUtils.h>
+#include <pmSourcePhotometry.h>
+#include <pmGrowthCurveGenerate.h>
+#include <pmSourceVisual.h>
+#include <pmSourceMatch.h>
+#include <pmDetEff.h>
+#include <pmPCMdata.h>
+
+// The following headers are from random locations, here because they cross bounds
+#include <pmReadoutFake.h>
+#include <pmPSFEnvelope.h>
+#include <pmThreadTools.h>
+
+#endif
Index: /branches/eam_branches/psModules.20240412/templates/c
===================================================================
--- /branches/eam_branches/psModules.20240412/templates/c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/templates/c	(revision 42651)
@@ -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_branches/psModules.20240412/templates/h
===================================================================
--- /branches/eam_branches/psModules.20240412/templates/h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/templates/h	(revision 42651)
@@ -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_branches/psModules.20240412/test/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/Makefile.am	(revision 42651)
@@ -0,0 +1,9 @@
+SUBDIRS = tap pstap $(SRCDIRS)
+
+TESTS = test.pl
+
+EXTRA_DIST = test.pl
+
+CLEANFILES = core core.* *~
+
+test: check
Index: /branches/eam_branches/psModules.20240412/test/astrom/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/astrom/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/astrom/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/astrom/tap_pmAstrometry.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/astrom/tap_pmAstrometry.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/astrom/tap_pmAstrometry.c	(revision 42651)
@@ -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.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-01-29 19:58:01 $
+ *
+ *  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, cameraName);
+    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_branches/psModules.20240412/test/astrom/tap_pmAstrometryWCS.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/astrom/tap_pmAstrometryWCS.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/astrom/tap_pmAstrometryWCS.c	(revision 42651)
@@ -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, 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*PS_DEG_RAD, 0.0, "projection center RA %f", fpa->toSky->R*PS_DEG_RAD);
+        ok_float(fpa->toSky->R*PS_DEG_RAD, 0.0, "projection center DEC %f", fpa->toSky->R*PS_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, 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*PS_DEG_RAD, 0.0, "projection center RA %f", fpa->toSky->R*PS_DEG_RAD);
+        ok_float(fpa->toSky->R*PS_DEG_RAD, 0.0, "projection center DEC %f", fpa->toSky->R*PS_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_branches/psModules.20240412/test/astrom/tap_pmAstrometryWCS_DVO.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/astrom/tap_pmAstrometryWCS_DVO.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/astrom/tap_pmAstrometryWCS_DVO.c	(revision 42651)
@@ -0,0 +1,528 @@
+#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*PS_DEG_RAD, sky2->r*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", sky1->r*PS_DEG_RAD, sky2->r*PS_DEG_RAD, sky1->r*PS_DEG_RAD - sky2->r*PS_DEG_RAD);
+            ok_float(sky1->d*PS_DEG_RAD, sky2->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", sky1->d*PS_DEG_RAD, sky2->d*PS_DEG_RAD, sky1->d*PS_DEG_RAD - sky2->d*PS_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*PS_DEG_RAD, sky2->r*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", sky1->r*PS_DEG_RAD, sky2->r*PS_DEG_RAD, sky1->r*PS_DEG_RAD - sky2->r*PS_DEG_RAD);
+            ok_float(sky1->d*PS_DEG_RAD, sky2->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", sky1->d*PS_DEG_RAD, sky2->d*PS_DEG_RAD, sky1->d*PS_DEG_RAD - sky2->d*PS_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*PS_DEG_RAD, sky2->r*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", sky1->r*PS_DEG_RAD, sky2->r*PS_DEG_RAD, sky1->r*PS_DEG_RAD - sky2->r*PS_DEG_RAD);
+            ok_float(sky1->d*PS_DEG_RAD, sky2->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", sky1->d*PS_DEG_RAD, sky2->d*PS_DEG_RAD, sky2->d*PS_DEG_RAD - sky2->d*PS_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*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, sky->r*PS_DEG_RAD, rDVO - sky->r*PS_DEG_RAD);
+            ok_float(dDVO, sky->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, sky->d*PS_DEG_RAD, dDVO - sky->d*PS_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*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, sky->r*PS_DEG_RAD, rDVO - sky->r*PS_DEG_RAD);
+            ok_float(dDVO, sky->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, sky->d*PS_DEG_RAD, dDVO - sky->d*PS_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*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, sky->r*PS_DEG_RAD, rDVO - sky->r*PS_DEG_RAD);
+            ok_float(dDVO, sky->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, sky->d*PS_DEG_RAD, dDVO - sky->d*PS_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(2);
+
+    ok(true, "Skipping tests: (libdvo not available)");
+    note("pmAstrometryWCS tests compared with DVO coords routines : SKIPPED (libdvo not available)");
+
+    return exit_status();
+}
+
+# endif
+
Index: /branches/eam_branches/psModules.20240412/test/astrom/tap_pmAstrometryWCS_DVO2.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/astrom/tap_pmAstrometryWCS_DVO2.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/astrom/tap_pmAstrometryWCS_DVO2.c	(revision 42651)
@@ -0,0 +1,630 @@
+#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, 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*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, onSky->r*PS_DEG_RAD, rDVO - onSky->r*PS_DEG_RAD);
+            ok_float(dDVO, onSky->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, onSky->d*PS_DEG_RAD, dDVO - onSky->d*PS_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, 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*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, onSky->r*PS_DEG_RAD, rDVO - onSky->r*PS_DEG_RAD);
+            ok_float(dDVO, onSky->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, onSky->d*PS_DEG_RAD, dDVO - onSky->d*PS_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, 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*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, onSky->r*PS_DEG_RAD, rDVO - onSky->r*PS_DEG_RAD);
+            ok_float(dDVO, onSky->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, onSky->d*PS_DEG_RAD, dDVO - onSky->d*PS_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, 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*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, onSky->r*PS_DEG_RAD, rDVO - onSky->r*PS_DEG_RAD);
+            ok_float(dDVO, onSky->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, onSky->d*PS_DEG_RAD, dDVO - onSky->d*PS_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, 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*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, onSky->r*PS_DEG_RAD, rDVO - onSky->r*PS_DEG_RAD);
+            ok_float(dDVO, onSky->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, onSky->d*PS_DEG_RAD, dDVO - onSky->d*PS_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, 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*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, onSky->r*PS_DEG_RAD, rDVO - onSky->r*PS_DEG_RAD);
+            ok_float(dDVO, onSky->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, onSky->d*PS_DEG_RAD, dDVO - onSky->d*PS_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, 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(2);
+
+    ok(true, "Skipping tests: (libdvo not available)");
+    note("pmAstrometryWCS tests compared with DVO coords routines : SKIPPED (libdvo not available)");
+
+    return exit_status();
+}
+
+# endif
+
Index: /branches/eam_branches/psModules.20240412/test/astrom/tap_pmAstrometryWCS_DVO3.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/astrom/tap_pmAstrometryWCS_DVO3.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/astrom/tap_pmAstrometryWCS_DVO3.c	(revision 42651)
@@ -0,0 +1,655 @@
+#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, 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*PS_DEG_RAD, bSky->r*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->r*PS_DEG_RAD, bSky->r*PS_DEG_RAD, aSky->r*PS_DEG_RAD - bSky->r*PS_DEG_RAD);
+            ok_float(aSky->d*PS_DEG_RAD, bSky->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->d*PS_DEG_RAD, bSky->d*PS_DEG_RAD, aSky->d*PS_DEG_RAD - bSky->d*PS_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, 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*PS_DEG_RAD, bSky->r*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->r*PS_DEG_RAD, bSky->r*PS_DEG_RAD, aSky->r*PS_DEG_RAD - bSky->r*PS_DEG_RAD);
+            ok_float(aSky->d*PS_DEG_RAD, bSky->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->d*PS_DEG_RAD, bSky->d*PS_DEG_RAD, aSky->d*PS_DEG_RAD - bSky->d*PS_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, 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*PS_DEG_RAD, bSky->r*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->r*PS_DEG_RAD, bSky->r*PS_DEG_RAD, aSky->r*PS_DEG_RAD - bSky->r*PS_DEG_RAD);
+            ok_float(aSky->d*PS_DEG_RAD, bSky->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->d*PS_DEG_RAD, bSky->d*PS_DEG_RAD, aSky->d*PS_DEG_RAD - bSky->d*PS_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, 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*PS_DEG_RAD, bSky->r*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->r*PS_DEG_RAD, bSky->r*PS_DEG_RAD, aSky->r*PS_DEG_RAD - bSky->r*PS_DEG_RAD);
+            ok_float(aSky->d*PS_DEG_RAD, bSky->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->d*PS_DEG_RAD, bSky->d*PS_DEG_RAD, aSky->d*PS_DEG_RAD - bSky->d*PS_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, 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*PS_DEG_RAD, bSky->r*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->r*PS_DEG_RAD, bSky->r*PS_DEG_RAD, aSky->r*PS_DEG_RAD - bSky->r*PS_DEG_RAD);
+            ok_float(aSky->d*PS_DEG_RAD, bSky->d*PS_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->d*PS_DEG_RAD, bSky->d*PS_DEG_RAD, aSky->d*PS_DEG_RAD - bSky->d*PS_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, 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*PS_DEG_RAD, bSky->r*PS_DEG_RAD, 0.01/3600.0, "coordinate match: %f vs %f (delta = %f)", aSky->r*PS_DEG_RAD, bSky->r*PS_DEG_RAD, aSky->r*PS_DEG_RAD - bSky->r*PS_DEG_RAD);
+            ok_float_tol(aSky->d*PS_DEG_RAD, bSky->d*PS_DEG_RAD, 0.01/3600.0, "coordinate match: %f vs %f (delta = %f)", aSky->d*PS_DEG_RAD, bSky->d*PS_DEG_RAD, aSky->d*PS_DEG_RAD - bSky->d*PS_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(2);
+
+    ok(true, "Skipping tests: (libdvo not available)");
+    note("pmAstrometryWCS tests compared with DVO coords routines : SKIPPED (libdvo not available)");
+
+    return exit_status();
+}
+
+# endif
+
Index: /branches/eam_branches/psModules.20240412/test/astrom/tap_pmAstrometryWCS_DVO4.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/astrom/tap_pmAstrometryWCS_DVO4.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/astrom/tap_pmAstrometryWCS_DVO4.c	(revision 42651)
@@ -0,0 +1,956 @@
+#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, 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, 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*PS_DEG_RAD, rDVO, "coordinate match: %f vs %f (delta = %f)", onSky->r*PS_DEG_RAD, rDVO, 3600.0*(onSky->r*PS_DEG_RAD - rDVO));
+            ok_float(onSky->d*PS_DEG_RAD, dDVO, "coordinate match: %f vs %f (delta = %f)", onSky->d*PS_DEG_RAD, dDVO, 3600.0*(onSky->d*PS_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, 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, 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, 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, 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, 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, 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(2);
+
+    ok(true, "Skipping tests: (libdvo not available)");
+    note("pmAstrometryWCS tests compared with DVO coords routines : SKIPPED (libdvo not available)");
+
+    return exit_status();
+}
+
+# endif
+
Index: /branches/eam_branches/psModules.20240412/test/astrom/tap_pmHDU.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/astrom/tap_pmHDU.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/astrom/tap_pmHDU.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/astrom/tst_pmAstrometry.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/astrom/tst_pmAstrometry.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/astrom/tst_pmAstrometry.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/astrom/tst_pmAstrometry01.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/astrom/tst_pmAstrometry01.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/astrom/tst_pmAstrometry01.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/camera/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/Makefile.am	(revision 42651)
@@ -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_pmHDUUtils \
+	tap_pmFPALevel \
+	tap_pmFPAFlags \
+	tap_pmFPAExtent \
+	tap_pmFPAUtils \
+	tap_pmFPAConstruct \
+	tap_pmFPAView \
+	tap_pmFPAHeader \
+	tap_pmFPAMaskW \
+	tap_pmFPACellSquish \
+	tap_pmFPACopy
+
+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_branches/psModules.20240412/test/camera/data/SampleIPPConfig
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/data/SampleIPPConfig	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/data/SampleIPPConfig	(revision 42651)
@@ -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_branches/psModules.20240412/test/camera/data/camera0/camera.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/data/camera0/camera.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/data/camera0/camera.config	(revision 42651)
@@ -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_branches/psModules.20240412/test/camera/data/camera0/format0.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/data/camera0/format0.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/data/camera0/format0.config	(revision 42651)
@@ -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_branches/psModules.20240412/test/camera/data/camera0/format1.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/data/camera0/format1.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/data/camera0/format1.config	(revision 42651)
@@ -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_branches/psModules.20240412/test/camera/data/camera0/recipe0.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/data/camera0/recipe0.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/data/camera0/recipe0.config	(revision 42651)
@@ -0,0 +1,4 @@
+R0_KEY0		STR	RecipeString0
+R0_KEY1		STR	RecipeString1
+R0_KEY2		S32	2
+R0_KEY3		F32	3
Index: /branches/eam_branches/psModules.20240412/test/camera/data/camera0/recipe1.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/data/camera0/recipe1.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/data/camera0/recipe1.config	(revision 42651)
@@ -0,0 +1,4 @@
+R1_KEY0		STR	RecipeString10
+R1_KEY1		STR	RecipeString11
+R1_KEY2		S32	12
+R1_KEY3		F32	13
Index: /branches/eam_branches/psModules.20240412/test/camera/data/camera1/camera.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/data/camera1/camera.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/data/camera1/camera.config	(revision 42651)
@@ -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_branches/psModules.20240412/test/camera/data/camera1/format0.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/data/camera1/format0.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/data/camera1/format0.config	(revision 42651)
@@ -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_branches/psModules.20240412/test/camera/data/camera1/format1.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/data/camera1/format1.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/data/camera1/format1.config	(revision 42651)
@@ -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_branches/psModules.20240412/test/camera/data/camera1/recipe0.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/data/camera1/recipe0.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/data/camera1/recipe0.config	(revision 42651)
@@ -0,0 +1,4 @@
+R0_KEY0		STR	RecipeString120
+R0_KEY1		STR	RecipeString121
+R0_KEY2		S32	122
+R0_KEY3		F32	123
Index: /branches/eam_branches/psModules.20240412/test/camera/data/camera1/recipe1.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/data/camera1/recipe1.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/data/camera1/recipe1.config	(revision 42651)
@@ -0,0 +1,4 @@
+R1_KEY0		STR	RecipeString230
+R1_KEY1		STR	RecipeString2311
+R1_KEY2		S32	232
+R1_KEY3		F32	233
Index: /branches/eam_branches/psModules.20240412/test/camera/data/path2/SampleIPPConfig2
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/data/path2/SampleIPPConfig2	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/data/path2/SampleIPPConfig2	(revision 42651)
@@ -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_branches/psModules.20240412/test/camera/data/sampleFitsFile.fits
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/data/sampleFitsFile.fits	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/data/sampleFitsFile.fits	(revision 42651)
@@ -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_branches/psModules.20240412/test/camera/tap_pmAstrometry.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmAstrometry.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmAstrometry.c	(revision 42651)
@@ -0,0 +1,667 @@
+/** @file  tst_psAstrometry01.c
+*
+* XXX: The source code that is tested here is in the pmAstrometry.c and
+* pmAstrometry.h files.  However, those files are not included in the
+* current automake configuration.  These tests are therefore, not
+* runnable.  We include them in this distribution for the future.
+*
+* XXX: These tests need to be converted to tap format.
+*
+* 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.
+*
+*  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 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
+
+psS32 currentId = 0;
+psS32 memLeaks = 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->fromTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    myFPA->toTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    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;
+        int myChipRow0 = 0;
+        int myChipCol0 = 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) myChipCol0;
+        myChip->toFPA->y->coeff[1][1] = 0.0;
+        myChip->toFPA->x->coeff[0][0] = (psF64) myChipRow0;
+        myChip->toFPA->x->coeff[1][1] = 0.0;
+
+        myChip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+        myChip->fromFPA->y->coeff[0][0] = (psF64) (- myChipCol0);
+        myChip->fromFPA->y->coeff[1][1] = 0.0;
+        myChip->fromFPA->x->coeff[0][0] = (psF64) (- myChipRow0);
+        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;
+            int myCellRow0 = 0;
+            int myCellCol0 = cellID * (CELL_WIDTH + CELL_GAP);
+            myCell->toChip = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+            myCell->toChip->y->coeff[0][0] = (psF64) myCellCol0;
+            myCell->toChip->y->coeff[1][1] = 0.0;
+            myCell->toChip->x->coeff[0][0] = (psF64) myCellRow0;
+            myCell->toChip->x->coeff[1][1] = 0.0;
+
+            myCell->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+            myCell->toFPA->y->coeff[0][0] = (psF64) (myCellCol0 + myChipCol0);
+            myCell->toFPA->y->coeff[1][1] = 0.0;
+            myCell->toFPA->x->coeff[0][0] = (psF64) (myCellRow0 + myChipRow0);
+            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)));
+                    }
+                }
+                int myReadoutRow0 = myCellRow0;
+                int myReadoutCol0 = myCellCol0;
+                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);
+}
+
+psS32 main( psS32 argc, char* argv[] )
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    plan_tests(92);
+
+    test3();
+    test4();
+    test5();
+}
Index: /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPA.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPA.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPA.c	(revision 42651)
@@ -0,0 +1,492 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested.
+*/
+
+#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->variance = 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] = psMemDecrRefCounter((psPtr) 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] = psMemDecrRefCounter((psPtr) 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, NULL);
+    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);
+    if (camera != NULL) {
+        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] = psMemDecrRefCounter((psPtr) generateSimpleChip(fpa));
+    }
+
+    return(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    plan_tests(92);
+
+    // ------------------------------------------------------------------------
+    // pmFPAAlloc() tests
+    // Call pmFPAAlloc() with NULL pmCamera input.
+    {
+        psMemId id = psMemGetId();
+        pmFPA* fpa = pmFPAAlloc(NULL, NULL);
+        ok(fpa != NULL && psMemCheckFPA(fpa), "pmFPAAlloc() returned a non-NULL pmFPA with a NULL pmCamera input");
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Call pmFPAAlloc() with acceptable input parameters.
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = pmFPAAlloc(camera, NULL);
+        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");
+        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 (Populate the pmFPA struct with real data)");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmFPAFreeData() tests
+    // Call pmFPAFreeData() with NULL pmFPA input parameter
+    {
+        psMemId id = psMemGetId();
+        pmFPAFreeData(NULL);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks (Call pmFPAFreeData() with NULL pmFPA input parameter)");
+    }
+
+
+    // Call pmFPAFreeData() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmFPA* fpa = generateSimpleFPA(NULL);
+        fpa->hdu->images = psArrayAlloc(10);
+        fpa->hdu->variances = psArrayAlloc(10);
+        fpa->hdu->masks = psArrayAlloc(10);
+        pmFPAFreeData(fpa);
+        ok(fpa->hdu->images == NULL, "pmFPAFreeData() correctly set fpa->hdu->images to NULL");
+        ok(fpa->hdu->variances == NULL, "pmFPAFreeData() correctly set fpa->hdu->weights to NULL");
+        ok(fpa->hdu->masks == NULL, "pmFPAFreeData() correctly set fpa->hdu->masks to NULL");
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmChipAlloc() tests
+    // Call pmChipAlloc() with NULL input parameters
+    {
+        psMemId id = psMemGetId();
+        pmChip *chip = pmChipAlloc(NULL, CHIP_ALLOC_NAME);
+        ok(chip != NULL, "pmChipAlloc() returned non-NULL with NULL pmFPA input parameter");
+        psFree(chip);
+        pmFPA* fpa = generateSimpleFPA(NULL);
+        chip = pmChipAlloc(fpa, NULL);
+        ok(chip != NULL, "pmChipAlloc() returned non-NULL with NULL chip name input parameter");
+        psFree(fpa);
+        psFree(chip);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmChipAlloc() tests
+    // XXX: Add tests for NULL inputs.
+    {
+        psMemId id = psMemGetId();
+        pmFPA* fpa = generateSimpleFPA(NULL);
+        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");
+        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 (Populate the pmChip struct with real data)");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmChipFreeData() tests
+    // Call pmChipFreeData() with NULL pmFPA input parameter
+    {
+        psMemId id = psMemGetId();
+        pmChipFreeData(NULL);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks (Call pmChipFreeData() with NULL pmFPA input parameter)");
+    }
+
+
+    // Call pmChipFreeData() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmChip* chip = generateSimpleChip(NULL);
+        chip->hdu->images = psArrayAlloc(10);
+        chip->hdu->variances = psArrayAlloc(10);
+        chip->hdu->masks = psArrayAlloc(10);
+        pmChipFreeData(chip);
+        ok(chip->hdu->images == NULL, "pmChipFreeData() correctly set chip->hdu->images to NULL");
+        ok(chip->hdu->variances == NULL, "pmChipFreeData() correctly set chip->hdu->weights to NULL");
+        ok(chip->hdu->masks == NULL, "pmChipFreeData() correctly set chip->hdu->masks to NULL");
+        psFree(chip);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmChipFreeCells() tests
+    // Call pmChipFreeCells() with NULL pmFPA input parameter
+    {
+        psMemId id = psMemGetId();
+        pmChipFreeCells(NULL);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks (Call pmChipFreeCells() with NULL pmFPA input parameter)");
+    }
+
+
+    // Call pmChipFreeCells() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmChip* chip = generateSimpleChip(NULL);
+        pmChipFreeCells(chip);
+        ok(chip->cells->n == 0, "pmChipFreeCells() free'ed chip->cells correctly");
+        psFree(chip);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmCellAlloc() tests
+    // Call pmCellAlloc() with NULL input parameters
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = pmCellAlloc(NULL, CELL_ALLOC_NAME);
+        ok(cell != NULL, "pmCellAlloc returned non-NULL with NULL pmChip input parameter");
+        psFree(cell);
+
+        pmChip *chip = pmChipAlloc(NULL, NULL);
+        cell = pmCellAlloc(chip, NULL);
+        ok(cell != NULL, "pmCellAlloc returned non-NULL with NULL cell name input parameter");
+        psFree(chip);
+        psFree(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmCellAlloc() tests
+    // Call pmCellAlloc() with acceptable input parameters
+    {
+        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");
+        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 (Populate the pmCell struct with real data)");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmCellFreeData() tests
+    // Call pmCellFreeData() with NULL pmFPA input parameter
+    {
+        psMemId id = psMemGetId();
+        pmCellFreeData(NULL);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks (Call pmCellFreeData() with NULL pmFPA input parameter)");
+    }
+
+
+    // Call pmCellFreeData() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        cell->hdu->images = psArrayAlloc(10);
+        cell->hdu->variances = psArrayAlloc(10);
+        cell->hdu->masks = psArrayAlloc(10);
+        pmCellFreeData(cell);
+        ok(cell->hdu->images == NULL, "pmCellFreeData() correctly set cell->hdu->images to NULL");
+        ok(cell->hdu->variances == NULL, "pmCellFreeData() correctly set cell->hdu->weights to NULL");
+        ok(cell->hdu->masks == NULL, "pmCellFreeData() correctly set cell->hdu->masks to 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
+    // Call pmReadoutAlloc() with NULL input parameters
+    {
+        psMemId id = psMemGetId();
+        pmReadout *readout = pmReadoutAlloc(NULL);
+        ok(readout != NULL, "pmReadoutAlloc() returned non-NULL with NULL pmCell input parameter");
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmReadoutAlloc() with acceptable parameters
+    {
+        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->variance == 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");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmReadoutFreeData() tests
+    // Call pmReadoutFreeData() with NULL pmFPA input parameter
+    {
+        psMemId id = psMemGetId();
+        pmReadoutFreeData(NULL);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks (Call pmReadoutFreeData() with NULL pmFPA input parameter)");
+    }
+
+
+    // Call pmReadoutFreeData() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        pmReadoutFreeData(readout);
+        ok(readout->image == NULL, "pmReadoutFreeData() correctly set readout->image to NULL");
+        ok(readout->variance == NULL, "pmReadoutFreeData() correctly set readout->weight to NULL");
+        ok(readout->mask == NULL, "pmReadoutFreeData() correctly set readout->mask to NULL");
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmFPACheckParents() tests
+    // psBool pmFPACheckParents(pmFPA *fpa)
+    // Call pmFPACheckParents() with NULL pmFPA input parameter
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmFPACheckParents(NULL);
+        ok(rc == false, "pmFPACheckParents() returned FALSE with NULL pmFPA input parameter");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPACheckParents() tests
+    // Call pmFPACheckParents() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmFPA* fpa = generateSimpleFPA(NULL);
+        bool rc = pmFPACheckParents(fpa);
+        ok(rc == true, "pmFPACheckParents() returned FALSE with acceptable input parameters");
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+}
Index: /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPACellSquish.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPACellSquish.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPACellSquish.c	(revision 42651)
@@ -0,0 +1,193 @@
+#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->variance = 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] = psMemDecrRefCounter((psPtr) generateSimpleReadout(cell));
+    }
+
+    // First try to read data from ../dataFiles, then try dataFiles.
+    bool rc = pmConfigFileRead(&cell->hdu->format, "../dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&cell->hdu->format, "dataFiles/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->variances = 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->variances->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->variances->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);
+
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = psMemDecrRefCounter((psPtr) 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, NULL);
+    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);
+    if (camera != NULL) {
+        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);
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = psMemDecrRefCounter((psPtr) generateSimpleChip(fpa));
+    }
+
+    pmConceptsBlankFPA(fpa);
+    return(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(4);
+
+
+    // ----------------------------------------------------------------------
+    // 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");
+        psFree(camera);
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAConstruct.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAConstruct.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAConstruct.c	(revision 42651)
@@ -0,0 +1,72 @@
+#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(9);
+
+    // ----------------------------------------------------------------------
+    // pmFPAConstruct() tests
+    // pmFPA *pmFPAConstruct(const psMetadata *camera)
+    // test will NULL camera input param
+    {
+        psMemId id = psMemGetId();
+        pmFPA* fpa = pmFPAConstruct(NULL, 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, "dataFiles/camera0/camera.config", "CAMERA 0 config file");
+        if (!rc) {
+             rc = pmConfigFileRead(&camera, "../dataFiles/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, NULL);
+        ok(fpa != NULL, "pmFPAConstruct() returned non-NULL");
+        if (VERBOSE) {
+            pmFPAPrint(stdout, fpa, true, true);
+	}
+        bool errorFlag = false;
+        ok(fpa->chips->n == 2, "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_branches/psModules.20240412/test/camera/tap_pmFPACopy.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPACopy.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPACopy.c	(revision 42651)
@@ -0,0 +1,601 @@
+#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_SOURCE		32
+#define MISC_NUM_TARGET		16
+#define MISC_NAME_SOURCE	"META00_SOURCE"
+#define MISC_NAME_TARGET	"META00_TARGET"
+#define NUM_BIAS_DATA		10
+#define SOURCE_NUM_ROWS		16
+#define SOURCE_NUM_COLS		8
+#define TARGET_NUM_ROWS		15
+#define TARGET_NUM_COLS		5
+#define NUM_READOUTS		4
+#define NUM_CELLS		6
+#define NUM_CHIPS		8
+#define SOURCE_BASE		10
+#define BASE_INC		20
+#define TARGET_BASE		(SOURCE_BASE + BASE_INC)
+#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 *generateSimpleReadoutSource(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(SOURCE_NUM_COLS, SOURCE_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(SOURCE_NUM_COLS, SOURCE_NUM_ROWS, PS_TYPE_U8);
+    readout->variance = psImageAlloc(SOURCE_NUM_COLS, SOURCE_NUM_ROWS, PS_TYPE_F32);
+    for (int i = 0 ; i < SOURCE_NUM_ROWS ; i++) {
+        for (int j = 0 ; j < SOURCE_NUM_COLS ; j++) {
+            readout->image->data.F32[i][j] = (float) (i + j + SOURCE_BASE);
+            readout->mask->data.U8[i][j] = (psU8) (i + j + SOURCE_BASE);
+            readout->variance->data.F32[i][j] = (float) (i + j + SOURCE_BASE);
+	}
+    }
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(SOURCE_NUM_COLS, SOURCE_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_SOURCE, 0, NULL, MISC_NUM_SOURCE);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCellSource(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME_SOURCE, 0, NULL, MISC_NUM_SOURCE);
+    cell->hdu = pmHDUAlloc(NULL);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME_SOURCE, 0, NULL, MISC_NUM_SOURCE);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, "CELL.XPARITY", PS_META_REPLACE, NULL, 1);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, "CELL.YPARITY", PS_META_REPLACE, NULL, 1);
+
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = psMemDecrRefCounter((psPtr) generateSimpleReadoutSource(cell));
+    }
+    cell->data_exists = true;
+    return(cell);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmChip *generateSimpleChipSource(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_SOURCE, 0, NULL, MISC_NUM_SOURCE);
+    psMetadataAddS32(chip->concepts, PS_LIST_HEAD, MISC_NAME_SOURCE, 0, NULL, MISC_NUM_SOURCE);
+    chip->hdu = pmHDUAlloc(NULL);
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = psMemDecrRefCounter((psPtr) generateSimpleCellSource(chip));
+    }
+    chip->data_exists = true;
+    return(chip);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmFPA* generateSimpleFPASource(psMetadata *camera)
+{
+    pmFPA* fpa = pmFPAAlloc(camera, NULL);
+    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_SOURCE, 0, NULL, MISC_NUM_SOURCE);
+    if (camera != NULL) {
+        psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME_SOURCE, 0, NULL, MISC_NUM_SOURCE);
+    }
+    psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME_SOURCE, 0, NULL, MISC_NUM_SOURCE);
+    fpa->hdu = pmHDUAlloc(NULL);
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = psMemDecrRefCounter((psPtr) generateSimpleChipSource(fpa));
+    }
+
+    return(fpa);
+}
+
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadoutTarget(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TARGET_NUM_COLS, TARGET_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TARGET_NUM_COLS, TARGET_NUM_ROWS, PS_TYPE_U8);
+    readout->variance = psImageAlloc(TARGET_NUM_COLS, TARGET_NUM_ROWS, PS_TYPE_F32);
+    for (int i = 0 ; i < TARGET_NUM_ROWS ; i++) {
+        for (int j = 0 ; j < TARGET_NUM_COLS ; j++) {
+            readout->image->data.F32[i][j] = (float) (i + j + TARGET_BASE);
+            readout->mask->data.U8[i][j] = (psU8) (i + j + TARGET_BASE);
+            readout->variance->data.F32[i][j] = (float) (i + j + TARGET_BASE);
+	}
+    }
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TARGET_NUM_COLS, TARGET_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_TARGET, 0, NULL, MISC_NUM_TARGET);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCellTarget(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME_TARGET, 0, NULL, MISC_NUM_TARGET);
+    cell->hdu = pmHDUAlloc(NULL);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME_TARGET, 0, NULL, MISC_NUM_TARGET);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, "CELL.XPARITY", PS_META_REPLACE, NULL, 1);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, "CELL.YPARITY", PS_META_REPLACE, NULL, 1);
+    psArrayRealloc(cell->readouts, 0);
+    for (int i = 0 ; i < 0 ; i++) {
+        cell->readouts->data[i] = psMemDecrRefCounter((psPtr) generateSimpleReadoutTarget(cell));
+    }
+    return(cell);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmChip *generateSimpleChipTarget(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_TARGET, 0, NULL, MISC_NUM_TARGET);
+    psMetadataAddS32(chip->concepts, PS_LIST_HEAD, MISC_NAME_TARGET, 0, NULL, MISC_NUM_TARGET);
+    chip->hdu = pmHDUAlloc(NULL);
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = psMemDecrRefCounter((psPtr) generateSimpleCellTarget(chip));
+    }
+    return(chip);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmFPA* generateSimpleFPATarget(psMetadata *camera)
+{
+    pmFPA* fpa = pmFPAAlloc(camera, NULL);
+    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_TARGET, 0, NULL, MISC_NUM_TARGET);
+    if (camera != NULL) {
+        psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME_TARGET, 0, NULL, MISC_NUM_TARGET);
+    }
+    psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME_TARGET, 0, NULL, MISC_NUM_TARGET);
+    fpa->hdu = pmHDUAlloc(NULL);
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = psMemDecrRefCounter((psPtr) generateSimpleChipTarget(fpa));
+    }
+
+    return(fpa);
+}
+
+bool testCellCopy(pmCell* cellTarget, pmCell *cellSource) {
+    bool errorFlag = false;
+    for (int readoutID = 0 ; readoutID < cellTarget->readouts->n ; readoutID++) {
+        pmReadout *readoutTarget = cellTarget->readouts->data[readoutID];
+        pmReadout *readoutSource = cellSource->readouts->data[readoutID];
+
+        psImage *imageTarget = readoutTarget->image;
+        psImage *imageSource = readoutSource->image;
+        for (int i = 0 ; i < SOURCE_NUM_ROWS ; i++) {
+            for (int j = 0 ; j < SOURCE_NUM_COLS ; j++) {
+                if (imageSource->data.F32[i][j] != imageTarget->data.F32[i][j]) {
+                    diag("ERROR: target readout[%d] image[%d][%d] is %.2f, should be %.2f",
+                          readoutID, i, j, imageTarget->data.F32[i][j], imageSource->data.F32[i][j]);
+                    errorFlag = true;
+		}
+	    }
+	}
+        if (errorFlag) {
+            diag("ERROR: pmCellCopy() did not set the data for readout %d, image correctly", readoutID);
+	}
+
+        psImage *maskTarget = readoutTarget->mask;
+        psImage *maskSource = readoutSource->mask;
+        for (int i = 0 ; i < SOURCE_NUM_ROWS ; i++) {
+            for (int j = 0 ; j < SOURCE_NUM_COLS ; j++) {
+                if (maskTarget->data.U8[i][j] != maskSource->data.U8[i][j]) {
+                    diag("ERROR: target readout[%d] mask[%d][%d] is %d, should be %d",
+                          readoutID, i, j, maskTarget->data.U8[i][j], maskSource->data.U8[i][j]);
+                    errorFlag = true;
+		}
+	    }
+	}
+        if (errorFlag) {
+            diag("ERROR: pmCellCopy() did not set the data for readout %d, mask correctly", readoutID);
+	}
+
+        psImage *varianceTarget = readoutTarget->variance;
+        psImage *varianceSource = readoutSource->variance;
+        for (int i = 0 ; i < SOURCE_NUM_ROWS ; i++) {
+            for (int j = 0 ; j < SOURCE_NUM_COLS ; j++) {
+                if (varianceTarget->data.F32[i][j] != varianceSource->data.F32[i][j]) {
+                    diag("ERROR: target readout[%d] variance[%d][%d] is %.2f, should be %.2f",
+                          readoutID, i, j, varianceTarget->data.F32[i][j], varianceSource->data.F32[i][j]);
+                    errorFlag = true;
+		}
+	    }
+	}
+        if (errorFlag) {
+            diag("ERROR: pmCellCopy() did not set the data for readout %d, variance correctly", readoutID);
+	}
+    }
+    return(errorFlag);
+}
+
+
+bool testChipCopy(pmChip* chipTarget, pmChip *chipSource) {
+    bool errorFlag = false;
+    for (int cellID = 0 ; cellID < chipSource->cells->n ; cellID++) {
+        pmCell *cellTarget = chipTarget->cells->data[cellID];
+        pmCell *cellSource = chipSource->cells->data[cellID];
+        errorFlag|= testCellCopy(cellTarget, cellSource);
+    }
+    return(errorFlag);
+}
+
+bool testFPACopy(pmFPA* fpaTarget, pmFPA *fpaSource) {
+    bool errorFlag = false;
+    for (int chipID = 0 ; chipID < fpaSource->chips->n ; chipID++) {
+        pmChip *chipTarget = fpaTarget->chips->data[chipID];
+        pmChip *chipSource = fpaSource->chips->data[chipID];
+        errorFlag|= testChipCopy(chipTarget, chipSource);
+    }
+    return(errorFlag);
+}
+
+
+bool testCellCopyStructure(pmCell* cellTarget, pmCell *cellSource) {
+    bool errorFlag = false;
+    for (int readoutID = 0 ; readoutID < cellTarget->readouts->n ; readoutID++) {
+        pmReadout *readoutTarget = cellTarget->readouts->data[readoutID];
+        if (readoutTarget->image->numRows != SOURCE_NUM_ROWS ||
+            readoutTarget->image->numCols != SOURCE_NUM_COLS) {
+            diag("ERROR: readoutTarget->image size is (%d by %d)\n", readoutTarget->image->numRows, 
+                  readoutTarget->image->numCols);
+            errorFlag = true;
+	}
+
+        if (readoutTarget->mask->numRows != SOURCE_NUM_ROWS ||
+            readoutTarget->mask->numCols != SOURCE_NUM_COLS) {
+            diag("ERROR: readoutTarget->mask size is (%d by %d)\n", readoutTarget->mask->numRows, 
+                  readoutTarget->mask->numCols);
+            errorFlag = true;
+	}
+        if (readoutTarget->variance->numRows != SOURCE_NUM_ROWS ||
+            readoutTarget->variance->numCols != SOURCE_NUM_COLS) {
+            diag("ERROR: readoutTarget->variance size is (%d by %d)\n", readoutTarget->variance->numRows, 
+                  readoutTarget->variance->numCols);
+            errorFlag = true;
+	}
+    }
+    return(errorFlag);
+}
+
+
+bool testChipCopyStructure(pmChip* chipTarget, pmChip *chipSource) {
+    bool errorFlag = false;
+    for (int cellID = 0 ; cellID < chipSource->cells->n ; cellID++) {
+        pmCell *cellTarget = chipTarget->cells->data[cellID];
+        pmCell *cellSource = chipSource->cells->data[cellID];
+        errorFlag|= testCellCopyStructure(cellTarget, cellSource);
+    }
+    return(errorFlag);
+}
+
+bool testFPACopyStructure(pmFPA* fpaTarget, pmFPA *fpaSource) {
+    bool errorFlag = false;
+    for (int chipID = 0 ; chipID < fpaSource->chips->n ; chipID++) {
+        pmChip *chipTarget = fpaTarget->chips->data[chipID];
+        pmChip *chipSource = fpaSource->chips->data[chipID];
+        errorFlag|= testChipCopyStructure(chipTarget, chipSource);
+    }
+    return(errorFlag);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(92);
+
+
+    // ------------------------------------------------------------------------
+    // pmFPACopy() tests
+    // Call pmFPACopy() with bad input parameters.
+    if (1) {
+        psMemId id = psMemGetId();
+        pmFPA* fpaSource = generateSimpleFPASource(NULL);
+        pmFPA* fpaTarget = generateSimpleFPATarget(NULL);
+        bool rc = pmFPACopy(NULL, fpaSource);
+        ok(rc == FALSE, "pmFPACopy() returned FALSE with NULL target pmFPA input parameter");
+        rc = pmFPACopy(fpaTarget, NULL);
+        ok(rc == FALSE, "pmFPACopy() returned FALSE with NULL source pmFPA input parameter");
+        psFree(fpaSource);
+        psFree(fpaTarget);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmFPACopy() with acceptable input parameters.
+    if (1) {
+        psMemId id = psMemGetId();
+        pmFPA* fpaSource = generateSimpleFPASource(NULL);
+        pmFPA* fpaTarget = generateSimpleFPATarget(NULL);
+        bool rc = pmFPACopy(fpaTarget, fpaSource);
+        ok(rc == true, "pmFPACopy() returned TRUE with acceptable input parameters");
+        int tmpS32 = psMetadataLookupS32(&rc, fpaTarget->concepts, MISC_NAME_TARGET);
+        ok(rc, "psMetadataLookupStr(NULL, fpaTarget->concepts, MISC_NAME_TARGET) was successful");
+        ok(tmpS32 == MISC_NUM_TARGET, "pmFPACopy() copied the source FPA concepts correctly");
+
+        tmpS32 = psMetadataLookupS32(&rc, fpaTarget->concepts, MISC_NAME_SOURCE);
+        ok(rc, "psMetadataLookupStr(NULL, fpaTarget->concepts, MISC_NAME_SOURCE) was successful");
+        ok(tmpS32 == MISC_NUM_SOURCE, "pmFPACopy() copied the source FPA concepts correctly");
+        ok(!testFPACopy(fpaTarget, fpaSource), "pmFPACopy() set the pmReadout data correctly");
+
+        psFree(fpaSource);
+        psFree(fpaTarget);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmChipCopy() tests
+    // Call pmChipCopy() with bad input parameters.
+    if (1) {
+        psMemId id = psMemGetId();
+        pmChip* chipSource = generateSimpleChipSource(NULL);
+        pmChip* chipTarget = generateSimpleChipTarget(NULL);
+        bool rc = pmChipCopy(NULL, chipSource);
+        ok(rc == FALSE, "pmChipCopy() returned FALSE with NULL target pmChip input parameter");
+        rc = pmChipCopy(chipTarget, NULL);
+        ok(rc == FALSE, "pmChipCopy() returned FALSE with NULL source pmChip input parameter");
+        psFree(chipSource);
+        psFree(chipTarget);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmChipCopy() with acceptable input parameters.
+    if (1) {
+        psMemId id = psMemGetId();
+        pmFPA *fpaSource = generateSimpleFPASource(NULL);
+        pmFPA *fpaTarget = generateSimpleFPATarget(NULL);
+        pmChip* chipSource = generateSimpleChipSource(fpaSource);
+        pmChip* chipTarget = generateSimpleChipTarget(fpaTarget);
+        bool rc = pmChipCopy(chipTarget, chipSource);
+        ok(rc == true, "pmChipCopy() returned TRUE with acceptable input parameters");
+        ok(!testChipCopy(chipTarget, chipSource), "pmChipCopy() set the pmReadout data correctly");
+        psFree(chipSource);
+        psFree(chipTarget);
+        psFree(fpaSource);
+        psFree(fpaTarget);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmCellCopy() tests
+    // Call pmCellCopy() with bad input parameters.
+    if (1) {
+        psMemId id = psMemGetId();
+        pmCell* cellSource = generateSimpleCellSource(NULL);
+        pmCell* cellTarget = generateSimpleCellTarget(NULL);
+        bool rc = pmCellCopy(NULL, cellSource);
+        ok(rc == FALSE, "pmCellCopy() returned FALSE with NULL target pmCell input parameter");
+        rc = pmCellCopy(cellTarget, NULL);
+        ok(rc == FALSE, "pmCellCopy() returned FALSE with NULL source pmCell input parameter");
+        psFree(cellSource);
+        psFree(cellTarget);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // Call pmCellCopy() with acceptable input parameters.
+    if (1) {
+        psMemId id = psMemGetId();
+        pmChip *parentSource = generateSimpleChipSource(NULL);
+        pmChip *parentTarget = generateSimpleChipSource(NULL);
+        pmCell* cellSource = generateSimpleCellSource(parentSource);
+        pmCell* cellTarget = generateSimpleCellTarget(parentTarget);
+        bool rc = pmCellCopy(cellTarget, cellSource);
+        ok(rc == true, "pmCellCopy() returned TRUE with NULL target pmCell input parameter");
+        ok(!testCellCopy(cellTarget, cellSource), "pmChipCopy() set the pmReadout data correctly");
+        psFree(cellSource);
+        psFree(cellTarget);
+        psFree(parentSource);
+        psFree(parentTarget);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmFPACopyStructure() tests
+    // bool pmFPACopyStructure(pmFPA *target, const pmFPA *source, int xBin, int yBin)
+    // Call pmFPACopyStructure() with bad input parameters.
+    if (1) {
+        psMemId id = psMemGetId();
+        pmFPA* fpaSource = generateSimpleFPASource(NULL);
+        pmFPA* fpaTarget = generateSimpleFPATarget(NULL);
+        bool rc = pmFPACopyStructure(NULL, fpaSource, 1.0, 1.0);
+        ok(rc == FALSE, "pmFPACopyStructure() returned FALSE with NULL target pmFPA input parameter");
+        rc = pmFPACopyStructure(fpaTarget, NULL, 1.0, 1.0);
+        ok(rc == FALSE, "pmFPACopyStructure() returned FALSE with NULL source pmFPA input parameter");
+        rc = pmFPACopyStructure(fpaTarget, fpaSource, 0.0, 1.0);
+        ok(rc == FALSE, "pmFPACopyStructure() returned FALSE with non-positive xBin input parameter");
+        rc = pmFPACopyStructure(fpaTarget, fpaSource, 1.0, 0.0);
+        ok(rc == FALSE, "pmFPACopyStructure() returned FALSE with non-positive yBin input parameter");
+        psFree(fpaSource);
+        psFree(fpaTarget);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmFPACopyStructure() with acceptable input parameters.
+    if (1) {
+        psMemId id = psMemGetId();
+        pmFPA* fpaSource = generateSimpleFPASource(NULL);
+        pmFPA* fpaTarget = generateSimpleFPATarget(NULL);
+        bool rc = pmFPACopyStructure(fpaTarget, fpaSource, 1.0, 1.0);
+        ok(rc == true, "pmFPACopyStructure() returned TRUE with acceptable input parameters");
+        ok(!testFPACopyStructure(fpaTarget, fpaSource), "pmFPACopyStructure() set the pmReadout data correctly");
+        int tmpS32 = psMetadataLookupS32(&rc, fpaTarget->concepts, MISC_NAME_TARGET);
+        ok(tmpS32 == MISC_NUM_TARGET, "pmFPACopy() copied the source FPA concepts correctly");
+        psFree(fpaSource);
+        psFree(fpaTarget);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmChipCopyStructure() tests
+    // bool pmChipCopyStructure(pmChip *target, const pmChip *source, int xBin, int yBin)
+    // Call pmChipCopyStructure() with bad input parameters.
+    if (1) {
+        psMemId id = psMemGetId();
+        pmChip* chipSource = generateSimpleChipSource(NULL);
+        pmChip* chipTarget = generateSimpleChipTarget(NULL);
+        bool rc = pmChipCopyStructure(NULL, chipSource, 1.0, 1.0);
+        ok(rc == FALSE, "pmChipCopyStructure() returned FALSE with NULL target pmChip input parameter");
+        rc = pmChipCopyStructure(chipTarget, NULL, 1.0, 1.0);
+        ok(rc == FALSE, "pmChipCopyStructure() returned FALSE with NULL source pmChip input parameter");
+        rc = pmChipCopyStructure(chipTarget, chipSource, 0.0, 1.0);
+        ok(rc == FALSE, "pmChipCopyStructure() returned FALSE with non-positive xBin input parameter");
+        rc = pmChipCopyStructure(chipTarget, chipSource, 1.0, 0.0);
+        ok(rc == FALSE, "pmChipCopyStructure() returned FALSE with non-positive yBin input parameter");
+        psFree(chipSource);
+        psFree(chipTarget);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmChipCopyStructure() with acceptable parameters.
+    if (1) {
+        psMemId id = psMemGetId();
+        pmFPA *fpaSource = generateSimpleFPASource(NULL);
+        pmFPA *fpaTarget = generateSimpleFPATarget(NULL);
+        pmChip* chipSource = generateSimpleChipSource(fpaSource);
+        pmChip* chipTarget = generateSimpleChipTarget(fpaTarget);
+        bool rc = pmChipCopyStructure(chipTarget, chipSource, 1.0, 1.0);
+        ok(rc == true, "pmChipCopyStructure() returned TRUE with acceptable input parameters");
+        ok(!testChipCopyStructure(chipTarget, chipSource), "pmChipCopyStructure() set the pmReadout data correctly");
+        psFree(chipSource);
+        psFree(chipTarget);
+        psFree(fpaSource);
+        psFree(fpaTarget);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmCellCopyStructure() tests
+    // bool pmCellCopyStructure(pmCell *target, const pmCell *source, int xBin, int yBin)
+    // Call pmCellCopyStructure() with bad input parameters.
+    if (1) {
+        psMemId id = psMemGetId();
+        pmCell* cellSource = generateSimpleCellSource(NULL);
+        pmCell* cellTarget = generateSimpleCellTarget(NULL);
+        bool rc = pmCellCopyStructure(NULL, cellSource, 1.0, 1.0);
+        ok(rc == FALSE, "pmCellCopyStructure() returned FALSE with NULL target pmCell input parameter");
+        rc = pmCellCopyStructure(cellTarget, NULL, 1.0, 1.0);
+        ok(rc == FALSE, "pmCellCopyStructure() returned FALSE with NULL source pmCell input parameter");
+        rc = pmCellCopyStructure(cellTarget, cellSource, 0.0, 1.0);
+        ok(rc == FALSE, "pmCellCopyStructure() returned FALSE with non-positive xBin input parameter");
+        rc = pmCellCopyStructure(cellTarget, cellSource, 1.0, 0.0);
+        ok(rc == FALSE, "pmCellCopyStructure() returned FALSE with non-positive yBin input parameter");
+        psFree(cellSource);
+        psFree(cellTarget);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmCellCopyStructure() with acceptable input parameters.
+    if (1) {
+        psMemId id = psMemGetId();
+        pmChip *parentSource = generateSimpleChipSource(NULL);
+        pmChip *parentTarget = generateSimpleChipSource(NULL);
+        pmCell* cellSource = generateSimpleCellSource(parentSource);
+        pmCell* cellTarget = generateSimpleCellTarget(parentTarget);
+        bool rc = pmCellCopyStructure(cellTarget, cellSource, 1.0, 1.0);
+        ok(rc == true, "pmCellCopyStructure() returned TRUE with NULL target pmCell input parameter");
+        ok(!testCellCopyStructure(cellTarget, cellSource), "pmCellCopyStructure() set the pmReadout data correctly");
+        psFree(cellSource);
+        psFree(cellTarget);
+        psFree(parentSource);
+        psFree(parentTarget);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmChipDuplicate() tests
+    // pmChip *pmChipDuplicate(pmFPA *fpa, const pmChip *source);
+    // Call pmChipDuplicate() with bad input parameters.
+    if (0) {
+        psMemId id = psMemGetId();
+        pmChip *chipSource = generateSimpleChipSource(NULL);
+        pmChip *chipTarget = pmChipDuplicate(NULL, NULL);
+        ok(chipTarget == NULL, "pmChipDuplicate() returned NULL with NULL pmChip input parameter");
+        psFree(chipSource);
+        psFree(chipTarget);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
Index: /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAExtent.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAExtent.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAExtent.c	(revision 42651)
@@ -0,0 +1,425 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested.
+*/
+
+// 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->variance = 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] = psMemDecrRefCounter((psPtr) generateSimpleReadout(cell));
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&cell->hdu->format, "../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->variances = 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->variances->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->variances->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, 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);
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = psMemDecrRefCounter((psPtr) 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, NULL);
+    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);
+    if (camera != NULL) {
+        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);
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = psMemDecrRefCounter((psPtr) generateSimpleChip(fpa));
+    }
+
+    pmConceptsBlankFPA(fpa);
+    return(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");
+
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmCellExtent() tests: NULL input
+    {
+        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");
+
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    // pmChipExtent() tests: NULL input
+    {
+        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");
+
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmChipPixels() tests: NULL input
+    {
+        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");
+
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAPixels() tests: NULL input
+    {
+        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);
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAFlags.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAFlags.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAFlags.c	(revision 42651)
@@ -0,0 +1,1021 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS
+    TESTED:
+        pmFPASetFileStatus()
+        pmChipSetFileStatus()
+        pmCellSetFileStatus()
+
+        pmFPACheckFileStatus()
+        pmChipCheckFileStatus()
+        pmCellCheckFileStatus()
+
+        pmFPASetDataStatus()
+        pmChipSetDataStatus()
+        pmCellSetDataStatus()
+
+        pmFPACheckDataStatus()
+        pmChipCheckDataStatus()
+        pmCellCheckDataStatus()
+        pmReadoutCheckDataStatus()
+    MUST TEST:
+        pmFPAviewCheckDataStatus()
+        pmFPASelectChip()
+        pmChipSelectCell()
+        pmFPAExcludeChip()
+        pmChipExcludeCell()
+*/
+
+// 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->variance = 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] = psMemDecrRefCounter((psPtr) generateSimpleReadout(cell));
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&cell->hdu->format, "../dataFiles/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->variances = 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->variances->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->variances->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, 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);
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = psMemDecrRefCounter((psPtr) 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, NULL);
+    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);
+    if (camera != NULL) {
+        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);
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = psMemDecrRefCounter((psPtr) generateSimpleChip(fpa));
+    }
+
+    // XXX: Eventually, when you finish the pmConcepts tests, add full concept
+    // reading code from wherever.
+    pmConceptsBlankFPA(fpa);
+    return(fpa);
+}
+
+void SetCellFileExists(pmCell *cell) {
+    cell->file_exists = true;
+    for (int i = 0 ; i < cell->readouts->n ; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        readout->file_exists = true;
+    }
+}
+
+void SetChipFileExists(pmChip *chip) {
+    chip->file_exists = true;
+    for (int i = 0 ; i < chip->cells->n ; i++) {
+        pmCell *cell = chip->cells->data[i];
+        cell->file_exists = true;
+        SetCellFileExists(cell);
+    }
+}
+
+void SetFPAFileExists(pmFPA *fpa) {
+    for (int i = 0 ; i < fpa->chips->n ; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        chip->file_exists = true;
+        SetChipFileExists(chip);
+    }
+}
+
+void SetReadoutDataExists(pmReadout *readout) {
+    readout->data_exists = true;
+}
+
+void SetCellDataExists(pmCell *cell) {
+    cell->data_exists = true;
+    for (int i = 0 ; i < cell->readouts->n ; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        readout->data_exists = true;
+    }
+}
+
+void SetChipDataExists(pmChip *chip) {
+    chip->data_exists = true;
+    for (int i = 0 ; i < chip->cells->n ; i++) {
+        pmCell *cell = chip->cells->data[i];
+        cell->data_exists = true;
+        SetCellDataExists(cell);
+    }
+}
+
+void SetFPADataExists(pmFPA *fpa) {
+    for (int i = 0 ; i < fpa->chips->n ; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        chip->data_exists = true;
+        SetChipDataExists(chip);
+    }
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(107);
+
+
+    // ----------------------------------------------------------------------
+    // 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");
+
+        psFree(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");
+
+
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmCellSetFileStatus() tests: verify with NULL pmCell param
+    {
+        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");
+
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPACheckFileStatus() tests
+    // bool pmFPACheckFileStatus(const pmFPA *fpa)
+    // Call with NULL pmFPA input parameter
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmFPACheckFileStatus(NULL);
+        ok(rc == false, "pmFPACheckFileStatus() returned FALSE with NULL pmFPA input parameter");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmFPA *fpa = generateSimpleFPA(NULL);
+        bool rc = pmFPACheckFileStatus(fpa);
+        ok(rc == false, "pmFPACheckFileStatus() returned FALSE with NULL pmFPA input parameter");
+        SetFPAFileExists(fpa);
+        rc = pmFPACheckFileStatus(fpa);
+        ok(rc == true, "pmFPACheckFileStatus() returned TRUE with NULL pmFPA input parameter");
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmChipCheckFileStatus() tests
+    // Call with NULL pmChip input parameter
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmChipCheckFileStatus(NULL);
+        ok(rc == false, "pmChipCheckFileStatus() returned FALSE with NULL pmChip input parameter");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call with acceptable input parameter
+    {
+        psMemId id = psMemGetId();
+        pmChip *chip = generateSimpleChip(NULL);
+        bool rc = pmChipCheckFileStatus(chip);
+        ok(rc == false, "pmChipCheckFileStatus() returned FALSE with NULL pmChip input parameter");
+        SetChipFileExists(chip);
+        rc = pmChipCheckFileStatus(chip);
+        ok(rc == true, "pmChipCheckFileStatus() returned TRUE with NULL pmChip input parameter");
+        psFree(chip);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmCellCheckFileStatus() tests
+    // bool pmCellCheckFileStatus(const pmCell *cell)
+    // Call with NULL pmCell input parameter
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmCellCheckFileStatus(NULL);
+        ok(rc == false, "pmCellCheckFileStatus() returned FALSE with NULL pmCell input parameter");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        bool rc = pmCellCheckFileStatus(cell);
+        ok(rc == false, "pmCellCheckFileStatus() returned FALSE with acceptable input parameters");
+        SetCellFileExists(cell);
+        rc = pmCellCheckFileStatus(cell);
+        ok(rc == true, "pmCellCheckFileStatus() returned TRUE with acceptable input parameters");
+        psFree(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPASetDataStatus() tests: verify with NULL pmFPA param
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmFPASetDataStatus(NULL, false);
+        ok(!rc, "pmFPASetDataStatus() 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");
+
+        psFree(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");
+
+
+        psFree(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");
+
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPACheckDataStatus() tests
+    // bool pmFPACheckDataStatus(const pmFPA *fpa)
+    // Call with NULL pmFPA input parameter
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmFPACheckDataStatus(NULL);
+        ok(rc == false, "pmFPACheckDataStatus() returned FALSE with NULL pmFPA input parameter");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmFPA *fpa = generateSimpleFPA(NULL);
+        bool rc = pmFPACheckDataStatus(fpa);
+        ok(rc == false, "pmFPACheckDataStatus() returned FALSE with NULL pmFPA input parameter");
+        SetFPADataExists(fpa);
+        rc = pmFPACheckDataStatus(fpa);
+        ok(rc == true, "pmFPACheckDataStatus() returned TRUE with NULL pmFPA input parameter");
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmChipCheckDataStatus() tests
+    // Call with NULL pmChip input parameter
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmChipCheckDataStatus(NULL);
+        ok(rc == false, "pmChipCheckDataStatus() returned FALSE with NULL pmChip input parameter");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call with acceptable input parameter
+    {
+        psMemId id = psMemGetId();
+        pmChip *chip = generateSimpleChip(NULL);
+        bool rc = pmChipCheckDataStatus(chip);
+        ok(rc == false, "pmChipCheckDataStatus() returned FALSE with NULL pmChip input parameter");
+        SetChipDataExists(chip);
+        rc = pmChipCheckDataStatus(chip);
+        ok(rc == true, "pmChipCheckDataStatus() returned TRUE with NULL pmChip input parameter");
+        psFree(chip);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmCellCheckDataStatus() tests
+    // bool pmCellCheckDataStatus(const pmCell *cell)
+    // Call with NULL pmCell input parameter
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmCellCheckDataStatus(NULL);
+        ok(rc == false, "pmCellCheckDataStatus() returned FALSE with NULL pmCell input parameter");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        bool rc = pmCellCheckDataStatus(cell);
+        ok(rc == false, "pmCellCheckDataStatus() returned FALSE with acceptable input parameters");
+        SetCellDataExists(cell);
+        rc = pmCellCheckDataStatus(cell);
+        ok(rc == true, "pmCellCheckDataStatus() returned TRUE with acceptable input parameters");
+        psFree(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmReadoutCheckDataStatus() tests
+    // bool pmReadoutCheckDataStatus(const pmReadout *readout)
+    // Call with NULL pmReadout input parameter
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmReadoutCheckDataStatus(NULL);
+        ok(rc == false, "pmReadoutCheckDataStatus() returned FALSE with NULL pmReadout input parameter");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        bool rc = pmReadoutCheckDataStatus(readout);
+        ok(rc == false, "pmReadoutCheckDataStatus() returned FALSE with acceptable input parameters");
+        SetReadoutDataExists(readout);
+        rc = pmReadoutCheckDataStatus(readout);
+        ok(rc == true, "pmReadoutCheckDataStatus() returned TRUE with acceptable input parameters");
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAHeader.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAHeader.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAHeader.c	(revision 42651)
@@ -0,0 +1,449 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested.
+*/
+
+#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->variance = 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] = psMemDecrRefCounter((psPtr) generateSimpleReadout(cell));
+    }
+
+    // First try to read data from ../dataFiles, then try dataFiles.
+    bool rc = pmConfigFileRead(&cell->hdu->format, "../dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&cell->hdu->format, "dataFiles/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->variances = 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->variances->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->variances->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.
+    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] = psMemDecrRefCounter((psPtr) generateSimpleCell(chip, i));
+    }
+    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, NULL);
+    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);
+    if (camera != NULL) {
+        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);
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = psMemDecrRefCounter((psPtr) generateSimpleChip(fpa, i));
+    }
+    pmConceptsBlankFPA(fpa);
+    return(fpa);
+}
+
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(75);
+
+
+    // ----------------------------------------------------------------------
+    // 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, NULL), "pmCellReadHeader(cell, NULL) returned FALSE");
+        psFree(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, NULL), "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, "../dataFiles/camera0/format0.config", "Camera 0 Config Format");
+            if (!rc) {
+                rc = pmConfigFileRead(&hdu->format, "dataFiles/camera0/format0.config", "Camera 0 Config Format");
+	    }
+            ok(rc == true, "pmConfigFileRead() was successful");
+            rc = pmHDUWrite(hdu, fitsFileW, NULL);
+            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, NULL), "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.
+
+        psFree(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, NULL), "pmChipReadHeader(chip, NULL) returned FALSE");
+        psFree(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, NULL), "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, "../dataFiles/camera0/format0.config", "Camera 0 Config Format");
+            if (!rc) {
+               rc = pmConfigFileRead(&hdu->format, "dataFiles/camera0/format0.config", "Camera 0 Config Format");
+	    }
+            ok(rc == true, "pmConfigFileRead() was successful");
+            rc = pmHDUWrite(hdu, fitsFileW, NULL);
+            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, NULL), "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.
+
+        psFree(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, NULL), "pmFPAReadHeader(fpa, NULL) returned FALSE");
+        psFree(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, NULL), "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, "../dataFiles/camera0/format0.config", "Camera 0 Config Format");
+            if (!rc) {
+                rc = pmConfigFileRead(&hdu->format, "dataFiles/camera0/format0.config", "Camera 0 Config Format");
+	    }
+            ok(rc == true, "pmConfigFileRead() was successful");
+            rc = pmHDUWrite(hdu, fitsFileW, NULL);
+            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, NULL), "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.
+
+        psFree(fpa);
+        psFree(camera);
+        psFree(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
Index: /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPALevel.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPALevel.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPALevel.c	(revision 42651)
@@ -0,0 +1,76 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested.
+    XXX: Add tests for bad input parameters.
+*/
+
+#define	ERR_TRACE_LEVEL		0
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(14);
+
+    // ----------------------------------------------------------------------
+    // 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");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPALevelFromName(): tests
+    {
+        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_branches/psModules.20240412/test/camera/tap_pmFPAMaskW.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAMaskW.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAMaskW.c	(revision 42651)
@@ -0,0 +1,429 @@
+#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: 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->variance = 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->variance->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] = psMemDecrRefCounter((psPtr) generateSimpleReadout(cell));
+    }
+
+    // First try to read data from ../dataFiles, then try dataFiles.
+    bool rc = pmConfigFileRead(&cell->hdu->format, "../dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&cell->hdu->format, "dataFiles/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->variances = 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->variances->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->variances->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, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.
+    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);
+
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = psMemDecrRefCounter((psPtr) 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, NULL);
+    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);
+    if (camera != NULL) {
+        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);
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = psMemDecrRefCounter((psPtr) generateSimpleChip(fpa));
+    }
+    pmConceptsBlankFPA(fpa);
+    return(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");
+        psFree(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");
+
+        psFree(fpa);    
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    // pmReadoutSetVariance() tests: NULL inputs
+    // bool pmReadoutSetVariance(pmReadout *readout, const psImage *noiseMap, 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 pmReadoutSetVariance() returnes FALSE, with no seg faults, memory leaks
+        rc = pmReadoutSetVariance(NULL, NULL, false);
+        ok(!rc, "pmReadoutSetVariance(NULL, 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");
+/*
+ * Getting the section below to run requires generating a noiseMap
+ *
+        // Call pmReadoutSetVariance() and then verify that the mask data was set correctly
+        rc = pmReadoutSetVariance(readout, false);
+        ok(rc, "pmReadoutSetVariance(readout, false) returned TRUE with acceptable input data");
+        bool errorFlag = false;
+        for (int i = 0 ; i < readout->variance->numRows ; i++) {
+            for (int j = 0 ; j < readout->variance->numCols ; j++) {
+                psF32 exp = CELL_READNOISE * CELL_READNOISE / CELL_GAIN / CELL_GAIN;
+                if(abs(readout->variance->data.F32[i][j] - exp) > 1e-4) {
+                    if (VERBOSE) {
+                        diag("TEST ERROR: weight[%d][%d] is %.2f, should be %.2f\n",
+                              i, j, readout->variance->data.F32[i][j], exp);
+		    }
+                    errorFlag = true;
+		}
+	    }
+	}
+        ok(!errorFlag, "pmReadoutSetVariance() 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 pmReadoutSetVariance() and then verify that the mask data was set correctly
+        rc = pmReadoutSetVariance(readout, true);
+        ok(rc, "pmReadoutSetVariance(readout, true) returned TRUE with acceptable input data");
+        errorFlag = false;
+        for (int i = 0 ; i < readout->variance->numRows ; i++) {
+            for (int j = 0 ; j < readout->variance->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->variance->data.F32[i][j] - exp) > 1e-4) {
+                    if (VERBOSE) {
+                        diag("TEST ERROR: weight[%d][%d] is %.2f, should be %.2f\n",
+                              i, j, readout->variance->data.F32[i][j], exp);
+		    }
+                    errorFlag = true;
+		}
+	    }
+	}
+
+        ok(!errorFlag, "pmReadoutSetWeight() set the weight values correctly (Poisson)");
+*/	
+        psFree(fpa);    
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
Index: /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAReadWrite.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAReadWrite.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAReadWrite.c	(revision 42651)
@@ -0,0 +1,1218 @@
+#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		10
+
+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->variance = 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] = psMemDecrRefCounter((psPtr) generateSimpleReadout(cell));
+    }
+
+    // First try to read data from ../dataFiles, then try dataFiles.
+    bool rc = pmConfigFileRead(&cell->hdu->format, "../dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&cell->hdu->format, "dataFiles/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->variances = 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->variances->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->variances->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, 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);
+
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = psMemDecrRefCounter((psPtr) 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, NULL);
+    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);
+    if (camera != NULL) {
+        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);
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = psMemDecrRefCounter((psPtr) generateSimpleChip(fpa));
+    }
+    pmConceptsBlankFPA(fpa);
+    return(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(22);
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmCellWrite(): tests
+    // Verify pmCellWrite() with NULL pmCell arg
+    if (0) {
+        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 (0) {
+        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
+    if (0) {
+        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");
+        psFree(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);
+        psFree(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");
+        psFree(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");
+        skip_start(!rc, 1, "Skipping tests because pmCellRead returned NULL");
+        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);
+        }
+        skip_end();
+        psFitsClose(fitsFileR);
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmCellWriteVariance(): tests
+    // Verify pmCellWriteVariance() with NULL pmCell arg
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp00", "w");
+        ok(!pmCellWriteVariance(NULL, fitsFileW, NULL, false), "pmCellWriteVariance() returned FALSE with NULL pmCell input");
+        psFitsClose(fitsFileW);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellWriteVariance() 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(!pmCellWriteVariance(cell, NULL, NULL, false), "pmCellWriteVariance() returned FALSE with NULL psFits param");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellWriteVariance() with acceptable input params
+    // We first write a FITS file with the pmCellWriteVariance(), then we read it and verify.
+    // First call pmCellWriteVariance()
+    {
+        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 pmCellWriteVariance() to write weight data to the FITS file
+        bool rc = pmCellWriteVariance(cell, fitsFileW, NULL, false);
+        ok(rc, "pmCellWriteVariance() returned TRUE");
+
+        //  Close the FITS file, free memory
+        psFitsClose(fitsFileW);
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmCellReadVariance() tests 
+    // Verify pmCellReadVariance() with NULL pmCell param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        ok(!pmCellReadVariance(NULL, fitsFileR, NULL), "pmCellReadVariance() returned FALSE with NULL pmCell param");
+        psFitsClose(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellReadVariance() 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(!pmCellReadVariance(cell, NULL, NULL), "pmCellReadVariance() returned FALSE with NULL pmFits param");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellReadVariance() 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->variances);
+        cell->hdu->variances = NULL;
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+
+        rc = pmCellReadVariance(cell, fitsFileR, NULL);
+        ok(rc, "pmCellReadVariance() returned TRUE");
+        for (int k = 0 ; k < cell->hdu->variances->n ; k++) {
+            bool errorFlag = false;
+            psImage *msk = cell->hdu->variances->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, "pmCellWriteVariance()/pmCellReadVariance() properly set the weight data (image %d)", k);
+        }
+        psFitsClose(fitsFileR);
+        psFree(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");
+        psFree(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);
+        psFree(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");
+        psFree(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);
+        psFree(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");
+        psFree(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);
+        psFree(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");
+        psFree(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];
+            psFree(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("NOTE: 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);
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmChipWriteVariance() tests
+    // Verify pmChipWriteVariance() with NULL pmChip param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp01", "w");
+        ok(!pmChipWriteVariance(NULL, fitsFileW, NULL, false, true), "pmChipWriteVariance() returned NULL with NULL pmChip param");
+        psFitsClose(fitsFileW);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmChipWriteVariance() 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(!pmChipWriteVariance(chip, NULL, NULL, false, true), "pmChipWriteVariance() returned NULL with NULL pmFits param");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmChipWriteVariance() 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 pmChipWriteVariance() to write image data to the FITS file
+        bool rc = pmChipWriteVariance(chip, fitsFileW, NULL, false, true);
+        ok(rc, "pmChipWriteVariance() returned TRUE");
+
+        //  Close the FITS file, free memory
+        psFitsClose(fitsFileW);
+        psFree(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");
+        psFree(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];
+            psFree(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->variances->n ; k++) {
+                if (VERBOSE) diag("NOTE: image %d\n", k);
+                psImage *wgt = cell->hdu->variances->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, "pmChipWriteVariance()/pmChipReadVariance() properly set the variance data (cell %d)", chipID);
+	}
+
+        psFitsClose(fitsFileR);
+        psFree(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");
+        psFree(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];
+            psFree(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("NOTE: 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);
+        psFree(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");
+        psFree(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);
+        psFree(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");
+        psFree(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];
+                psFree(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("NOTE: 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);
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmFPAWriteVariance() tests
+    // pmFPAWriteVariance(pmFPA *fpa, psFits *fits, psDB *db, bool blank, bool recurse)
+    // Verify pmFPAWriteVariance() with NULL pmFPA param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp01", "w");
+        ok(!pmFPAWriteVariance(NULL, fitsFileW, NULL, false, true), "pmFPAWriteVariance() returned NULL with NULL pmFPA param");
+        psFitsClose(fitsFileW);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPAWriteVariance() 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(!pmFPAWriteVariance(fpa, NULL, NULL, false, true), "pmFPAWriteVariance() returned NULL with NULL pmFits param");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPAWriteVariance() 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 = pmFPAWriteVariance(fpa, fitsFileW, NULL, false, true);
+        ok(rc, "pmFPAWriteVariance() returned TRUE");
+        //  Close the FITS file, free memory
+        psFitsClose(fitsFileW);
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAReadVariance() tests
+    // Verify pmFPAReadWeight() with NULL pmFPA param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        ok(!pmFPARead(NULL, fitsFileR, NULL), "pmFPAReadVariance() returned NULL with NULL pmFPA param");
+        psFitsClose(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPAReadVariance() with NULL pmFits param
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        ok(!pmFPARead(fpa, NULL, NULL), "pmFPAReadVariance() returned NULL with NULL pmFits param");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPAReadVariance() 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 pmFPAReadVariance() actually reads the data from file
+        if (0) {
+            for (int chipID = 0 ; chipID < chip->cells->n ; chipID++) {
+                pmCell *cell = (pmCell *) chip->cells->data[chipID];
+                psFree(cell);
+                cell = NULL;
+            }
+	}
+
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        rc = pmFPARead(fpa, fitsFileR, NULL);
+        ok(rc, "pmFPAReadVariance() 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->variances->n ; k++) {
+                    if (VERBOSE) diag("NOTE: image %d\n", k);
+                    psImage *wgt = cell->hdu->variances->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, "pmFPAWriteVariance()/pmFPAReadVariance() properly set the image data (chip %d, cell %d)", fpaID, chipID);
+    	    }
+	}
+
+        psFitsClose(fitsFileR);
+        psFree(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");
+        psFree(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);
+        psFree(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");
+        psFree(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];
+                psFree(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("NOTE: 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);
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
Index: /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAUtils.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAUtils.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAUtils.c	(revision 42651)
@@ -0,0 +1,295 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested.
+*/
+
+#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->variance = 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] = psMemDecrRefCounter((psPtr) generateSimpleReadout(cell));
+    }
+
+    // First try to read data from ../dataFiles, then try dataFiles.
+    bool rc = pmConfigFileRead(&cell->hdu->format, "dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&cell->hdu->format, "../dataFiles/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->variances = 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->variances->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->variances->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, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.
+    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);
+
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = psMemDecrRefCounter((psPtr) 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, NULL);
+    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);
+    if (camera != NULL) {
+        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);
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = psMemDecrRefCounter((psPtr) generateSimpleChip(fpa));
+    }
+    pmConceptsBlankFPA(fpa);
+    return(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(40);
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAFindChip() tests
+    // Call pmFPAFindChip() with NULL pmFPA input parameter
+    {
+        psMemId id = psMemGetId();
+        int rc = pmFPAFindChip(NULL, "chip-0");
+        ok(-1 == rc, "pmFPAFindChip() returns -1 with NULL pmFPA input parameter");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmFPAFindChip() with bogus chip name
+    {
+        psMemId id = psMemGetId();
+        pmFPA* fpa = generateSimpleFPA(NULL);
+        int rc = pmFPAFindChip(fpa, "bogus");
+        ok(-1 == rc, "pmFPAFindChip() returns -1 with bogus chip name");
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmFPAFindChip() with NULL chip name
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        pmFPA* fpa = generateSimpleFPA(NULL);
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        int rc = pmFPAFindChip(fpa, NULL);
+        ok(-1 == rc, "pmFPAFindChip() returns -1 with NULL name input param");
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmFPAFindChip() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        pmFPA* fpa = generateSimpleFPA(NULL);
+        ok(fpa != NULL, "Allocated a pmFPA 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);
+	}
+
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    // pmChipFindCell() tests
+    // Call pmChipFindCell() with bogus cell name
+    {
+        psMemId id = psMemGetId();
+        int rc = pmChipFindCell(NULL, "cell-0");
+        ok(-1 == rc, "pmChipFindCell() returns -1 with NULL pmChip input param");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Call pmChipFindCell() with bogus cell name
+    {
+        psMemId id = psMemGetId();
+        pmFPA* fpa = generateSimpleFPA(NULL);
+        pmChip *chip = fpa->chips->data[0];
+        int rc = pmChipFindCell(chip, "bogus");
+        ok(-1 == rc, "pmChipFindCell() returns -1 with bogus cell name");
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmChipFindCell() with NULL cell name
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmChip heirarchy
+        pmFPA* fpa = generateSimpleFPA(NULL);
+        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");
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmChipFindCell() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmChip heirarchy
+        pmFPA* fpa = generateSimpleFPA(NULL);
+        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);
+	}
+
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+}
Index: /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAView.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAView.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmFPAView.c	(revision 42651)
@@ -0,0 +1,1023 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested.
+*/
+
+#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->variance = 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] = psMemDecrRefCounter((psPtr) generateSimpleReadout(cell));
+    }
+
+    // First try to read data from ../dataFiles, then try dataFiles.
+    bool rc = pmConfigFileRead(&cell->hdu->format, "dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&cell->hdu->format, "../dataFiles/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->variances = 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->variances->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->variances->data[k], (float) (BASE_WEIGHT+k));
+    }
+    cell->hdu->blankPHU = true;
+
+    //XXX: Should the region be set some other way?  Like through the various config files?
+    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, "dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&chip->hdu->format, "../dataFiles/camera0/format0.config", "Camera format 0");
+        if (!rc) {
+            diag("pmConfigFileRead() was unsuccessful (from generateSimpleChip())");
+        }
+    }
+    chip->hdu->blankPHU = true;
+
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = psMemDecrRefCounter((psPtr) 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, NULL);
+    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);
+    if (camera != NULL) {
+        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, "dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&fpa->hdu->format, "../dataFiles/camera0/format0.config", "Camera format 0");
+        if (!rc) {
+            diag("pmConfigFileRead() was unsuccessful (from generateSimpleFPA())");
+        }
+    }
+    fpa->hdu->blankPHU = true;
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = psMemDecrRefCounter((psPtr) generateSimpleChip(fpa));
+    }
+    pmConceptsBlankFPA(fpa);
+    return(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(174);
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAViewAlloc() tests
+    {
+        psMemId id = psMemGetId();
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(tmpView != NULL && psMemCheckFPAview(tmpView), "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");
+
+        psFree(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;
+        psFree(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");
+        psFree(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");
+
+        psFree(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(fpa->chips->data[0] == pmFPAviewNextChip(tmpView, fpa, 1), "pmFPAviewNextChip(pmFPAView, fpa, 1) 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)");
+
+        psFree(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");
+
+        psFree(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
+    {
+        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;
+        psFree(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");
+        psFree(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");
+
+        psFree(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)");
+
+        psFree(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");
+
+        psFree(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
+    {
+        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;
+        psFree(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");
+
+        psFree(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");
+
+        psFree(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)");
+
+        psFree(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");
+        psFree(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
+    {
+        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 = NUM_CHIPS/2;
+
+        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 = NUM_CELLS/2;
+
+        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 = NUM_READOUTS/2;
+
+        hdu = pmFPAviewThisHDU(tmpView, fpa);
+        ok(hdu, "pmFPAviewThisHDU() returned non-NULL");
+        ok(hdu == pmHDUFromReadout(pmFPAviewThisReadout(tmpView, fpa)), "pmFPAviewThisHDU() returned the correct HDU");
+
+        psFree(fpa);
+        psFree(camera);
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAviewThisPHU() tests: NULL view input
+    // pmHDU *pmFPAviewThisPHU(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 == pmFPAviewThisPHU(NULL, fpa), "pmFPAviewThisPHU(NULL, fpa) returned NULL");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // pmFPAviewThisPHU() tests: NULL pmFPA input
+    // pmHDU *pmFPAviewThisPHU(const pmFPAview *view, const pmFPA *fpa)
+    {
+        psMemId id = psMemGetId();
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(NULL == pmFPAviewThisPHU(tmpView, NULL), "pmFPAviewThisPHU(pmFPAView, NULL) returned NULL");
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAviewThisPHU() 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);
+
+        tmpView->chip = -1;
+        pmHDU *hdu = pmFPAviewThisPHU(tmpView, fpa);
+        ok(hdu, "pmFPAviewThisPHU() returned non-NULL with tmpView->chip = -1");
+        ok(hdu == pmHDUFromFPA(fpa), "pmFPAviewThisPHU() returned the correct HDU");
+        tmpView->chip = NUM_CHIPS/2;
+
+        tmpView->cell = -1;
+        hdu = pmFPAviewThisPHU(tmpView, fpa);
+        ok(hdu, "pmFPAviewThisPHU() returned non-NULL with tmpView->cell = -1");
+        ok(hdu == pmHDUFromChip(pmFPAviewThisChip(tmpView, fpa)), "pmFPAviewThisPHU() returned the correct HDU");
+        tmpView->cell = NUM_CELLS/2;
+
+        tmpView->readout = -1;
+        hdu = pmFPAviewThisPHU(tmpView, fpa);
+        ok(hdu, "pmFPAviewThisPHU() returned non-NULL with tmpView->readout = -1");
+        ok(hdu == pmHDUFromCell(pmFPAviewThisCell(tmpView, fpa)), "pmFPAviewThisPHU() returned the correct HDU");
+        tmpView->readout = NUM_READOUTS/2;
+
+        hdu = pmFPAviewThisPHU(tmpView, fpa);
+        ok(hdu == NULL, "pmFPAviewThisPHU() returned NULL");
+
+        psFree(fpa);
+        psFree(camera);
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAviewGenerate() tests: NULL pmFPA input
+    {
+        psMemId id = psMemGetId();
+        int chipID = NUM_CHIPS/2;
+        int cellID = NUM_CELLS/2;
+        int readoutID = NUM_READOUTS/2;
+        pmFPA* fpa = generateSimpleFPA(NULL);
+        pmChip *chip = fpa->chips->data[chipID];
+        pmCell *cell = chip->cells->data[cellID];
+        pmReadout *readout = cell->readouts->data[readoutID];
+        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");
+        pmFPAview *view = pmFPAviewGenerate(NULL, chip, cell, readout);
+        ok(view == NULL, "pmFPAviewGenerate(NULL, chip, cell, readout) returned NULL");
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAviewGenerate() tests: acceptable input parameters
+    // XX: Add more input combinations.
+    {
+        psMemId id = psMemGetId();
+        int chipID = NUM_CHIPS/2;
+        int cellID = NUM_CELLS/2;
+        int readoutID = NUM_READOUTS/2;
+        pmFPA* fpa = generateSimpleFPA(NULL);
+        pmChip *chip = fpa->chips->data[chipID];
+        pmCell *cell = chip->cells->data[cellID];
+        pmReadout *readout = cell->readouts->data[readoutID];
+        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");
+        pmFPAview *view = pmFPAviewGenerate(fpa, chip, cell, readout);
+        ok(view != NULL && psMemCheckFPAview(view), "pmFPAviewGenerate() returned an pmFPAviw with acceptable input parameters");
+        ok(view->chip == chipID, "pmFPAviewGenerate() set pmFPAview->chip correctly");
+        ok(view->cell == cellID, "pmFPAviewGenerate() set pmFPAview->cell correctly");
+        ok(view->readout == readoutID, "pmFPAviewGenerate() set pmFPAview->readout correctly");
+        psFree(view);
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
Index: /branches/eam_branches/psModules.20240412/test/camera/tap_pmHDU.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmHDU.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmHDU.c	(revision 42651)
@@ -0,0 +1,569 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested.
+*/
+
+#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, NULL);
+        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, "../dataFiles/camera0/format0.config", "Camera 0 Config Format");
+        if (!rc) {
+            rc = pmConfigFileRead(&hdu->format, "dataFiles/camera0/format0.config", "Camera 0 Config Format");
+	}
+        ok(rc == true, "pmConfigFileRead() was successful");
+        rc = pmHDUWrite(hdu, NULL, 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("../dataFiles/sampleFitsFile.fits", "r");
+        if (!fitsFile) {
+            fitsFile = psFitsOpen("dataFiles/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, "../dataFiles/camera0/format0.config", "Camera format 0");
+        if (!rc) {
+            rc = pmConfigFileRead(&hdu->format, "dataFiles/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, NULL);
+        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, NULL);
+        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, "../dataFiles/camera0/format0.config", "Camera 0 Config Format");
+        if (!rc) {
+            rc = pmConfigFileRead(&hdu->format, "dataFiles/camera0/format0.config", "Camera 0 Config Format");
+	}
+        ok(rc == true, "pmConfigFileRead() was successful");
+        rc = pmHDUWriteWeight(hdu, NULL, 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("../dataFiles/sampleFitsFile.fits", "r");
+        if (!fitsFile) {
+            fitsFile = psFitsOpen("dataFiles/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, "../dataFiles/camera0/format0.config", "Camera format 0");
+        if (!rc) {
+            rc = pmConfigFileRead(&hdu->format, "dataFiles/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, NULL);
+        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, NULL);
+        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, "dataFiles/camera0/format0.config", "Camera 0 Config Format");
+        if (!rc) {
+             pmConfigFileRead(&hdu->format, "../dataFiles/camera0/format0.config", "Camera 0 Config Format");
+	}
+        ok(rc == true, "pmConfigFileRead() was successful");
+        rc = pmHDUWriteMask(hdu, NULL, 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("../dataFiles/sampleFitsFile.fits", "r");
+        if (!fitsFile) {
+            fitsFile = psFitsOpen("dataFiles/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, "../dataFiles/camera0/format0.config", "Camera format 0");
+        if (!rc) {
+            rc = pmConfigFileRead(&hdu->format, "dataFiles/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, NULL);
+        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("../dataFiles/sampleFitsFile.fits", "r");
+        if (!fitsFile) {
+            fitsFile = psFitsOpen("dataFiles/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,
+                         "psStr Item", extname);
+
+            bool rc = pmConfigFileRead(&hdu->format, "../dataFiles/camera0/format0.config", "Camera 0 Config Format");
+            if (!rc) {
+                rc = pmConfigFileRead(&hdu->format, "dataFiles/camera0/format0.config", "Camera 0 Config Format");
+	    }
+            ok(rc == true, "pmConfigFileRead() was successful");
+            rc = pmHDUWrite(hdu, fitsFileW, NULL);
+            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_branches/psModules.20240412/test/camera/tap_pmHDUUtils.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmHDUUtils.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmHDUUtils.c	(revision 42651)
@@ -0,0 +1,480 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested.
+*/
+
+#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->variance = 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] = psMemDecrRefCounter((psPtr) generateSimpleReadout(cell));
+    }
+
+    // First try to read data from ../dataFiles, then try dataFiles.
+    bool rc = pmConfigFileRead(&cell->hdu->format, "../dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&cell->hdu->format, "dataFiles/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->variances = 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->variances->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->variances->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, 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, "dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&chip->hdu->format, "../dataFiles/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] = psMemDecrRefCounter((psPtr) 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, NULL);
+    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);
+    if (camera != NULL) {
+        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, "dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&fpa->hdu->format, "../dataFiles/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] = psMemDecrRefCounter((psPtr) generateSimpleChip(fpa));
+    }
+    pmConceptsBlankFPA(fpa);
+    return(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(73);
+
+
+    // ----------------------------------------------------------------------
+    // pmHDUFromFPA() tests
+    // Call pmHDUFromFPA() 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");
+    }
+
+
+    // Call pmHDUFromFPA() 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");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmHDUFromChip() tests
+    // Call pmHDUFromChip() 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");
+    }
+
+
+    // Call pmHDUFromChip() 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");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmHDUFromChip() 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");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmHDUFromCell() tests
+    // Call pmHDUFromCell() 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");
+    }
+
+
+    // Call pmHDUFromCell() 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");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmHDUFromCell() 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");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmHDUFromReadout() tests
+    // Call pmHDUFromReadout() 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");
+    }
+
+
+    // Call pmHDUFromReadout() 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");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmHDUGetLowest() tests
+    // Call pmHDUGetLowest() 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");
+    }
+
+
+    // Call pmHDUGetLowest() 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)");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmHDUGetLowest() 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)");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmHDUGetLowest() 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)");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    //pmHDUGetHighest() tests
+    // Call pmHDUGetHighest() 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");
+    }
+
+
+    // Call pmHDUGetHighest() 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)");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmHDUGetHighest() 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)");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmHDUGetHighest() 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)");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branches/psModules.20240412/test/camera/tap_pmReadoutFake.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmReadoutFake.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmReadoutFake.c	(revision 42651)
@@ -0,0 +1,177 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    Only one function in the source code: pmReadoutFakeFromSources()
+
+    pmReadoutFakeFromSources() is only tested with bad input parameters.
+    Tests must be written to exercise it with legitimate input sources, and
+    verify the output.
+*/
+
+#define MISC_NUM                32
+#define MISC_NAME               "META00"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           5
+#define TEST_NUM_COLS           8
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+#define NUM_SOURCES		5
+
+/******************************************************************************
+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);
+    psImageInit(readout->image, 1.0);
+    psImageInit(readout->mask, 2);
+    psImageInit(readout->weight, 3.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);
+}
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(11);
+
+    // ------------------------------------------------------------------------
+    // pmReadoutFakeFromSources() tests
+    // bool pmReadoutFakeFromSources(pmReadout *readout, int numCols, int numRows, const psArray *sources,
+    //                               const psVector *xOffset, const psVector *yOffset, const pmPSF *psf,
+    //                               float minFlux, int radius, bool circularise)
+    //
+    // Call pmReadoutFakeFromSources() with bad input parameters.
+    {
+        psMemId id = psMemGetId();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        psVector *xOffset = psVectorAlloc(NUM_SOURCES, PS_TYPE_S32);
+        psVector *yOffset = psVectorAlloc(NUM_SOURCES, PS_TYPE_S32);
+        psVector *xOffsetBig = psVectorAlloc(NUM_SOURCES*2, PS_TYPE_S32);
+        psVector *xOffsetF32 = psVectorAlloc(NUM_SOURCES, PS_TYPE_F32);
+        psVector *yOffsetF32 = psVectorAlloc(NUM_SOURCES, PS_TYPE_F32);
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        for (int i = 0; i < sources->n ; i++) {
+            sources->data[i] = pmSourceAlloc();
+        }
+        pmPSFOptions *psfOptions = pmPSFOptionsAlloc();
+        psfOptions->psfTrendNx = 1;
+        psfOptions->psfTrendNy = 2;
+        psfOptions->psfFieldNx = 3;
+        psfOptions->psfFieldNy = 4;
+        psfOptions->psfFieldXo = 5;
+        psfOptions->psfFieldYo = 6;
+        pmModelClassInit();
+        psfOptions->type = pmModelClassGetType("PS_MODEL_GAUSS");
+        pmPSF *psf = pmPSFAlloc(psfOptions);
+
+        // NULL pmReadout input parameter
+        bool rc = pmReadoutFakeFromSources(NULL, TEST_NUM_COLS, TEST_NUM_ROWS, sources,
+                                           xOffset, yOffset, psf, 0.0, 1.0, false);
+        ok(rc == false, "pmReadoutFakeFromSources() returned FALSE with pmReadout input parameter");
+
+        // Non-positive numCols input parameter
+        rc = pmReadoutFakeFromSources(readout, 0, TEST_NUM_ROWS, sources,
+                                           xOffset, yOffset, psf, 0.0, 1.0, false);
+        ok(rc == false, "pmReadoutFakeFromSources() returned FALSE with Non-positive numCols input parameter");
+
+        // Non-positive numRows input parameter
+        rc = pmReadoutFakeFromSources(readout, TEST_NUM_COLS, 0, sources,
+                                           xOffset, yOffset, psf, 0.0, 1.0, false);
+        ok(rc == false, "pmReadoutFakeFromSources() returned FALSE with Non-positive numRow input parameter");
+
+        // NULL pmSource input parameter
+        rc = pmReadoutFakeFromSources(readout, TEST_NUM_COLS, TEST_NUM_ROWS, NULL,
+                                           xOffset, yOffset, psf, 0.0, 1.0, false);
+        ok(rc == false, "pmReadoutFakeFromSources() returned FALSE with pmSource input parameter");
+
+        // NULL pmPSF input parameter
+        rc = pmReadoutFakeFromSources(readout, TEST_NUM_COLS, TEST_NUM_ROWS, sources,
+                                           xOffset, yOffset, NULL, 0.0, 1.0, false);
+        ok(rc == false, "pmReadoutFakeFromSources() returned FALSE with input parameter");
+
+        // NULL incorrect type xOffset input parameter
+        rc = pmReadoutFakeFromSources(readout, TEST_NUM_COLS, TEST_NUM_ROWS, sources,
+                                           xOffsetF32, yOffset, psf, 0.0, 1.0, false);
+        ok(rc == false, "pmReadoutFakeFromSources() returned FALSE with incorrect type xOffset input parameter");
+
+        // NULL incorrect type yOffset input parameter
+        rc = pmReadoutFakeFromSources(readout, TEST_NUM_COLS, TEST_NUM_ROWS, sources,
+                                           xOffset, yOffsetF32, psf, 0.0, 1.0, false);
+        ok(rc == false, "pmReadoutFakeFromSources() returned FALSE with incorrect type yOffset input parameter");
+
+        // NULL incorrect size xOffset input parameter
+        rc = pmReadoutFakeFromSources(readout, TEST_NUM_COLS, TEST_NUM_ROWS, sources,
+                                           xOffsetBig, yOffset, psf, 0.0, 1.0, false);
+        ok(rc == false, "pmReadoutFakeFromSources() returned FALSE with incorrect size xOffset input parameter");
+
+        psFree(readout);
+        psFree(xOffset);
+        psFree(yOffset);
+        psFree(xOffsetBig);
+        psFree(xOffsetF32);
+        psFree(yOffsetF32);
+        psFree(sources);
+        psFree(psfOptions);
+        psFree(psf);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmReadoutFakeFromSources() with acceptable input parameters.
+    {
+        psMemId id = psMemGetId();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        psVector *xOffset = psVectorAlloc(NUM_SOURCES, PS_TYPE_S32);
+        psVector *yOffset = psVectorAlloc(NUM_SOURCES, PS_TYPE_S32);
+        psVector *xOffsetF32 = psVectorAlloc(NUM_SOURCES, PS_TYPE_F32);
+        psVector *yOffsetF32 = psVectorAlloc(NUM_SOURCES, PS_TYPE_F32);
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        for (int i = 0; i < sources->n ; i++) {
+            sources->data[i] = pmSourceAlloc();
+        }
+        pmPSFOptions *psfOptions = pmPSFOptionsAlloc();
+        psfOptions->psfTrendNx = 1;
+        psfOptions->psfTrendNy = 2;
+        psfOptions->psfFieldNx = 3;
+        psfOptions->psfFieldNy = 4;
+        psfOptions->psfFieldXo = 5;
+        psfOptions->psfFieldYo = 6;
+        pmModelClassInit();
+        psfOptions->type = pmModelClassGetType("PS_MODEL_GAUSS");
+        pmPSF *psf = pmPSFAlloc(psfOptions);
+
+        bool rc = pmReadoutFakeFromSources(readout, TEST_NUM_COLS, TEST_NUM_ROWS, sources,
+                                           xOffset, yOffset, psf, 0.0, 1.0, false);
+        ok(rc == true, "pmReadoutFakeFromSources() returned TRUE with acceptable input parameters");
+
+        psFree(readout);
+        psFree(xOffset);
+        psFree(yOffset);
+        psFree(xOffsetF32);
+        psFree(yOffsetF32);
+        psFree(sources);
+        psFree(psfOptions);
+        psFree(psf);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
Index: /branches/eam_branches/psModules.20240412/test/camera/tap_pmReadoutStack.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/camera/tap_pmReadoutStack.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/camera/tap_pmReadoutStack.c	(revision 42651)
@@ -0,0 +1,250 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested.
+*/
+
+#define CELL_ALLOC_NAME		"CellName"
+#define MISC_NUM		32
+#define MISC_NAME		"META00"
+#define NUM_BIAS_DATA		10
+#define TEST_NUM_ROWS		5
+#define TEST_NUM_COLS		8
+#define NUM_INPUTS		10
+#define NUM_READOUTS		4
+#define VERBOSE			0
+#define ERR_TRACE_LEVEL		10
+
+/******************************************************************************
+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);
+    psImageInit(readout->image, 1.0);
+    psImageInit(readout->mask, 2);
+    psImageInit(readout->weight, 3.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(int ID)
+{
+    pmCell *cell = pmCellAlloc(NULL, 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);
+    psRegion *region = psRegionAlloc(0.0, (float) (10 + ID), 0.0, (float) (20 + ID));
+    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);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = psMemDecrRefCounter((psPtr) generateSimpleReadout(cell));
+    }
+
+    return(cell);
+}
+
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(29);
+
+    // ------------------------------------------------------------------------
+    // pmReadoutUpdateSize() tests
+    // Call pmReadoutUpdateSize() with NULL pmReadout input parameter.
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmReadoutUpdateSize(NULL, 0, 0, TEST_NUM_COLS, TEST_NUM_ROWS, false);
+        ok(rc == false, "pmReadoutUpdateSize() returned FALSE with NULL pmReadout input parameter");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmReadoutUpdateSize() with acceptable input parameters (mask == false).
+    {
+        psMemId id = psMemGetId();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        bool rc = pmReadoutUpdateSize(readout, 0, 0, 2*TEST_NUM_COLS, 2*TEST_NUM_ROWS, false);
+        ok(rc == true, "pmReadoutUpdateSize() returned TRUE with acceptable input parameters");
+        ok(readout->image->numCols == (2*TEST_NUM_COLS) &&
+           readout->image->numRows == (2*TEST_NUM_ROWS), "pmReadoutUpdateSize() generated the correct size pmReadout->image");
+        bool errorFlag = false;
+        for (int i = 0 ; i < readout->image->numRows ; i++) {
+            for (int j = 0 ; j < readout->image->numCols ; j++) {
+                psF32 correctF32;
+                if (i < TEST_NUM_ROWS && j < TEST_NUM_COLS) {
+                   correctF32 = 1.0;
+                } else {
+                   correctF32 = 0.0;
+                }
+                if (readout->image->data.F32[i][j] != correctF32) {
+                    diag("ERROR: readout->image[%d][%d] is %.2f, should be %.2f", i, j, readout->image->data.F32[i][j], correctF32);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmReadoutUpdateSize() initialized pmReadout->image to zero");
+        ok(readout->mask->numCols == (TEST_NUM_COLS) &&
+           readout->mask->numRows == (TEST_NUM_ROWS), "pmReadoutUpdateSize() generated the correct size pmReadout->mask");
+
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmReadoutUpdateSize() with acceptable input parameters (mask == true).
+    {
+        psMemId id = psMemGetId();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        bool rc = pmReadoutUpdateSize(readout, 0, 0, 2*TEST_NUM_COLS, 2*TEST_NUM_ROWS, true);
+        ok(rc == true, "pmReadoutUpdateSize() returned TRUE with acceptable input parameters");
+        ok(readout->image->numCols == (2*TEST_NUM_COLS) &&
+           readout->image->numRows == (2*TEST_NUM_ROWS), "pmReadoutUpdateSize() generated the correct size pmReadout->image");
+        bool errorFlag = false;
+        for (int i = 0 ; i < readout->image->numRows ; i++) {
+            for (int j = 0 ; j < readout->image->numCols ; j++) {
+                psF32 correctF32;
+                if (i < TEST_NUM_ROWS && j < TEST_NUM_COLS) {
+                   correctF32 = 1.0;
+                } else {
+                   correctF32 = 0.0;
+                }
+                if (readout->image->data.F32[i][j] != correctF32) {
+                    diag("ERROR: readout->image[%d][%d] is %.2f, should be %.2f", i, j, readout->image->data.F32[i][j], correctF32);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmReadoutUpdateSize() initialized pmReadout->image to zero");
+        ok(readout->mask->numCols == (2*TEST_NUM_COLS) &&
+           readout->mask->numRows == (2*TEST_NUM_ROWS), "pmReadoutUpdateSize() generated the correct size pmReadout->mask");
+        errorFlag = false;
+        for (int i = 0 ; i < readout->mask->numRows ; i++) {
+            for (int j = 0 ; j < readout->mask->numCols ; j++) {
+                psF32 correctU8;
+                if (i < TEST_NUM_ROWS && j < TEST_NUM_COLS) {
+                   correctU8 = 2;
+                } else {
+                   correctU8 = 0;
+                }
+                if (readout->mask->data.U8[i][j] != correctU8) {
+                    diag("ERROR: readout->mask[%d][%d] is %d, should be %d", i, j, readout->mask->data.U8[i][j], correctU8);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmReadoutUpdateSize() initialized pmReadout->mask to zero");
+
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ------------------------------------------------------------------------
+    // pmReadoutStackValidate() tests
+    // Call pmReadoutStackValidate() with bad input parameters.
+    {
+        psMemId id = psMemGetId();
+        int minInputCols, maxInputCols, minInputRows, maxInputRows, numCols, numRows;
+        minInputCols = maxInputCols = minInputRows = maxInputRows = numCols = numRows = 0;
+        bool rc = pmReadoutStackValidate(&minInputCols, &maxInputCols, &minInputRows, &maxInputRows,
+                                         &numCols, &numRows, NULL);
+        psArray *inputs = psArrayAlloc(NUM_INPUTS);
+        for (int i = 0 ; i < NUM_INPUTS ; i++) {
+            inputs->data[i] = (psPtr *) generateSimpleReadout(NULL);
+        }
+
+        // NULL psArray input parameter
+        rc = pmReadoutStackValidate(&minInputCols, &maxInputCols, &minInputRows, &maxInputRows,
+                                   &numCols, &numRows, NULL);
+        ok(rc == false, "pmReadoutStackValidate() returned FALSE with NULL pmArray input parameter");
+
+        // NULL minInputColsPtr
+        rc = pmReadoutStackValidate(NULL, &maxInputCols, &minInputRows, &maxInputRows,
+                                    &numCols, &numRows, inputs);
+        ok(rc == false, "pmReadoutStackValidate() returned FALSE with NULL minInputColsPtr input parameter");
+
+        // NULL maxInputColsPtr
+        rc = pmReadoutStackValidate(&minInputCols, NULL, &minInputRows, &maxInputRows,
+                                    &numCols, &numRows, inputs);
+        ok(rc == false, "pmReadoutStackValidate() returned FALSE with NULL maxInputColsPtr input parameter");
+
+        // NULL minInputRowsPtr
+        rc = pmReadoutStackValidate(&minInputCols, &maxInputCols, NULL, &maxInputRows,
+                                    &numCols, &numRows, inputs);
+        ok(rc == false, "pmReadoutStackValidate() returned FALSE with NULL minInputRowsPtr input parameter");
+
+        // NULL maxInputRowsPtr
+        rc = pmReadoutStackValidate(&minInputCols, &maxInputCols, &minInputRows, NULL,
+                                    &numCols, &numRows, inputs);
+        ok(rc == false, "pmReadoutStackValidate() returned FALSE with NULL maxInputRowsPtr input parameter");
+
+        // NULL numColsPtr
+        rc = pmReadoutStackValidate(&minInputCols, &maxInputCols, &minInputRows, &maxInputRows,
+                                    NULL, &numRows, inputs);
+        ok(rc == false, "pmReadoutStackValidate() returned FALSE with NULL numColsPtr input parameter");
+
+        // NULL numRowsPtr
+        rc = pmReadoutStackValidate(&minInputCols, &maxInputCols, &minInputRows, &maxInputRows,
+                                    &numCols, NULL, inputs);
+        ok(rc == false, "pmReadoutStackValidate() returned FALSE with NULL numRowsPtr input parameter");
+
+        psFree(inputs);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmReadoutStackValidate() with acceptable input parameters.
+    {
+        psMemId id = psMemGetId();
+        int minInputCols, maxInputCols, minInputRows, maxInputRows, numCols, numRows;
+        minInputCols = maxInputCols = minInputRows = maxInputRows = numCols = numRows = 0;
+        pmCell *cells[NUM_INPUTS];
+        psArray *inputs = psArrayAlloc(NUM_INPUTS);
+        for (int i = 0 ; i < NUM_INPUTS ; i++) {
+            cells[i] = generateSimpleCell(i);
+            inputs->data[i] = (psPtr *) generateSimpleReadout(cells[i]);
+        }
+        bool rc = pmReadoutStackValidate(&minInputCols, &maxInputCols, &minInputRows, &maxInputRows,
+                                   &numCols, &numRows, inputs);
+        ok(rc == true, "pmReadoutStackValidate() returned TRUE with acceptable input parameters");
+        ok(minInputCols == 0, "pmReadoutStackValidate() set minInputCols correctly");
+        ok(maxInputCols == TEST_NUM_COLS, "pmReadoutStackValidate() set maxInputCols correctly");
+        ok(minInputRows == 0, "pmReadoutStackValidate() set minInputRows correctly");
+        ok(maxInputRows == TEST_NUM_ROWS, "pmReadoutStackValidate() set maxInputRows correctly");
+        ok(numCols == (10 + NUM_INPUTS - 1), "pmReadoutStackValidate() set numCols correctly");
+        ok(numRows == (20 + NUM_INPUTS - 1), "pmReadoutStackValidate() set numRows correctly");
+
+        for (int i = 0 ; i < NUM_INPUTS ; i++) {
+            psFree(cells[i]);
+        }
+        psFree(inputs);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+}
Index: /branches/eam_branches/psModules.20240412/test/concepts/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/concepts/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/concepts/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/concepts/tap_pmConcepts.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/concepts/tap_pmConcepts.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/concepts/tap_pmConcepts.c	(revision 42651)
@@ -0,0 +1,518 @@
+    /** @file tst_pmConcepts.c
+ *
+ *  @brief Contains the tests for pmConcepts.c:
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-01-29 20:45:54 $
+*   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);
+}
+
+psMetadataItem *dummyConceptCopier(
+    const psMetadataItem *source,
+    const psMetadataItem *target,
+    const psMetadata *cameraFormat,
+    const pmFPA *fpa,
+    const pmChip *chip,
+    const pmCell *cell)
+{
+    if (target == NULL ||
+        source == PM_CONCEPT_SOURCE_NONE ||
+        cameraFormat == NULL ||
+        fpa == NULL ||
+        chip == NULL ||
+        cell == NULL) {
+        printf("dummyConceptCopier() args are NULL\n");
+    }
+    return(NULL);
+}
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel(".", ERR_TRACE_LEVEL);
+    plan_tests(126);
+    
+    // --------------------------------------------------------------------
+    // 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, dummyConceptCopier, 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->copy == dummyConceptCopier, "pmConceptSpecAlloc() set the ->copy 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, 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, 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_branches/psModules.20240412/test/concepts/tap_pmConceptsAverage.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/concepts/tap_pmConceptsAverage.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/concepts/tap_pmConceptsAverage.c	(revision 42651)
@@ -0,0 +1,443 @@
+#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->variance = 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] = psMemDecrRefCounter((psPtr) generateSimpleReadout(cell));
+    }
+
+    // First try to read data from ../dataFiles, then try dataFiles.
+    bool rc = pmConfigFileRead(&cell->hdu->format, "../dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&cell->hdu->format, "dataFiles/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->variances = 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->variances->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->variances->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);
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = psMemDecrRefCounter((psPtr) 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, NULL);
+    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);
+    if (camera != NULL) {
+        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);
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = psMemDecrRefCounter((psPtr) generateSimpleChip(fpa));
+    }
+    pmConceptsBlankFPA(fpa);
+    return(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++) {
+            psFree(sourceFPA[fpaID]);
+	}
+        psFree(sources);
+        psFree(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++) {
+            psFree(sourceFPA[fpaID]);
+	}
+        psFree(sources);
+        psFree(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];
+        psMetadata *cameras[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++) {
+            cameras[fpaID] = psMetadataAlloc();
+            sourceFPA[fpaID] = generateSimpleFPA(cameras[fpaID]);
+            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);
+	    }
+	}
+        // XXX: The memory leak occurs during the following single call
+        ok(pmConceptsAverageFPAs(targetFPA, sources), "pmConceptsAverage(targetFPA, sources) returned TRUE");
+        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() returns 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++) {
+            psFree(sourceFPA[fpaID]);
+            psFree(cameras[fpaID]);
+	}
+        psFree(sources);
+//        psFree(filter);
+        psFree(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);
+
+        psFree(tgtFPA);
+        psFree(srcFPA);
+        psFree(tgtCamera);
+        psFree(srcCamera);
+        psFree(biassec);
+        psFree(trimsec);
+        psFree(sources);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+}
+
+
Index: /branches/eam_branches/psModules.20240412/test/concepts/tap_pmConceptsPhotcode.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/concepts/tap_pmConceptsPhotcode.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/concepts/tap_pmConceptsPhotcode.c	(revision 42651)
@@ -0,0 +1,83 @@
+#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 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(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(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] = "../dataFiles/SampleIPPConfig";
+        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);
+
+        skip_start(!config, 2, "Skipping tests because pmConfigRead() failed");        
+        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(file, view);
+        ok(testPhotcode, "pmConceptsPhotcodeForView(config, file, view) returned non-NULL");
+        ok(!strcmp(goodPhotcode, testPhotcode), "pmConceptsPhotcodeForView() produced the correct string");
+        skip_end();
+        psFree(config);
+        psFree(file);
+        psFree(view);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
Index: /branches/eam_branches/psModules.20240412/test/concepts/tap_pmConceptsUpdate.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/concepts/tap_pmConceptsUpdate.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/concepts/tap_pmConceptsUpdate.c	(revision 42651)
@@ -0,0 +1,248 @@
+#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->variance = 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] = psMemDecrRefCounter((psPtr) generateSimpleReadout(cell));
+    }
+
+    // First try to read data from ../dataFiles, then try dataFiles.
+    bool rc = pmConfigFileRead(&cell->hdu->format, "../dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&cell->hdu->format, "dataFiles/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->variances= 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->variances->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->variances->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.
+    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);
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = psMemDecrRefCounter((psPtr) 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, NULL);
+    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);
+    if (camera != NULL) {
+        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);
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = psMemDecrRefCounter((psPtr) generateSimpleChip(fpa));
+    }
+    pmConceptsBlankFPA(fpa);
+    return(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);
+        }
+
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+}
+
Index: /branches/eam_branches/psModules.20240412/test/config/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/Makefile.am	(revision 42651)
@@ -0,0 +1,33 @@
+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_pmConfigCommand \
+	tap_pmErrorCodes \
+	tap_pmVersion
+
+check_DATA =
+
+
+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_branches/psModules.20240412/test/config/data/SampleIPPConfig
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/SampleIPPConfig	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/SampleIPPConfig	(revision 42651)
@@ -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_branches/psModules.20240412/test/config/data/basicConfig
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/basicConfig	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/basicConfig	(revision 42651)
@@ -0,0 +1,25 @@
+
+TEST MULTI
+TEST STR FOO
+TEST STR BAR
+
+PATH              STR     .
+
+# load the site-specific information from here
+SITE              STR     site.config
+
+# load the system configuration information from here
+SYSTEM            STR     system.config
+
+### psLib setup
+LOGLEVEL	S32	5			# Logging level; 3=INFO
+LOGFORMAT	STR	M			# Log format
+LOGDEST		STR	STDERR			# Log destination
+TRACEDEST	STR	STDERR			# Trace destination
+
+# place default trace lines here
+TRACE		METADATA			# Trace levels
+  err		S32	10
+# psLib.db      S32	10
+END
+
Index: /branches/eam_branches/psModules.20240412/test/config/data/camera0/camera.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/camera0/camera.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/camera0/camera.config	(revision 42651)
@@ -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_branches/psModules.20240412/test/config/data/camera0/format0.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/camera0/format0.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/camera0/format0.config	(revision 42651)
@@ -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_branches/psModules.20240412/test/config/data/camera0/format1.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/camera0/format1.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/camera0/format1.config	(revision 42651)
@@ -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_branches/psModules.20240412/test/config/data/camera0/recipe0.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/camera0/recipe0.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/camera0/recipe0.config	(revision 42651)
@@ -0,0 +1,4 @@
+R0_KEY0		STR	RecipeString0
+R0_KEY1		STR	RecipeString1
+R0_KEY2		S32	2
+R0_KEY3		F32	3
Index: /branches/eam_branches/psModules.20240412/test/config/data/camera0/recipe1.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/camera0/recipe1.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/camera0/recipe1.config	(revision 42651)
@@ -0,0 +1,4 @@
+R1_KEY0		STR	RecipeString10
+R1_KEY1		STR	RecipeString11
+R1_KEY2		S32	12
+R1_KEY3		F32	13
Index: /branches/eam_branches/psModules.20240412/test/config/data/camera1/camera.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/camera1/camera.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/camera1/camera.config	(revision 42651)
@@ -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_branches/psModules.20240412/test/config/data/camera1/format0.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/camera1/format0.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/camera1/format0.config	(revision 42651)
@@ -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_branches/psModules.20240412/test/config/data/camera1/format1.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/camera1/format1.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/camera1/format1.config	(revision 42651)
@@ -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_branches/psModules.20240412/test/config/data/camera1/recipe0.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/camera1/recipe0.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/camera1/recipe0.config	(revision 42651)
@@ -0,0 +1,4 @@
+R0_KEY0		STR	RecipeString120
+R0_KEY1		STR	RecipeString121
+R0_KEY2		S32	122
+R0_KEY3		F32	123
Index: /branches/eam_branches/psModules.20240412/test/config/data/camera1/recipe1.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/camera1/recipe1.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/camera1/recipe1.config	(revision 42651)
@@ -0,0 +1,4 @@
+R1_KEY0		STR	RecipeString230
+R1_KEY1		STR	RecipeString2311
+R1_KEY2		S32	232
+R1_KEY3		F32	233
Index: /branches/eam_branches/psModules.20240412/test/config/data/path2/SampleIPPConfig2
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/path2/SampleIPPConfig2	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/path2/SampleIPPConfig2	(revision 42651)
@@ -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_branches/psModules.20240412/test/config/data/recipe_file00
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/recipe_file00	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/recipe_file00	(revision 42651)
@@ -0,0 +1,2 @@
+recipe00		BOOL    TRUE
+recipe01		BOOL    FALSE
Index: /branches/eam_branches/psModules.20240412/test/config/data/recipe_file01
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/recipe_file01	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/recipe_file01	(revision 42651)
@@ -0,0 +1,2 @@
+recipe00		BOOL    FALSE
+recipe01		BOOL    TRUE
Index: /branches/eam_branches/psModules.20240412/test/config/data/recipes/recipe1.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/recipes/recipe1.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/recipes/recipe1.config	(revision 42651)
@@ -0,0 +1,12 @@
+
+KEY1	STR	VALUE1
+KEY2	STR	VALUE2
+
+RECIPE1_ALT METADATA
+  KEY2  STR     VALUE2_ALT
+END
+
+KEY3 MULTI 
+KEY3 STR V1
+KEY3 STR V2
+KEY3 STR V3
Index: /branches/eam_branches/psModules.20240412/test/config/data/recipes_masks.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/recipes_masks.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/recipes_masks.config	(revision 42651)
@@ -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_branches/psModules.20240412/test/config/data/site.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/site.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/site.config	(revision 42651)
@@ -0,0 +1,32 @@
+
+# place your data directories here and refer to as path://PATH/remainder
+DATAPATH	METADATA
+	TEST1	STR	dataTest1
+	TEST2	STR	dataTest2
+END
+
+# List of tessellations, and their DVO CATDIR
+TESSELLATIONS	METADATA
+	TESS1		STR	path://TEST1/skycells/
+	TESS2		STR	path://TEST2/skycells/
+END
+
+# dvo databases used for output
+DVO.CATDIRS	METADATA
+	CATDIR1		STR	path://TEST1/catdir/
+END
+
+# dvo databases used for psastro reference
+PSASTRO.CATDIRS	METADATA
+	CATDIR1		STR	path://TEST1/catdir/
+END
+
+# nebulous server
+NEB_SERVER	STR	http://alala:80/nebulous	# Nebulous server
+
+# Database configuration
+DBSERVER	STR	ipp000			# Database host name (for psDBInit)
+DBNAME		STR	XXX			# Database name (for psDBInit)
+DBUSER		STR	XXX			# Database user name (for psDBInit)
+DBPASSWORD	STR	XXX			# Database password (for psDBInit)
+
Index: /branches/eam_branches/psModules.20240412/test/config/data/system.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/system.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/system.config	(revision 42651)
@@ -0,0 +1,24 @@
+## system-wide options : these are concepts not specific to any camera or recipe
+
+### Setups for each camera system
+CAMERAS		METADATA
+	TEST1			STR	testCamera1/camera.config
+END
+
+### camera names as expected by DVO
+DVO.CAMERAS		METADATA
+	TEST1			STR	testCamera1
+END
+
+# Header keywords for skycell concepts; required because DVO doesn't read HIERARCH
+SKYCELLS	METADATA
+	FPA.TIME	STR	MJD-OBS
+	CELL.TIME	STR	MJD-OBS
+	FPA.EXPOSURE	STR	EXPTIME
+	CELL.EXPOSURE	STR	EXPTIME
+	FPA.AIRMASS	STR	AIRMASS
+END
+
+RECIPES		METADATA		# Site-level recipes
+	RECIPE1		STR		recipes/recipe1.config  # Simple recipe
+END
Index: /branches/eam_branches/psModules.20240412/test/config/data/testCamera1/camera.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/testCamera1/camera.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/testCamera1/camera.config	(revision 42651)
@@ -0,0 +1,12 @@
+
+RECIPES	METADATA	# Site-level recipes
+	RECIPE1	STR	testCamera1/recipe1.config  # Simple recipe
+END
+
+FORMATS METADATA
+  	RAW  	STR	testCamera1/format.config
+END
+
+FPA	METADATA
+	chip	STR	cell
+END
Index: /branches/eam_branches/psModules.20240412/test/config/data/testCamera1/format.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/testCamera1/format.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/testCamera1/format.config	(revision 42651)
@@ -0,0 +1,53 @@
+# sample MEF format with HDU = cell
+
+# How to identify this type
+RULE	METADATA
+	TELESCOP	STR	TestTelescope
+	DETECTOR	STR	TestDetector
+	NAXIS           S32     0
+	EXTEND		BOOL	T
+END
+
+# How to read this data
+FILE	METADATA
+	PHU		STR	FPA	# The FITS file represents an entire FPA
+	EXTENSIONS	STR	CELL	# The extensions represent cells
+	FPA.NAME	STR	EXPNUM	# A PHU keyword for unique identifier within the hierarchy level
+END
+
+# What's in the FITS file?
+CONTENTS	METADATA
+	# Extension name, chip name:type
+	amp		STR	chip:LeftAmp:left
+END
+
+# Specify the cell data
+CELLS	METADATA
+	left	METADATA	# Left amplifier
+		CELL.BIASSEC.SOURCE	STR	HEADER
+		CELL.TRIMSEC.SOURCE	STR	HEADER
+		CELL.BIASSEC		STR	BIASSEC
+		CELL.TRIMSEC		STR	DATASEC
+		CELL.XPARITY		S32	1 # We could have specified this as a DEFAULT, but this works
+		CELL.X0			S32	1
+		CELL.Y0			S32	1
+	END
+END
+
+# How to translate PS concepts into FITS headers
+TRANSLATION	METADATA
+	FPA.AIRMASS		STR	AIRMASS
+	FPA.FILTERID		STR	FILTER
+END
+
+# Default PS concepts that may be specified by value
+DEFAULTS	METADATA
+	FPA.TELESCOPE		STR	CFHT
+	FPA.INSTRUMENT		STR	MEGACAM
+END
+
+# Where there might be some ambiguity, specify the format
+FORMATS		METADATA
+	FPA.RA		STR	HOURS
+	FPA.DEC		STR	DEGREES
+END
Index: /branches/eam_branches/psModules.20240412/test/config/data/testCamera1/recipe1.config
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/data/testCamera1/recipe1.config	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/data/testCamera1/recipe1.config	(revision 42651)
@@ -0,0 +1,10 @@
+
+KEY1	STR	VALUE1_CAMERA
+# KEY2	STR	VALUE2_CAMERA
+
+RECIPE1_ALT METADATA
+  KEY1  STR     VALUE1_CAMERA_ALT
+END
+
+KEY3 MULTI RESET
+KEY3 STR V4
Index: /branches/eam_branches/psModules.20240412/test/config/tap_pmConfig.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/tap_pmConfig.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/tap_pmConfig.c	(revision 42651)
@@ -0,0 +1,1284 @@
+/** @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.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-06-10 20:58:28 $
+ *
+ *  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         10
+#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("../dataFiles/path2:dataFiles/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:../dataFiles/path2:dataFiles/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:../dataFiles/path2:dataFiles/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
+    {
+        psMemId id = psMemGetId();
+        psMetadata *config = NULL;
+        psString name = "../dataFiles/SampleIPPConfig";
+        psString name2 = "dataFiles/SampleIPPConfig";
+        psString desc = "DESCRIPTION";
+        // First try to read data from ../dataFiles, then try dataFiles.
+        bool rc = pmConfigFileRead(&config, name, desc);
+        if (!rc) {
+            rc = pmConfigFileRead(&config, name2, 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 = "../dataFiles/SampleIPPConfig";
+        psString name2 = "dataFiles/SampleIPPConfig";
+        psString desc = "DESCRIPTION";
+        bool rc = pmConfigFileRead(&config, name, desc);
+        if (!rc) {
+            rc = pmConfigFileRead(&config, name2, 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 = "../dataFiles/SampleIPPConfig";
+        psString name2 = "dataFiles/SampleIPPConfig";
+        psString desc = "DESCRIPTION";
+        bool rc = pmConfigFileRead(&config, name, desc);
+        if (!rc) {
+            rc = pmConfigFileRead(&config, name2, 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("../dataFiles/path2:dataFiles/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, "../dataFiles/camera0/format0.config", "Camera 0 Config File");
+        if (!rc) {
+            rc = pmConfigFileRead(&camera, "dataFiles/camera0/format0.config", "Camera 0 Config File");
+	}
+        ok(rc == true, "pmConfigFileRead() read ../dataFiles/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, "../dataFiles/SampleIPPConfig", "DESCRIPTION");
+        if (!rc) {
+            rc = pmConfigFileRead(&config->site, "dataFiles/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, "../dataFiles/SampleIPPConfig", "DESCRIPTION");
+        if (!rc) {
+            rc = pmConfigFileRead(&config->site, "dataFiles/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, "../dataFiles/SampleIPPConfig", "DESCRIPTION");
+        if (!rc) {
+            rc = pmConfigFileRead(&config->site, "dataFiles/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, "../dataFiles/camera0/format0.config", "Camera 0 Config File");
+        if (!rc) {
+            rc = pmConfigFileRead(&format, "dataFiles/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] = "../dataFiles/SampleIPPConfig";
+        char *filename = "FILENAME";
+        char *list = "LIST";
+        psArray *array = pmConfigFileSets(NULL, str, filename, list);
+        if (!array) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            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] = "../dataFiles/SampleIPPConfig";
+        int myArgc = -1;
+        char *filename = "FILENAME";
+        char *list = "LIST";
+        psArray *array = pmConfigFileSets(&myArgc, str, filename, list);
+        if (!array) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            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] = "../dataFiles/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] = "../dataFiles/SampleIPPConfig";
+        int myArgc = 3;
+        char *list = "LIST";
+        psArray *array = pmConfigFileSets(&myArgc, str, NULL, list);
+        if (!array) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            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] = "../dataFiles/SampleIPPConfig";
+        int myArgc = 3;
+        char *filename = "FILENAME";
+        psArray *array = pmConfigFileSets(&myArgc, str, filename, NULL);
+        if (!array) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            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] = "../dataFiles/SampleIPPConfig";
+        int myArgc = 3;
+        char *filename = "FILENAME";
+        char *list = "LIST";
+        psArray *array = pmConfigFileSets(&myArgc, str, filename, list);
+        if (!array) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            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] = "../dataFiles/SampleIPPConfig";
+        int myArgc = 3;
+        char *name = "NAME";
+        char *file = "FILENAME";
+        char *list = "LIST";
+        bool rc = pmConfigFileSetsMD(NULL, &myArgc, str, name, file, list);
+        if (!rc) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            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] = "../dataFiles/SampleIPPConfig";
+        int myArgc = 3;
+        char *file = "FILENAME";
+        char *list = "LIST";
+        bool rc = pmConfigFileSetsMD(metadata, &myArgc, str, NULL, file, list);
+        if (!rc) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            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] = "../dataFiles/SampleIPPConfig";
+        int myArgc = 3;
+        char *name = "NAME";
+        char *list = "LIST";
+        bool rc = pmConfigFileSetsMD(metadata, &myArgc, str, name, NULL, list);
+        if (!rc) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            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] = "../dataFiles/SampleIPPConfig";
+        int myArgc = 3;
+        char *name = "NAME";
+        char *file = "FILENAME";
+        bool rc = pmConfigFileSetsMD(metadata, &myArgc, str, name, file, NULL);
+        if (!rc) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            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] = "../dataFiles/SampleIPPConfig";
+        int myArgc = 3;
+        char *name = "NAME";
+        char *file = "FILENAME";
+        char *list = "LIST";
+        bool rc = pmConfigFileSetsMD(metadata, &myArgc, str, name, file, list);
+        if (!rc) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            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, false);
+        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, false);
+        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, "../dataFiles/SampleIPPConfig", "DESCRIPTION");
+        if (!rc) {
+            rc = pmConfigFileRead(&config->site, "dataFiles/SampleIPPConfig", "DESCRIPTION");
+	}
+        ok(rc == true, "pmConfigFileRead() was successful");
+        bool create = false;
+        psString tmpStr = pmConfigConvertFilename(filename, (const pmConfig *) config, create, false);
+        ok(NULL == tmpStr, "pmConfigConvertFilename() returned NULL with create==FALSE");
+        create = true;
+        tmpStr = pmConfigConvertFilename(filename, (const pmConfig *) config, create, false);
+        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] = "../dataFiles/SampleIPPConfig";
+        pmConfig *myConfig = pmConfigRead(NULL, str, "RecipeName");
+        if (!myConfig) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            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] = "../dataFiles/SampleIPPConfig";
+        int numArgs = 3;
+        pmConfig *myConfig = pmConfigRead(&numArgs, str, NULL);
+        if (!myConfig) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            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] = "../dataFiles/SampleIPPConfig";
+        psS32 argc = 3;
+        printf("----------------------------------------------------------------\n");
+        printf("Calling pmConfigRead() with acceptable arguments.\n");
+        bool rc;
+        pmConfig *myConfig = pmConfigRead(&argc, str, "RecipeName");
+        if (!myConfig) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            myConfig = pmConfigRead(&argc, str, "RecipeName");
+	}
+        ok(myConfig, "pmConfigRead() returned non-NULL");
+        if (myConfig == NULL) {
+            diag("TEST ERROR: pmConfigRead() returned NULL\n");
+            testStatus = false;
+        } else {
+            //
+            // Ensure that the various trace and logging values are set properly
+            //
+            if (1 != psTraceGetLevel("dummyTraceFunc01")) {
+                diag("TEST ERROR: failed to properly set tracelevel for dummyTraceFunc01\n");
+                diag("    tracelevel was %d, should be 1.\n", psTraceGetLevel("dummyTraceFunc01"));
+                testStatus = false;
+            }
+
+            if (2 != psTraceGetLevel("dummyTraceFunc02")) {
+                diag("TEST ERROR: failed to properly set tracelevel for dummyTraceFunc02\n");
+                diag("    tracelevel was %d, should be 2.\n", psTraceGetLevel("dummyTraceFunc02"));
+                testStatus = false;
+            }
+
+            if (1 != psLogGetDestination()) {
+                diag("TEST ERROR: failed to properly set log destination.\n");
+                diag("    it was %d, should be 1\n", psLogGetDestination());
+                testStatus = false;
+            }
+
+            if (3 != psLogGetLevel()) {
+                diag("TEST ERROR: failed to properly set log level.\n");
+                diag("    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)) {
+                diag("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)) {
+                diag("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)) {
+                diag("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)) {
+                diag("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)) {
+                diag("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) {
+                diag("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"))) {
+                    diag("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"))) {
+                    diag("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"))) {
+                    diag("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"))) {
+                    diag("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"))) {
+                    diag("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"))) {
+                diag("TEST ERROR: failed to properly set metadata string DBSERVER.\n");
+                testStatus = false;
+            }
+
+            tmpStr = psMetadataLookupStr(&rc, site, "DBUSER");
+            if ((rc == false) || (0 != strcmp(tmpStr, "ipp"))) {
+                diag("TEST ERROR: failed to properly set metadata string DBUSER.\n");
+                testStatus = false;
+            }
+
+            tmpStr = psMetadataLookupStr(&rc, site, "DBPASSWORD");
+            if ((rc == false) || (0 != strcmp(tmpStr, "password"))) {
+                diag("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_branches/psModules.20240412/test/config/tap_pmConfigCommand.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/tap_pmConfigCommand.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/tap_pmConfigCommand.c	(revision 42651)
@@ -0,0 +1,155 @@
+/** @file tst_pmConfigCommand.c
+ *
+ *  @brief Contains the tests for pmConfigCommand.c:
+ *
+ * This code will test the pmConfigCommand() routine.
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-01-02 20:49:09 $
+ *
+ *  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] = "../dataFiles/SampleIPPConfig";
+        psS32 argc = 3;
+        pmConfig *config = pmConfigRead(&argc, str, "RecipeName");
+        if (!config) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            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] = "../dataFiles/SampleIPPConfig";
+        psS32 argc = 3;
+        pmConfig *config = pmConfigRead(&argc, str, "RecipeName");
+        if (!config) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            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] = "../dataFiles/SampleIPPConfig";
+        psS32 argc = 3;
+        pmConfig *config = pmConfigRead(&argc, str, "RecipeName");
+        if (!config) {
+            str[2] = "dataFiles/SampleIPPConfig";
+            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_branches/psModules.20240412/test/config/tap_pmConfigMask.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/tap_pmConfigMask.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/tap_pmConfigMask.c	(revision 42651)
@@ -0,0 +1,89 @@
+/** @file tst_pmConfigMask.c
+ *
+ *  @brief Contains the tests for pmConfigMask.c:
+ *
+ * This code will test the pmConfigMask() routine.
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-01-02 20:49:10 $
+ *
+ *  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(7);
+
+
+    // --------------------------------------------------------------------
+    // 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] = "dataFiles/SampleIPPConfig";
+        psS32 argc = 3;
+        pmConfig *config = pmConfigRead(&argc, str, "RecipeName");
+        if (!config) {
+            str[2] = "../dataFiles/SampleIPPConfig";
+            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
+    if (0) {
+        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 ../dataFiles/recipes_masks.config (
+        char *masks = "DETECTOR RANGE";
+        psMaskType correctMask = 0x02 | 0x04;
+        psString str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "dataFiles/SampleIPPConfig";
+        psS32 argc = 3;
+        pmConfig *config = pmConfigRead(&argc, str, "RecipeName");
+        if (!config) {
+            str[2] = "../dataFiles/SampleIPPConfig";
+            config = pmConfigRead(&argc, str, "RecipeName");
+	}
+        ok(config != NULL, "pmConfigRead() successful");
+        skip_start(config == NULL, 2, "Skipping tests because pmConfigRead() failed");
+        psMaskType mask = pmConfigMask(masks, config);
+        ok(mask, "pmConfigMask returned non-zero with acceptable input params");
+        ok(mask == correctMask, "pmConfigMask() generated the correct output mask (%x).  Should be (%x)", mask, correctMask);
+        skip_end();
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branches/psModules.20240412/test/config/tap_pmErrorCodes.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/tap_pmErrorCodes.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/tap_pmErrorCodes.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/config/tap_pmVersion.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/tap_pmVersion.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/tap_pmVersion.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/config/tst_pmConfig.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/config/tst_pmConfig.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/config/tst_pmConfig.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/detrend/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/detrend/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/detrend/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/detrend/tap_pmBias.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/detrend/tap_pmBias.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/detrend/tap_pmBias.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/detrend/tap_pmFlatField.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/detrend/tap_pmFlatField.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/detrend/tap_pmFlatField.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/detrend/tap_pmMaskBadPixels.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/detrend/tap_pmMaskBadPixels.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/detrend/tap_pmMaskBadPixels.c	(revision 42651)
@@ -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 = psRandomAllocSpecific(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 = psRandomAllocSpecific(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 = psRandomAllocSpecific(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 = psRandomAllocSpecific(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 = psRandomAllocSpecific(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 = psRandomAllocSpecific(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 = psRandomAllocSpecific(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 = psRandomAllocSpecific(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 = psRandomAllocSpecific(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 = psRandomAllocSpecific(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 = psRandomAllocSpecific(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 = psRandomAllocSpecific(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_branches/psModules.20240412/test/detrend/tap_pmNonLinear.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/detrend/tap_pmNonLinear.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/detrend/tap_pmNonLinear.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/detrend/tap_pmShutterCorrection.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/detrend/tap_pmShutterCorrection.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/detrend/tap_pmShutterCorrection.c	(revision 42651)
@@ -0,0 +1,430 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "tap.h"
+
+int main (void)
+{
+    plan_tests(46);
+
+    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, 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, 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_branches/psModules.20240412/test/detrend/tst_pmFlatField.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/detrend/tst_pmFlatField.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/detrend/tst_pmFlatField.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/detrend/tst_pmMaskBadPixels.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/detrend/tst_pmMaskBadPixels.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/detrend/tst_pmMaskBadPixels.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/detrend/tst_pmNonLinear.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/detrend/tst_pmNonLinear.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/detrend/tst_pmNonLinear.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/detrend/tst_pmSubtractBias.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/detrend/tst_pmSubtractBias.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/detrend/tst_pmSubtractBias.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/detrend/tst_pmSubtractSky.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/detrend/tst_pmSubtractSky.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/detrend/tst_pmSubtractSky.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/extras/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/extras/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/extras/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/imcombine/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/imcombine/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/imcombine/Makefile.am	(revision 42651)
@@ -0,0 +1,28 @@
+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_pmImageCombine
Index: /branches/eam_branches/psModules.20240412/test/imcombine/tap_pmImageCombine.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/imcombine/tap_pmImageCombine.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/imcombine/tap_pmImageCombine.c	(revision 42651)
@@ -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 = psRandomAllocSpecific(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_branches/psModules.20240412/test/imcombine/tst_pmImageCombine.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/imcombine/tst_pmImageCombine.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/imcombine/tst_pmImageCombine.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/imcombine/tst_pmImageSubtract.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/imcombine/tst_pmImageSubtract.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/imcombine/tst_pmImageSubtract.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/imcombine/tst_pmReadoutCombine.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/imcombine/tst_pmReadoutCombine.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/imcombine/tst_pmReadoutCombine.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/objects/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/Makefile.am	(revision 42651)
@@ -0,0 +1,57 @@
+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_pmSourceFitModel
+
+#	tap_pmPeaks \
+#	tap_pmGrowthCurve \
+#	tap_pmMoments \
+#	tap_pmSource \
+#	tap_pmModel \
+#	tap_pmModelUtils \
+#	tap_pmModelClass \
+#	tap_pmModel_CentralPixel \
+#	tap_pmModel_CentralPixel_v2 \
+#	tap_pmModel_SET_FWHM \
+#	tap_pmPSF \
+#	tap_pmTrend2D \
+#	tap_pmResiduals \
+#	tap_pmSourceExtendedPars \
+#	tap_pmSourceSky \
+#	tap_pmSourceMoments \
+#	tap_pmSourceIO_PS1_DEV_0 \
+#	tap_pmSourceIO_PS1_DEV_1 \
+#	tap_pmSourceIO_SMPDATA \
+#	tap_pmSourceContour \
+#	tap_pmSourceUtils \
+#	tap_pmSourceFitSet \
+#	tap_pmPSF_IO \
+#	tap_pmSourceIO_SMPDATA
+
+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_branches/psModules.20240412/test/objects/tap_pmFringe.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmFringe.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmFringe.c	(revision 42651)
@@ -0,0 +1,130 @@
+#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 = psRandomAllocSpecific(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");
+    }
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmGrowthCurve.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmGrowthCurve.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmGrowthCurve.c	(revision 42651)
@@ -0,0 +1,378 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/*
+    STATUS:
+	All functions are tested.  However, some of the pmGrowthCurveCorrect()
+	tests that were supplied by IfA ae failing.
+*/
+
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(70);
+
+    // ----------------------------------------------------------------------
+    // pmGrowthCurveAlloc() tests
+    // call pmGrowthCurveAlloc() with acceptable input parameters.
+    {
+        psMemId id = psMemGetId();
+        pmGrowthCurve *growthCurve = pmGrowthCurveAlloc(1.0, 2.0, 3.0);
+        ok(growthCurve && psMemCheckGrowthCurve(growthCurve), "pmGrowthCurveAlloc() allocated a pmGrowthCurve correctly");
+        ok(growthCurve->radius && psMemCheckVector(growthCurve->radius), "pmGrowthCurveAlloc() allocated the radius psVector correctly");
+        ok(growthCurve->apMag  && psMemCheckVector(growthCurve->apMag) &&
+           growthCurve->apMag->n == growthCurve->radius->n, "pmGrowthCurveAlloc() allocated the apMag psVector correctly");
+        ok(growthCurve->refRadius == 3.0, "pmGrowthCurveAlloc() set growthCurve->refRadius correctly");
+        ok(growthCurve->maxRadius == 2.0, "pmGrowthCurveAlloc() set growthCurve->maxRadius correctly");
+        ok(growthCurve->apLoss == 0.0, "pmGrowthCurveAlloc() set growthCurve->apLoss correctly");
+        ok(growthCurve->fitMag == 0.0, "pmGrowthCurveAlloc() set growthCurve->fitMag correctly");
+        psFree(growthCurve);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmGrowthCurveCorrect() tests
+    // Call pmGrowthCurveCorrect() with NULL input pmGrowthCurve
+    {
+        psMemId id = psMemGetId();
+        pmGrowthCurve *growthCurve = pmGrowthCurveAlloc(1.0, 2.0, 3.0);
+        ok(isnan(pmGrowthCurveCorrect(NULL, 0.0)), "pmGrowthCurveCorrect() returned NAN with NULL input pmGrowthCurve");
+        psFree(growthCurve);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmGrowthCurveCorrect() with acceptable input parameters.
+    {
+        #define RADIUS 0.5
+        psMemId id = psMemGetId();
+        pmGrowthCurve *growthCurve = pmGrowthCurveAlloc(1.0, 2.0, 3.0);
+        float testCor = pmGrowthCurveCorrect(growthCurve, RADIUS);
+        float actRad = psVectorInterpolate (growthCurve->radius, growthCurve->apMag, RADIUS);
+        float actCor = growthCurve->apRef - actRad;
+
+        ok(!isnan(testCor), "pmGrowthCurveCorrect() call was successful");
+        ok(actCor == testCor, "pmGrowthCurveCorrect() calculated the correction correctly");
+
+        psFree(growthCurve);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // EM test 01: test allocation
+    // 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
+        pmModelClassInit();
+        pmPSF *psf = pmPSFBuildSimple("PS_MODEL_GAUSS", 1.5, 1.5, 0.0);
+        psf->growth = growth;
+
+        pmGrowthCurveGenerate(readout, psf, false, 0, 0);
+
+        // 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->maskObj = psMemIncrRefCounter (readout->mask);
+
+        source->modelPSF->dparams->data.F32[PM_PAR_I0] = 1;
+        source->mode = PM_SOURCE_MODE_PSFSTAR;
+
+        source->apRadius = 15.0;
+
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        double refMag = source->apMag;
+
+        source->apRadius = 10.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        ok_float_tol(refMag - source->apMag, +0.0000, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->apRadius = 8.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        ok_float_tol(refMag - source->apMag, +0.0000, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->apRadius = 6.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        ok_float_tol(refMag - source->apMag, +0.0003, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->apRadius = 4.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        ok_float_tol(refMag - source->apMag, +0.0020, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->apRadius = 3.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        ok_float_tol(refMag - source->apMag, -0.0001, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->apRadius = 2.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        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");
+    }
+
+
+    // EM test 02: test allocation
+    // 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, 0, 0);
+
+        // 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->maskObj = psMemIncrRefCounter (readout->mask);
+
+        source->modelPSF->dparams->data.F32[PM_PAR_I0] = 1;
+        source->mode = PM_SOURCE_MODE_PSFSTAR;
+
+        source->apRadius = 15.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        double refMag = source->apMag;
+
+        source->apRadius = 10.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        ok_float_tol(refMag - source->apMag, +0.0000, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->apRadius = 8.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        ok_float_tol(refMag - source->apMag, +0.0000, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->apRadius = 6.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        ok_float_tol(refMag - source->apMag, +0.0004, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->apRadius = 4.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        ok_float_tol(refMag - source->apMag, +0.0026, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->apRadius = 3.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        ok_float_tol(refMag - source->apMag, -0.0001, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->apRadius = 2.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        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");
+    }
+
+
+    // EM test 03: test allocation
+    // 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, 0, 0);
+
+        // 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->maskObj = psMemIncrRefCounter (readout->mask);
+
+        source->modelPSF->dparams->data.F32[PM_PAR_I0] = 1;
+        source->mode = PM_SOURCE_MODE_PSFSTAR;
+
+        source->apRadius = 15.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        double refMag = source->apMag;
+
+        source->apRadius = 10.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        ok_float_tol(refMag - source->apMag, +0.0000, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->apRadius = 8.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        ok_float_tol(refMag - source->apMag, +0.0000, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->apRadius = 6.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        ok_float_tol(refMag - source->apMag, +0.0006, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->apRadius = 4.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        ok_float_tol(refMag - source->apMag, +0.0038, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->apRadius = 3.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        ok_float_tol(refMag - source->apMag, +0.0000, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->apRadius = 2.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP, 0);
+        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");
+    }
+}
+
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmModel.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmModel.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmModel.c	(revision 42651)
@@ -0,0 +1,460 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested.  However...
+
+    The source code for pmModelAddWithOffset() and pmModelSubWithOffset() is
+    almost exactly the same as the source code for pmModelAdd() and
+    pmModelSub().  We do not test them here.
+*/
+
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           (8+1)
+#define TEST_NUM_COLS           (8+1)
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+#define NUM_ROWS		8
+#define NUM_COLS		16
+#define TEST_FLOATS_EQUAL(X, Y) (abs(X - Y) < 0.01)
+
+/******************************************************************************
+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->variance = 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(53);
+
+    // ----------------------------------------------------------------------
+    // pmModelAlloc() tests
+    // call pmModelAlloc() with unallowable model class type
+    {
+        psMemId id = psMemGetId();
+        pmModelClassInit();
+        pmModel *model = pmModelAlloc(-1);
+        ok(model == NULL, "pmModelAlloc() returned a NULL with unallowable model class type");
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // call pmModelAlloc() with acceptable input params
+    {
+        #define TEST_MODEL_CLASS_TYPE 1
+        psMemId id = psMemGetId();
+        pmModelClassInit();
+        pmModel *model = pmModelAlloc(TEST_MODEL_CLASS_TYPE);
+        ok(model != NULL && psMemCheckModel(model), "pmModelAlloc() returned a non-NULL pmModel");
+        skip_start(!model, 1, "Skipping tests because pmModelAlloc() returned NULL");
+        ok(model->type == TEST_MODEL_CLASS_TYPE, "pmModelAlloc() set pmModel->type correctly");
+        ok(model->chisq == 0.0, "pmModelAlloc() set pmModel->chisq correctly");
+        ok(model->nDOF == 0, "pmModelAlloc() set pmModel->nDOF correctly");
+        ok(model->nIter == 0, "pmModelAlloc() set pmModel->nIter correctly");
+        ok(model->fitRadius == 0, "pmModelAlloc() set pmModel->fitRadius correctly");
+        ok(model->flags == PM_MODEL_STATUS_NONE, "pmModelAlloc() set pmModel->flags correctly");
+        ok(model->residuals == NULL, "pmModelAlloc() set pmModel->residuals correctly");
+        int nParams = pmModelClassParameterCount(TEST_MODEL_CLASS_TYPE);
+        ok(model->params != NULL && model->params->n == nParams, "pmModelAlloc() set the pmModel->params psVector correctly");
+        ok(model->dparams != NULL && model->dparams->n == nParams, "pmModelAlloc() set the pmModel->dparams psVector correctly");
+        bool errorFlag = false;
+        for (psS32 i = 0; i < nParams; i++) {
+            if (model->params->data.F32[i] != 0.0 || model->dparams->data.F32[i] != 0.0) {
+                if (VERBOSE) {
+                    diag("ERROR: params/dparams[%d] is (%.2f %.2f) should be (%.2f %.2f)\n", i, model->params->data.F32[i], model->dparams->data.F32[i], 0.0, 0.0);
+		}
+                errorFlag = true;
+	    }
+        }
+        ok(!errorFlag, "pmModelAlloc() set the members of the model->params and model->dparams psVectors correctly");
+        pmModelClass *class = pmModelClassSelect(TEST_MODEL_CLASS_TYPE);
+        ok(model->modelFunc == class->modelFunc, "pmModelAlloc() set pmModel->modelFunc correctly");
+        ok(model->modelFlux == class->modelFlux, "pmModelAlloc() set pmModel->modelFlux correctly");
+        ok(model->modelRadius == class->modelRadius, "pmModelAlloc() set pmModel->modelRadius correctly");
+        ok(model->modelLimits == class->modelLimits, "pmModelAlloc() set pmModel->modelLimits correctly");
+        ok(model->modelGuess == class->modelGuess, "pmModelAlloc() set pmModel->modelGuess correctly");
+        ok(model->modelFromPSF == class->modelFromPSF, "pmModelAlloc() set pmModel->modelFromPSF correctly");
+        ok(model->modelParamsFromPSF == class->modelParamsFromPSF, "pmModelAlloc() set pmModel->modelParamsFromPSF correctly");
+        ok(model->modelFitStatus == class->modelFitStatus, "pmModelAlloc() set pmModel->modelFitStatus correctly");
+
+        psFree(model);
+        skip_end();
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmModelCopy() tests
+    // call pmModelCopy() with NULL input pmModel parameter
+    {
+        psMemId id = psMemGetId();
+        pmModel *model = pmModelCopy(NULL);
+        ok(model == NULL, "pmModelCopy() returned NULL with NULL input pmModel parameter");
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // call pmModelCopy() with acceptable input params
+    {
+        #define TEST_MODEL_CLASS_TYPE 1
+        psMemId id = psMemGetId();
+        pmModelClassInit();
+        pmModel *modelSrc = pmModelAlloc(TEST_MODEL_CLASS_TYPE);
+        ok(modelSrc != NULL && psMemCheckModel(modelSrc), "pmModelAlloc() returned a non-NULL pmModel");
+        modelSrc->chisq = 1.0;
+        modelSrc->nDOF = 2;
+        modelSrc->nIter = 3;
+        modelSrc->flags = PM_MODEL_STATUS_NONE;
+        modelSrc->fitRadius = 4;
+        pmModel *modelDst = pmModelCopy(modelSrc);
+        ok(modelDst != NULL && psMemCheckModel(modelDst), "pmModelCopy() returned a non-NULL pmModel");
+        ok(modelDst->chisq == 1.0, "pmModelCopy() set the pmModel->chisq member correctly");
+        ok(modelDst->nDOF == 2, "pmModelCopy() set the pmModel->nDOF member correctly");
+        ok(modelDst->nIter == 3, "pmModelCopy() set the pmModel->nIter member correctly");
+        ok(modelDst->flags == PM_MODEL_STATUS_NONE, "pmModelCopy() set the pmModel->flags member correctly");
+        ok(modelDst->fitRadius == 4, "pmModelCopy() set the pmModel->fitRadius member correctly");
+
+        psFree(modelSrc);
+        psFree(modelDst);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmModelEval() tests
+    // call pmModelEval() with NULL input pmModel parameter
+    {
+        psMemId id = psMemGetId();
+        pmModel *model = pmModelAlloc(TEST_MODEL_CLASS_TYPE);
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psF32 tmpF;
+        tmpF = pmModelEval(NULL, img, 0, 0);
+        ok(isnan(tmpF), "pmModelEval() returned NAN with NULL input pmModel parameter");
+        psFree(model);
+        psFree(img);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // psF32 pmModelEval(pmModel *model, psImage *image, psS32 col, psS32 row)
+    // call pmModelEval() with NULL input psImage parameter
+    {
+        psMemId id = psMemGetId();
+        pmModel *model = pmModelAlloc(TEST_MODEL_CLASS_TYPE);
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psF32 tmpF;
+        tmpF = pmModelEval(model, NULL, 0, 0);
+        ok(isnan(tmpF), "pmModelEval() returned NAN with NULL input psImage parameter");
+        psFree(model);
+        psFree(img);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // psF32 pmModelEval(pmModel *model, psImage *image, psS32 col, psS32 row)
+    // call pmModelEval() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmModel *model = pmModelAlloc(TEST_MODEL_CLASS_TYPE);
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        for (int i = 0 ; i < NUM_ROWS ; i++) {
+            for (int j = 0 ; j < NUM_COLS ; j++) {
+                img->data.F32[i][j] = (float) (i + j);
+	    }
+	}
+        psVector *x = psVectorAlloc(2, PS_TYPE_F32);
+        x->data.F32[0] = (float) (NUM_COLS / 2);
+        x->data.F32[1] = (float) (NUM_ROWS / 2);
+        psF32 testF = model->modelFunc (NULL, model->params, x);
+        psF32 tmpF;
+        tmpF = pmModelEval(model, img, (int) x->data.F32[0], (int) x->data.F32[1]);
+        ok(!isnan(tmpF), "pmModelEval() returned successfully");
+        ok(testF == tmpF, "pmModelEval() evaluated the model correctly");
+        psFree(model);
+        psFree(img);
+        psFree(x);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+/* XXX: This seems to have disappeared from pmModel.h
+    // ----------------------------------------------------------------------
+    // pmModelEvalWithOffset() tests
+    // psF32 pmModelEvalWithOffset(pmModel *model, psImage *image, 
+    //                             psS32 col, psS32 row, int dx, int dy)
+    // call pmModelEvalWithOffset() with NULL input pmModel parameter
+    {
+        psMemId id = psMemGetId();
+        pmModel *model = pmModelAlloc(TEST_MODEL_CLASS_TYPE);
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psF32 tmpF;
+        tmpF = pmModelEvalWithOffset(NULL, img, 0, 0, 0, 0);
+        ok(isnan(tmpF), "pmModelEvalWithOffset() returned NAN with NULL input pmModel parameter");
+        psFree(model);
+        psFree(img);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // psF32 pmModelEvalWithOffset(pmModel *model, psImage *image, 
+    //                             psS32 col, psS32 row, int dx, int dy)
+    // call pmModelEvalWithOffset() with NULL input psImage parameter
+    {
+        psMemId id = psMemGetId();
+        pmModel *model = pmModelAlloc(TEST_MODEL_CLASS_TYPE);
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psF32 tmpF;
+        tmpF = pmModelEvalWithOffset(model, NULL, 0, 0, 0, 0);
+        ok(isnan(tmpF), "pmModelEvalWithOffset() returned NAN with NULL input psImage parameter");
+        psFree(model);
+        psFree(img);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // psF32 pmModelEvalWithOffset(pmModel *model, psImage *image, psS32 col, psS32 row)
+    // call pmModelEvalWithOffset() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmModel *model = pmModelAlloc(TEST_MODEL_CLASS_TYPE);
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        for (int i = 0 ; i < NUM_ROWS ; i++) {
+            for (int j = 0 ; j < NUM_COLS ; j++) {
+                img->data.F32[i][j] = (float) (i + j);
+	    }
+	}
+        psVector *x = psVectorAlloc(2, PS_TYPE_F32);
+        x->data.F32[0] = (float) ((NUM_COLS / 2) + (NUM_COLS / 4));
+        x->data.F32[1] = (float) ((NUM_ROWS / 2) + (NUM_ROWS / 4));
+        psF32 testF = model->modelFunc (NULL, model->params, x);
+        psF32 tmpF;
+        tmpF = pmModelEvalWithOffset(model, img, (int) x->data.F32[0], (int) x->data.F32[1], NUM_COLS/4, NUM_ROWS/4);
+        ok(!isnan(tmpF), "pmModelEvalWithOffset() returned successfully");
+        ok(testF == tmpF, "pmModelEvalWithOffset() evaluated the model correctly");
+        psFree(model);
+        psFree(img);
+        psFree(x);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+*/
+
+
+    // ----------------------------------------------------------------------
+    // pmModelAdd() tests
+    // call pmModelAdd() with bad input parameters
+    {
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psImage *imgS32 = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_S32);
+        psImage *mask = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_U8);
+        for (int i = 0 ; i < NUM_ROWS ; i++) {
+            for (int j = 0 ; j < NUM_COLS ; j++) {
+                img->data.F32[i][j] = 0.0;
+                mask->data.U8[i][j] = 0;
+            }
+        }
+        pmModel *model = pmModelAlloc(pmModelClassGetType("PS_MODEL_GAUSS"));
+        psF32 *PAR = model->params->data.F32;
+        PAR[PM_PAR_XPOS] = (float) (NUM_COLS/2);
+        PAR[PM_PAR_YPOS] = (float) (NUM_ROWS/2);
+        PAR[PM_PAR_SXX] = 1.0;
+        PAR[PM_PAR_SYY] = 1.0;
+
+        // NULL psImage input parameter
+        bool rc = pmModelAdd(NULL, mask, model, PM_MODEL_OP_FUNC, 1);
+        ok(rc == false, "pmModelAdd() returned FALSE with NULL psImage input parameter");
+
+        // NULL pmModel input parameter
+        rc = pmModelAdd(img, mask, NULL, PM_MODEL_OP_FUNC, 1);
+        ok(rc == false, "pmModelAdd() returned FALSE with NULL pmModel input parameter");
+
+        // Incorrect type psImage input parameter
+        rc = pmModelAdd(imgS32, mask, model, PM_MODEL_OP_FUNC, 1);
+        ok(rc == false, "pmModelAdd() returned FALSE with Incorrect type psImage input parameter");
+
+        psFree(img);
+        psFree(imgS32);
+        psFree(mask);
+        psFree(model);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // call pmModelAdd() with acceptable parameters
+    // We only test with a single Gaussian model, with no residuals or masks.
+    // For completeness, additional tests should be added.
+    {
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psImage *mask = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_U8);
+        for (int i = 0 ; i < NUM_ROWS ; i++) {
+            for (int j = 0 ; j < NUM_COLS ; j++) {
+                img->data.F32[i][j] = 0.0;
+                mask->data.U8[i][j] = 0;
+            }
+        }
+        pmModel *model = pmModelAlloc(pmModelClassGetType("PS_MODEL_GAUSS"));
+        psF32 *PAR = model->params->data.F32;
+        PAR[PM_PAR_I0] = 5.0;
+        PAR[PM_PAR_XPOS] = 0.0;
+        PAR[PM_PAR_YPOS] = 0.0;
+        PAR[PM_PAR_XPOS] = (float) (NUM_COLS/2);
+        PAR[PM_PAR_YPOS] = (float) (NUM_ROWS/2);
+        PAR[PM_PAR_SXX] = 10.0;
+        PAR[PM_PAR_SYY] = 10.0;
+
+        bool rc = pmModelAdd(img, mask, model, PM_MODEL_OP_FUNC, 1);
+        ok(rc == true, "pmModelAdd() returned TRUE with acceptable input parameters");
+        psVector *x = psVectorAlloc(2, PS_TYPE_F32);
+        bool errorFlag = false;
+        for (int i = 0 ; i < NUM_ROWS ; i++) {
+            for (int j = 0 ; j < NUM_COLS ; j++) {
+                x->data.F32[0] = (float) j;
+                x->data.F32[1] = (float) i;
+                psF32 modF = model->modelFunc (NULL, model->params, x);
+                psF32 imgF = img->data.F32[i][j];
+                if (!TEST_FLOATS_EQUAL(modF, imgF)) {
+                    diag("ERROR: img[%d][%d] is %.2f, should be %.2f\n", i, j, img->data.F32[i][j], modF);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmModelAdd() set the image pixels correctly");
+        psFree(x);
+        psFree(img);
+        psFree(mask);
+        psFree(model);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmModelSub() tests
+    // call pmModelSub() with bad input parameters
+    {
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psImage *imgS32 = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_S32);
+        psImage *mask = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_U8);
+        for (int i = 0 ; i < NUM_ROWS ; i++) {
+            for (int j = 0 ; j < NUM_COLS ; j++) {
+                img->data.F32[i][j] = 0.0;
+                mask->data.U8[i][j] = 0;
+            }
+        }
+        pmModel *model = pmModelAlloc(pmModelClassGetType("PS_MODEL_GAUSS"));
+        psF32 *PAR = model->params->data.F32;
+        PAR[PM_PAR_XPOS] = (float) (NUM_COLS/2);
+        PAR[PM_PAR_YPOS] = (float) (NUM_ROWS/2);
+        PAR[PM_PAR_SXX] = 1.0;
+        PAR[PM_PAR_SYY] = 1.0;
+
+        // NULL psImage input parameter
+        bool rc = pmModelSub(NULL, mask, model, PM_MODEL_OP_FUNC, 1);
+        ok(rc == false, "pmModelSub() returned FALSE with NULL psImage input parameter");
+
+        // NULL pmModel input parameter
+        rc = pmModelSub(img, mask, NULL, PM_MODEL_OP_FUNC, 1);
+        ok(rc == false, "pmModelSub() returned FALSE with NULL pmModel input parameter");
+
+        // Incorrect type psImage input parameter
+        rc = pmModelSub(imgS32, mask, model, PM_MODEL_OP_FUNC, 1);
+        ok(rc == false, "pmModelSub() returned FALSE with Incorrect type psImage input parameter");
+
+        psFree(img);
+        psFree(imgS32);
+        psFree(mask);
+        psFree(model);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // call pmModelSub() with acceptable parameters
+    // We only test with a single Gaussian model, with no residuals or masks.
+    // For completeness, additional tests should be added.
+    {
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psImage *mask = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_U8);
+        for (int i = 0 ; i < NUM_ROWS ; i++) {
+            for (int j = 0 ; j < NUM_COLS ; j++) {
+                img->data.F32[i][j] = 0.0;
+                mask->data.U8[i][j] = 0;
+            }
+        }
+        pmModel *model = pmModelAlloc(pmModelClassGetType("PS_MODEL_GAUSS"));
+        psF32 *PAR = model->params->data.F32;
+        PAR[PM_PAR_I0] = 5.0;
+        PAR[PM_PAR_XPOS] = 0.0;
+        PAR[PM_PAR_YPOS] = 0.0;
+        PAR[PM_PAR_XPOS] = (float) (NUM_COLS/2);
+        PAR[PM_PAR_YPOS] = (float) (NUM_ROWS/2);
+        PAR[PM_PAR_SXX] = 10.0;
+        PAR[PM_PAR_SYY] = 10.0;
+
+        bool rc = pmModelSub(img, mask, model, PM_MODEL_OP_FUNC, 1);
+        ok(rc == true, "pmModelSub() returned TRUE with acceptable input parameters");
+        psVector *x = psVectorAlloc(2, PS_TYPE_F32);
+        bool errorFlag = false;
+        for (int i = 0 ; i < NUM_ROWS ; i++) {
+            for (int j = 0 ; j < NUM_COLS ; j++) {
+                x->data.F32[0] = (float) j;
+                x->data.F32[1] = (float) i;
+                psF32 modF = model->modelFunc (NULL, model->params, x);
+                psF32 imgF = img->data.F32[i][j];
+                if (!TEST_FLOATS_EQUAL(modF, -imgF)) {
+                    diag("ERROR: img[%d][%d] is %.2f, should be %.2f\n", i, j, img->data.F32[i][j], modF);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmModelSub() set the image pixels correctly");
+        psFree(x);
+        psFree(img);
+        psFree(mask);
+        psFree(model);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // XXX: The source code for pmModelAddWithOffset() and pmModelSubWithOffset() is
+    // almost exactly the same as the source code for pmModelAdd() and pmModelSub().
+    // We do not test them here.
+}
+
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmModelClass.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmModelClass.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmModelClass.c	(revision 42651)
@@ -0,0 +1,181 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested except pmModelClassCleanup() which is deferred
+    because there's no way to test that it frees a static variable, except
+    throug hthe memory leak tests
+*/
+
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           (8+1)
+#define TEST_NUM_COLS           (8+1)
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+#include "models/pmModel_GAUSS.c"
+#include "models/pmModel_PGAUSS.c"
+
+/******************************************************************************
+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->variance = 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(37);
+
+    // ----------------------------------------------------------------------
+    // Test pmModelClassAlloc()
+    // pmModelClass *pmModelClassAlloc (int nModels)
+    {
+        psMemId id = psMemGetId();
+        pmModelClass *modelClass = pmModelClassAlloc(4);
+        ok(modelClass != NULL && psMemCheckModelClass(modelClass), "pmModelClassAlloc() returned a non-NULL pmModelClass");
+        psFree(modelClass);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // Test pmModelClassCleanup(), pmModelClassSelect()
+    // Basically, call pmModelClassInit(), then pmModelClassCleanup(), and ensure that
+    // various default models are not there.
+    // XXX: We don't run this test because the spec changed and pmModelClassSelect() now calls
+    // pmModelClassInit().
+    if (0) {
+        psMemId id = psMemGetId();
+        pmModelClassInit();
+        pmModelClassCleanup();
+        ok(NULL == pmModelClassSelect(0), "pmModelClassCleanup(): removed PS_MODEL_GAUSS");
+        ok(NULL == pmModelClassSelect(1), "pmModelClassCleanup(): removed PS_MODEL_PGAUSS");
+        ok(NULL == pmModelClassSelect(2), "pmModelClassCleanup(): removed PS_MODEL_QGAUSS");
+        ok(NULL == pmModelClassSelect(3), "pmModelClassCleanup(): removed PS_MODEL_RGAUSS");
+        ok(NULL == pmModelClassSelect(4), "pmModelClassCleanup(): removed PS_MODEL_SERSIC");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // Test pmModelClassInit(), pmModelClassGetName()
+    // Basically, call pmModelClassCleanup(), then pmModelClassInit() and ensure that
+    // various default models are there.
+    {
+        psMemId id = psMemGetId();
+
+        pmModelClassCleanup();
+        ok(pmModelClassInit(), "pmModelClassInit() returned TRUE");
+        ok(!strcmp(pmModelClassGetName(0), "PS_MODEL_GAUSS"), "pmModelClassInit() added pmModel PS_MODEL_GAUSS");
+        ok(!strcmp(pmModelClassGetName(1), "PS_MODEL_PGAUSS"), "pmModelClassInit() added pmModel PS_MODEL_PGAUSS");
+        ok(!strcmp(pmModelClassGetName(2), "PS_MODEL_QGAUSS"), "pmModelClassInit() added pmModel PS_MODEL_QGAUSS");
+        ok(!strcmp(pmModelClassGetName(3), "PS_MODEL_RGAUSS"), "pmModelClassInit() added pmModel PS_MODEL_RGAUSS");
+        ok(!strcmp(pmModelClassGetName(4), "PS_MODEL_SERSIC"), "pmModelClassInit() added pmModel PS_MODEL_SERSIC");
+        ok(!pmModelClassInit(), "pmModelClassInit() returned FALSE (2nd time)");
+        ok(NULL == pmModelClassGetName(-1), "pmModelClassGetName(-1) returned NULL");
+        ok(NULL == pmModelClassGetName(1000), "pmModelClassGetName(1000) returned NULL");
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // Test pmModelClassGetType()
+    {
+        psMemId id = psMemGetId();
+        pmModelClassInit();
+        ok(-1 == pmModelClassGetType("BOGUS"), "pmModelClassGetType(BOGUS) returned -1");
+        ok(0 == pmModelClassGetType("PS_MODEL_GAUSS"), "pmModelClassGetType(PS_MODEL_GAUSS) successful");
+        ok(1 == pmModelClassGetType("PS_MODEL_PGAUSS"), "pmModelClassGetType(PS_MODEL_PGAUSS) successful");
+        ok(2 == pmModelClassGetType("PS_MODEL_QGAUSS"), "pmModelClassGetType(PS_MODEL_QGAUSS) successful");
+        ok(3 == pmModelClassGetType("PS_MODEL_RGAUSS"), "pmModelClassGetType(PS_MODEL_RGAUSS) successful");
+        ok(4 == pmModelClassGetType("PS_MODEL_SERSIC"), "pmModelClassGetType(PS_MODEL_SERSIC) successful");
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // Test pmModelClassParameterCount()
+    {
+        psMemId id = psMemGetId();
+        pmModelClassInit();
+        ok(0 == pmModelClassParameterCount(-1), "pmModelClassParameterCount(-1) returned 0");
+        ok(0 == pmModelClassParameterCount(1000), "pmModelClassParameterCount(1000) returned 0");
+        ok(7 == pmModelClassParameterCount(0), "pmModelClassParameterCount(0) returned 7");
+        ok(7 == pmModelClassParameterCount(1), "pmModelClassParameterCount(1) returned 7");
+        ok(8 == pmModelClassParameterCount(2), "pmModelClassParameterCount(2) returned 8");
+        ok(8 == pmModelClassParameterCount(3), "pmModelClassParameterCount(3) returned 8");
+        ok(8 == pmModelClassParameterCount(4), "pmModelClassParameterCount(4) returned 8");
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // Test pmModelClassSelect()
+    // pmModelClass *pmModelClassSelect (pmModelType type)
+    {
+        psMemId id = psMemGetId();
+        pmModelClassInit();
+        pmModelClass *model = NULL;
+        model = pmModelClassSelect(-1);
+        ok(model == NULL, "pmModelClassSelect(-1) successful");
+        model = pmModelClassSelect(1000);
+        ok(model == NULL, "pmModelClassSelect(1000) successful");
+        model = pmModelClassSelect(0);
+        ok(model != NULL && !strcmp(model->name, "PS_MODEL_GAUSS"), "pmModelClassSelect(0) successful");
+        model = pmModelClassSelect(1);
+        ok(model != NULL && !strcmp(model->name, "PS_MODEL_PGAUSS"), "pmModelClassSelect(1) successful");
+        model = pmModelClassSelect(2);
+        ok(model != NULL && !strcmp(model->name, "PS_MODEL_QGAUSS"), "pmModelClassSelect(2) successful");
+        model = pmModelClassSelect(3);
+        ok(model != NULL && !strcmp(model->name, "PS_MODEL_RGAUSS"), "pmModelClassSelect(3) successful");
+        model = pmModelClassSelect(4);
+        ok(model != NULL && !strcmp(model->name, "PS_MODEL_SERSIC"), "pmModelClassSelect(4) successful");
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // Test pmModelClassAdd()
+    // We create a new modelClass, then add it, then ensure that it exists.
+    {
+        psMemId id = psMemGetId();
+        pmModelClassInit();
+        pmModelClass *newModel = pmModelClassAlloc(1);
+        newModel->name = psStringCopy("PS_MODEL_NEW00");
+        newModel->nParams = 22;
+        pmModelClassAdd(newModel);
+        int ID = pmModelClassGetType("PS_MODEL_NEW00");
+        ok(ID != -1 && !strcmp("PS_MODEL_NEW00", pmModelClassGetName(ID)), "pmModelClassAdd() added the new model successfully");
+        psFree(newModel->name);
+        psFree(newModel);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmModelUtils.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmModelUtils.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmModelUtils.c	(revision 42651)
@@ -0,0 +1,203 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+	All functions are tested.
+*/
+
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+#define NUM_ROWS		8
+#define NUM_COLS		16
+#define TEST_MODEL_CLASS_TYPE 1
+#define TEST_FLOATS_EQUAL(X, Y) (abs(X - Y) < 0.0001)
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(46);
+
+    // ----------------------------------------------------------------------
+    // pmModelFromPSF() tests
+    // pmModel *pmModelFromPSF (pmModel *modelEXT, pmPSF *psf)
+    // call pmModelFromPSF() with NULL pmModel input parameter
+    {
+        psMemId id = psMemGetId();
+        pmModelClassInit();
+        pmModel *model = pmModelAlloc(TEST_MODEL_CLASS_TYPE);
+        pmPSFOptions *psfOptions = pmPSFOptionsAlloc();
+        psfOptions->type = pmModelClassGetType("PS_MODEL_GAUSS");
+        pmPSF *psf = pmPSFAlloc(psfOptions);
+        pmModel *tmpModel = pmModelFromPSF(NULL, psf);
+        ok(tmpModel == NULL, "pmModelFromPSF() returned NULL with NULL pmModel input parameter");
+        psFree(model);
+        psFree(psfOptions);
+        psFree(psf);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // call pmModelFromPSF() with NULL pmPSF input parameter
+    {
+        psMemId id = psMemGetId();
+        pmModelClassInit();
+        pmModel *model = pmModelAlloc(TEST_MODEL_CLASS_TYPE);
+        pmPSFOptions *psfOptions = pmPSFOptionsAlloc();
+        psfOptions->type = pmModelClassGetType("PS_MODEL_GAUSS");
+        pmPSF *psf = pmPSFAlloc(psfOptions);
+        pmModel *tmpModel = pmModelFromPSF(model, NULL);
+        ok(tmpModel == NULL, "pmModelFromPSF() returned NULL with NULL pmPSF input parameter");
+        psFree(model);
+        psFree(psfOptions);
+        psFree(psf);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // call pmModelFromPSF() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmModelClassInit();
+        pmModel *model = pmModelAlloc(TEST_MODEL_CLASS_TYPE);
+        pmPSFOptions *psfOptions = pmPSFOptionsAlloc();
+        psfOptions->type = pmModelClassGetType("PS_MODEL_GAUSS");
+        pmPSF *psf = pmPSFAlloc(psfOptions);
+        pmModel *tmpModel = pmModelFromPSF(model, psf);
+        ok(tmpModel != NULL, "pmModelFromPSF() returned non-NULL with acceptable input parameters");
+
+        pmModel *testModelPSF = pmModelAlloc(psf->type);
+        model->modelFromPSF(testModelPSF, model, psf);
+        ok(tmpModel->type == testModelPSF->type, "pmModelFromPSF() set the model->type correctly");
+        ok(tmpModel->chisq == testModelPSF->chisq, "pmModelFromPSF() set the model->chisq correctly");
+        ok(tmpModel->chisqNorm == testModelPSF->chisqNorm, "pmModelFromPSF() set the model->chisqNorm correctly");
+        ok(tmpModel->nDOF == testModelPSF->nDOF, "pmModelFromPSF() set the model->nDOF correctly");
+        ok(tmpModel->nIter == testModelPSF->nIter, "pmModelFromPSF() set the model->nIter correctly");
+        ok(tmpModel->flags == testModelPSF->flags, "pmModelFromPSF() set the model->flags correctly");
+        ok(tmpModel->fitRadius == testModelPSF->fitRadius, "pmModelFromPSF() set the model->fitRadius correctly");
+        ok(tmpModel->modelFunc == testModelPSF->modelFunc, "pmModelFromPSF() set the model->modelFunc correctly");
+        ok(tmpModel->modelFlux == testModelPSF->modelFlux, "pmModelFromPSF() set the model->modelFlux correctly");
+        ok(tmpModel->modelRadius == testModelPSF->modelRadius, "pmModelFromPSF() set the model->modelRadius correctly");
+        ok(tmpModel->modelLimits == testModelPSF->modelLimits, "pmModelFromPSF() set the model->modelLimits correctly");
+        ok(tmpModel->modelGuess == testModelPSF->modelGuess, "pmModelFromPSF() set the model->modelGuess correctly");
+        ok(tmpModel->modelFromPSF == testModelPSF->modelFromPSF, "pmModelFromPSF() set the model->modelFromPSF correctly");
+        ok(tmpModel->modelParamsFromPSF == testModelPSF->modelParamsFromPSF, "pmModelFromPSF() set the model->modelParamsFromPSF correctly");
+        ok(tmpModel->modelFitStatus == testModelPSF->modelFitStatus, "pmModelFromPSF() set the model->modelFitStatus correctly");
+
+        psFree(testModelPSF);
+        psFree(tmpModel);
+        psFree(model);
+        psFree(psfOptions);
+        psFree(psf);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmModelFromPSFforXY() tests
+    // pmModel *pmModelFromPSFforXY (pmPSF *psf, float Xo, float Yo, float Io)
+    // call pmModelFromPSFforXY() with NULL pmPSF input parameter
+    {
+        psMemId id = psMemGetId();
+        pmModelClassInit();
+        pmPSFOptions *psfOptions = pmPSFOptionsAlloc();
+        psfOptions->type = pmModelClassGetType("PS_MODEL_GAUSS");
+        pmPSF *psf = pmPSFAlloc(psfOptions);
+        pmModel *tmpModel = pmModelFromPSFforXY(NULL, 1.0, 2.0, 3.0);
+        ok(tmpModel == NULL, "pmModelFromPSFforXY() returned NULL with NULL pmPSF input parameter");
+        psFree(psfOptions);
+        psFree(psf);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // call pmModelFromPSFforXY() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmModelClassInit();
+        pmPSFOptions *psfOptions = pmPSFOptionsAlloc();
+        psfOptions->type = pmModelClassGetType("PS_MODEL_GAUSS");
+        pmPSF *psf = pmPSFAlloc(psfOptions);
+
+        pmModel *testModelPSF = pmModelAlloc (psf->type);
+        testModelPSF->modelParamsFromPSF(testModelPSF, psf, 1.0, 2.0, 3.0);
+
+        pmModel *tmpModel = pmModelFromPSFforXY(psf, 1.0, 2.0, 3.0);
+        ok(tmpModel != NULL, "pmModelFromPSFforXY() returned non-NULL with acceptable input parameters");
+
+        ok(tmpModel->type == testModelPSF->type, "pmModelFromPSF() set the model->type correctly");
+        ok(tmpModel->chisq == testModelPSF->chisq, "pmModelFromPSF() set the model->chisq correctly");
+        ok(TEST_FLOATS_EQUAL(tmpModel->chisqNorm, testModelPSF->chisqNorm), "pmModelFromPSF() set the model->chisqNorm correctly");
+        ok(tmpModel->nDOF == testModelPSF->nDOF, "pmModelFromPSF() set the model->nDOF correctly");
+        ok(tmpModel->nIter == testModelPSF->nIter, "pmModelFromPSF() set the model->nIter correctly");
+        ok(tmpModel->flags == testModelPSF->flags, "pmModelFromPSF() set the model->flags correctly");
+        ok(tmpModel->fitRadius == testModelPSF->fitRadius, "pmModelFromPSF() set the model->fitRadius correctly");
+        ok(tmpModel->modelFunc == testModelPSF->modelFunc, "pmModelFromPSF() set the model->modelFunc correctly");
+        ok(tmpModel->modelFlux == testModelPSF->modelFlux, "pmModelFromPSF() set the model->modelFlux correctly");
+        ok(tmpModel->modelRadius == testModelPSF->modelRadius, "pmModelFromPSF() set the model->modelRadius correctly");
+        ok(tmpModel->modelLimits == testModelPSF->modelLimits, "pmModelFromPSF() set the model->modelLimits correctly");
+        ok(tmpModel->modelGuess == testModelPSF->modelGuess, "pmModelFromPSF() set the model->modelGuess correctly");
+        ok(tmpModel->modelFromPSF == testModelPSF->modelFromPSF, "pmModelFromPSF() set the model->modelFromPSF correctly");
+        ok(tmpModel->modelParamsFromPSF == testModelPSF->modelParamsFromPSF, "pmModelFromPSF() set the model->modelParamsFromPSF correctly");
+        ok(tmpModel->modelFitStatus == testModelPSF->modelFitStatus, "pmModelFromPSF() set the model->modelFitStatus correctly");
+
+        psFree(tmpModel);
+        psFree(testModelPSF);
+        psFree(psfOptions);
+        psFree(psf);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmModelSetFlux() tests
+    // bool pmModelSetFlux(pmModel *model, float flux) {
+    // call pmModelSetFlux() with NULL pmPSF input parameter
+    {
+        psMemId id = psMemGetId();
+        pmModelClassInit();
+        pmModel *model = pmModelAlloc(TEST_MODEL_CLASS_TYPE);
+        bool rc = pmModelSetFlux(NULL, 1.0);
+        ok(!rc, "pmModelSetFlux(() returned FALSE with NULL pmModel input parameter");
+        psFree(model);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // call pmModelSetFlux() with acceptable input parameters
+    // XXX: We should probably test with more input values
+    {
+        psMemId id = psMemGetId();
+        pmModelClassInit();
+        pmModel *model = pmModelAlloc(TEST_MODEL_CLASS_TYPE);
+        model->params->data.F32[PM_PAR_SXX] = 1.0;
+        model->params->data.F32[PM_PAR_SYY] = 1.0;
+        model->params->data.F32[PM_PAR_SXY] = 1.0;
+
+        // Compute the test flux value
+        float tmpF = model->params->data.F32[PM_PAR_I0];
+        model->params->data.F32[PM_PAR_I0] = 1.0;
+        float testFlux = model->modelFlux (model->params);
+        testFlux = 1.0 / testFlux;    
+        model->params->data.F32[PM_PAR_I0] = tmpF;
+
+        bool rc = pmModelSetFlux(model, 1.0);
+        ok(rc, "pmModelSetFlux(() returned TRUE with acceptable input parameters");
+        ok(TEST_FLOATS_EQUAL(testFlux, model->params->data.F32[PM_PAR_I0]), "pmModelSetFlux() set the flux correctly");
+        psFree(model);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmModel_CentralPixel.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmModel_CentralPixel.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmModel_CentralPixel.c	(revision 42651)
@@ -0,0 +1,202 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "tap.h"
+#include "pstap.h"
+
+int main (int argc, char **argv)
+{
+    if (argc != 3) {
+	fprintf (stderr, "USAGE: tap_pmModel_CentralPixel (model) (set)\n");
+	exit (2);
+    }
+
+    int set = atoi(argv[2]);
+
+    psMemId id = psMemGetId();
+
+    plan_tests(240);
+
+    pmModelCP *t0 = pmModelCP_Alloc();
+    ok (t0, "allocated pmModelCP");
+    psFree (t0);
+
+    pmModelCPset *t1 = pmModelCPset_Alloc();
+    ok (t1, "allocated pmModelCPset");
+    psFree (t1);
+    
+    pmModelCPset *cpset = pmModelCP_Load (argv[1]);
+    ok (cpset, "loaded pmModelCPset from file");
+
+    ok (cpset->RmajorNitem ==  4, "correct number of Rmajor values");
+    ok (cpset->AratioNitem ==  7, "correct number of Aratio values");
+    ok (cpset->SindexNitem == 10, "correct number of Sindex values");
+    ok (cpset->images->n == 280, "correct number of CP images");
+
+    pmModelCP *cp = NULL;
+    
+    if (1) {
+	cp = pmModelCP_GetImage (cpset, 0.0, 1.0, 1.0);
+	ok (cp, "returned a cp image");
+	if (cp) {
+	    ok_float_tol (cp->Rmajor, 0.0, 0.001, "got image with correct Rmajor");
+	    ok_float_tol (cp->Aratio, 1.0, 0.001, "got image with correct Aratio");
+	    ok_float_tol (cp->Sindex, 1.0, 0.001, "got image with correct Sindex");
+	}
+
+	cp = pmModelCP_GetImage (cpset, 1.0, 1.0, 1.0);
+	ok (cp, "returned a cp image");
+	if (cp) {
+	    ok_float_tol (cp->Rmajor, 1.0, 0.001, "got image with correct Rmajor");
+	    ok_float_tol (cp->Aratio, 1.0, 0.001, "got image with correct Aratio");
+	    ok_float_tol (cp->Sindex, 1.0, 0.001, "got image with correct Sindex");
+	}
+    
+	cp = pmModelCP_GetImage (cpset, 0.0, 0.4, 1.0);
+	ok (cp, "returned a cp image");
+	if (cp) {
+	    ok_float_tol (cp->Rmajor, 0.0, 0.001, "got image with correct Rmajor");
+	    ok_float_tol (cp->Aratio, 0.4, 0.001, "got image with correct Aratio");
+	    ok_float_tol (cp->Sindex, 1.0, 0.001, "got image with correct Sindex");
+	}
+    
+	cp = pmModelCP_GetImage (cpset, 0.0, 0.4, 3.5);
+	ok (cp, "returned a cp image");
+	if (cp) {
+	    ok_float_tol (cp->Rmajor, 0.0, 0.001, "got image with correct Rmajor");
+	    ok_float_tol (cp->Aratio, 0.4, 0.001, "got image with correct Aratio");
+	    ok_float_tol (cp->Sindex, 3.5, 0.001, "got image with correct Sindex");
+	}
+    }
+    
+    float valuePixel = NAN;
+    float valueModel = NAN;
+
+    switch (set) {
+      case 0:
+	cp = pmModelCP_GetImage (cpset, 1.0, 1.0, 1.0);
+	valuePixel = pmModelCP_GetFlux (cp, 0.0, 0.0, 0.0);
+	valueModel = pmModelCP_FullSersic (0.0, 0.0, 0.0, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	fprintf (stdout, "%f / %f = %f\n", valuePixel, valueModel, valuePixel / valueModel);
+	break;
+
+      case 1:
+	for (int i = 0; i < cpset->images->n; i++) {
+	    cp = cpset->images->data[i];
+	    valuePixel = pmModelCP_GetFlux (cp, 0.0, 0.0, 0.0);
+	    valueModel = pmModelCP_FullSersic (0.0, 0.0, 0.0, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	    fprintf (stdout, "%f / %f = %f | %f  %f  %f\n", valuePixel, valueModel, valuePixel / valueModel, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	}
+	break;
+
+      case 2:
+	for (int i = 0; i < cpset->images->n; i++) {
+	    cp = cpset->images->data[i];
+	    valuePixel = pmModelCP_GetFlux (cp, 0.0, 0.0, 30.0);
+	    valueModel = pmModelCP_FullSersic (0.0, 0.0, 30.0, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	    fprintf (stdout, "%f / %f = %f | %f  %f  %f\n", valuePixel, valueModel, valuePixel / valueModel, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	}
+	break;
+
+      case 3:
+	for (int i = 0; i < cpset->images->n; i++) {
+	    cp = cpset->images->data[i];
+	    valuePixel = pmModelCP_GetFlux (cp, 0.0, 0.5, 0.0);
+	    valueModel = pmModelCP_FullSersic (0.0, 0.5, 0.0, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	    fprintf (stdout, "%f / %f = %f | %f  %f  %f\n", valuePixel, valueModel, valuePixel / valueModel, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	}
+	break;
+
+      case 4:
+	for (int i = 0; i < cpset->images->n; i++) {
+	    cp = cpset->images->data[i];
+	    valuePixel = pmModelCP_GetFlux (cp, 0.5, 0.5, 0.0);
+	    valueModel = pmModelCP_FullSersic (0.5, 0.5, 0.0, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	    fprintf (stdout, "%f / %f = %f | %f  %f  %f\n", valuePixel, valueModel, valuePixel / valueModel, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	}
+	break;
+
+      case 5:
+	for (int i = 0; i < cpset->images->n; i++) {
+	    cp = cpset->images->data[i];
+	    valuePixel = pmModelCP_GetFlux (cp, -0.5, 0.5, 30.0);
+	    valueModel = pmModelCP_FullSersic (-0.5, 0.5, 30.0, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	    fprintf (stdout, "%f / %f = %f | %f  %f  %f\n", valuePixel, valueModel, valuePixel / valueModel, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	}
+	break;
+
+      case 6:
+	for (int i = 0; i < cpset->images->n; i++) {
+	    cp = cpset->images->data[i];
+	    valuePixel = pmModelCP_GetFlux (cp, 0.0, -0.5, 0.0);
+	    valueModel = pmModelCP_FullSersic (0.0, -0.5, 0.0, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	    fprintf (stdout, "%f / %f = %f | %f  %f  %f\n", valuePixel, valueModel, valuePixel / valueModel, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	}
+	break;
+
+      case 7:
+	cp = cpset->images->data[224];
+	float delta = 1.0 / 11.0;
+	float offset = -10*delta;
+	for (float dx = offset; dx <= 1.0; dx += delta) {
+	    valuePixel = pmModelCP_GetFlux (cp, dx, 0.0, 0.0);
+	    valueModel = pmModelCP_FullSersic (dx, 0.0, 0.0, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	    fprintf (stdout, "%f / %f = %f | %f  %f  %f\n", valuePixel, valueModel, valuePixel / valueModel, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	}
+	break;
+
+      case 8:
+	cp = cpset->images->data[224];
+	valuePixel = pmModelCP_GetFlux (cp, 0.0, 0.0, 45.0);
+	valueModel = pmModelCP_FullSersic (0.0, 0.0, 45.0, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	fprintf (stdout, "%f / %f = %f | %f  %f  %f\n", valuePixel, valueModel, valuePixel / valueModel, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	break;
+
+      case 9:
+	for (int i = 0; i < cpset->images->n; i++) {
+	    cp = cpset->images->data[i];
+	    valuePixel = pmModelCP_GetFlux (cp, -0.5, -0.5, 0.0);
+	    valueModel = pmModelCP_FullSersic (-0.5, -0.5, 0.0, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	    fprintf (stdout, "%f / %f = %f | %f  %f  %f\n", valuePixel, valueModel, valuePixel / valueModel, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	}
+	break;
+      case 10:
+	for (int i = 0; i < cpset->images->n; i++) {
+	    cp = cpset->images->data[i];
+	    valuePixel = pmModelCP_GetFlux (cp, 0.5, 0.0, 0.0);
+	    valueModel = pmModelCP_FullSersic (0.5, 0.0, 0.0, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	    fprintf (stdout, "%f / %f = %f | %f  %f  %f\n", valuePixel, valueModel, valuePixel / valueModel, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	}
+	break;
+
+      case 11:
+	for (int i = 0; i < cpset->images->n; i++) {
+	    cp = cpset->images->data[i];
+	    valuePixel = pmModelCP_GetFlux (cp, -0.5, 0.0, 0.0);
+	    valueModel = pmModelCP_FullSersic (-0.5, 0.0, 0.0, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	    fprintf (stdout, "%f / %f = %f | %f  %f  %f\n", valuePixel, valueModel, valuePixel / valueModel, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	}
+	break;
+      case 12:
+	psTimerStart ("test");
+	for (int i = 0; i < cpset->images->n; i++) {
+	    cp = cpset->images->data[i];
+	    valuePixel = pmModelCP_GetFlux (cp, 0.0, 0.0, 0.0);
+	}
+	fprintf (stderr, "CP code: %f\n", psTimerMark ("test"));
+	psTimerStart ("test");
+	for (int i = 0; i < cpset->images->n; i++) {
+	    cp = cpset->images->data[i];
+	    valueModel = pmModelCP_FullSersic (0.0, 0.0, 0.0, pow(10.0, cp->Rmajor), cp->Aratio, cp->Sindex);
+	}
+	fprintf (stderr, "Full code: %f\n", psTimerMark ("test"));
+	break;
+    }
+
+    psFree (cpset);
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+
+    return exit_status();
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmModel_CentralPixel_v2.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmModel_CentralPixel_v2.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmModel_CentralPixel_v2.c	(revision 42651)
@@ -0,0 +1,52 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "tap.h"
+#include "pstap.h"
+
+int main (int argc, char **argv)
+{
+    if (argc != 8) {
+	fprintf (stderr, "USAGE: tap_pmModel_CentralPixel (model) (dx) (dy) (theta) (Reff) (Arat) (Sidx)\n");
+	exit (2);
+    }
+
+    float dx = atof(argv[2]);
+    float dy = atof(argv[3]);
+    float theta = atof(argv[4]);
+
+    float Reff = atof(argv[5]);
+    float Arat = atof(argv[6]);
+    float Sidx = atof(argv[7]);
+
+    psMemId id = psMemGetId();
+
+    plan_tests(6);
+
+    pmModelCPset *cpset = pmModelCP_Load (argv[1]);
+    ok (cpset, "loaded pmModelCPset from file");
+
+    ok (cpset->RmajorNitem ==  4, "correct number of Rmajor values");
+    ok (cpset->AratioNitem ==  7, "correct number of Aratio values");
+    ok (cpset->SindexNitem == 10, "correct number of Sindex values");
+    ok (cpset->images->n == 280, "correct number of CP images");
+
+    pmModelCP *cp = NULL;
+    
+    float valuePixel = NAN;
+    float valueModel = NAN;
+
+    cp = pmModelCP_GetImage (cpset, log10(Reff), Arat, Sidx);
+    valuePixel = pmModelCP_GetFlux (cp, dx, dy, theta);
+    valueModel = pmModelCP_FullSersic (dx, dy, theta, Reff, Arat, Sidx);
+    fprintf (stdout, "%f / %f = %f\n", valuePixel, valueModel, valuePixel / valueModel);
+
+    fprintf (stdout, "%f, %f, %f\n", cp->Rmajor, cp->Aratio, cp->Sindex);
+
+    psFree (cpset);
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+
+    return exit_status();
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmModel_SET_FWHM.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmModel_SET_FWHM.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmModel_SET_FWHM.c	(revision 42651)
@@ -0,0 +1,42 @@
+#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);
+  psTraceSetLevel("err", 5);
+  plan_tests(53);
+
+  // call pmModelAlloc() with acceptable input params
+  {
+    psMemId id = psMemGetId();
+    pmModelClassInit();
+    
+    pmModelType type = pmModelClassGetType ("PS_MODEL_PS1_V1");
+    ok(type == 3, "pmModelClassGetType returned the right value");
+    
+    pmModel *model = pmModelAlloc(type);
+    ok(model != NULL && psMemCheckModel(model), "pmModelAlloc() returned a non-NULL pmModel");
+
+    int nParams = pmModelClassParameterCount(type);
+    ok(model->params != NULL && model->params->n == nParams, "pmModelAlloc() set the pmModel->params psVector correctly");
+    ok(model->dparams != NULL && model->dparams->n == nParams, "pmModelAlloc() set the pmModel->dparams psVector correctly");
+
+    float core;
+    for (core = -1.33; core < 15.0; core += 0.33) {
+      model->params->data.F32[PM_PAR_7] = core;
+      float fwhm = model->class->modelSetFWHM(model->params, 1.0);
+      fprintf (stderr, "%f : %f\n", core, fwhm);
+    }
+    psFree(model);
+    pmModelClassCleanup();
+    ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+  }
+}
+
+
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmMoments.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmMoments.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmMoments.c	(revision 42651)
@@ -0,0 +1,46 @@
+#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->Mx == 0.0, "pmMomentsAlloc set->x correctly");
+        ok(tmpMoments->My == 0.0, "pmMomentsAlloc set->x correctly");
+        ok(tmpMoments->Mxx == 0.0, "pmMomentsAlloc set->x correctly");
+        ok(tmpMoments->Myy == 0.0, "pmMomentsAlloc set->x correctly");
+
+        ok(tmpMoments->Mxxx == 0.0, "pmMomentsAlloc set->x correctly");
+        ok(tmpMoments->Mxxy == 0.0, "pmMomentsAlloc set->x correctly");
+        ok(tmpMoments->Mxyy == 0.0, "pmMomentsAlloc set->x correctly");
+        ok(tmpMoments->Myyy == 0.0, "pmMomentsAlloc set->x correctly");
+
+        ok(tmpMoments->Mxxxx == 0.0, "pmMomentsAlloc set->x correctly");
+        ok(tmpMoments->Mxxxy == 0.0, "pmMomentsAlloc set->x correctly");
+        ok(tmpMoments->Mxxyy == 0.0, "pmMomentsAlloc set->x correctly");
+        ok(tmpMoments->Mxyyy == 0.0, "pmMomentsAlloc set->x correctly");
+        ok(tmpMoments->Myyyy == 0.0, "pmMomentsAlloc set->x correctly");
+
+        ok(tmpMoments->Sum == 0.0, "pmMomentsAlloc set->Sy correctly");
+        ok(tmpMoments->Peak == 0.0, "pmMomentsAlloc set->Sxy correctly");
+        ok(tmpMoments->Sky == 0.0, "pmMomentsAlloc set->Sum correctly");
+        ok(tmpMoments->dSky == 0.0, "pmMomentsAlloc set->Peak correctly");
+        ok(tmpMoments->SN == 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_branches/psModules.20240412/test/objects/tap_pmPSF.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmPSF.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmPSF.c	(revision 42651)
@@ -0,0 +1,326 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/*
+Tested:
+    pmPSFOptions *pmPSFOptionsAlloc()
+    pmPSF *pmPSFAlloc (pmPSFOptions *options)
+    bool psMemCheckPSF(psPtr ptr)
+Must test:
+    double pmPSF_SXYfromModel (psF32 *modelPar)
+    double pmPSF_SXYtoModel (psF32 *fittedPar)
+    bool pmGrowthCurveGenerate (pmReadout *readout, pmPSF *psf, bool ignore, psMas!
+    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)
+*/
+
+
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         10
+#define TEST_FLOATS_EQUAL(X, Y) (abs((X) - (Y)) < 0.0001)
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(83);
+
+    // ----------------------------------------------------------------------
+    // pmPSFOptionsAlloc() tests
+    // pmPSFOptions *pmPSFOptionsAlloc()
+    {
+        psMemId id = psMemGetId();
+        pmPSFOptions *psfOptions = pmPSFOptionsAlloc();
+        ok(psfOptions != NULL && psMemCheckPSFOptions(psfOptions), "pmPSFOptionsAlloc() returned non-NULL");
+        ok(psfOptions->type == 0, "pmPSFOptionsAlloc set pmPSFOptions->type ");
+        ok(psfOptions->stats == NULL, "pmPSFOptionsAlloc set pmPSFOptions->stats ");
+        ok(psfOptions->psfTrendMode == PM_TREND_NONE, "pmPSFOptionsAlloc set pmPSFOptions->psfTrendMode ");
+        ok(psfOptions->psfTrendNx == 0, "pmPSFOptionsAlloc set pmPSFOptions->psfTrendNx ");
+        ok(psfOptions->psfTrendNy == 0, "pmPSFOptionsAlloc set pmPSFOptions->psfTrendNy ");
+        ok(psfOptions->psfFieldNx == 0, "pmPSFOptionsAlloc set pmPSFOptions->psfFieldNx ");
+        ok(psfOptions->psfFieldNy == 0, "pmPSFOptionsAlloc set pmPSFOptions->psfFieldNy ");
+        ok(psfOptions->psfFieldXo == 0, "pmPSFOptionsAlloc set pmPSFOptions->psfFieldXo ");
+        ok(psfOptions->psfFieldYo == 0, "pmPSFOptionsAlloc set pmPSFOptions->psfFieldYo ");
+        ok(psfOptions->poissonErrorsPhotLMM == true, "pmPSFOptionsAlloc set pmPSFOptions->poissonErrorsPhotLMM ");
+        ok(psfOptions->poissonErrorsPhotLin == false, "pmPSFOptionsAlloc set pmPSFOptions->poissonErrorsPhotLin ");
+        ok(psfOptions->poissonErrorsParams  == true, "pmPSFOptionsAlloc set pmPSFOptions->poissonErrorsParams ");
+        psFree(psfOptions);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmPSFAlloc() tests
+    // call pmPSFAlloc() with NULL input parameters
+    #define TEST_POISSON_ERRORS true
+    {
+        psMemId id = psMemGetId();
+        pmPSF *psf = pmPSFAlloc(NULL);
+        ok(psf == NULL, "pmPSFAlloc() returned NULL with NULL input parameters");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // call pmPSFAlloc() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmPSFOptions *psfOptions = pmPSFOptionsAlloc();
+        psfOptions->psfTrendNx = 1;
+        psfOptions->psfTrendNy = 2;
+        psfOptions->psfFieldNx = 3;
+        psfOptions->psfFieldNy = 4;
+        psfOptions->psfFieldXo = 5;
+        psfOptions->psfFieldYo = 6;
+        pmModelClassInit();
+        psfOptions->type = pmModelClassGetType("PS_MODEL_GAUSS");
+        pmPSF *psf = pmPSFAlloc(psfOptions);
+        ok(psf != NULL && psMemCheckPSF(psf), "pmPSFAlloc() returned non-NULL");
+        ok(psf->type == psfOptions->type, "pmPSFAlloc() set the pmPSF->type correctly");
+        ok(psf->chisq == 0.0, "pmPSFAlloc() set the pmPSF->chisq correctly");
+        ok(psf->ApResid == 0.0, "pmPSFAlloc() set the pmPSF->ApResid correctly");
+        ok(psf->dApResid == 0.0, "pmPSFAlloc() set the pmPSF->dApResid correctly");
+        ok(psf->skyBias == 0.0, "pmPSFAlloc() set the pmPSF->skyBias correctly");
+        ok(psf->skySat == 0.0, "pmPSFAlloc() set the pmPSF->skySat correctly");
+        ok(psf->nPSFstars == 0, "pmPSFAlloc() set the pmPSF->nPSFstars correctly");
+        ok(psf->nApResid == 0, "pmPSFAlloc() set the pmPSF->nApResid correctly");
+        int Nparams = pmModelClassParameterCount(psfOptions->type);
+        ok(psf->params != NULL &&
+           psMemCheckArray(psf->params) &&
+           psf->params->n == Nparams, "pmPSFAlloc() set the pmPSF->params correctly");
+        ok(psf->poissonErrorsPhotLMM == psfOptions->poissonErrorsPhotLMM, "pmPSFAlloc() set the pmPSF->poissonErrorsPhotLMM");
+        ok(psf->poissonErrorsPhotLin == psfOptions->poissonErrorsPhotLin, "pmPSFAlloc() set the pmPSF->poissonErrorsPhotLin");
+        ok(psf->poissonErrorsParams == psfOptions->poissonErrorsParams, "pmPSFAlloc() set the pmPSF->poissonErrorsParams");
+        ok(psf->ApTrend == NULL, "pmPSFAlloc() set the pmPSF->ApTrend");
+        ok(psf->FluxScale == NULL, "pmPSFAlloc() set the pmPSF->FluxScale");
+        ok(psf->growth == NULL, "pmPSFAlloc() set the pmPSF->growth");
+        ok(psf->residuals == NULL, "pmPSFAlloc() set the pmPSF->residuals");
+        ok(psf->psfTrendMode == psfOptions->psfTrendMode, "pmPSFAlloc() set the pmPSF->psfTrendMode");
+        ok(psf->trendNx == psfOptions->psfTrendNx, "pmPSFAlloc() set the pmPSF->trendNx (%d %d)", psf->trendNx, psfOptions->psfTrendNx);
+        ok(psf->trendNy == psfOptions->psfTrendNy, "pmPSFAlloc() set the pmPSF->trendNy (%d %d)", psf->trendNy, psfOptions->psfTrendNy);
+        ok(psf->fieldNx == psfOptions->psfFieldNx, "pmPSFAlloc() set the pmPSF->fieldNx");
+        ok(psf->fieldNy == psfOptions->psfFieldNy, "pmPSFAlloc() set the pmPSF->fieldNy");
+        ok(psf->fieldXo == psfOptions->psfFieldXo, "pmPSFAlloc() set the pmPSF->fieldXo");
+        ok(psf->fieldYo == psfOptions->psfFieldYo, "pmPSFAlloc() set the pmPSF->fieldYo");
+        ok(psf->ApTrend == NULL, "pmPSFAlloc() set the pmPSF->ApTrend correctly");
+        ok(psf->FluxScale == NULL, "pmPSFAlloc() set the pmPSF->FluxScale correctly");
+
+        if (psf->poissonErrorsPhotLMM) {
+            ok(psf->ChiTrend->nX == 1, "pmPSFAlloc() set the pmPSF->ChiTrend correctly");
+	} else {
+            ok(psf->ChiTrend->nX == 2, "pmPSFAlloc() set the pmPSF->ChiTrend correctly");
+	}
+        ok(psf->growth == NULL, "pmPSFAlloc() set the pmPSF->growth correctly");
+        ok(psf->residuals == NULL, "pmPSFAlloc() set the pmPSF->residuals correctly");
+        ok(psf->params != NULL && psMemCheckArray(psf->params) && psf->params->n == Nparams, 
+           "pmPSFAlloc() set the pmPSF->params psVector correctly");
+
+        pmModelClassCleanup();
+        psFree(psf);
+        psFree(psfOptions);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmPSF_SXYfromModel() tests
+    // Call pmPSF_SXYfromModel() with NULL input parameters
+    {
+        psMemId id = psMemGetId();
+        double tmpD = pmPSF_SXYfromModel(NULL);
+        ok(isnan(tmpD), "pmPSF_SXYfromModel() returned NULL with NULL input parameters");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPSF_SXYfromModel() with NULL input parameters
+    {
+        psMemId id = psMemGetId();
+        psF32 modelPar[20];
+        modelPar[PM_PAR_SXX] = 2.0;
+        modelPar[PM_PAR_SYY] = 3.0;
+        modelPar[PM_PAR_SXY] = 5.0;
+        double SXX = modelPar[PM_PAR_SXX];
+        double SYY = modelPar[PM_PAR_SYY];
+        double SXY = modelPar[PM_PAR_SXY];
+        psF32 verF = SXY / PS_SQR(1.0 / PS_SQR(SXX) + 1.0 / PS_SQR(SYY));
+        psF32 testF = pmPSF_SXYfromModel(modelPar);
+        ok(verF == testF, "pmPSF_SXYfromModel() calculated correctly");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmPSF_SXYtoModel() tests
+    // Call pmPSF_SXYtoModel() with NULL input parameters
+    {
+        psMemId id = psMemGetId();
+        double tmpD = pmPSF_SXYtoModel(NULL);
+        ok(isnan(tmpD), "pmPSF_SXYtoModel() returned NULL with NULL input parameters");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPSF_SXYtoModel() with NULL input parameters
+    {
+        psMemId id = psMemGetId();
+        psF32 fittedPar[20];
+        fittedPar[PM_PAR_SXX] = 2.0;
+        fittedPar[PM_PAR_SYY] = 3.0;
+        fittedPar[PM_PAR_SXY] = 5.0;
+        double SXX = fittedPar[PM_PAR_SXX];
+        double SYY = fittedPar[PM_PAR_SYY];
+        double fit = fittedPar[PM_PAR_SXY];
+        double verF = fit * PS_SQR(1.0 / PS_SQR(SXX) + 1.0 / PS_SQR(SYY));
+        psF32 testF = pmPSF_SXYtoModel(fittedPar);
+        ok(TEST_FLOATS_EQUAL(verF, testF), "pmPSF_SXYtoModel() calculated correctly (%f %f)", verF, testF);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmPSF_FitToModel() tests
+    // Call pmPSF_FitToModel() with NULL input parameters
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmPSF_FitToModel(NULL, 0.0);
+        ok(rc == false, "pmPSF_FitToModel() returned NULL with NULL input parameters");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPSF_FitToModel() with NULL input parameters
+    {
+        #define MIN_MINOR_AXIS 1.0
+        psMemId id = psMemGetId();
+        psF32 origFittedPar[3], testFittedPar[3];
+        psEllipsePol pol;
+        pol.e0 = origFittedPar[PM_PAR_E0] = testFittedPar[PM_PAR_E0] = 2.0;
+        pol.e1 = origFittedPar[PM_PAR_E1] = testFittedPar[PM_PAR_E1] = 3.0;
+        pol.e2 = origFittedPar[PM_PAR_E2] = testFittedPar[PM_PAR_E2] = 5.0;
+        ok(pmPSF_FitToModel(testFittedPar, MIN_MINOR_AXIS), "pmPSF_FitToModel() returned TRUE with acceptable input parameters");
+
+        psEllipseAxes axes;
+        psEllipsePolToAxes(pol, MIN_MINOR_AXIS);
+        psEllipseShape shape = psEllipseAxesToShape(axes);
+
+        ok(TEST_FLOATS_EQUAL(testFittedPar[PM_PAR_SXX], shape.sx * M_SQRT2),
+          "pmPSF_FitToModel() set fittedPar[PM_PAR_SXX] correctly");
+        ok(TEST_FLOATS_EQUAL(testFittedPar[PM_PAR_SYY], shape.sy * M_SQRT2),
+          "pmPSF_FitToModel() set fittedPar[PM_PAR_SYY] correctly");
+        ok(TEST_FLOATS_EQUAL(testFittedPar[PM_PAR_SXY], shape.sxy),
+          "pmPSF_FitToModel() set fittedPar[PM_PAR_SXY] correctly");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmPSF_ModelToFit() tests
+    // psEllipsePol pmPSF_ModelToFit (psF32 *modelPar)
+    // Call pmPSF_ModelToFit() with NULL input parameters
+    {
+        psMemId id = psMemGetId();
+        psEllipsePol pol = pmPSF_ModelToFit(NULL);
+        ok(isnan(pol.e0), "pmPSF_ModelToFit() returned NULL (psEllipsePol) with NULL input parameters");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPSF_ModelToFit() with NULL input parameters
+    {
+        #define MIN_MINOR_AXIS 1.0
+        psMemId id = psMemGetId();
+        psF32 modelPar[3];
+        modelPar[PM_PAR_SXX] = 2.0;
+        modelPar[PM_PAR_SYY] = 3.0;
+        modelPar[PM_PAR_SXY] = 5.0;
+
+        psEllipsePol pol = pmPSF_ModelToFit(modelPar);
+        ok(!isnan(pol.e0), "pmPSF_ModelToFit() returned TRUE with acceptable input parameters");
+
+        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 actPol = psEllipseShapeToPol(shape);
+        ok(TEST_FLOATS_EQUAL(pol.e0, actPol.e0), "pmPSF_ModelToFit() set psEllipsePol.e0 correctly");
+        ok(TEST_FLOATS_EQUAL(pol.e1, actPol.e1), "pmPSF_ModelToFit() set psEllipsePol.e1 correctly");
+        ok(TEST_FLOATS_EQUAL(pol.e2, actPol.e2), "pmPSF_ModelToFit() set psEllipsePol.e2 correctly");
+
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmPSF_ModelToAxes() tests
+    // psEllipseAxes pmPSF_ModelToAxes (psF32 *modelPar, double maxAR)
+    // Call pmPSF_ModelToAxes() with NULL input parameters
+    {
+        psMemId id = psMemGetId();
+        psEllipseAxes axes = pmPSF_ModelToAxes(NULL, 1.0);
+        ok(isnan(axes.major), "pmPSF_ModelToAxes() returned NULL (psEllipseAxes) with NULL input parameters");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPSF_ModelToAxes() with NULL input parameters
+    {
+        #define MAX_AX 1.0
+        psMemId id = psMemGetId();
+        psF32 modelPar[3];
+        modelPar[PM_PAR_SXX] = 2.0;
+        modelPar[PM_PAR_SYY] = 3.0;
+        modelPar[PM_PAR_SXY] = 5.0;
+
+        psEllipseShape shape;
+        shape.sx  = modelPar[PM_PAR_SXX] / M_SQRT2;
+        shape.sy  = modelPar[PM_PAR_SYY] / M_SQRT2;
+        shape.sxy = modelPar[PM_PAR_SXY];
+        psEllipseAxes axes = psEllipseShapeToAxes (shape, MAX_AX);
+
+        psEllipseAxes actAxes = pmPSF_ModelToAxes(modelPar, MAX_AX);
+        ok(!isnan(actAxes.major), "pmPSF_ModelToAxes() returned TRUE with acceptable input parameters");
+        ok(TEST_FLOATS_EQUAL(actAxes.major, axes.major), "pmPSF_ModelToAxes() set psEllipseAxes.major correctly");
+        ok(TEST_FLOATS_EQUAL(actAxes.minor, axes.minor), "pmPSF_ModelToAxes() set psEllipseAxes.minor correctly");
+        ok(TEST_FLOATS_EQUAL(actAxes.theta, axes.theta), "pmPSF_ModelToAxes() set psEllipseAxes.theta correctly");
+
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmPSF_AxesToModel() tests
+    // bool pmPSF_AxesToModel (psF32 *modelPar, psEllipseAxes axes)
+    // Call pmPSF_AxesToModel() with NULL input parameters
+    {
+        psMemId id = psMemGetId();
+        psEllipseAxes axes;
+        bool rc = pmPSF_AxesToModel(NULL, axes);
+        ok(rc == false, "pmPSF_AxesToModel() returned NULL with NULL input parameters");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPSF_AxesToModel() with NULL input parameters
+    {
+        #define MIN_MINOR_AXIS 1.0
+        psMemId id = psMemGetId();
+        psF32 modelPar[3];
+        psEllipseAxes axes;
+        axes.major = 2.0;
+        axes.minor = 3.0;
+        axes.theta = 5.0;
+        ok(pmPSF_AxesToModel(modelPar, axes), "pmPSF_AxesToModel() returned TRUE with acceptable input parameters");
+        psEllipseShape shape = psEllipseAxesToShape(axes);
+        ok(TEST_FLOATS_EQUAL(modelPar[PM_PAR_SXX], shape.sx * M_SQRT2), "pmPSF_AxesToModel() set modelPar[PM_PAR_SXX] correctly");
+        ok(TEST_FLOATS_EQUAL(modelPar[PM_PAR_SYY], shape.sy * M_SQRT2), "pmPSF_AxesToModel() set modelPar[PM_PAR_SYY] correctly");
+        ok(TEST_FLOATS_EQUAL(modelPar[PM_PAR_SXY], shape.sxy), "pmPSF_AxesToModel() set modelPar[PM_PAR_SXY] correctly");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmPSF_IO.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmPSF_IO.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmPSF_IO.c	(revision 42651)
@@ -0,0 +1,467 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS
+    Uder construction.  Only lightly tested so far.
+*/
+
+#define TEST_FLOATS_EQUAL(X, Y) (abs(X - Y) < 0.0001)
+#define NUM_MODELS		5
+
+#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->variance = 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] = psMemDecrRefCounter((psPtr) generateSimpleReadout(cell));
+    }
+
+    // First try to read data from ../dataFiles, then try dataFiles.
+    bool rc = pmConfigFileRead(&cell->hdu->format, "../dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&cell->hdu->format, "dataFiles/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->variances = 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->variances->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->variances->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);
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = psMemDecrRefCounter((psPtr) 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, NULL);
+    fpa->hdu = pmHDUAlloc("cellExtName");
+    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);
+    if (camera != NULL) {
+        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);
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = psMemDecrRefCounter((psPtr) generateSimpleChip(fpa));
+    }
+    pmConceptsBlankFPA(fpa);
+    return(fpa);
+}
+
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(28);
+
+
+    // ----------------------------------------------------------------------
+    // pmPSFmodelCheckDataStatusForView() tests
+    // bool pmPSFmodelCheckDataStatusForView (const pmFPAview *view, const pmFPAfile *file)
+    // Call pmPSFmodelCheckDataStatusForView() with NULL pmFPAview input parameter
+    if (1) {
+        psMemId id = psMemGetId();
+
+        pmFPAview *view = pmFPAviewAlloc(32);
+        pmFPAfile *file = pmFPAfileAlloc();
+        bool rc = pmPSFmodelCheckDataStatusForView(NULL, file);
+        ok(rc == false, "pmPSFmodelCheckDataStatusForView() returned FALSE with NULL pmFPAview input parameter");
+        psFree(view);
+        psFree(file);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPSFmodelCheckDataStatusForView() with NULL pmFPAfile input parameter
+    if (1) {
+        psMemId id = psMemGetId();
+
+        pmFPAview *view = pmFPAviewAlloc(32);
+        pmFPAfile *file = pmFPAfileAlloc();
+        bool rc = pmPSFmodelCheckDataStatusForView(view, NULL);
+        ok(rc == false, "pmPSFmodelCheckDataStatusForView() returned FALSE with NULL pmFPAfile input parameter");
+        psFree(view);
+        psFree(file);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPSFmodelCheckDataStatusForView() with acceptable input parameters
+    if (1) {
+        psMemId id = psMemGetId();
+
+        pmFPAview *view = pmFPAviewAlloc(32);
+        view->chip = -1;
+        view->cell = -1;
+        pmFPAfile *file = pmFPAfileAlloc();
+        psMetadata *camera = psMetadataAlloc();
+        file->fpa = generateSimpleFPA(camera);
+        bool rc = pmPSFmodelCheckDataStatusForView(view, file);
+        ok(rc == false, "pmPSFmodelCheckDataStatusForView() returned FALSE acceptable input parameters, but no PSPHOT.PSF");
+
+        // Add PSPHOT.PSF to chip->analysis and call pmPSFmodelCheckDataStatusForChip()
+        pmChip *chip = file->fpa->chips->data[0];
+        psVector *junk = psVectorAlloc(10, PS_TYPE_F32);
+        bool rc2 = psMetadataAddPtr(chip->analysis, PS_LIST_HEAD, "PSPHOT.PSF", PS_DATA_VECTOR, NULL, junk);
+        ok(rc2 == true, "added PSPHOT.PSF metadata to chip->analysis");
+        rc = pmPSFmodelCheckDataStatusForView(view, file);
+        ok(rc == true, "pmPSFmodelCheckDataStatusForView() returned TRUE acceptable input parameters");
+
+        psFree(view);
+        psFree(file->fpa);
+        file->fpa = NULL;
+        psFree(file);
+        psFree(camera);
+        psFree(junk);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmPSFmodelCheckDataStatusForFPA() tests
+    // bool pmPSFmodelCheckDataStatusForFPA (const pmFPA *fpa)
+    // Call pmPSFmodelCheckDataStatusForFPA() with NULL pmFPA input parameter
+    if (1) {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA *fpa = generateSimpleFPA(camera);
+        bool rc = pmPSFmodelCheckDataStatusForFPA(NULL);
+        ok(rc == false, "pmPSFmodelCheckDataStatusForFPA() returned FALSE with NULL pmFPA input parameter");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPSFmodelCheckDataStatusForFPA() with NULL pmFPA->chips input parameter
+    if (1) {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA *fpa = generateSimpleFPA(camera);
+        for (int i = 0 ; i < fpa->chips->n ; i++) {
+            psFree(fpa->chips->data[i]);
+	}
+        bool rc = pmPSFmodelCheckDataStatusForFPA(NULL);
+        ok(rc == false, "pmPSFmodelCheckDataStatusForFPA() returned FALSE with NULL pmFPA->chips input parameter");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPSFmodelCheckDataStatusForFPA() with acceptable input parameters
+    if (1) {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA *fpa = generateSimpleFPA(camera);
+        bool rc = pmPSFmodelCheckDataStatusForFPA(fpa);
+        ok(rc == false, "pmPSFmodelCheckDataStatusForFPA() returned FALSE with acceptable input parameters, but no PSPHOT.PSF");
+
+        // Add PSPHOT.PSF to chip->analysis and call pmPSFmodelCheckDataStatusForChip()
+        psVector *junk = psVectorAlloc(10, PS_TYPE_F32);
+        pmChip *chip = fpa->chips->data[0];
+        bool rc2 = psMetadataAddPtr(chip->analysis, PS_LIST_HEAD, "PSPHOT.PSF", PS_DATA_VECTOR, NULL, junk);
+        ok(rc2 == true, "added PSPHOT.PSF metadata to chip->analysis");
+        rc = pmPSFmodelCheckDataStatusForFPA(fpa);
+        ok(rc == true, "pmPSFmodelCheckDataStatusForFPA() returned TRUE with acceptable input parameters");
+
+        psFree(fpa);
+        psFree(camera);
+        psFree(junk);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmPSFmodelCheckDataStatusForChip() tests
+    // bool pmPSFmodelCheckDataStatusForChip (const pmChip *chip)
+    // Call pmPSFmodelCheckDataStatusForChip() with NULL pmChip input parameter
+    if (1) {
+        psMemId id = psMemGetId();
+        bool rc = pmPSFmodelCheckDataStatusForChip(NULL);
+        ok(rc == false, "pmPSFmodelCheckDataStatusForChip() returned FALSE with NULL pmChip input parameter");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPSFmodelCheckDataStatusForChip() with acceptable input parameters
+    if (1) {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA *fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        bool rc = pmPSFmodelCheckDataStatusForChip(chip);
+        ok(rc == false, "pmPSFmodelCheckDataStatusForChip() returned false with acceptable pmChip, but no PSPHOT.PSF");
+
+        // Add PSPHOT.PSF to chip->analysis and call pmPSFmodelCheckDataStatusForChip()
+        psVector *junk = psVectorAlloc(10, PS_TYPE_F32);
+        bool rc2 = psMetadataAddPtr(chip->analysis, PS_LIST_HEAD, "PSPHOT.PSF", PS_DATA_VECTOR, NULL, junk);
+        ok(rc2 == true, "added PSPHOT.PSF metadata to chip->analysis");
+        rc = pmPSFmodelCheckDataStatusForChip(chip);
+        ok(rc == true, "pmPSFmodelCheckDataStatusForChip() returned TRUE with acceptable input parameters");
+    
+        psFree(fpa);
+        psFree(camera);
+        psFree(junk);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmPSFmodelWrite() tests
+    // bool pmPSFmodelWrite (psMetadata *analysis, const pmFPAview *view,
+    //                       pmFPAfile *file, const pmConfig *config)
+    // Call pmPSFmodelWrite() with NULL pmFPAview input parameter
+    if (1) {
+        psMemId id = psMemGetId();
+        pmFPAview *view = pmFPAviewAlloc(32);
+        pmFPAfile *file = pmFPAfileAlloc();
+        psMetadata *camera = psMetadataAlloc();
+        file->fpa = generateSimpleFPA(camera);
+        psMetadata *analysis = psMetadataAlloc();
+        pmConfig *config = pmConfigAlloc();
+
+        bool rc = pmPSFmodelWrite(analysis, NULL, file, config);
+        ok(rc == false, "pmPSFmodelWrite() returned FALSE with NULL pmFPAview input parameter");
+
+        psFree(view);
+        psFree(file->fpa);
+        file->fpa = NULL;
+        psFree(file);
+        psFree(camera);
+        psFree(analysis);
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPSFmodelWrite() with NULL pmFPAfile input parameter
+    if (1) {
+        psMemId id = psMemGetId();
+        pmFPAview *view = pmFPAviewAlloc(32);
+        pmFPAfile *file = pmFPAfileAlloc();
+        psMetadata *camera = psMetadataAlloc();
+        file->fpa = generateSimpleFPA(camera);
+        psMetadata *analysis = psMetadataAlloc();
+        pmConfig *config = pmConfigAlloc();
+
+        bool rc = pmPSFmodelWrite(analysis, view, NULL, config);
+        ok(rc == false, "pmPSFmodelWrite() returned FALSE with NULL pmFPAfile input parameter");
+
+        psFree(view);
+        psFree(file->fpa);
+        file->fpa = NULL;
+        psFree(file);
+        psFree(camera);
+        psFree(analysis);
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPSFmodelWrite() with NULL pmFPAfile->file input parameter
+    if (1) {
+        psMemId id = psMemGetId();
+        pmFPAview *view = pmFPAviewAlloc(32);
+        pmFPAfile *file = pmFPAfileAlloc();
+        psMetadata *analysis = psMetadataAlloc();
+        pmConfig *config = pmConfigAlloc();
+
+        bool rc = pmPSFmodelWrite(analysis, view, NULL, config);
+        ok(rc == false, "pmPSFmodelWrite() returned FALSE with NULL pmFPAfile->fpa input parameter");
+
+        psFree(view);
+        psFree(file);
+        psFree(analysis);
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPSFmodelWrite() with acceptable input parameters
+    // XXX: This is currently being coded.  It does not work.
+    if (0) {
+        psMemId id = psMemGetId();
+        pmFPAview *view = pmFPAviewAlloc(32);
+        pmFPAfile *file = pmFPAfileAlloc();
+        psMetadata *camera = psMetadataAlloc();
+        file->fpa = generateSimpleFPA(camera);
+        pmConfigFileRead(&file->camera, "../dataFiles/camera0/camera.config", "CAMERA");
+        psMetadataPrint(stdout, file->camera, 0);
+        psMetadata *menu = psMetadataLookupMetadata(NULL, file->camera, "EXTNAME.RULES");
+        if (!menu) {
+            printf("NOTE: missing EXTNAME.RULES in camera.config\n");
+            exit(1);
+        }
+
+
+        psMetadata *analysis = psMetadataAlloc();
+        pmConfig *config = pmConfigAlloc();
+
+        // XXX: I failed here and then moved on.
+        /*
+        The function reads the PSPHOT recipes and requires that it contains a
+	PSPHOT key in the metadata.  I'm not sure how to do that, but I could
+	have hacked around it.  Then, however, it required several keys in
+	that metadata like PSF.CLUMP.X, PSF.CLUMP.Y, PSF.CLUMP.DX,
+	PSF.CLUMP.DY.  I could also put them in there, but I just gave up.
+	What should I do here?  I could easily create a metadata file with all
+	those metadata keys, but I'd rather use an existing one.
+        */
+
+        if (1) {
+            psMetadata *junk = psMetadataAlloc();
+            bool rc0 = pmConfigFileRead(&junk, "../dataFiles/recipes/psphot.config", "SAVE.PSF");
+            if (!rc0) {
+                rc0 = pmConfigFileRead(&junk, "dataFiles/recipes/psphot.config", "SAVE.PSF");
+	    }
+            ok(rc0, "Successfully read the PSPHOT recipe file");
+//          psMetadata *recipe = psMetadataLookupPtr(&rc0, junk, "SAVE.OUTPUT");
+            bool rc2 = psMetadataLookupBool(&rc0, junk, "SAVE.OUTPUT");
+            printf("rc0 is %d\n", (int) rc0);
+            printf("rc2 is %d\n", (int) rc2);
+//          if (recipe == NULL) printf("ERROR: recipe is NULL\n");
+            psMetadataPrint(stdout, junk, 0);
+            psFree(junk);
+	}
+        if (config->recipes == NULL) printf("COOL: config->recipes is NULL");
+        bool rc0 = pmConfigFileRead(&config->recipes, "../dataFiles/camera0/recipes.config", "PSPHOT");
+        if (!rc0) {
+            rc0 = pmConfigFileRead(&config->recipes, "dataFiles/camera0/recipes.config", "PSPHOT");
+	}
+        ok(rc0, "Successfully read the PSPHOT recipe file");
+psMetadataPrint(stdout, config->recipes, 0);
+
+        if (config->recipes == NULL) printf("FUCK: config->recipes is NULL");
+        psMetadata *recipe = psMetadataLookupPtr(NULL, config->recipes, "PSPHOT");
+        if (!recipe) {
+            printf("FUCK: missing recipe %s\n", "PSPHOT");
+            exit(1);
+        }
+
+
+        bool rc = pmPSFmodelWrite(analysis, view, file, config);
+        ok(rc == true, "pmPSFmodelWrite() returned TRUE with acceptable input parameters");
+
+        psFree(view);
+        psFree(file->fpa);
+        file->fpa = NULL;
+        psFree(file);
+        psFree(camera);
+        psFree(analysis);
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmPSFtoMetadata.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmPSFtoMetadata.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmPSFtoMetadata.c	(revision 42651)
@@ -0,0 +1,52 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    Under construction: 10% complete.
+*/
+
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           (8)
+#define TEST_NUM_COLS           (16)
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+#define TEST_FLOATS_EQUAL(X, Y) (abs(X - Y) < 0.0001)
+#define NUM_SOURCES		100
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(1);
+
+
+    // ----------------------------------------------------------------------
+    // pmPSFtoMetadata() tests
+    // psMetadata *pmPSFtoMetadata (psMetadata *metadata, pmPSF *psf)
+    // Call pmPSFtoMetadata() with NULL psPSF input parameter
+    if (1) {
+        psMemId id = psMemGetId();
+        psMetadata *metadata = psMetadataAlloc();
+        pmPSFOptions *psfOptions = pmPSFOptionsAlloc();
+        psfOptions->psfTrendNx = 1;
+        psfOptions->psfTrendNy = 2;
+        psfOptions->psfFieldNx = 3;
+        psfOptions->psfFieldNy = 4;
+        psfOptions->psfFieldXo = 5;
+        psfOptions->psfFieldYo = 6;
+        pmModelClassInit();
+        psfOptions->type = pmModelClassGetType("PS_MODEL_GAUSS");
+        pmPSF *psf = pmPSFAlloc(psfOptions);
+        psMetadata *meta2 = pmPSFtoMetadata(metadata, NULL);
+        ok(meta2 == NULL, "pmPSFtoMetadata() returned NULL with NULL psPSF input parameter");
+        psFree(metadata);
+        psFree(psfOptions);
+        psFree(psf);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmPeaks.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmPeaks.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmPeaks.c	(revision 42651)
@@ -0,0 +1,652 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested.
+        pmPeaksInImage(): Must debug tests for small images (1-by-1, N-by-1, 1-by-N)
+*/
+
+#define TST01_VECTOR_LENGTH 10
+#define NUM_ROWS 10
+#define NUM_COLS 10
+#define TST02_NUM_ROWS 5
+#define TST02_NUM_COLS 5
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+/******************************************************************************
+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->detValue, 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->detValue, 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->detValue);
+                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);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(69);
+
+
+    // ------------------------------------------------------------------------
+    // 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->detValue == 3.0, "pmPeakAlloc() set pmPeak->detValue");
+        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");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // 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");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmPeakSortBySN() tests
+    // int pmPeakSortBySN (const void **a, const void **b)
+    // Call pmPeakSortBySN() with acceptable input parameters.
+    // XXX: We don't test with NULL input parameters since this functions has no PS_ASSERTS to protect
+    // against that.
+    {
+        psMemId id = psMemGetId();
+        pmPeak **peak1 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak1 = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        (*peak1)->SN = 10.0;
+        pmPeak **peak2 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak2 = pmPeakAlloc(3, 4, 3.0, PM_PEAK_LONE);
+        (*peak2)->SN = 20.0;
+        int rc = pmPeakSortBySN((const void **)peak1, (const void **) peak2);
+        ok(rc == 1, "pmPeakSortBySN() returned correct result (peak1 < peak2) (%d)", rc);
+        rc = pmPeakSortBySN((const void **)peak2, (const void **) peak1);
+        ok(rc == -1, "pmPeakSortBySN() returned correct result (peak2 < peak1) (%d)", rc);
+        rc = pmPeakSortBySN((const void **)peak1, (const void **) peak1);
+        ok(rc == 0, "pmPeakSortBySN() returned correct result (peak1 == peak2) (%d)", rc);
+        psFree(*peak1);
+        psFree(peak1);
+        psFree(*peak2);
+        psFree(peak2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmPeakSortByY() tests
+    // int pmPeakSortByY (const void **a, const void **b)
+    // Call pmPeakSortByY() with acceptable input parameters.
+    // XXX: We don't test with NULL input parameters since this functions has no PS_ASSERTS to protect
+    // against that.
+    {
+        psMemId id = psMemGetId();
+        pmPeak **peak1 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak1 = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        (*peak1)->y = 10.0;
+        pmPeak **peak2 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak2 = pmPeakAlloc(3, 4, 3.0, PM_PEAK_LONE);
+        (*peak2)->y = 20.0;
+        int rc = pmPeakSortByY((const void **)peak1, (const void **) peak2);
+        ok(rc == -1, "pmPeakSortByY() returned correct result (peak1 < peak2) (%d)", rc);
+        rc = pmPeakSortByY((const void **)peak2, (const void **) peak1);
+        ok(rc == 1, "pmPeakSortByY() returned correct result (peak2 < peak1) (%d)", rc);
+        rc = pmPeakSortByY((const void **)peak1, (const void **) peak1);
+        ok(rc == 0, "pmPeakSortByY() returned correct result (peak1 == peak2) (%d)", rc);
+        psFree(*peak1);
+        psFree(peak1);
+        psFree(*peak2);
+        psFree(peak2);
+        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);
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmResiduals.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmResiduals.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmResiduals.c	(revision 42651)
@@ -0,0 +1,46 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+	All functions are tested.
+*/
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    plan_tests(11);
+
+
+    // Test pmFringeRegionsAlloc()
+    // pmResiduals *pmResidualsAlloc (int xSize, int ySize, int xBin, int yBin);
+    {
+        psMemId id = psMemGetId();
+        int xSize = 1;
+        int ySize = 2;
+        int xBin = 3;
+        int yBin = 4;
+        pmResiduals *resid = pmResidualsAlloc(xSize, ySize, xBin, yBin);
+        ok(resid != NULL && psMemCheckResiduals(resid), "pmResidualsAlloc() allocated a pmResiduals struct correctly");
+        ok(resid->Ro && psMemCheckImage(resid->Ro), "pmResidualsAlloc() allocated the resid->Ro image");
+        ok(resid->Rx && psMemCheckImage(resid->Rx), "pmResidualsAlloc() allocated the resid->Rx image");
+        ok(resid->Ry && psMemCheckImage(resid->Ry), "pmResidualsAlloc() allocated the resid->Ry image");
+        ok(resid->variance && psMemCheckImage(resid->variance ), "pmResidualsAlloc() allocated the resid->variance image");
+        ok(resid->mask && psMemCheckImage(resid->mask), "pmResidualsAlloc() allocated the resid->mask image");
+
+        int nX = xSize * xBin;
+        int nY = ySize * yBin;
+        nX = (nX % 2) ? nX : nX + 1;
+        nY = (nY % 2) ? nY : nY + 1;
+        ok(resid->xBin == xBin, "pmResidualsAlloc() set resid->xBin correctly");
+        ok(resid->yBin == yBin, "pmResidualsAlloc() set resid->yBin correctly");
+        ok(resid->xCenter == 0.5*(nX - 1), "pmResidualsAlloc() set resid->xCenter correctly");
+        ok(resid->yCenter == 0.5*(nY - 1), "pmResidualsAlloc() set resid->xCenter correctly");
+
+        psFree(resid);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmSource.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmSource.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmSource.c	(revision 42651)
@@ -0,0 +1,859 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/*
+Tested:
+    Tested
+        pmSourceAlloc()
+        pmSourceCopy()
+        pmSourceDefinePixels()
+        pmSourceRedefinePixels()
+        pmSourcePSFClump()
+        pmSourceGetModel()
+        pmSourceAdd()
+        pmSourceSub()
+        pmSourceAddWithOffset()
+        pmSourceSubWithOffset()
+        pmSourceOp()
+        pmSourceCacheModel()
+        pmSourceCachePSF()
+        pmSourceSortByFlux()	(COMPILER ERRORS)
+        pmSourceSortByY()	(COMPILER ERRORS)
+    Must test
+        pmSourceMoments()
+        pmSourceRoughClass()
+
+*/
+
+
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           (4+1)
+#define TEST_NUM_COLS           (4+1)
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+#define TEST_FLOATS_EQUAL(X, Y) (abs(X - Y) < 0.01)
+
+/******************************************************************************
+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->variance = 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(128);
+
+    // ----------------------------------------------------------------------
+    // Test pmSourceAlloc()
+    // pmSource *pmSourceAlloc();
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        ok(src != NULL && psMemCheckSource(src), "pmSourceAlloc() returned a non-NULL pmSource");
+        skip_start(src == NULL, 24, "Skipping tests because pmSourceAlloc() returned NULL");
+        ok(src->peak == NULL, "pmSourceAlloc() pmSource->peak correctly");
+        ok(src->pixels == NULL, "pmSourceAlloc() pmSource->pixels correctly");
+        ok(src->variance == NULL, "pmSourceAlloc() pmSource->variance correctly");
+        ok(src->maskObj == NULL, "pmSourceAlloc() pmSource->maskObj correctly");
+        ok(src->maskView == NULL, "pmSourceAlloc() pmSource->maskView correctly");
+        ok(src->modelFlux == NULL, "pmSourceAlloc() pmSource->modelFlux correctly");
+        ok(src->psfFlux == NULL, "pmSourceAlloc() pmSource->psfFlux correctly");
+        ok(src->moments == NULL, "pmSourceAlloc() pmSource->moments correctly");
+        ok(src->blends == NULL, "pmSourceAlloc() pmSource->blends correctly");
+        ok(src->modelPSF == NULL, "pmSourceAlloc() pmSource->modelPSF correctly");
+        ok(src->modelEXT == NULL, "pmSourceAlloc() pmSource->modelEXT correctly");
+        ok(src->type == PM_SOURCE_TYPE_UNKNOWN, "pmSourceAlloc() pmSource->type correctly");
+        ok(src->mode == PM_SOURCE_MODE_DEFAULT, "pmSourceAlloc() pmSource->mode correctly");
+
+        ok(isnan(src->psfMag), "pmSourceAlloc() pmSource->psfMag correctly");
+        ok(isnan(src->extMag), "pmSourceAlloc() pmSource->extMag correctly");
+        ok(isnan(src->psfMagErr), "pmSourceAlloc() pmSource->psfMagErr correctly");
+        ok(isnan(src->apMag), "pmSourceAlloc() pmSource->apMag correctly");
+        ok(isnan(src->sky), "pmSourceAlloc() pmSource->sky correctly");
+        ok(isnan(src->skyErr), "pmSourceAlloc() pmSource->skyErr correctly");
+        ok(isnan(src->pixWeight), "pmSourceAlloc() pmSource->pixWeight correctly");
+        int srcID = src->id;
+        psFree(src);
+
+        // Allocate another pmSource to ensure that pmSource->id is incremented
+        pmSource *src = pmSourceAlloc();
+        ok(src != NULL, "pmSourceAlloc() returned a non-NULL pmSource");
+        ok(src->id == srcID+1, "pmSourceAlloc() incremented the pmSource->id correctly");
+        psFree(src);
+
+        skip_end();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceCopy() tests
+    // Call pmSourceCopy() with NULL input
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceCopy(NULL);
+        ok(src == NULL, "pmSourceCopy(NULL) returned NULL");
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceCopy() with non-NULL input, but NULL members
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        ok(src != NULL, "pmSourceAlloc() returned a non-NULL pmSource");
+        pmSource *dst = pmSourceCopy(src);
+        ok(dst != NULL, "pmSourceCopy() returned a non-NULL pmSource");
+        ok(dst != src, "pmSourceCopy() allocated a new pmSource");
+        ok(dst->type == src->type, "pmSourceCopy() set the pmSource->type");
+        ok(dst->mode == src->mode, "pmSourceCopy() set the pmSource->mode");
+        psFree(src);
+        psFree(dst);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceCopy() with non-NULL input, non-NULL members
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        ok(src != NULL, "pmSourceAlloc() returned a non-NULL pmSource");
+        // Set pmPeak values
+        src->peak = pmPeakAlloc (1, 2, 3.0, PM_PEAK_LONE);
+        src->peak->xf = 4.0;
+        src->peak->yf = 5.0;
+        src->peak->flux = 6.0;
+        src->peak->SN = 10.0;
+        src->moments = pmMomentsAlloc();
+        src->moments->Mx = 11.0;
+        src->moments->My = 11.0;
+        src->moments->Mxx = 11.0;
+        src->moments->Myy = 11.0;
+        src->moments->Mxxx = 12.0;
+        src->moments->Mxxy = 12.0;
+        src->moments->Mxyy = 12.0;
+        src->moments->Myyy = 12.0;
+        src->moments->Mxxxx = 13.0;
+        src->moments->Mxxxy = 13.0;
+        src->moments->Mxyyy = 13.0;
+        src->moments->Myyyy = 13.0;
+        src->moments->Sum = 14.0;
+        src->moments->Peak = 14.0;
+        src->moments->Sky = 14.0;
+        src->moments->dSky = 14.0;
+        src->moments->nPixels = 14.0;
+
+        src->pixels = psImageAlloc(2, 4, PS_TYPE_F32);
+        src->variance = psImageAlloc(6, 8, PS_TYPE_F32);
+        src->maskView  = psImageAlloc(10, 12, PS_TYPE_U8);
+        pmSource *dst = pmSourceCopy(src);
+        ok(dst != NULL, "pmSourceCopy() returned a non-NULL pmSource");
+        ok(dst != src, "pmSourceCopy() allocated a new pmSource");
+        ok(dst->type == src->type, "pmSourceCopy() set the pmSource->type");
+        ok(dst->mode == src->mode, "pmSourceCopy() set the pmSource->mode");
+        ok(dst->peak != NULL, "pmSourceCopy() allocated a new pmSource->peak");
+        ok(dst->peak->xf == src->peak->xf, "pmSourceCopy() pmSource->peak->xf");
+        ok(dst->peak->yf == src->peak->yf, "pmSourceCopy() pmSource->peak->yf");
+        ok(dst->peak->flux == src->peak->flux, "pmSourceCopy() pmSource->peak->flux");
+        ok(dst->peak->SN == src->peak->SN, "pmSourceCopy() pmSource->peak->SN");
+        ok(dst->moments != NULL, "pmSourceCopy() allocated a new pmSource->moments");
+        ok(dst->moments->Mx == src->moments->Mx, "pmSourceCopy() pmSource->moments->Mx");
+        ok(dst->moments->My == src->moments->My, "pmSourceCopy() pmSource->moments->My");
+        ok(dst->moments->Mxx == src->moments->Mxx, "pmSourceCopy() pmSource->moments->Mxx");
+        ok(dst->moments->Mxy == src->moments->Mxy, "pmSourceCopy() pmSource->moments->Mxy");
+        ok(dst->moments->Myy == src->moments->Myy, "pmSourceCopy() pmSource->moments->Myy");
+        ok(dst->moments->Mxxx == src->moments->Mxxx, "pmSourceCopy() pmSource->moments->Mxxx");
+        ok(dst->moments->Mxxy == src->moments->Mxxy, "pmSourceCopy() pmSource->moments->Mxxy");
+        ok(dst->moments->Mxyy == src->moments->Mxyy, "pmSourceCopy() pmSource->moments->Mxyy");
+        ok(dst->moments->Myyy == src->moments->Myyy, "pmSourceCopy() pmSource->moments->Myyy");
+        ok(dst->moments->Mxxxx == src->moments->Mxxxx, "pmSourceCopy() pmSource->moments->Mxxxx");
+        ok(dst->moments->Mxxxy == src->moments->Mxxxy, "pmSourceCopy() pmSource->moments->Mxxxy");
+        ok(dst->moments->Mxxyy == src->moments->Mxxyy, "pmSourceCopy() pmSource->moments->Mxxyy");
+        ok(dst->moments->Mxxxy == src->moments->Mxxxy, "pmSourceCopy() pmSource->moments->Mxxxy");
+        ok(dst->moments->Myyyy == src->moments->Myyyy, "pmSourceCopy() pmSource->moments->Myyyy");
+        ok(dst->moments->Sum == src->moments->Sum, "pmSourceCopy() pmSource->moments->Sum");
+        ok(dst->moments->Peak == src->moments->Peak, "pmSourceCopy() pmSource->moments->Peak");
+        ok(dst->moments->Sky == src->moments->Sky, "pmSourceCopy() pmSource->moments->Sky");
+        ok(dst->moments->dSky == src->moments->dSky, "pmSourceCopy() pmSource->moments->dSky");
+        ok(dst->moments->SN == src->moments->SN, "pmSourceCopy() pmSource->moments->SN");
+        ok(dst->moments->nPixels == src->moments->nPixels, "pmSourceCopy() pmSource->moments->nPixels");
+
+        // XXX: We should possibly do a better job testing that these images are copied correctly.
+        ok(dst->pixels != NULL, "pmSourceCopy() allocated a new pmSource->pixels");
+        ok(dst->pixels->numCols == src->pixels->numCols && dst->pixels->numRows == src->pixels->numRows, 
+           "pmSourceCopy() generated correct size pmSource->pixels");
+        ok(dst->variance != NULL, "pmSourceCopy() allocated a new pmSource->variance");
+        ok(dst->variance->numCols == src->variance->numCols && dst->variance->numRows == src->variance->numRows, 
+           "pmSourceCopy() generated correct size pmSource->variance");
+        ok(dst->maskView != NULL, "pmSourceCopy() allocated a new pmSource->maskView");
+        ok(dst->maskView->numCols == src->maskView->numCols && dst->maskView->numRows == src->maskView->numRows, 
+           "pmSourceCopy() generated correct size pmSource->maskView");
+
+        psFree(src);
+        psFree(dst);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceDefinePixels() tests
+    // Call pmSourceDefinePixels() with NULL pmSource input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        ok(!pmSourceDefinePixels(NULL, readout, 1.0, 2.0, 3.0),
+           "pmSourceDefinePixels() returned false with NULL pmSource input parameter");
+        psFree(src);
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceDefinePixels() with NULL pmReadout input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        ok(!pmSourceDefinePixels(src, NULL, 1.0, 2.0, 3.0),
+           "pmSourceDefinePixels() returned false with NULL pmReadout input parameter");
+        psFree(src);
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceDefinePixels() with NULL pmReadout->image input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        psFree(readout->image);
+        readout->image = NULL;
+        ok(!pmSourceDefinePixels(src, NULL, 1.0, 2.0, 3.0),
+           "pmSourceDefinePixels() returned false with NULL pmReadout input parameter");
+        psFree(src);
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceDefinePixels() with negative radius input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        ok(!pmSourceDefinePixels(src, readout, 1.0, 2.0, -3.0),
+           "pmSourceDefinePixels() returned false with negative radius input parameter");
+        psFree(src);
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceDefinePixels() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        for (int i = 0 ; i < readout->image->numRows ; i++) {
+            for (int j = 0 ; j < readout->image->numCols ; j++) {
+                readout->image->data.F32[i][j] = (float) (i + j);
+	    }
+	}
+        bool rc = pmSourceDefinePixels(src, readout, (float) TEST_NUM_COLS/2, (float) TEST_NUM_ROWS/2, 2.0);
+        ok(rc, "pmSourceDefinePixels() returned TRUE with acceptable input parameters");
+        int expectedNumCols = 1 + (TEST_NUM_COLS/2);
+        int expectedNumRows = 1 + (TEST_NUM_COLS/2);
+        // XX: We only verify the size of the pixels, variance, maskView, and maskObj
+        // images.  A better test would verify that the actual data is set correctly.
+        // We don't do that here since it basically duplicated psLib function tests.
+        ok(src->pixels->numCols == expectedNumCols && src->pixels->numRows == expectedNumRows, 
+               "pmSourceDefinePixels() set the size of pmSource->pixels correctly");
+        ok(src->variance->numCols == expectedNumCols && src->variance->numRows == expectedNumRows, 
+               "pmSourceDefinePixels() set the size of pmSource->variance correctly");
+        ok(src->maskView->numCols == expectedNumCols && src->maskView->numRows == expectedNumRows, 
+               "pmSourceDefinePixels() set the size of pmSource->maskView correctly");
+        ok(src->maskObj->numCols == expectedNumCols && src->maskObj->numRows == expectedNumRows, 
+               "pmSourceDefinePixels() set the size of pmSource->maskObj correctly");
+
+        psRegion region = psRegionForSquare((float) TEST_NUM_COLS/2, (float) TEST_NUM_ROWS/2, 2.0);
+        region = psRegionForImage(readout->image, region);
+        ok(src->region.x0 == region.x0 && src->region.x1 == region.x1 &&
+           src->region.y0 == region.y0 && src->region.y1 == region.y1,
+          "pmSourceDefinePixels() set pmSource->region correctly");
+
+        psFree(src);
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceRedefinePixels() tests
+    // Call pmSourceRedefinePixels() with NULL pmSource input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        ok(!pmSourceRedefinePixels(NULL, readout, 1.0, 2.0, 3.0),
+           "pmSourceRedefinePixels() returned false with NULL pmSource input parameter");
+        psFree(src);
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceRedefinePixels() with NULL pmReadout input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        ok(!pmSourceRedefinePixels(src, NULL, 1.0, 2.0, 3.0),
+           "pmSourceRedefinePixels() returned false with NULL pmReadout input parameter");
+        psFree(src);
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceRedefinePixels() with NULL pmReadout->image input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        psFree(readout->image);
+        readout->image = NULL;
+        ok(!pmSourceRedefinePixels(src, NULL, 1.0, 2.0, 3.0),
+           "pmSourceRedefinePixels() returned false with NULL pmReadout input parameter");
+        psFree(src);
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceRedefinePixels() with negative radius input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        ok(!pmSourceRedefinePixels(src, readout, 1.0, 2.0, -3.0),
+           "pmSourceRedefinePixels() returned false with negative radius input parameter");
+        psFree(src);
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceRedefinePixels() with acceptable input parameters
+    if (1) {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        for (int i = 0 ; i < readout->image->numRows ; i++) {
+            for (int j = 0 ; j < readout->image->numCols ; j++) {
+                readout->image->data.F32[i][j] = (float) (i + j);
+	    }
+	}
+        // First set the pixels region to a square of radius 1.
+        bool rc = pmSourceDefinePixels(src, readout, (float) TEST_NUM_COLS/2, (float) TEST_NUM_ROWS/2, 1.0);
+        ok(rc, "pmSourceDefinePixels() returned TRUE with radius 1");
+
+        // Set these flux images so we can verify they are later psFree'ed and set to NULL
+        src->modelFlux = psImageAlloc(2, 2, PS_TYPE_F32);
+        src->psfFlux = psImageAlloc(2, 2, PS_TYPE_F32);
+
+        // Now set the radius to 2, and call pmSourceRedefinePixels()
+        rc = pmSourceRedefinePixels(src, readout, (float) TEST_NUM_COLS/2, (float) TEST_NUM_ROWS/2, 2.0);
+        ok(rc, "pmSourceRedefinePixels() returned TRUE with acceptable input parameters");
+        int expectedNumCols = 1 + (TEST_NUM_COLS/2);
+        int expectedNumRows = 1 + (TEST_NUM_COLS/2);
+        // XX: We only verify the size of the pixels, variance, maskView, and maskObj
+        // images.  A better test would verify that the actual data is set correctly.
+        // We don't do that here since it basically duplicated psLib function tests.
+        ok(src->pixels->numCols == expectedNumCols && src->pixels->numRows == expectedNumRows, 
+               "pmSourceRedefinePixels() set the size of pmSource->pixels correctly");
+        ok(src->variance->numCols == expectedNumCols && src->variance->numRows == expectedNumRows, 
+               "pmSourceRedefinePixels() set the size of pmSource->variance correctly");
+        ok(src->maskView->numCols == expectedNumCols && src->maskView->numRows == expectedNumRows, 
+               "pmSourceRedefinePixels() set the size of pmSource->maskView correctly");
+        ok(src->maskObj->numCols == expectedNumCols && src->maskObj->numRows == expectedNumRows, 
+               "pmSourceRedefinePixels() set the size of pmSource->maskObj correctly");
+
+        psRegion region = psRegionForSquare((float) TEST_NUM_COLS/2, (float) TEST_NUM_ROWS/2, 2.0);
+        region = psRegionForImage(readout->image, region);
+        ok(src->region.x0 == region.x0 && src->region.x1 == region.x1 &&
+           src->region.y0 == region.y0 && src->region.y1 == region.y1,
+          "pmSourceRedefinePixels() set pmSource->region correctly");
+
+        ok(src->modelFlux == NULL, "pmSourceRedefinePixels() set the pmSource->modelFlux to NULL");
+        ok(src->psfFlux == NULL, "pmSourceRedefinePixels() set the pmSource->psfFlux to NULL");
+
+        psFree(src);
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourcePSFClump() tests
+    // pmPSFClump pmSourcePSFClump(psArray *sources, psMetadata *recipe)
+    // Call pmSourcePSFClump() with NULL pmSource input parameter
+    #define NUM_SOURCES 10
+    {
+        psMemId id = psMemGetId();
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        psMetadata *recipe = psMetadataAlloc();
+        bool rc = psMetadataAddF32(recipe, PS_LIST_HEAD, "PSF_SN_LIM", 0, NULL, 0.0);
+        rc = psMetadataAddF32(recipe, PS_LIST_HEAD, "MOMENTS_SX_MAX", 0, NULL, 10.0);
+        rc = psMetadataAddF32(recipe, PS_LIST_HEAD, "MOMENTS_SY_MAX", 0, NULL, 10.0);
+        rc = psMetadataAddF32(recipe, PS_LIST_HEAD, "MOMENTS_AR_MAX", 0, NULL, 3.0);
+
+        pmPSFClump clump = pmSourcePSFClump(NULL, NULL, recipe);
+        ok(clump.X == -1.0 && clump.dX == -1.0 && 
+           clump.Y == 0.0 && clump.dY == 0.0, "pmSourcePSFClump(NULL, recipe) returned the error clump");
+
+        psFree(sources);
+        psFree(recipe);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourcePSFClump() with NULL recipe input parameter
+    {
+        psMemId id = psMemGetId();
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        psMetadata *recipe = psMetadataAlloc();
+        bool rc = psMetadataAddF32(recipe, PS_LIST_HEAD, "PSF_SN_LIM", 0, NULL, 0.0);
+        rc = psMetadataAddF32(recipe, PS_LIST_HEAD, "MOMENTS_SX_MAX", 0, NULL, 10.0);
+        rc = psMetadataAddF32(recipe, PS_LIST_HEAD, "MOMENTS_SY_MAX", 0, NULL, 10.0);
+        rc = psMetadataAddF32(recipe, PS_LIST_HEAD, "MOMENTS_AR_MAX", 0, NULL, 3.0);
+
+        pmPSFClump clump = pmSourcePSFClump(NULL, sources, NULL);
+        ok(clump.X == -1.0 && clump.dX == -1.0 && 
+           clump.Y == 0.0 && clump.dY == 0.0, "pmSourcePSFClump(sources, NULL) returned the error clump");
+
+        psFree(sources);
+        psFree(recipe);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourcePSFClump() with acceptable input parameters
+    // XX: This is a fairly simplistic test.  We set the moments of all test pmSources
+    // to (5.0, 5.0), so the clump should be fairly easy to detect.  For further testing
+    // add:
+    //	  Add outliers to the Mx and My data, make sure they aren't used in the calculation
+    //    Force it to get the various metadata from the recipes arg.
+    //    Add a few non pmSource types to the sources input pmArray
+    //    Add a few NULL pmSources, or NULL pmSource->moments to the sources input pmArray
+    //    Ensure the data is saved with the KEEP_PSF_CLUMP metadata item
+    //    Add cases where you fail to find a peak.
+    //    Modify data so that the inputs have different moments
+    //
+    {
+        psMemId id = psMemGetId();
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        for (int i = 0 ; i < sources->n ; i++) {
+            pmSource *src = pmSourceAlloc();
+            src->moments = pmMomentsAlloc();
+            src->moments->Mx = 5.0;
+            src->moments->My = 5.0;
+            sources->data[i] = src;
+	}
+        psMetadata *recipe = psMetadataAlloc();
+        bool rc = psMetadataAddF32(recipe, PS_LIST_HEAD, "PSF_SN_LIM", 0, NULL, 0.0);
+        rc = psMetadataAddF32(recipe, PS_LIST_HEAD, "MOMENTS_SX_MAX", 0, NULL, 10.0);
+        rc = psMetadataAddF32(recipe, PS_LIST_HEAD, "MOMENTS_SY_MAX", 0, NULL, 10.0);
+        rc = psMetadataAddF32(recipe, PS_LIST_HEAD, "MOMENTS_AR_MAX", 0, NULL, 3.0);
+
+        pmPSFClump clump = pmSourcePSFClump(NULL, sources, recipe);
+        ok(clump.X == 5.0 && clump.dX == 0.0 && 
+           clump.Y == 5.0 && clump.dY == 0.0, "pmSourcePSFClump(sources, NULL) returned the correct clump");
+
+        if (VERBOSE) {
+            printf("clump: (%.2f %.2f %.2f %.2f)\n", clump.X, clump.dX, clump.Y, clump.dY);
+	}
+        psFree(sources);
+        psFree(recipe);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceGetModel() tests
+    // pmModel *pmSourceGetModel (bool *isPSF, const pmSource *source)
+    // Call pmSourceGetModel() with NULL pmSource input parameter
+    #define NUM_SOURCES 10
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        bool isPDF;
+        pmModel *model = pmSourceGetModel(&isPDF, NULL);
+        ok(model == NULL, "pmSourceGetModel() returned NULL with NULL pmSource input parameter");
+
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Call pmSourceGetModel() with acceptable input parameters
+    #define NUM_SOURCES 10
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        // For testing only:
+        src->modelPSF = (pmModel *) 1;
+        src->modelEXT = (pmModel *) 3;
+        bool isPDF;
+
+        src->type = PM_SOURCE_TYPE_UNKNOWN;
+        pmModel *model = pmSourceGetModel(&isPDF, src);
+        ok(model == NULL, "pmSourceGetModel() returned a NULL pmModel with acceptable input parameters and src->type = PM_SOURCE_TYPE_UNKNOWN");
+        ok(false == isPDF, "pmSourceGetModel() set isPDF to FALSE");
+
+        src->type = PM_SOURCE_TYPE_STAR;
+        model = pmSourceGetModel(&isPDF, src);
+        ok(model != NULL, "pmSourceGetModel() returned a non-NULL pmModel with acceptable input parameters");
+        //ok(1 == (int) model, "pmSourceGetModel() returned the correct model with pmSource->type == PM_SOURCE_TYPE_STAR");
+        ok(true == isPDF, "pmSourceGetModel() set isPDF to TRUE");
+
+        src->type = PM_SOURCE_TYPE_EXTENDED;
+        model = pmSourceGetModel(&isPDF, src);
+        ok(model != NULL, "pmSourceGetModel() returned a non-NULL pmModel with acceptable input parameters");
+        //ok(2 == (int) model, "pmSourceGetModel() returned the correct model with pmSource->type == PM_SOURCE_TYPE_EXTENDED (%d)", (int) model);
+        ok(false == isPDF, "pmSourceGetModel() set isPDF to FALSE");
+
+        src->type = PM_SOURCE_TYPE_EXTENDED;
+        model = pmSourceGetModel(&isPDF, src);
+        ok(model != NULL, "pmSourceGetModel() returned a non-NULL pmModel with acceptable input parameters");
+        //ok(3 == (int) model, "pmSourceGetModel() returned the correct model with pmSource->type == PM_SOURCE_TYPE_EXTENDED");
+        ok(false == isPDF, "pmSourceGetModel() set isPDF to FALSE");
+
+        src->modelPSF = NULL;
+        src->modelEXT = NULL;
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceOp() tests
+    // bool pmSourceOp (pmSource *source, pmModelOpMode mode, bool add,
+    //                  psMaskType maskVal, int dx, int dy)
+    // Call pmSourceOp() with NULL pmSource input parameter
+    #define NUM_SOURCES 10
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        bool rc = pmSourceOp(NULL, PM_MODEL_OP_NONE, true, 1, 0, 0);
+        ok(!rc, "pmSourceOpl() returned FALSE with NULL pmSource input parameter");
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // call pmSourceOp() with acceptable parameters
+    // We only test with a single Gaussian model, with no residuals or masks.
+    // For completeness, additional tests should be added.
+        // We should also set mode &= PM_MODEL_OP_NOISE to test that the src->variances are added.
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->pixels = 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++) {
+                src->pixels->data.F32[i][j] = 0.0;
+            }
+        }
+        src->peak = pmPeakAlloc(TEST_NUM_COLS/2, TEST_NUM_ROWS/2, 5.0, PM_PEAK_LONE);
+        src->type = PM_SOURCE_TYPE_STAR;
+        src->modelPSF = pmModelAlloc(pmModelClassGetType("PS_MODEL_GAUSS"));
+        psF32 *PAR = src->modelPSF->params->data.F32;
+        PAR[PM_PAR_I0] = 5.0;
+        PAR[PM_PAR_XPOS] = 0.0;
+        PAR[PM_PAR_YPOS] = 0.0;
+        PAR[PM_PAR_XPOS] = (float) (TEST_NUM_COLS/2);
+        PAR[PM_PAR_YPOS] = (float) (TEST_NUM_ROWS/2);
+        PAR[PM_PAR_SXX] = 10.0;
+        PAR[PM_PAR_SYY] = 10.0;
+
+        bool rc = pmSourceOp(src, PM_SOURCE_MODE_PSFMODEL, true, 0, 0, 0);
+        ok(rc == true, "pmSourceOp() returned TRUE with acceptable input parameters");
+        psVector *x = psVectorAlloc(2, PS_TYPE_F32);
+        bool errorFlag = false;
+        for (int i = 0 ; i < TEST_NUM_ROWS ; i++) {
+            for (int j = 0 ; j < TEST_NUM_COLS ; j++) {
+                x->data.F32[0] = (float) j;
+                x->data.F32[1] = (float) i;
+                psF32 modF = src->modelPSF->modelFunc (NULL, src->modelPSF->params, x);
+                psF32 imgF = src->pixels->data.F32[i][j];
+                if (!TEST_FLOATS_EQUAL(modF, imgF)) {
+                    diag("ERROR: src->pixels[%d][%d] is %.2f, should be %.2f", i, j, src->pixels->data.F32[i][j], modF);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmSourceOp() set the image pixels correctly (PSF function)");
+        psFree(x);
+        psFree(src);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // call pmSourceOp() with acceptable parameters
+    // Test source->modelFlux
+        // We should also set mode &= PM_MODEL_OP_NOISE to test that the src->variances are added.
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->pixels = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        src->modelFlux = 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++) {
+                src->pixels->data.F32[i][j] = 0.0;
+                src->modelFlux->data.F32[i][j] = (float) (i + j);
+            }
+        }
+        src->peak = pmPeakAlloc(TEST_NUM_COLS/2, TEST_NUM_ROWS/2, 5.0, PM_PEAK_LONE);
+        src->type = PM_SOURCE_TYPE_STAR;
+        src->modelPSF = pmModelAlloc(pmModelClassGetType("PS_MODEL_GAUSS"));
+        psF32 *PAR = src->modelPSF->params->data.F32;
+        PAR[PM_PAR_I0] = 1.0;
+        PAR[PM_PAR_XPOS] = 0.0;
+        PAR[PM_PAR_YPOS] = 0.0;
+        PAR[PM_PAR_XPOS] = (float) (TEST_NUM_COLS/2);
+        PAR[PM_PAR_YPOS] = (float) (TEST_NUM_ROWS/2);
+        PAR[PM_PAR_SXX] = 10.0;
+        PAR[PM_PAR_SYY] = 10.0;
+
+        bool rc = pmSourceOp(src, PM_SOURCE_MODE_PSFMODEL, true, 0, 0, 0);
+        ok(rc == true, "pmSourceOp() returned TRUE with acceptable input parameters");
+        psVector *x = psVectorAlloc(2, PS_TYPE_F32);
+        bool errorFlag = false;
+        for (int i = 0 ; i < TEST_NUM_ROWS ; i++) {
+            for (int j = 0 ; j < TEST_NUM_COLS ; j++) {
+                x->data.F32[0] = (float) j;
+                x->data.F32[1] = (float) i;
+                psF32 modF = src->modelFlux->data.F32[i][j];
+                psF32 imgF = src->pixels->data.F32[i][j];
+                if (!TEST_FLOATS_EQUAL(modF, imgF)) {
+                    diag("ERROR: src->pixels[%d][%d] is %.2f, should be %.2f", i, j, src->pixels->data.F32[i][j], modF);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmSourceOp() set the image pixels correctly (src->modelFlux cache image)");
+        psFree(x);
+        psFree(src);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceCacheModel() tests
+    // call pmSourceCacheModel() with NULL pmSource input parameter
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmSourceCacheModel(NULL, 0);
+        ok(rc == false, "pmSourceCacheModel() returned FALSE with NULL pmSource input parameter");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceCacheModel() tests
+    // bool pmSourceCacheModel (pmSource *source, psMaskType maskVal) {
+    // call pmSourceCacheModel() with acceptable parameters
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->pixels = 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++) {
+                src->pixels->data.F32[i][j] = 0.0;
+            }
+        }
+        src->peak = pmPeakAlloc(TEST_NUM_COLS/2, TEST_NUM_ROWS/2, 5.0, PM_PEAK_LONE);
+        src->type = PM_SOURCE_TYPE_STAR;
+        src->modelPSF = pmModelAlloc(pmModelClassGetType("PS_MODEL_GAUSS"));
+        psF32 *PAR = src->modelPSF->params->data.F32;
+        PAR[PM_PAR_I0] = 1.0;
+        PAR[PM_PAR_XPOS] = 0.0;
+        PAR[PM_PAR_YPOS] = 0.0;
+        PAR[PM_PAR_XPOS] = (float) (TEST_NUM_COLS/2);
+        PAR[PM_PAR_YPOS] = (float) (TEST_NUM_ROWS/2);
+        PAR[PM_PAR_SXX] = 10.0;
+        PAR[PM_PAR_SYY] = 10.0;
+
+        bool rc = pmSourceCacheModel(src, 0);
+        ok(rc == true, "pmSourceCacheModel() returned TRUE with acceptable input parameters");
+
+        psVector *x = psVectorAlloc(2, PS_TYPE_F32);
+        bool errorFlag = false;
+        for (int i = 0 ; i < TEST_NUM_ROWS ; i++) {
+            for (int j = 0 ; j < TEST_NUM_COLS ; j++) {
+                x->data.F32[0] = (float) j;
+                x->data.F32[1] = (float) i;
+                psF32 modF = src->modelPSF->modelFunc (NULL, src->modelPSF->params, x);
+                psF32 imgF = src->modelFlux->data.F32[i][j];
+                if (!TEST_FLOATS_EQUAL(modF, imgF)) {
+                    diag("ERROR: src->modelFlux[%d][%d] is %.2f, should be %.2f", i, j, src->modelFlux->data.F32[i][j], modF);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmSourceCacheModel() set the src->modelFlux correctly (PSF function)");
+        psFree(x);
+        psFree(src);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceCachePSF() tests
+    // bool pmSourceCachePSF (pmSource *source, psMaskType maskVal) {
+    // call pmSourceCachePSF() with NULL pmSource input parameter
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmSourceCachePSF(NULL, 0);
+        ok(rc == false, "pmSourceCachePSF() returned FALSE with NULL pmSource input parameter");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // call pmSourceCachePSF() with acceptable parameters
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->pixels = 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++) {
+                src->pixels->data.F32[i][j] = 0.0;
+            }
+        }
+        src->peak = pmPeakAlloc(TEST_NUM_COLS/2, TEST_NUM_ROWS/2, 5.0, PM_PEAK_LONE);
+        src->type = PM_SOURCE_TYPE_STAR;
+        src->modelPSF = pmModelAlloc(pmModelClassGetType("PS_MODEL_GAUSS"));
+        psF32 *PAR = src->modelPSF->params->data.F32;
+        PAR[PM_PAR_I0] = 1.0;
+        PAR[PM_PAR_XPOS] = 0.0;
+        PAR[PM_PAR_YPOS] = 0.0;
+        PAR[PM_PAR_XPOS] = (float) (TEST_NUM_COLS/2);
+        PAR[PM_PAR_YPOS] = (float) (TEST_NUM_ROWS/2);
+        PAR[PM_PAR_SXX] = 10.0;
+        PAR[PM_PAR_SYY] = 10.0;
+
+        bool rc = pmSourceCachePSF(src, 0);
+        ok(rc == true, "pmSourceCachePSF() returned TRUE with acceptable input parameters");
+
+        psVector *x = psVectorAlloc(2, PS_TYPE_F32);
+        bool errorFlag = false;
+        for (int i = 0 ; i < TEST_NUM_ROWS ; i++) {
+            for (int j = 0 ; j < TEST_NUM_COLS ; j++) {
+                x->data.F32[0] = (float) j;
+                x->data.F32[1] = (float) i;
+                psF32 modF = src->modelPSF->modelFunc (NULL, src->modelPSF->params, x);
+                psF32 imgF = src->psfFlux->data.F32[i][j];
+                if (!TEST_FLOATS_EQUAL(modF, imgF)) {
+                    diag("ERROR: src->psfFlux[%d][%d] is %.2f, should be %.2f", i, j, src->psfFlux->data.F32[i][j], modF);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmSourceCachePSF() set the src->psfFlux correctly (PSF function)");
+        psFree(x);
+        psFree(src);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------
+    // pmSourceSortByFlux() tests
+    // int pmSourceSortByFlux (const void **a, const void **b)
+    // Call pmSourceSortByFlux() with acceptable input parameters.
+    // XXX: We don't test with NULL input parameters since this function has no PS_ASSERTS to protect
+    // against that.
+/* XXXX: Compiler errors: fix this
+    {
+        psMemId id = psMemGetId();
+        pmSource *src1 = pmSourceAlloc();
+        src1->peak = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        src1->peak->SN = 10.0;
+        pmSource *src2 = pmSourceAlloc();
+        src2->peak = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        src2->peak->SN = 20.0;
+
+        int rc = pmSourceSortByFlux((const void **) &src1, (const void **) &src2);
+        ok(rc == 1, "pmSourceSortByFlux() returned correct result (source1 < source2) (%d)", rc);
+        rc = pmSourceSortByFlux((const void **) &src2, (const void **) &src1);
+        ok(rc == -1, "pmSourceSortByFlux() returned correct result (source2 < source1) (%d)", rc);
+        rc = pmSourceSortByFlux((const void **) &src1, (const void **) &src1);
+        ok(rc == 0, "pmSourceSortByFlux() returned correct result (source1 == source2) (%d)", rc);
+
+        psFree(src1);
+        psFree(src2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------
+    // pmSourceSortByY() tests
+    // int pmSourceSortByY (const void **a, const void **b)
+    // Call pmSourceSortByY() with acceptable input parameters.
+    // XXX: We don't test with NULL input parameters since this function has no PS_ASSERTS to protect
+    // against that.
+    {
+        psMemId id = psMemGetId();
+        pmSource *src1 = pmSourceAlloc();
+        src1->peak = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        src1->peak->y = 10.0;
+        pmSource *src2 = pmSourceAlloc();
+        src2->peak = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        src2->peak->y = 20.0;
+
+        int rc = pmSourceSortByY((const void **) &src1, (const void **) &src2);
+        ok(rc == -1, "pmSourceSortByY() returned correct result (source1 < source2) (%d)", rc);
+        rc = pmSourceSortByY((const void **) &src2, (const void **) &src1);
+        ok(rc == 1, "pmSourceSortByY() returned correct result (source2 < source1) (%d)", rc);
+        rc = pmSourceSortByY((const void **) &src1, (const void **) &src1);
+        ok(rc == 0, "pmSourceSortByY() returned correct result (source1 == source2) (%d)", rc);
+
+        psFree(src1);
+        psFree(src2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+*/
+
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceContour.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceContour.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceContour.c	(revision 42651)
@@ -0,0 +1,179 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    XXX: All functions only tested with unallowable input parameters.
+    I tried to use acceptable data, but could not get the source code to work the
+    way I thought it should have.
+*/
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           (10)
+#define TEST_NUM_COLS           (16)
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         10
+#define TEST_FLOATS_EQUAL(X, Y) (abs(X - Y) < 0.0001)
+#define NUM_SOURCES		100
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(11);
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceContour() tests
+    // psArray *pmSourceContour (psImage *image, int xc, int yc, float threshold)
+    // Call pmSourceContour() with NULL psImage input parameter
+    {
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psArray *array = pmSourceContour(NULL, 1, 2, 3.0);
+        ok(array == NULL, "pmSourceContour() returned NULL with NULL psImage input parameter");
+        psFree(img);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceContour() with unallowed row/column numbers
+    {
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psArray *array = pmSourceContour(img, -1, 2, 3.0);
+        ok(array == NULL, "pmSourceContour() returned NULL with column = -1");
+        array = pmSourceContour(img, TEST_NUM_COLS, 2, 3.0);
+        ok(array == NULL, "pmSourceContour() returned NULL with column >= numCols");
+        array = pmSourceContour(img, 1, -1, 3.0);
+        ok(array == NULL, "pmSourceContour() returned NULL with row = -1");
+        array = pmSourceContour(img, 1, TEST_NUM_ROWS, 3.0);
+        ok(array == NULL, "pmSourceContour() returned NULL with row >= numRows");
+
+        psFree(img);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceContour() with acceptable input parameters
+    // XXX: These tests currently fail
+    if (0) {
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        for (int i = 0 ; i < img->numRows ; i++) {
+            for (int j = 0 ; j < img->numCols ; j++) {
+                img->data.F32[i][j] = (float) ((TEST_NUM_ROWS + TEST_NUM_COLS) - (abs(i - (TEST_NUM_ROWS/2)) + abs(j - (TEST_NUM_COLS/2))));
+	    }
+	}
+        if (1) {
+            for (int i = 0 ; i < img->numRows ; i++) {
+                for (int j = 0 ; j < img->numCols ; j++) {
+                    printf("(%.0f)", img->data.F32[i][j]);
+    	    }
+                printf("\n");
+	    }
+	}
+
+        psArray *array = pmSourceContour(img, TEST_NUM_COLS/2, TEST_NUM_ROWS/2, 22.0);
+        ok(array != NULL, "pmSourceContour() returned non-NULL with acceptable input parameters");
+        for (int i = 0 ; i < array->n ; i++) {
+            psVector *vec = (psVector *) array->data[i];
+            printf("Point %d: (%.2f %.2f)\n", i, vec->data.F32[0], vec->data.F32[1]);
+	}
+
+        psFree(array);
+        psFree(img);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceContour_Crude() tests
+    // psArray *pmSourceContour_Crude_Crude(pmSource *source, psImage *image, psF32 level)
+    // Call pmSourceContour_Crude() with NULL psSource input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        src->pixels = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        src->moments = pmMomentsAlloc();
+        src->modelEXT = pmModelAlloc(1);
+        psImage *img = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        for (int i = 0 ; i < img->numRows ; i++) {
+            for (int j = 0 ; j < img->numCols ; j++) {
+                if ((i >= TEST_NUM_ROWS/4) && (i < 3*TEST_NUM_ROWS/4) &&
+                    (j >= TEST_NUM_COLS/4) && (j < 3*TEST_NUM_COLS/4)) {
+                    img->data.F32[i][j] = 5.0;
+		} else {
+                    img->data.F32[i][j] = 0.0;
+		}
+	    }
+	}
+        psArray *array = pmSourceContour_Crude(NULL, img, 3.0);
+        ok(array == NULL, "pmSourceContour_Crude() returned NULL with NULL pmSource input parameter");
+        psFree(img);
+        psFree(src);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceContour_Crude() with NULL psImage input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        src->pixels = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        src->moments = pmMomentsAlloc();
+        src->modelEXT = pmModelAlloc(1);
+        psImage *img = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        for (int i = 0 ; i < img->numRows ; i++) {
+            for (int j = 0 ; j < img->numCols ; j++) {
+                if ((i >= TEST_NUM_ROWS/4) && (i < 3*TEST_NUM_ROWS/4) &&
+                    (j >= TEST_NUM_COLS/4) && (j < 3*TEST_NUM_COLS/4)) {
+                    img->data.F32[i][j] = 5.0;
+		} else {
+                    img->data.F32[i][j] = 0.0;
+		}
+	    }
+	}
+        psArray *array = pmSourceContour_Crude(src, NULL, 3.0);
+        ok(array == NULL, "pmSourceContour_Crude() returned NULL with NULL pmImage input parameter");
+        psFree(img);
+        psFree(src);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceContour_Crude() with acceptable input parameters
+    // XXX: Must correct this
+    if (0) {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        src->pixels = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        src->moments = pmMomentsAlloc();
+        src->modelEXT = pmModelAlloc(1);
+        psImage *img = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+
+        if (0) {
+            for (int i = 0 ; i < img->numRows ; i++) {
+                for (int j = 0 ; j < img->numCols ; j++) {
+                    img->data.F32[i][j] = (float) ((TEST_NUM_ROWS + TEST_NUM_COLS) - (abs(i - (TEST_NUM_ROWS/2)) + abs(j - (TEST_NUM_COLS/2))));
+		}
+	    }
+	}
+        printf("Calling pmSourceContour_Crude()\n");
+        psArray *array = pmSourceContour_Crude(src, img, 22.0);
+        printf("Called pmSourceContour_Crude()\n");
+        ok(array != NULL, "pmSourceContour_Crude() returned non-NULL with NULL pmImage input parameter");
+        psFree(img);
+        psFree(src);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceExtendedPars.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceExtendedPars.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceExtendedPars.c	(revision 42651)
@@ -0,0 +1,121 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+        All functions are tested.
+*/
+
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(35);
+
+    // ----------------------------------------------------------------------
+    // pmSourceExtendedParsAlloc() tests
+    {
+        psMemId id = psMemGetId();
+        pmSourceExtendedPars *tmp = pmSourceExtendedParsAlloc();
+        ok(tmp && psMemCheckSourceExtendedPars(tmp), "pmSourceExtendedParsAlloc() allocated a pmSourceExtendedPars struct");
+
+        ok(tmp->profile == NULL, "pmSourceExtendedParsAlloc() set the ->profile member to NULL");
+        ok(tmp->annuli == NULL, "pmSourceExtendedParsAlloc() set the ->annuli member to NULL");
+        ok(tmp->isophot == NULL, "pmSourceExtendedParsAlloc() set the ->isophot member to NULL");
+        ok(tmp->petrosian == NULL, "pmSourceExtendedParsAlloc() set the ->petrosian member to NULL");
+        ok(tmp->kron == NULL, "pmSourceExtendedParsAlloc() set the ->kron member to NULL");
+
+        psFree(tmp);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceRadialProfileAlloc() tests
+    {
+        psMemId id = psMemGetId();
+        pmSourceRadialProfile *tmp = pmSourceRadialProfileAlloc();
+        ok(tmp && psMemCheckSourceRadialProfile(tmp), "pmSourceRadialProfile() allocated a pmSourceRadialProfilestruct");
+
+        ok(tmp->radius == NULL, "pmSourceRadialProfileAlloc() set the ->radius member to NULL");
+        ok(tmp->flux == NULL, "pmSourceRadialProfileAlloc() set the ->flux member to NULL");
+        ok(tmp->variance == NULL, "pmSourceRadialProfileAlloc() set the ->variance member to NULL");
+
+        psFree(tmp);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceIsophotalValuesAlloc() tests
+    {
+        psMemId id = psMemGetId();
+        pmSourceIsophotalValues *tmp = pmSourceIsophotalValuesAlloc();
+        ok(tmp && psMemCheckSourceIsophotalValues(tmp), "pmSourceIsophotalValues() allocated a pmSourceIsophotalValuesstruct");
+
+        ok(tmp->mag == 0.0, "pmSourceIsophotalValuesAlloc() set the ->mag member to 0.0");
+        ok(tmp->magErr == 0.0, "pmSourceIsophotalValuesAlloc() set the ->magErr member to 0.0");
+        ok(tmp->rad == 0.0, "pmSourceIsophotalValuesAlloc() set the ->rad member to 0.0");
+        ok(tmp->radErr == 0.0, "pmSourceIsophotalValuesAlloc() set the ->radErr member to 0.0");
+
+        psFree(tmp);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourcePetrosianValuesAlloc() tests
+    {
+        psMemId id = psMemGetId();
+        pmSourcePetrosianValues *tmp = pmSourcePetrosianValuesAlloc();
+        ok(tmp && psMemCheckSourcePetrosianValues(tmp), "pmSourcePetrosianValues() allocated a pmSourcePetrosianValuesstruct");
+
+        ok(tmp->mag == 0.0, "pmSourcePetrosianValuesAlloc() set the ->mag member to 0.0");
+        ok(tmp->magErr == 0.0, "pmSourcePetrosianValuesAlloc() set the ->magErr member to 0.0");
+        ok(tmp->rad == 0.0, "pmSourcePetrosianValuesAlloc() set the ->rad member to 0.0");
+        ok(tmp->radErr == 0.0, "pmSourcePetrosianValuesAlloc() set the ->radErr member to 0.0");
+
+        psFree(tmp);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceKronValuesAlloc() tests
+    {
+        psMemId id = psMemGetId();
+        pmSourceKronValues *tmp = pmSourceKronValuesAlloc();
+        ok(tmp && psMemCheckSourceKronValues(tmp), "pmSourceKronValues() allocated a pmSourceKronValuesstruct");
+
+        ok(tmp->mag == 0.0, "pmSourceKronValuesAlloc() set the ->mag member to 0.0");
+        ok(tmp->magErr == 0.0, "pmSourceKronValuesAlloc() set the ->magErr member to 0.0");
+        ok(tmp->rad == 0.0, "pmSourceKronValuesAlloc() set the ->rad member to 0.0");
+        ok(tmp->radErr == 0.0, "pmSourceKronValuesAlloc() set the ->radErr member to 0.0");
+
+        psFree(tmp);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceAnnuliAlloc() tests
+    {
+        psMemId id = psMemGetId();
+        pmSourceAnnuli *tmp = pmSourceAnnuliAlloc();
+        ok(tmp && psMemCheckSourceAnnuli(tmp), "pmSourceAnnuli() allocated a pmSourceAnnulistruct");
+
+        ok(tmp->flux == NULL, "pmSourceAnnuliAlloc() set the ->flux member to NULL");
+        ok(tmp->fluxErr == NULL, "pmSourceAnnuliAlloc() set the ->fluxErr member to NULL");
+        ok(tmp->fluxVar == NULL, "pmSourceAnnuliAlloc() set the ->fluxVar member to NULL");
+
+        psFree(tmp);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceFitModel.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceFitModel.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceFitModel.c	(revision 42651)
@@ -0,0 +1,311 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "tap.h"
+#include "pstap.h"
+
+# define SKY_NOISE 10.0
+# define READ_NOISE 0.0
+
+bool fitModels (psRandom *seed, float flux, float radius, float sigma);
+bool fitModelFlux (psRandom *seed, float flux, float radius, float sigma);
+bool fitModelFluxOne (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)
+{
+    pmModelClassInit ();
+    // pmSourceFitModelInit (15, 0.01, 1.0, true);
+
+    // psTraceSetLevel ("psModules.objects.pmSourceFitModel", 10);
+    // psTraceSetLevel ("psLib.math.psMinimizeLMChi2_Alt", 5);
+
+    plan_tests(240);
+
+    // build a gauss-deviate vector (mean = 0.0, sigma = 1.0)
+    psRandom *seed = psRandomAllocSpecific (PS_RANDOM_TAUS, 0);
+
+    // try a single model
+    // fitModelFluxOne (seed, 10000.0, 10.0, 2.0);
+    // return exit_status();
+
+    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");
+    // exit (1);
+
+    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*(SKY_NOISE*SKY_NOISE + PS_SQR(READ_NOISE)));
+    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 < 1.5), "Io ref/fit stdev: %e : %e sigma",  stats->sampleStdev, stats->sampleStdev/dMag);
+    psVectorStats (stats, par2, NULL, NULL, 0);
+    ok ((stats->sampleStdev/dPos < 1.5), "Xo ref-fit stdev: %e : %e sigma", stats->sampleStdev, stats->sampleStdev/dPos);
+    psVectorStats (stats, par3, NULL, NULL, 0);
+    ok ((stats->sampleStdev/dPos < 1.5), "Yo ref-fit stdev: %e : %e sigma", stats->sampleStdev, stats->sampleStdev/dPos);
+    psVectorStats (stats, par4, NULL, NULL, 0);
+    ok ((stats->sampleStdev/dMag < 1.5), "Sx ref/fit stdev: %e : %e sigma", stats->sampleStdev, stats->sampleStdev/dMag);
+    psVectorStats (stats, par5, NULL, NULL, 0);
+    ok ((stats->sampleStdev/dMag < 1.5), "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)
+{
+
+  psImageMaskType maskVal = 0x01;
+
+    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->variance = psImageAlloc (100, 100, PS_TYPE_F32);
+    source->maskObj  = psImageAlloc (100, 100, PS_TYPE_IMAGE_MASK);
+    psImageInit (source->pixels, 0.0);
+    psImageInit (source->variance, 0.0);
+    psImageInit (source->maskObj, 0);
+
+    // create an image with the model, and add noise: gain is 1, subtracted sky is 100, readnoise is 5
+    pmModelAdd (source->pixels, source->maskObj, source->modelEXT, PM_MODEL_OP_FULL, maskVal);
+    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 + PS_SQR(SKY_NOISE) + PS_SQR(READ_NOISE);
+            source->pixels->data.F32[j][i] += rnd->data.F32[npix]*sqrt(var);
+            source->variance->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);
+
+    pmSourceFitOptions *fitOptions = pmSourceFitOptionsAlloc();
+    fitOptions->mode          = PM_SOURCE_FIT_PSF;
+    fitOptions->covarFactor   = 1.0;
+
+    // save the original model, modify params
+    pmModel *guess = pmModelCopy (source->modelEXT);
+    if (fitOptions->mode == PM_SOURCE_FIT_PSF) {
+      guess->params->data.F32[1] *= 0.9;
+      guess->params->data.F32[2] += 1.0;
+      guess->params->data.F32[3] -= 1.0;
+    } 
+    if (fitOptions->mode == PM_SOURCE_FIT_EXT) {
+      guess->params->data.F32[1] *= 0.9;
+      guess->params->data.F32[4] *= 0.9;
+      guess->params->data.F32[5] *= 0.9;
+    }
+
+    // use maskVal to exclude pixels outside the circle
+    psImageKeepCircle (source->maskObj, 50, 50, radius, "OR", maskVal);
+
+    bool status = pmSourceFitModel (source, guess, fitOptions, maskVal);
+    // fprintf (stderr, "Io: %8.1f %8.1f\n", source->modelEXT->params->data.F32[1], guess->params->data.F32[1]);
+    if (!status) {
+        psFree (rnd);
+        psFree (source);
+        psFree (guess);
+        return false;
+    }
+    psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(maskVal));
+
+    // par1->data.F32[par1->n] = (guess->params->data.F32[1]);
+    par1->data.F32[par1->n] = (guess->params->data.F32[1] / source->modelEXT->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);
+    psFree (fitOptions);
+
+    return true;
+}
+
+bool fitModelFluxOne (psRandom *seed, float flux, float radius, float sigma)
+{
+
+  psImageMaskType maskVal = 0x01;
+
+  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->variance = psImageAlloc (100, 100, PS_TYPE_F32);
+  source->maskObj  = psImageAlloc (100, 100, PS_TYPE_IMAGE_MASK);
+  psImageInit (source->pixels, 0.0);
+  psImageInit (source->variance, 0.0);
+  psImageInit (source->maskObj, 0);
+
+  // create an image with the model, and add noise: gain is 1, subtracted sky is 100, readnoise is 5
+  pmModelAdd (source->pixels, source->maskObj, source->modelEXT, PM_MODEL_OP_FULL, maskVal);
+  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->variance->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);
+
+  // EXT vs PSF fitting:
+  // EXT fits Io, Sxx, Sxy, Syy
+  // PSF fits Io, Xo, Yo
+  pmSourceFitOptions *fitOptions = pmSourceFitOptionsAlloc();
+  fitOptions->mode = PM_SOURCE_FIT_EXT;
+  // fitOptions->mode = PM_SOURCE_FIT_PSF;
+  fitOptions->covarFactor   = 1.0;
+
+  // save the original model, modify params
+  pmModel *guess = pmModelCopy (source->modelEXT);
+  if (fitOptions->mode == PM_SOURCE_FIT_PSF) {
+    guess->params->data.F32[1] *= 0.9;
+    guess->params->data.F32[2] += 1.0;
+    guess->params->data.F32[3] -= 1.0;
+  } 
+  if (fitOptions->mode == PM_SOURCE_FIT_EXT) {
+    guess->params->data.F32[1] *= 0.9;
+    guess->params->data.F32[4] *= 0.9;
+    guess->params->data.F32[5] *= 0.9;
+  }
+
+  // use maskVal to exclude pixels outside the circle
+  psImageKeepCircle (source->maskObj, 50, 50, radius, "OR", maskVal);
+
+  // I need to use psTrace to get verbosity from the fit...
+  bool status = pmSourceFitModel (source, guess, fitOptions, maskVal);
+  if (!status) {
+    psFree (rnd);
+    psFree (source);
+    psFree (guess);
+    psFree (fitOptions);
+    fprintf (stderr, "failed\n");
+    return false;
+  }
+  psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(maskVal));
+
+  psFree (rnd);
+  psFree (source);
+  psFree (guess);
+  psFree (fitOptions);
+
+  return true;
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceFitModelBasic.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceFitModelBasic.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceFitModelBasic.c	(revision 42651)
@@ -0,0 +1,194 @@
+#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)
+{
+    pmModelClassInit ();
+    // 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 = psRandomAllocSpecific (PS_RANDOM_TAUS, 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");
+    exit (1);
+
+    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);
+
+    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)
+{
+
+  psImageMaskType maskVal = 0x01;
+
+    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->variance = psImageAlloc (100, 100, PS_TYPE_F32);
+    source->maskObj  = psImageAlloc (100, 100, PS_TYPE_IMAGE_MASK);
+    psImageInit (source->pixels, 0.0);
+    psImageInit (source->variance, 0.0);
+    psImageInit (source->maskObj, 0);
+
+    // create an image with the model, and add noise: gain is 1, subtracted sky is 100, readnoise is 5
+    pmModelAdd (source->pixels, source->maskObj, source->modelEXT, PM_MODEL_OP_FULL, maskVal);
+    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->variance->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;
+
+    pmSourceFitOptions *fitOptions = pmSourceFitOptionsAlloc();
+    fitOptions->mode          = PM_SOURCE_FIT_EXT;
+    fitOptions->covarFactor   = 1.0;
+
+    // use maskVal to exclude pixels outside the circle
+    psImageKeepCircle (source->maskObj, 50, 50, radius, "OR", maskVal);
+
+    bool status = pmSourceFitModel (source, guess, fitOptions, maskVal);
+    if (!status) {
+        psFree (rnd);
+        psFree (source);
+        psFree (guess);
+        return false;
+    }
+    psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(maskVal));
+
+    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_branches/psModules.20240412/test/objects/tap_pmSourceFitModel_Delta.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceFitModel_Delta.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceFitModel_Delta.c	(revision 42651)
@@ -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 = psRandomAllocSpecific (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 = psRandomAllocSpecific (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_branches/psModules.20240412/test/objects/tap_pmSourceFitSet.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceFitSet.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceFitSet.c	(revision 42651)
@@ -0,0 +1,726 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested except:
+        pmSourceFitSetCheckLimits()
+        pmSourceFitSetFunction()
+    Those functions set static variables which aer invisible to test code.
+
+    These functions are very lightly tested and must be augmented:
+	pmSourceFitSet()
+	pmSourceFitSetMasks()
+*/
+
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           (16)
+#define TEST_NUM_COLS           (30)
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+#define TEST_FLOATS_EQUAL(X, Y) (abs(X - Y) < 0.0001)
+#define NUM_MODELS		5
+
+pmSource *create_pmSource() {
+    pmSource *src = pmSourceAlloc();
+    if (1) {
+        src->pixels = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        src->variance = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        src->maskObj = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+        if (1) {
+            for (int i = 0 ; i < TEST_NUM_ROWS ; i++) {
+                for (int j = 0 ; j < TEST_NUM_COLS ; j++) {
+                    src->pixels->data.F32[i][j] = 0.0;
+                    src->variance->data.F32[i][j] = 1.0;
+                    src->maskObj->data.U8[i][j] = 0;
+                }
+            }
+        }
+        if (1) {
+            int halfRows = TEST_NUM_ROWS/2;
+            int halfCols = TEST_NUM_COLS/2;
+            for (int i = halfRows-1 ; i < halfRows+1 ; i++) {
+                for (int j = halfCols-1 ; j < halfCols+1 ; j++) {
+                    src->pixels->data.F32[i][j] = 1.0;
+                }
+            }
+            src->pixels->data.F32[halfRows][halfCols] = 5.0;
+        }
+    }
+    return(src);
+}
+
+bool call_pmSourceFitSet() {
+    psArray *modelSet = psArrayAlloc(NUM_MODELS);
+    for (int i = 0 ; i < NUM_MODELS ; i++) {
+        modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+    }
+    pmSource *src = create_pmSource();
+    bool rc = pmSourceFitSet(src, modelSet, PM_SOURCE_FIT_PSF, 1);
+    if (!rc) {
+        diag("ERROR: pmSourceFitSet() returned FALSE");
+        return(false);
+    }
+    psFree(src);
+    for (int i = 0 ; i < NUM_MODELS ; i++) {
+        psFree(modelSet->data[i]);
+        modelSet->data[i] = NULL;
+    }
+    psFree(modelSet);
+    pmModelClassCleanup();
+  
+    return(true);
+}
+
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(70);
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceFitSetDataAlloc() tests
+    // Call pmSourceFitSetDataAlloc() with NULL psArray input parameter
+    {
+        psMemId id = psMemGetId();
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < modelSet->n ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(1);
+	}
+        pmSourceFitSetData *fitSetData = pmSourceFitSetDataAlloc(NULL);
+        ok(fitSetData == NULL, "pmSourceFitSetDataAlloc() returned NULL with NULL psArray input parameter");
+        psFree(fitSetData);
+        pmModelClassCleanup();
+        psFree(modelSet); 
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceFitSetDataAlloc() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < modelSet->n ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSourceFitSetData *set = pmSourceFitSetDataAlloc(modelSet);
+        ok(set != NULL && psMemCheckSourceFitSetData(set),
+          "pmSourceFitSetDataAlloc() returned non-NULL with acceptable input parameters");
+        ok(set->paramSet != NULL && set->paramSet->n == modelSet->n, 
+          "pmSourceFitSetDataAlloc() set the set->paramSet psArray correctly");
+        ok(set->derivSet != NULL && set->derivSet->n == modelSet->n, 
+          "pmSourceFitSetDataAlloc() set the set->derivSet psArray correctly");
+        for (int i = 0 ; i < modelSet->n ; i++) {
+            int nParams = pmModelClassParameterCount(i);
+            psVector *tmpV = (psVector *) (set->paramSet->data[i]);
+            ok(tmpV != NULL && psMemCheckVector(tmpV) && tmpV->n == nParams,
+               "pmSourceFitSetDataAlloc() set the set->paramSet->data psVector correctly");
+
+            tmpV = (psVector *) (set->derivSet->data[i]);
+            ok(tmpV != NULL && psMemCheckVector(tmpV) && tmpV->n == nParams,
+               "pmSourceFitSetDataAlloc() set the set->derivSet->data psVector correctly");
+
+            bool errorFlag = false;
+            for (int i = 0 ; i < tmpV->n ; i++) {
+                if (tmpV->data.F32[i] != 0.0) {
+                    errorFlag = true;
+		}
+	    }
+            ok(!errorFlag, "pmSourceFitSetDataAlloc() set the set->derivSet->data correctly");
+	}
+        
+        psFree(set);
+        pmModelClassCleanup();
+        psFree(modelSet); 
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceFitSetCheckLimits() tests
+    // Call pmSourceFitSetCheckLimits() with thisSet == NULL
+    // XXX: Can not test full functionality of pmSourceFitSetCheckLimits() because it
+    // requires that the static variable thisSet be allocated.
+    {
+        psMemId id = psMemGetId();
+        #define NUM_PARAMS 10
+        psF32 *params = (psF32 *) psAlloc(NUM_PARAMS * sizeof(psF32));
+        psF32 *betas = (psF32 *) psAlloc(NUM_PARAMS * sizeof(psF32));
+        bool rc = pmSourceFitSetCheckLimits(PS_MINIMIZE_PARAM_MIN, 10, params, betas);
+        ok(rc == false, "pmSourceFitSetCheckLimits() returned NULL with thisSet == FALSE");
+        psFree(params);
+        psFree(betas);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceFitSetFunction() tests
+    // Call pmSourceFitSetFunction() with thisSet == NULL
+    // XXX: Can not test full functionality of pmSourceFitSetCheckLimits() because it
+    // requires that the static variable thisSet be allocated.
+    {
+        psMemId id = psMemGetId();
+        #define NUM_PARAMS 10
+        psVector *deriv = psVectorAlloc(NUM_PARAMS, PS_TYPE_F32);
+        psVector *param = psVectorAlloc(NUM_PARAMS, PS_TYPE_F32);
+        psVector *x = psVectorAlloc(NUM_PARAMS, PS_TYPE_F32);
+        psF32 tmpF = pmSourceFitSetFunction(deriv, param, x);
+        ok(isnan(tmpF), "pmSourceFitSetFunction() returned NULL with thisSet == FALSE");
+        psFree(deriv);
+        psFree(param);
+        psFree(x);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceFitSetJoin() tests
+    // Call pmSourceFitSetJoin() with NULL pmSourceFitSetData input parameter
+    {
+        psMemId id = psMemGetId();
+        #define NUM_PARAMS 10
+        psVector *deriv = psVectorAlloc(NUM_PARAMS, PS_TYPE_F32);
+        psVector *param = psVectorAlloc(NUM_PARAMS, PS_TYPE_F32);
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < modelSet->n ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSourceFitSetData *set = pmSourceFitSetDataAlloc(modelSet);
+        bool rc = pmSourceFitSetJoin(deriv, param, NULL);
+        ok(rc == false, "pmSourceFitSetJoin() returned FALSE with NULL pmSourceFitSetData input parameter");
+        psFree(deriv);
+        psFree(param);
+        psFree(modelSet); 
+        psFree(set);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceFitSetJoin() with unequal size set->paramSet and set->derivSet input parameters
+    {
+        psMemId id = psMemGetId();
+        psVector *deriv = psVectorAlloc(1000, PS_TYPE_F32);
+        psVector *param = psVectorAlloc(1000, PS_TYPE_F32);
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < modelSet->n ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSourceFitSetData *set = pmSourceFitSetDataAlloc(modelSet);
+        psFree(set->paramSet);
+        set->paramSet = psArrayAlloc(set->derivSet->n + 1);
+        bool rc = pmSourceFitSetJoin(deriv, param, set);
+        ok(rc == false, "pmSourceFitSetJoin() returned FALSE with unequal size set->paramSet and set->derivSet input parameters");
+        psFree(deriv);
+        psFree(param);
+        psFree(modelSet); 
+        psFree(set);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceFitSetJoin() with deriv and param input psVector too small
+    // XXX: Must add a PS_ASSERT to the source code to detect this
+    if (0) {
+        psMemId id = psMemGetId();
+        psVector *small = psVectorAlloc(1, PS_TYPE_F32);
+        psVector *big = psVectorAlloc(1000, PS_TYPE_F32);
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < modelSet->n ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSourceFitSetData *set = pmSourceFitSetDataAlloc(modelSet);
+        bool rc = pmSourceFitSetJoin(small, big, set);
+        ok(rc == false, "pmSourceFitSetJoin() returned FALSE with deriv input psVector too small");
+        rc = pmSourceFitSetJoin(big, small, set);
+        ok(rc == false, "pmSourceFitSetJoin() returned FALSE with param input psVector too small");
+        psFree(small);
+        psFree(big);
+        psFree(modelSet); 
+        psFree(set);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceFitSetJoin() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        psVector *deriv = psVectorAlloc(1000, PS_TYPE_F32);
+        psVector *param = psVectorAlloc(1000, PS_TYPE_F32);
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < modelSet->n ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSourceFitSetData *set = pmSourceFitSetDataAlloc(modelSet);
+
+        psF32 cnt = 0.0;
+        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++) {
+                paramOne->data.F32[j] = cnt;
+                derivOne->data.F32[j] = cnt;
+                cnt = cnt + 1.0;
+            }
+        }
+
+        bool rc = pmSourceFitSetJoin(deriv, param, set);
+        ok(rc == true, "pmSourceFitSetJoin() returned TRUE acceptable input parameters");
+
+        bool errorFlag = false;
+        for (int i = 0; i < (int) cnt ; i++) {
+            if (deriv->data.F32[i] != (float) i) {
+                diag("ERROR: deriv->data.F32[%d] is %.2ff, should be %.2f\n", i, deriv->data.F32[i], (float) i);
+                errorFlag = true;
+            }
+            if (param->data.F32[i] != (float) i) {
+                diag("ERROR: param->data.F32[%d] is %.2ff, should be %.2f\n", i, param->data.F32[i], (float) i);
+                errorFlag = true;
+            }
+        }
+        ok(!errorFlag, "pmSourceFitSetJoin() set the deriv and param psVectors correctly");
+
+        psFree(deriv);
+        psFree(param);
+        psFree(modelSet); 
+        psFree(set);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceFitSetSplit() tests
+    // Call pmSourceFitSetSplit() with NULL pmSourceFitSetData input parameter
+    {
+        psMemId id = psMemGetId();
+        #define NUM_PARAMS 10
+        psVector *deriv = psVectorAlloc(NUM_PARAMS, PS_TYPE_F32);
+        psVector *param = psVectorAlloc(NUM_PARAMS, PS_TYPE_F32);
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < modelSet->n ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSourceFitSetData *set = pmSourceFitSetDataAlloc(modelSet);
+        bool rc = pmSourceFitSetSplit(NULL, deriv, param);
+        ok(rc == false, "pmSourceFitSetSplit() returned FALSE with NULL pmSourceFitSetData input parameter");
+        psFree(deriv);
+        psFree(param);
+        psFree(modelSet); 
+        psFree(set);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceFitSetSplit() with NULL src->paramSet and src->derivSet input parameters and
+    // src->paramSet and src->derivSet of unequal size
+    {
+        psMemId id = psMemGetId();
+        #define NUM_PARAMS 10
+        psVector *deriv = psVectorAlloc(NUM_PARAMS, PS_TYPE_F32);
+        psVector *param = psVectorAlloc(NUM_PARAMS, PS_TYPE_F32);
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < modelSet->n ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSourceFitSetData *set = pmSourceFitSetDataAlloc(modelSet);
+        psArray *tmpArray = set->paramSet;
+        set->paramSet = NULL;
+        bool rc = pmSourceFitSetSplit(set, deriv, param);
+        ok(rc == false, "pmSourceFitSetSplit() returned FALSE with NULL src->paramSet input parameter");
+        set->paramSet = tmpArray;
+
+        tmpArray = set->derivSet;
+        set->derivSet = NULL;
+        rc = pmSourceFitSetSplit(set, deriv, param);
+        ok(rc == false, "pmSourceFitSetSplit() returned FALSE with NULL src->derivSet input parameter");
+        set->derivSet = tmpArray;
+
+        psFree(set->paramSet);
+        set->paramSet = psArrayAlloc(set->derivSet->n + 1);
+        ok(rc == false, "pmSourceFitSetSplit() returned FALSE with src->paramSet and src->derivSet of unequal size");
+
+        psFree(deriv);
+        psFree(param);
+        psFree(modelSet); 
+        psFree(set);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceFitSetSplit() with NULL param input parameter
+    {
+        psMemId id = psMemGetId();
+        psVector *deriv = psVectorAlloc(1000, PS_TYPE_F32);
+        psVector *param = psVectorAlloc(1000, PS_TYPE_F32);
+        for (int i = 0 ; i < 1000 ; i++) {
+            param->data.F32[i] = (float) i;
+            deriv->data.F32[i] = (float) i;
+        }
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < modelSet->n ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSourceFitSetData *set = pmSourceFitSetDataAlloc(modelSet);
+        bool rc = pmSourceFitSetSplit(set, deriv, NULL);
+        ok(rc == false, "pmSourceFitSetSplit() returned FALSE with NULL param input parameter");
+        psFree(deriv);
+        psFree(param);
+        psFree(modelSet); 
+        psFree(set);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceFitSetSplit() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        psVector *deriv = psVectorAlloc(1000, PS_TYPE_F32);
+        psVector *param = psVectorAlloc(1000, PS_TYPE_F32);
+        for (int i = 0 ; i < 1000 ; i++) {
+            deriv->data.F32[i] = (float) i;
+            param->data.F32[i] = (float) i;
+        }
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < modelSet->n ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSourceFitSetData *set = pmSourceFitSetDataAlloc(modelSet);
+        bool rc = pmSourceFitSetSplit(set, deriv, param);
+        ok(rc == true, "pmSourceFitSetSplit() returned FALSE with acceptable input parameters");
+
+        bool errorFlag = false;
+        psF32 cnt = 0.0;
+        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++) {
+                if (paramOne->data.F32[j] != cnt) {
+                    diag("ERROR: paramOne->data.F32[%d] is %.2ff, should be %.2f\n", i, paramOne->data.F32[i], (float) i);
+                    errorFlag = true;
+                }
+                if (derivOne->data.F32[j] != cnt) {
+                    diag("ERROR: derivOne->data.F32[%d] is %.2ff, should be %.2f\n", i, derivOne->data.F32[i], (float) i);
+                    errorFlag = true;
+                }
+                cnt = cnt + 1.0;
+            }
+        }
+        ok(!errorFlag, "pmSourceFitSetSplit() set the deriv and param psVectors correctly");
+
+        psFree(deriv);
+        psFree(param);
+        psFree(modelSet); 
+        psFree(set);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceFitSetValues() tests
+    // Call pmSourceFitSetValues() with bad input parameters
+    {
+        psMemId id = psMemGetId();
+        #define VEC_SIZE 1000
+        #define NUM_ITER 32
+        #define TOL 0.1
+        psVector *param = psVectorAlloc(VEC_SIZE, PS_TYPE_F32);
+        psVector *dparam = psVectorAlloc(VEC_SIZE, PS_TYPE_F32);
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < modelSet->n ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSourceFitSetData *set = pmSourceFitSetDataAlloc(modelSet);
+        pmSource *src = create_pmSource();
+        psMinimization *myMin = psMinimizationAlloc(NUM_ITER, TOL);
+        int nPix = 10;
+        bool fitStatus = true;
+        // NULL set input parameter
+        bool rc = pmSourceFitSetValues(NULL, dparam, param, src, myMin, nPix, fitStatus);
+        ok(rc == false, "pmSourceFitSetValues() returned FALSE with NULL set input parameter");
+
+        // NULL set->paramSet
+        psArray *tmpArray = set->paramSet;
+        set->paramSet = NULL;
+        rc = pmSourceFitSetValues(set, dparam, param, src, myMin, nPix, fitStatus);
+        ok(rc == false, "pmSourceFitSetValues() returned FALSE with NULL set->paramSet");
+        set->paramSet = tmpArray;
+
+        // NULL dparam input parameter
+        rc = pmSourceFitSetValues(set, NULL, param, src, myMin, nPix, fitStatus);
+        ok(rc == false, "pmSourceFitSetValues() returned FALSE with NULL dparam input parameter");
+
+        // NULL param input parameter
+        rc = pmSourceFitSetValues(set, dparam, NULL, src, myMin, nPix, fitStatus);
+        ok(rc == false, "pmSourceFitSetValues() returned FALSE with NULL param input parameter");
+
+        // NULL pmSource input parameter
+        rc = pmSourceFitSetValues(set, dparam, param, NULL, myMin, nPix, fitStatus);
+        ok(rc == false, "pmSourceFitSetValues() returned FALSE with NULL pmSource input parameter");
+
+        // NULL pmSource->pixels input parameter
+        psImage *tmpImg = src->pixels;
+        src->pixels = NULL;
+        rc = pmSourceFitSetValues(set, dparam, param, src, myMin, nPix, fitStatus);
+        ok(rc == false, "pmSourceFitSetValues() returned FALSE with NULL pmSource->pixels input parameter");
+        src->pixels = tmpImg;
+
+        // NULL psMinimization input parameter
+        rc = pmSourceFitSetValues(set, dparam, param, src, NULL, nPix, fitStatus);
+        ok(rc == false, "pmSourceFitSetValues() returned FALSE with NULL psMinimization input parameter");
+
+        psFree(param);
+        psFree(dparam);
+        psFree(modelSet); 
+        psFree(set);
+        psFree(src);
+        psFree(myMin);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmSourceFitSetValues() tests
+    // Call pmSourceFitSetValues() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        #define VEC_SIZE 1000
+        #define NUM_ITER 32
+        #define TOL 0.1
+        #define MIN_VALUE	22.0
+        #define NUM_PIX 100
+        psVector *param = psVectorAlloc(VEC_SIZE, PS_TYPE_F32);
+        psVector *dparam = psVectorAlloc(VEC_SIZE, PS_TYPE_F32);
+        for (int i = 0 ; i < VEC_SIZE ; i++) {
+            param->data.F32[i] = (float) i;
+            dparam->data.F32[i] = (float) i;
+        }
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < modelSet->n ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSourceFitSetData *set = pmSourceFitSetDataAlloc(modelSet);
+        pmSource *src = create_pmSource();
+        psMinimization *myMin = psMinimizationAlloc(NUM_ITER, TOL);
+        int nPix = NUM_PIX;
+        bool fitStatus = true;
+
+        bool rc = pmSourceFitSetValues(set, dparam, param, src, myMin, nPix, fitStatus);
+        ok(rc == true, "pmSourceFitSetValues() returned TRUE with acceptable input paramaters");
+
+        bool errorFlag = false;
+        psF32 cnt = 0.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++) {
+                if (model->params->data.F32[j] != cnt) {
+                    diag("ERROR: model->params->data.F32[%d] is %.2ff, should be %.2f\n", i, model->params->data.F32[i], (float) i);
+                    errorFlag = true;
+                }
+                if (model->dparams->data.F32[j] != cnt) {
+                    diag("ERROR: model->dparams->data.F32[%d] is %.2ff, should be %.2f\n", i, model->dparams->data.F32[i], (float) i);
+                    errorFlag = true;
+                }
+                if (model->chisq != myMin->value) {
+                    diag("ERROR: model->chisq is %.2f, should be %.2f", model->chisq, MIN_VALUE);
+                    errorFlag = true;
+                }
+                if (model->nIter != myMin->iter) {
+                    diag("ERROR: model->nIter is %.2f, should be %.2f", model->nIter, NUM_ITER);
+                    errorFlag = true;
+                }
+                if (model->nDOF != NUM_PIX - model->params->n) {
+                    diag("ERROR: model->nDOF is %d, should be %d", model->nDOF, NUM_PIX-model->params->n);
+                    errorFlag = true;
+                }
+
+                cnt = cnt + 1.0;
+            }
+        }
+        ok(!errorFlag, "pmSourceFitSetValues() set the deriv and param psVectors correctly");
+
+        psFree(param);
+        psFree(dparam);
+        psFree(modelSet); 
+        psFree(set);
+        psFree(src);
+        psFree(myMin);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceFitSetMasks() tests
+    // Call pmSourceFitSetMasks() with bad input parameters
+    {
+        psMemId id = psMemGetId();
+        #define VEC_SIZE 1000
+        #define NUM_ITER 32
+        #define TOL 0.1
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < modelSet->n ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSourceFitSetData *set = pmSourceFitSetDataAlloc(modelSet);
+        psMinConstraint *constraint = psMinConstraintAlloc();
+
+        // NULL psMinConstraint input parameter
+        bool rc = pmSourceFitSetMasks(NULL, set, PM_SOURCE_FIT_NORM);
+        ok(rc == false, "pmSourceFitSetMasks() returned TRUE with NULL psMinConstraint input parameter");
+
+        // NULL pmSourceFitSetData input parameter
+        rc = pmSourceFitSetMasks(constraint, NULL, PM_SOURCE_FIT_NORM);
+        ok(rc == false, "pmSourceFitSetMasks() returned TRUE with NULL pmSourceFitSetData input parameter");
+
+        psFree(modelSet); 
+        psFree(set);
+        psFree(constraint);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceFitSetMasks() with acceptable input parameters
+    // For thoroughness, we should test the PM_SOURCE_FIT_PSF and PM_SOURCE_FIT_EXT mode
+    {
+        psMemId id = psMemGetId();
+        #define VEC_SIZE 1000
+        #define NUM_ITER 32
+        #define TOL 0.1
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < modelSet->n ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSourceFitSetData *set = pmSourceFitSetDataAlloc(modelSet);
+        psMinConstraint *constraint = psMinConstraintAlloc();
+        constraint->paramMask = psVectorAlloc(1000, PS_TYPE_F32);
+
+        // Acceptable input parameters
+        bool rc = pmSourceFitSetMasks(constraint, set, PM_SOURCE_FIT_NORM);
+        ok(rc == true, "pmSourceFitSetMasks() returned TRUE with acceptable input parameters");
+
+        bool errorFlag = false;
+        int n = 0;
+        for (int i = 0; i < set->paramSet->n; i++) {
+            psVector *paramOne = set->paramSet->data[i];
+            for (int j = 0; j < paramOne->n; j++) {
+                if (j == PM_PAR_I0) continue;
+                if (constraint->paramMask->data.U8[n + j] != 1) {
+                    diag("ERROR: constraint->paramMask->data.U8[%d] is %d, should be a",
+                          n + j, constraint->paramMask->data.U8[n + j]);
+                    errorFlag = true;
+                }
+            }
+            n += paramOne->n;
+        }
+        ok(!errorFlag, "pmSourceFitSetMasks() constraint->paramMask psVector correctly");
+
+        psFree(modelSet); 
+        psFree(set);
+        psFree(constraint);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceFitSet() tests
+    // Call pmSourceFitSet() with NULL psSource input parameter
+    {
+        psMemId id = psMemGetId();
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < NUM_MODELS ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSource *src = create_pmSource();
+        bool rc = pmSourceFitSet(NULL, modelSet, PM_SOURCE_FIT_PSF, 1);
+        ok(rc == false, "pmSourceFitSet() returned FALSE with NULL psSource input parameter");
+        psFree(src);
+        for (int i = 0 ; i < NUM_MODELS ; i++) {
+            psFree(modelSet->data[i]);
+            modelSet->data[i] = NULL;
+        }
+        psFree(modelSet);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceFitSet() with NULL psSource pixels, variance, maskObj input parameters
+    {
+        psMemId id = psMemGetId();
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < NUM_MODELS ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSource *src = create_pmSource();
+        psImage *tmpImg = src->pixels;
+        src->pixels = NULL;
+        bool rc = pmSourceFitSet(src, modelSet, PM_SOURCE_FIT_PSF, 1);
+        ok(rc == false, "pmSourceFitSet() returned FALSE with NULL src->pixels input parameter");
+        src->pixels = tmpImg;
+
+        tmpImg = src->variance;
+        src->variance = NULL;
+        rc = pmSourceFitSet(src, modelSet, PM_SOURCE_FIT_PSF, 1);
+        ok(rc == false, "pmSourceFitSet() returned FALSE with NULL src->variance input parameter");
+        src->variance = tmpImg;
+
+        tmpImg = src->maskObj;
+        src->maskObj = NULL;
+        rc = pmSourceFitSet(src, modelSet, PM_SOURCE_FIT_PSF, 1);
+        ok(rc == false, "pmSourceFitSet() returned FALSE with NULL src->maskObj input parameter");
+        src->maskObj = tmpImg;
+        psFree(src);
+        for (int i = 0 ; i < NUM_MODELS ; i++) {
+            psFree(modelSet->data[i]);
+            modelSet->data[i] = NULL;
+        }
+        psFree(modelSet);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceFitSet() with acceptable input parameters
+    // This is a verly limited test.  It only uses a simple object in the pmSource
+    // parameter and simply tests that pmSourceFitSet() returns teh correct type and mode.
+    // Must add more extensive input types.
+    if (1) {
+        psMemId id = psMemGetId();
+        psArray *modelSet = psArrayAlloc(NUM_MODELS);
+        for (int i = 0 ; i < NUM_MODELS ; i++) {
+            modelSet->data[i] = (psPtr *) pmModelAlloc(i);
+	}
+        pmSource *src = create_pmSource();
+        bool rc = pmSourceFitSet(src, modelSet, PM_SOURCE_FIT_PSF, 1);
+        ok(rc == true, "pmSourceFitSet() returned TRUE with acceptable parameters");
+        ok(src->mode & PM_SOURCE_MODE_FITTED, "pmSourceFitSet() set source->mode |= PM_SOURCE_MODE_FITTED (%d)", src->mode);
+        ok(src->type == PM_SOURCE_TYPE_UNKNOWN, "pmSourceFitSet() set source->type correctly (%d)", src->type);
+
+        psFree(src);
+        for (int i = 0 ; i < NUM_MODELS ; i++) {
+            psFree(modelSet->data[i]);
+            modelSet->data[i] = NULL;
+        }
+        psFree(modelSet);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceIO_PS1_DEV_0.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceIO_PS1_DEV_0.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceIO_PS1_DEV_0.c	(revision 42651)
@@ -0,0 +1,245 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested.
+    XX: These tests read/write a file.  Must choose a more unique name.
+*/
+
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+#define TEST_FLOATS_EQUAL(X, Y) (abs(X - Y) < 0.0001)
+#define NUM_SOURCES		5
+#define FITS_FILENAME  ".tmp00"
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(79);
+
+
+    // ----------------------------------------------------------------------
+    // pmSourcesWrite_PS1_DEV_0() tests
+    // Call pmSourcesWrite_PS1_DEV_0() with NULL psFits input parameter
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(FITS_FILENAME, "w");
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        for (int i = 0 ; i < sources->n ; i++) {
+            pmSource *src = pmSourceAlloc();
+            src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+            src->type = PM_SOURCE_TYPE_STAR;
+            sources->data[i] = (psPtr *) src;
+	}
+        psMetadata *imageHeader = psMetadataAlloc();
+        psMetadata *tableHeader = psMetadataAlloc();
+        psString extname = psStringCopy("ext");
+        bool rc = pmSourcesWrite_PS1_DEV_0(NULL, sources, imageHeader, tableHeader, extname);
+        ok(rc == false, "pmSourcesWrite_PS1_DEV_0() returned FALSE with NULL psFits input parameter");
+        psFree(fitsFile);
+        psFree(sources);
+        psFree(imageHeader);
+        psFree(tableHeader);
+        psFree(extname);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourcesWrite_PS1_DEV_0() with NULL pmSource input parameter
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(FITS_FILENAME, "w");
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        for (int i = 0 ; i < sources->n ; i++) {
+            pmSource *src = pmSourceAlloc();
+            src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+            sources->data[i] = (psPtr *) src;
+	}
+        psMetadata *imageHeader = psMetadataAlloc();
+        psMetadata *tableHeader = psMetadataAlloc();
+        psString extname = psStringCopy("ext");
+        bool rc = pmSourcesWrite_PS1_DEV_0(fitsFile, NULL, imageHeader, tableHeader, extname);
+        ok(rc == false, "pmSourcesWrite_PS1_DEV_0() returned FALSE with NULL pmSource input parameter");
+        psFree(fitsFile);
+        psFree(sources);
+        psFree(imageHeader);
+        psFree(tableHeader);
+        psFree(extname);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourcesWrite_PS1_DEV_0() with NULL extname input parameter
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(FITS_FILENAME, "w");
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        for (int i = 0 ; i < sources->n ; i++) {
+            pmSource *src = pmSourceAlloc();
+            src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+            sources->data[i] = (psPtr *) src;
+	}
+        psMetadata *imageHeader = psMetadataAlloc();
+        psMetadata *tableHeader = psMetadataAlloc();
+        psString extname = psStringCopy("ext");
+        bool rc = pmSourcesWrite_PS1_DEV_0(fitsFile, sources, imageHeader, tableHeader, NULL);
+        ok(rc == false, "pmSourcesWrite_PS1_DEV_0() returned FALSE with NULL extname input parameter");
+        psFree(fitsFile);
+        psFree(sources);
+        psFree(imageHeader);
+        psFree(tableHeader);
+        psFree(extname);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourcesRead_PS1_DEV_0() tests
+    // Call pmSourcesRead_PS1_DEV_0() with NULL psFits input parameter
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(FITS_FILENAME, "r");
+        psMetadata *header = psMetadataAlloc();
+        psArray *array = pmSourcesRead_PS1_DEV_0(NULL, header);
+        ok(array == NULL, "pmSourcesRead_PS1_DEV_0() returned NULL with NULL psFits input parameter");
+        psFree(fitsFile);
+        psFree(header);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourcesRead_PS1_DEV_0() with NULL header input parameter
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(FITS_FILENAME, "r");
+        psMetadata *header = psMetadataAlloc();
+        psArray *array = pmSourcesRead_PS1_DEV_0(fitsFile, NULL);
+        ok(array == NULL, "pmSourcesRead_PS1_DEV_0() returned NULL with NULL header input parameter");
+        psFree(fitsFile);
+        psFree(header);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // Call pmSourcesWrite_PS1_DEV_0() with acceptable input parameters
+    #define TEST_BASE_X_POS		10.0
+    #define TEST_BASE_Y_POS		20.0
+    #define TEST_BASE_X_ERR		30.0
+    #define TEST_BASE_Y_ERR		40.0
+    #define TEST_BASE_PSF_MAG	50.0
+    #define TEST_BASE_ERR_MAG	60.0
+    #define TEST_BASE_SKY		70.0
+    #define TEST_BASE_SKY_ERR	80.0
+    #define TEST_BASE_PIX_WEIGHT	90.0
+    #define TEST_BASE_PEAK_FLUX	120.0
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(FITS_FILENAME, "w");
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        for (int i = 0 ; i < sources->n ; i++) {
+            pmSource *src = pmSourceAlloc();
+            src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+            // Set even numbered sources to PM_SOURCE_TYPE_STAR
+            pmModel *model = NULL;
+            if (i%2) {
+                src->type = PM_SOURCE_TYPE_STAR;
+                src->modelPSF = pmModelAlloc(1);
+                model = src->modelPSF;
+	    //} else {
+            //    src->type = PM_SOURCE_TYPE_EXTENDED;
+            //    src->modelConv = pmModelAlloc(1);
+            //    model = src->modelConv;
+ 	    //
+	    //}
+	    }
+            for (int p = 0 ; p < model->params->n ; p++) {
+                model->params->data.F32[p] = (float) (i + p);
+                model->dparams->data.F32[p] = (float) (i + p);
+	    }
+            model->params->data.F32[PM_PAR_XPOS] = TEST_BASE_X_POS + (float) i;
+            model->params->data.F32[PM_PAR_YPOS] = TEST_BASE_Y_POS + (float) i;
+            model->dparams->data.F32[PM_PAR_XPOS] = TEST_BASE_X_ERR + (float) i;
+            model->dparams->data.F32[PM_PAR_YPOS] = TEST_BASE_Y_ERR + (float) i;
+            src->psfMag = TEST_BASE_PSF_MAG + (float) i;
+            src->psfMagErr = TEST_BASE_ERR_MAG + (float) i;
+            src->peak->flux = TEST_BASE_PEAK_FLUX + (float) i;
+            src->sky = TEST_BASE_SKY + (float) i;
+            src->skyErr = TEST_BASE_SKY_ERR + (float) i;
+            src->pixWeight = TEST_BASE_PIX_WEIGHT + (float) i;
+            sources->data[i] = (psPtr *) src;
+	}
+        psMetadata *imageHeader = psMetadataAlloc();
+        psMetadata *tableHeader = psMetadataAlloc();
+        psString extname = psStringCopy("ext");
+        bool rc = pmSourcesWrite_PS1_DEV_0(fitsFile, sources, imageHeader, tableHeader, extname);
+        ok(rc == true, "pmSourcesWrite_PS1_DEV_0() returned TRUE with acceptable input parameters");
+        psFree(fitsFile);
+        psFree(sources);
+        psFree(imageHeader);
+        psFree(tableHeader);
+        psFree(extname);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourcesRead_PS1_DEV_0() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(FITS_FILENAME, "r");
+        psMetadata *header = psMetadataAlloc();
+        psArray *array = pmSourcesRead_PS1_DEV_0(fitsFile, header);
+        ok(array != NULL, "pmSourcesRead_PS1_DEV_0() returned non-NULL with acceptable input parameters");
+        skip_start(array == NULL, 1, "Skipping tests because pmSourcesRead_PS1_DEV_0() returned NULL");
+        for (int i = 0 ; i < array->n ; i++) {
+             pmSource *src = (pmSource *) array->data[i];
+             ok(src != NULL && psMemCheckSource(src), "pmSourcesRead_PS1_DEV_0() read source %d correctly", i);
+
+             // XXX: Source code always sets the type to PM_SOURCE_TYPE_STAR.  Is that right?
+             ok(src->type == PM_SOURCE_TYPE_STAR, "pmSourcesRead_PS1_DEV_0() set the source type correctly (is %d, should be %d)",
+                src->type, PM_SOURCE_TYPE_STAR);
+
+             ok(src->sky == (TEST_BASE_SKY + (float) i), "pmSourcesRead_PS1_DEV_0() set src->sky correctly (is %.2f, should be %.2f)",
+                src->sky, (TEST_BASE_SKY + (float) i));
+             ok(src->skyErr == (TEST_BASE_SKY_ERR + (float) i), "pmSourcesRead_PS1_DEV_0() set src->skyErr correctly (is %.2f, should be %.2f)",
+                src->skyErr, (TEST_BASE_SKY_ERR + (float) i));
+             ok(src->pixWeight == (TEST_BASE_PIX_WEIGHT + (float) i), "pmSourcesRead_PS1_DEV_0() set src->pixWeight correctly (is %.2f, should be %.2f)",
+                src->pixWeight, (TEST_BASE_PIX_WEIGHT + (float) i));
+             ok(TEST_FLOATS_EQUAL(src->peak->flux, (TEST_BASE_PEAK_FLUX + (float) i)), "pmSourcesRead_PS1_DEV_0() set src->peak->flux correctly (is %.2f, should be %.2f)",
+                src->peak->flux, (TEST_BASE_PEAK_FLUX + (float) i));
+             ok(src->psfMag == (TEST_BASE_PSF_MAG + (float) i), "pmSourcesRead_PS1_DEV_0() set src->psfMag correctly (is %.2f, should be %.2f)",
+                src->psfMag, (TEST_BASE_PSF_MAG + (float) i));
+             ok(src->psfMagErr == (TEST_BASE_ERR_MAG + (float) i), "pmSourcesRead_PS1_DEV_0() set src->psfMagErr correctly (is %.2f, should be %.2f)",
+                src->psfMagErr, (TEST_BASE_ERR_MAG + (float) i));
+
+             // XXX: Source code always sets src->modelPSF.  Is that right?
+             pmModel *model = src->modelPSF;
+             ok(model != NULL  && psMemCheckModel(model), "pmSourcesRead_PS1_DEV_0() set src->modelPSF correctly");
+             skip_start(model == NULL, 2, "Skipping tests because pmSourcesRead_PS1_DEV_0() did not set src->modelPSF");
+             ok(model->params->data.F32[PM_PAR_XPOS] == (TEST_BASE_X_POS + (float) i),
+               "pmSourcesRead_PS1_DEV_0() set src->model->params->data.F32[PM_PAR_XPOS] correctly (is %.2f, should be %.2f)",
+                model->params->data.F32[PM_PAR_XPOS], (TEST_BASE_X_POS + (float) i));
+             ok(model->params->data.F32[PM_PAR_YPOS] == (TEST_BASE_Y_POS + (float) i),
+               "pmSourcesRead_PS1_DEV_0() set src->model->params->data.F32[PM_PAR_YPOS] correctly (is %.2f, should be %.2f)",
+                model->params->data.F32[PM_PAR_YPOS], (TEST_BASE_Y_POS + (float) i));
+             ok(model->dparams->data.F32[PM_PAR_XPOS] == (TEST_BASE_X_ERR + (float) i),
+               "pmSourcesRead_PS1_DEV_0() set src->model->dparams->data.F32[PM_PAR_XPOS] correctly (is %.2f, should be %.2f)",
+                model->dparams->data.F32[PM_PAR_XPOS], (TEST_BASE_X_ERR + (float) i));
+             ok(model->dparams->data.F32[PM_PAR_YPOS] == (TEST_BASE_Y_ERR + (float) i),
+               "pmSourcesRead_PS1_DEV_0() set src->model->dparams->data.F32[PM_PAR_YPOS] correctly (is %.2f, should be %.2f)",
+                model->dparams->data.F32[PM_PAR_YPOS], (TEST_BASE_Y_ERR + (float) i));
+             skip_end();
+	}
+        skip_end();
+        psFree(fitsFile);
+        psFree(header);
+        psFree(array);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceIO_PS1_DEV_1.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceIO_PS1_DEV_1.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceIO_PS1_DEV_1.c	(revision 42651)
@@ -0,0 +1,310 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    No test for pmSourcesWrite_PS1_DEV_1_XSRC() since there is no associated
+        read function.
+    All other functions are tested.
+    XX: These tests read/write a file.  Must choose a more unique name.
+*/
+
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           (8)
+#define TEST_NUM_COLS           (16)
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+#define TEST_FLOATS_EQUAL(X, Y) (abs(X - Y) < 0.0001)
+#define NUM_SOURCES		5
+#define TABLE_FILENAME	"table.fits"
+const char* tableFilename = "table.fits";
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(79);
+
+
+    // ----------------------------------------------------------------------
+    // pmSourcesWrite_PS1_DEV_1() tests
+    // Call pmSourcesWrite_PS1_DEV_1() with NULL psFits input parameter
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(TABLE_FILENAME, "w");
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        for (int i = 0 ; i < sources->n ; i++) {
+            pmSource *src = pmSourceAlloc();
+            src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+            src->type = PM_SOURCE_TYPE_STAR;
+            sources->data[i] = (psPtr *) src;
+	}
+        psMetadata *imageHeader = psMetadataAlloc();
+        psMetadata *tableHeader = psMetadataAlloc();
+        psString extname = psStringCopy("ext");
+        bool rc = pmSourcesWrite_PS1_DEV_1(NULL, sources, imageHeader, tableHeader, extname);
+        ok(rc == false, "pmSourcesWrite_PS1_DEV_1() returned FALSE with NULL psFits input parameter");
+        psFree(fitsFile);
+        psFree(sources);
+        psFree(imageHeader);
+        psFree(tableHeader);
+        psFree(extname);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourcesWrite_PS1_DEV_1() with NULL pmSource input parameter
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(TABLE_FILENAME, "w");
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        for (int i = 0 ; i < sources->n ; i++) {
+            pmSource *src = pmSourceAlloc();
+            src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+            sources->data[i] = (psPtr *) src;
+	}
+        psMetadata *imageHeader = psMetadataAlloc();
+        psMetadata *tableHeader = psMetadataAlloc();
+        psString extname = psStringCopy("ext");
+        bool rc = pmSourcesWrite_PS1_DEV_1(fitsFile, NULL, imageHeader, tableHeader, extname);
+        ok(rc == false, "pmSourcesWrite_PS1_DEV_1() returned FALSE with NULL pmSource input parameter");
+        psFree(fitsFile);
+        psFree(sources);
+        psFree(imageHeader);
+        psFree(tableHeader);
+        psFree(extname);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourcesWrite_PS1_DEV_1() with NULL extname input parameter
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(TABLE_FILENAME, "w");
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        for (int i = 0 ; i < sources->n ; i++) {
+            pmSource *src = pmSourceAlloc();
+            src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+            sources->data[i] = (psPtr *) src;
+	}
+        psMetadata *imageHeader = psMetadataAlloc();
+        psMetadata *tableHeader = psMetadataAlloc();
+        psString extname = psStringCopy("ext");
+        bool rc = pmSourcesWrite_PS1_DEV_1(fitsFile, sources, imageHeader, tableHeader, NULL);
+        ok(rc == false, "pmSourcesWrite_PS1_DEV_1() returned FALSE with NULL extname input parameter");
+        psFree(fitsFile);
+        psFree(sources);
+        psFree(imageHeader);
+        psFree(tableHeader);
+        psFree(extname);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourcesRead_PS1_DEV_1() tests
+    // Call pmSourcesRead_PS1_DEV_1() with NULL psFits input parameter
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(TABLE_FILENAME, "r");
+        psMetadata *header = psMetadataAlloc();
+        psArray *array = pmSourcesRead_PS1_DEV_1(NULL, header);
+        ok(array == NULL, "pmSourcesRead_PS1_DEV_1() returned NULL with NULL psFits input parameter");
+        psFree(fitsFile);
+        psFree(header);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourcesRead_PS1_DEV_1() with NULL header input parameter
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(TABLE_FILENAME, "r");
+        psMetadata *header = psMetadataAlloc();
+        psArray *array = pmSourcesRead_PS1_DEV_1(fitsFile, NULL);
+        ok(array == NULL, "pmSourcesRead_PS1_DEV_1() returned NULL with NULL header input parameter");
+        psFree(fitsFile);
+        psFree(header);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // Call pmSourcesWrite_PS1_DEV_1() with acceptable input parameters
+    #define TEST_BASE_X_POS             10.0
+    #define TEST_BASE_Y_POS             20.0
+    #define TEST_BASE_X_ERR             30.0
+    #define TEST_BASE_Y_ERR             40.0
+    #define TEST_BASE_PSF_MAG		50.0
+    #define TEST_BASE_ERR_MAG		60.0
+    #define TEST_BASE_SKY               500.0
+    #define TEST_BASE_SKY_ERR		80.0
+    #define TEST_BASE_PIX_WEIGHT        90.0
+    #define TEST_BASE_PEAK_FLUX		120.0
+    #define TEST_BASE_PSF_PROB		150.0
+    #define TEST_BASE_CR_N_SIGMA	160.0
+    #define TEST_BASE_EXT_N_SIGMA       170.0   
+    #define TEST_BASE_MODE		1
+
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(TABLE_FILENAME, "w");
+        if (fitsFile == NULL) {
+            diag("ERROR: Could not create 'table' FITS file");
+            return false;
+        }
+
+        if (1) {
+            // make the PHU an image (per FITS standard, it must be)
+            psImage* image = psImageAlloc(16, 16, PS_TYPE_F32);
+            if (!psFitsWriteImage(fitsFile, NULL, image, 1, NULL)) {
+                diag("ERROR: Could not write PHU image");
+                return false;
+            }
+            psFree(image);
+        }
+
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        for (int i = 0 ; i < sources->n ; i++) {
+            pmSource *src = pmSourceAlloc();
+            src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+            // Set even numbered sources to PM_SOURCE_TYPE_STAR
+            pmModel *model = NULL;
+            if (i%2) {
+                src->type = PM_SOURCE_TYPE_STAR;
+                src->modelPSF = pmModelAlloc(1);
+                model = src->modelPSF;
+	    //} else {
+            //    src->type = PM_SOURCE_TYPE_EXTENDED;
+            //    src->modelConv = pmModelAlloc(1);
+            //    model = src->modelConv;
+	    //}
+	    }
+            for (int p = 0 ; p < model->params->n ; p++) {
+                model->params->data.F32[p] = (float) (i + p);
+                model->dparams->data.F32[p] = (float) (i + p);
+	    }
+
+            model->params->data.F32[PM_PAR_XPOS] = TEST_BASE_X_POS + (float) i;
+            model->params->data.F32[PM_PAR_YPOS] = TEST_BASE_Y_POS + (float) i;
+            model->dparams->data.F32[PM_PAR_XPOS] = TEST_BASE_X_ERR + (float) i;
+            model->dparams->data.F32[PM_PAR_YPOS] = TEST_BASE_Y_ERR + (float) i;
+            src->psfMag = TEST_BASE_PSF_MAG + (float) i;
+            src->psfMagErr = TEST_BASE_ERR_MAG + (float) i;
+            src->peak->flux = TEST_BASE_PEAK_FLUX + (float) i;
+            src->sky = TEST_BASE_SKY + (float) i;
+            src->skyErr = TEST_BASE_SKY_ERR + (float) i;
+            src->pixWeight = TEST_BASE_PIX_WEIGHT + (float) i;
+            sources->data[i] = (psPtr *) src;
+            src->psfMag = TEST_BASE_PSF_MAG + (float) i;
+            src->psfMagErr = TEST_BASE_ERR_MAG + (float) i;
+            src->sky = TEST_BASE_SKY + (float) i;
+            src->skyErr = TEST_BASE_SKY_ERR + (float) i;
+//            src->psfProb = TEST_BASE_PSF_PROB + (float) i;
+            src->crNsigma = TEST_BASE_CR_N_SIGMA + (float) i;
+            src->extNsigma = TEST_BASE_EXT_N_SIGMA + (float) i;
+            src->mode = TEST_BASE_MODE + i;
+            src->peak->SN = (float) (10 - i);
+            sources->data[i] = (psPtr *) src;
+	}
+        psMetadata *imageHeader = psMetadataAlloc();
+        psMetadata *tableHeader = psMetadataAlloc();
+        psString extname = psStringCopy("ext");
+        bool rc = pmSourcesWrite_PS1_DEV_1(fitsFile, sources, imageHeader, tableHeader, extname);
+        ok(rc == true, "pmSourcesWrite_PS1_DEV_1() returned TRUE with acceptable input parameters");
+        psFree(fitsFile);
+        psFree(sources);
+        psFree(imageHeader);
+        psFree(tableHeader);
+        psFree(extname);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourcesRead_PS1_DEV_1() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(tableFilename, "rw");
+        if (fitsFile == NULL) {
+            diag("ERROR: Could not create 'table' FITS file");
+            return false;
+        }
+
+        // XXX: I'm not exactly sure why, but without this, the psFitsReadTableSize() call
+        // in pmSourcesRead_PS1_DEV_1() fails.  However, Robert also did this in psFits.
+        if (1) {
+            psFitsMoveExtNum(fitsFile, 1, false);
+	}
+
+        // XX: Debugging purposes only.  Trying to duplicate the call to
+        // psFitsTableRead() from DEV_0
+        if (0) {
+            psArray *table = psFitsReadTable(fitsFile);
+            if (table == NULL) {
+                printf("ERROR: table is NULL\n");
+                exit(0);
+	    }
+            for (int i = 0; i < table->n; i++) {
+                psMetadata *row = table->data[i];
+                float sky = psMetadataLookupF32(NULL, row, "SKY");
+                printf("For row %d, the psFitsReadTable() produces a sky of %.2f\n", i, sky);
+	    }
+	}
+
+        psMetadata *header = psMetadataAlloc();
+        psArray *array = pmSourcesRead_PS1_DEV_1(fitsFile, header);
+        ok(array != NULL, "pmSourcesRead_PS1_DEV_1() returned non-NULL with acceptable input parameters");
+        skip_start(array == NULL, 1, "Skipping tests because pmSourcesRead_PS1_DEV_1() returned NULL");
+        for (int i = 0 ; i < array->n ; i++) {
+             pmSource *src = (pmSource *) array->data[i];
+             ok(src != NULL && psMemCheckSource(src), "pmSourcesRead_PS1_DEV_1() read source %d correctly", i);
+
+             // XXX: Source code always sets the type to PM_SOURCE_TYPE_STAR.  Is that right?
+             ok(src->type == PM_SOURCE_TYPE_STAR, "pmSourcesRead_PS1_DEV_1() set the source type correctly (is %d, should be %d)",
+                src->type, PM_SOURCE_TYPE_STAR);
+
+             ok(src->sky == (TEST_BASE_SKY + (float) i), "pmSourcesRead_PS1_DEV_1() set src->sky correctly (is %.2f, should be %.2f)",
+                src->sky, (TEST_BASE_SKY + (float) i));
+             ok(src->skyErr == (TEST_BASE_SKY_ERR + (float) i), "pmSourcesRead_PS1_DEV_1() set src->skyErr correctly (is %.2f, should be %.2f)",
+                src->skyErr, (TEST_BASE_SKY_ERR + (float) i));
+             ok(src->pixWeight == (TEST_BASE_PIX_WEIGHT + (float) i), "pmSourcesRead_PS1_DEV_1() set src->pixWeight correctly (is %.2f, should be %.2f)",
+                src->pixWeight, (TEST_BASE_PIX_WEIGHT + (float) i));
+             ok(TEST_FLOATS_EQUAL(src->peak->flux, (TEST_BASE_PEAK_FLUX + (float) i)), "pmSourcesRead_PS1_DEV_1() set src->peak->flux correctly (is %.2f, should be %.2f)",
+                src->peak->flux, (TEST_BASE_PEAK_FLUX + (float) i));
+             ok(src->psfMag == (TEST_BASE_PSF_MAG + (float) i), "pmSourcesRead_PS1_DEV_1() set src->psfMag correctly (is %.2f, should be %.2f)",
+                src->psfMag, (TEST_BASE_PSF_MAG + (float) i));
+             ok(src->psfMagErr == (TEST_BASE_ERR_MAG + (float) i), "pmSourcesRead_PS1_DEV_1() set src->psfMagErr correctly (is %.2f, should be %.2f)",
+                src->psfMagErr, (TEST_BASE_ERR_MAG + (float) i));
+
+             // XXX: Source code always sets src->modelPSF.  Is that right?
+             pmModel *model = src->modelPSF;
+             ok(model != NULL  && psMemCheckModel(model), "pmSourcesRead_PS1_DEV_1() set src->modelPSF correctly");
+             skip_start(model == NULL, 2, "Skipping tests because pmSourcesRead_PS1_DEV_1() did not set src->modelPSF");
+             ok(model->params->data.F32[PM_PAR_XPOS] == (TEST_BASE_X_POS + (float) i),
+               "pmSourcesRead_PS1_DEV_1() set src->model->params->data.F32[PM_PAR_XPOS] correctly (is %.2f, should be %.2f)",
+                model->params->data.F32[PM_PAR_XPOS], (TEST_BASE_X_POS + (float) i));
+             ok(model->params->data.F32[PM_PAR_YPOS] == (TEST_BASE_Y_POS + (float) i),
+               "pmSourcesRead_PS1_DEV_1() set src->model->params->data.F32[PM_PAR_YPOS] correctly (is %.2f, should be %.2f)",
+                model->params->data.F32[PM_PAR_YPOS], (TEST_BASE_Y_POS + (float) i));
+             ok(model->dparams->data.F32[PM_PAR_XPOS] == (TEST_BASE_X_ERR + (float) i),
+               "pmSourcesRead_PS1_DEV_1() set src->model->dparams->data.F32[PM_PAR_XPOS] correctly (is %.2f, should be %.2f)",
+                model->dparams->data.F32[PM_PAR_XPOS], (TEST_BASE_X_ERR + (float) i));
+             ok(model->dparams->data.F32[PM_PAR_YPOS] == (TEST_BASE_Y_ERR + (float) i),
+               "pmSourcesRead_PS1_DEV_1() set src->model->dparams->data.F32[PM_PAR_YPOS] correctly (is %.2f, should be %.2f)",
+                model->dparams->data.F32[PM_PAR_YPOS], (TEST_BASE_Y_ERR + (float) i));
+             skip_end();
+	}
+        skip_end();
+        psFree(fitsFile);
+        psFree(header);
+        psFree(array);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceIO_SMPDATA.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceIO_SMPDATA.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceIO_SMPDATA.c	(revision 42651)
@@ -0,0 +1,277 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested.
+*/
+
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+#define TEST_FLOATS_EQUAL(X, Y) (abs(X - Y) < 0.0001)
+#define NUM_SOURCES		5
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(69);
+
+
+    // ----------------------------------------------------------------------
+    // pmSourcesWrite_SMPDATA() tests
+    // Call pmSourcesWrite_SMPDATA() with NULL psFits input parameter
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(".tmp00", "w");
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        for (int i = 0 ; i < sources->n ; i++) {
+            pmSource *src = pmSourceAlloc();
+            src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+            src->type = PM_SOURCE_TYPE_STAR;
+            sources->data[i] = (psPtr *) src;
+	}
+        psMetadata *imageHeader = psMetadataAlloc();
+        psMetadata *tableHeader = psMetadataAlloc();
+        psString extname = psStringCopy("ext");
+        bool rc = pmSourcesWrite_SMPDATA(NULL, sources, imageHeader, tableHeader, extname);
+        ok(rc == false, "pmSourcesWrite_SMPDATA() returned FALSE with NULL psFits input parameter");
+        psFree(fitsFile);
+        psFree(sources);
+        psFree(imageHeader);
+        psFree(tableHeader);
+        psFree(extname);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourcesWrite_SMPDATA() with NULL pmSource input parameter
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(".tmp00", "w");
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        for (int i = 0 ; i < sources->n ; i++) {
+            pmSource *src = pmSourceAlloc();
+            src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+            sources->data[i] = (psPtr *) src;
+	}
+        psMetadata *imageHeader = psMetadataAlloc();
+        psMetadata *tableHeader = psMetadataAlloc();
+        psString extname = psStringCopy("ext");
+        bool rc = pmSourcesWrite_SMPDATA(fitsFile, NULL, imageHeader, tableHeader, extname);
+        ok(rc == false, "pmSourcesWrite_SMPDATA() returned FALSE with NULL pmSource input parameter");
+        psFree(fitsFile);
+        psFree(sources);
+        psFree(imageHeader);
+        psFree(tableHeader);
+        psFree(extname);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourcesWrite_SMPDATA() with NULL extname input parameter
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(".tmp00", "w");
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        for (int i = 0 ; i < sources->n ; i++) {
+            pmSource *src = pmSourceAlloc();
+            src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+            sources->data[i] = (psPtr *) src;
+	}
+        psMetadata *imageHeader = psMetadataAlloc();
+        psMetadata *tableHeader = psMetadataAlloc();
+        psString extname = psStringCopy("ext");
+        bool rc = pmSourcesWrite_SMPDATA(fitsFile, sources, imageHeader, tableHeader, NULL);
+        ok(rc == false, "pmSourcesWrite_SMPDATA() returned FALSE with NULL extname input parameter");
+        psFree(fitsFile);
+        psFree(sources);
+        psFree(imageHeader);
+        psFree(tableHeader);
+        psFree(extname);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourcesRead_SMPDATA() tests
+    // Call pmSourcesRead_SMPDATA() with NULL psFits input parameter
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(".tmp00", "r");
+        psMetadata *header = psMetadataAlloc();
+        psArray *array = pmSourcesRead_SMPDATA(NULL, header);
+        ok(array == NULL, "pmSourcesRead_SMPDATA() returned NULL with NULL psFits input parameter");
+        psFree(fitsFile);
+        psFree(header);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourcesRead_SMPDATA() with NULL header input parameter
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(".tmp00", "r");
+        psMetadata *header = psMetadataAlloc();
+        psArray *array = pmSourcesRead_SMPDATA(fitsFile, NULL);
+        ok(array == NULL, "pmSourcesRead_SMPDATA() returned NULL with NULL header input parameter");
+        psFree(fitsFile);
+        psFree(header);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // Call pmSourcesWrite_SMPDATA() with acceptable input parameters
+    #define TEST_BASE_X_POS		10.0
+    #define TEST_BASE_Y_POS		20.0
+    #define TEST_BASE_X_ERR		30.0
+    #define TEST_BASE_Y_ERR		40.0
+    #define TEST_BASE_PSF_MAG		50.0
+    #define TEST_BASE_ERR_MAG		60.0
+    #define TEST_BASE_SKY		70.0
+    #define TEST_BASE_SKY_ERR		80.0
+    #define TEST_BASE_PIX_WEIGHT	90.0
+    #define TEST_BASE_PEAK_FLUX		120.0
+    #define TEST_BASE_EXT_MAG		150.0
+    #define TEST_BASE_AP_MAG		160.0
+    // XXX: The following metadata items are not tested: FWHM_X, FWHM_Y, THETA
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(".tmp00", "w");
+        psArray *sources = psArrayAlloc(NUM_SOURCES);
+        for (int i = 0 ; i < sources->n ; i++) {
+            pmSource *src = pmSourceAlloc();
+            src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+            // Set even numbered sources to PM_SOURCE_TYPE_STAR
+            pmModel *model = NULL;
+            if (i%2) {
+                src->type = PM_SOURCE_TYPE_STAR;
+                src->modelPSF = pmModelAlloc(1);
+                model = src->modelPSF;
+	    //} else {
+            //    src->type = PM_SOURCE_TYPE_EXTENDED;
+            //    src->modelConv = pmModelAlloc(1);
+            //    model = src->modelConv;
+ 	    //}
+	    }
+            for (int p = 0 ; p < model->params->n ; p++) {
+                model->params->data.F32[p] = (float) (i + p);
+                model->dparams->data.F32[p] = (float) (i + p);
+	    }
+            model->params->data.F32[PM_PAR_XPOS] = TEST_BASE_X_POS + (float) i;
+            model->params->data.F32[PM_PAR_YPOS] = TEST_BASE_Y_POS + (float) i;
+            model->dparams->data.F32[PM_PAR_XPOS] = TEST_BASE_X_ERR + (float) i;
+            model->dparams->data.F32[PM_PAR_YPOS] = TEST_BASE_Y_ERR + (float) i;
+            src->psfMag = TEST_BASE_PSF_MAG + (float) i;
+            src->psfMagErr = TEST_BASE_ERR_MAG + (float) i;
+            src->peak->flux = TEST_BASE_PEAK_FLUX + (float) i;
+            src->sky = TEST_BASE_SKY + (float) i;
+            src->skyErr = TEST_BASE_SKY_ERR + (float) i;
+            src->pixWeight = TEST_BASE_PIX_WEIGHT + (float) i;
+            src->extMag = TEST_BASE_EXT_MAG + (float) i;
+            src->apMag = TEST_BASE_AP_MAG + (float) i;
+            sources->data[i] = (psPtr *) src;
+	}
+        psMetadata *imageHeader = psMetadataAlloc();
+        psMetadata *tableHeader = psMetadataAlloc();
+        psString extname = psStringCopy("ext");
+        bool rc = pmSourcesWrite_SMPDATA(fitsFile, sources, imageHeader, tableHeader, extname);
+        ok(rc == true, "pmSourcesWrite_SMPDATA() returned TRUE with acceptable input parameters");
+        psFree(fitsFile);
+        psFree(sources);
+        psFree(imageHeader);
+        psFree(tableHeader);
+        psFree(extname);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Call pmSourcesRead_SMPDATA() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen(".tmp00", "r");
+        float ZERO_POINT = 25.0;
+        psMetadata *header = psMetadataAlloc();
+        psArray *array = pmSourcesRead_SMPDATA(fitsFile, header);
+        ok(array != NULL, "pmSourcesRead_SMPDATA() returned non-NULL with acceptable input parameters");
+        skip_start(array == NULL, 1, "Skipping tests because pmSourcesRead_SMPDATA() returned NULL");
+        for (int i = 0 ; i < array->n ; i++) {
+             pmSource *src = (pmSource *) array->data[i];
+             ok(src != NULL && psMemCheckSource(src), "pmSourcesRead_SMPDATA() read source %d correctly", i);
+
+             // src->model data
+             pmModel *model = src->modelPSF;
+             ok(model != NULL  && psMemCheckModel(model), "pmSourcesRead_SMPDATA() set src->modelPSF correctly");
+             skip_start(model == NULL, 2, "Skipping tests because pmSourcesRead_SMPDATA() did not set src->modelPSF");
+             {
+                 ok(model->params->data.F32[PM_PAR_XPOS] == (TEST_BASE_X_POS + (float) i),
+                   "pmSourcesRead_SMPDATA() set src->model->params->data.F32[PM_PAR_XPOS] correctly (is %.2f, should be %.2f)",
+                    model->params->data.F32[PM_PAR_XPOS], (TEST_BASE_X_POS + (float) i));
+
+                 ok(model->params->data.F32[PM_PAR_YPOS] == (TEST_BASE_Y_POS + (float) i),
+                   "pmSourcesRead_SMPDATA() set src->model->params->data.F32[PM_PAR_YPOS] correctly (is %.2f, should be %.2f)",
+                    model->params->data.F32[PM_PAR_YPOS], (TEST_BASE_Y_POS + (float) i));
+
+                 float tmpSrcSky = TEST_BASE_SKY + (float) i;
+                 float lsky = (tmpSrcSky < 1.0) ? 0.0 : log10(tmpSrcSky);
+                 float tmpF = pow(10.0, lsky);
+                 ok(model->params->data.F32[PM_PAR_SKY] == tmpF,
+                   "pmSourcesRead_SMPDATA() set src->model->params->data.F32[PM_PAR_SKY] correctly (is %.2f, should be %.2f)",
+                    model->params->data.F32[PM_PAR_SKY], tmpF);
+	     }
+             ok(src->psfMag == (TEST_BASE_PSF_MAG + (float) i), "pmSourcesRead_SMPDATA() set src->psfMag correctly (is %.2f, should be %.2f)",
+                src->psfMag, (TEST_BASE_PSF_MAG + (float) i));
+             float tmpF =  0.001 * PS_MIN(999, (1000 * (TEST_BASE_ERR_MAG + (float) i)));
+             ok(src->psfMagErr == tmpF, "pmSourcesRead_SMPDATA() set src->psfMagErr correctly (is %.2f, should be %.2f)",
+                src->psfMagErr, tmpF);
+             tmpF = PS_MIN(99.0, (TEST_BASE_EXT_MAG + ZERO_POINT)) - ZERO_POINT;
+             ok(src->extMag == tmpF, "pmSourcesRead_SMPDATA() set src->extMag correctly (is %.2f, should be %.2f)",
+                src->extMag, tmpF);
+             tmpF = PS_MIN(99.0, (TEST_BASE_AP_MAG + ZERO_POINT)) - ZERO_POINT;
+             ok(src->apMag == tmpF, "pmSourcesRead_SMPDATA() set src->apMag correctly (is %.2f, should be %.2f)",
+                src->apMag, tmpF);
+             if (i%2) {
+                 ok(src->type == PM_SOURCE_TYPE_STAR, "pmSourcesRead_SMPDATA() set the source type correctly (is %d, should be %d)",
+                    src->type, PM_SOURCE_TYPE_STAR);
+	     } else {
+                 ok(src->type == PM_SOURCE_TYPE_EXTENDED, "pmSourcesRead_SMPDATA() set the source type correctly (is %d, should be %d)",
+                    src->type, PM_SOURCE_TYPE_EXTENDED);
+	     }
+             psU8 tmpU8 = (psU8) PS_MIN(255, PS_MAX(0, (255*(TEST_BASE_PIX_WEIGHT + (float) i))));
+             tmpF = (psF32) (tmpU8 / 255.0);
+             ok(src->pixWeight == tmpF, "pmSourcesRead_SMPDATA() set src->pixWeight correctly (is %.2f, should be %.2f)",
+                src->pixWeight, tmpF);
+
+             skip_end();
+
+             if (0) { // OLD
+                 // XXX: Source code always sets the type to PM_SOURCE_TYPE_STAR.  Is that right?
+                 ok(src->sky == (TEST_BASE_SKY + (float) i), "pmSourcesRead_SMPDATA() set src->sky correctly (is %.2f, should be %.2f)",
+                    src->sky, (TEST_BASE_SKY + (float) i));
+                 if (0) {
+                     ok(src->pixWeight == (TEST_BASE_PIX_WEIGHT + (float) i), "pmSourcesRead_SMPDATA() set src->pixWeight correctly (is %.2f, should be %.2f)",
+                        src->pixWeight, (TEST_BASE_PIX_WEIGHT + (float) i));
+		 }
+                 ok(TEST_FLOATS_EQUAL(src->peak->flux, 0.0), "pmSourcesRead_SMPDATA() set src->peak->flux correctly (is %.2f, should be %.2f)",
+                    src->peak->flux, 0.0);
+                 // XXX: Source code always sets src->modelPSF.  Is that right?
+                 ok(model->dparams->data.F32[PM_PAR_XPOS] == (TEST_BASE_X_ERR + (float) i),
+                   "pmSourcesRead_SMPDATA() set src->model->dparams->data.F32[PM_PAR_XPOS] correctly (is %.2f, should be %.2f)",
+                    model->dparams->data.F32[PM_PAR_XPOS], (TEST_BASE_X_ERR + (float) i));
+                 ok(model->dparams->data.F32[PM_PAR_YPOS] == (TEST_BASE_Y_ERR + (float) i),
+                   "pmSourcesRead_SMPDATA() set src->model->dparams->data.F32[PM_PAR_YPOS] correctly (is %.2f, should be %.2f)",
+                    model->dparams->data.F32[PM_PAR_YPOS], (TEST_BASE_Y_ERR + (float) i));
+	     }
+	}
+        skip_end();
+        psFree(fitsFile);
+        psFree(header);
+        psFree(array);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceMoments.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceMoments.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceMoments.c	(revision 42651)
@@ -0,0 +1,370 @@
+#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
+
+# define GAIN 1.0
+# define RDNOISE 10.0
+
+float Gaussian(float Io, float sigma, float Xc, float Yc, int ix, int iy);
+bool MakeGaussian (pmSource *source, float Io, float sigma, float Xc, float Yc);
+bool MakeGaussianNoNoise (pmSource *source, float Io, float sigma, float Xc, float Yc);
+double ppSimRandomGaussianNorm (void);
+double ppSimRandomGaussian (double mean, double sigma);
+
+bool GaussiansWindowsAndTopHatsNoNoise (pmSource *source, float Io, float sigma, float Xc, float Yc);
+bool GaussiansWindowsAndTopHats (pmSource *source, float Io, float sigma, float Xc, float Yc);
+bool CentroidWithGuessOffset (pmSource *source, float Io, float sigma, float Xc, float Yc, float dX, float dY);
+bool CentroidWithGuessOffsetIterate (pmSource *source, float Io, float sigma, float Xc, float Yc, float dX, float dY);
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(70);
+
+    // ----------------------------------------------------------------------
+    // pmSourceMomentsGetCentroid() tests (no noise)
+    {
+        psMemId id = psMemGetId();
+
+	// generate a sample source (no noise)
+	pmSource *source = pmSourceAlloc();
+        ok(source, "source allocated");
+	
+	bool status = true;
+
+	// need to have: peak, pixels, variance, 
+	int Nx = 100;
+	int Ny = 100;
+	float Xc = 52.0;
+	float Yc = 48.0;
+	float Io = 100.0;
+	float sigma = 4.0 / 2.35;
+
+	source->peak = pmPeakAlloc(Xc, Yc, Io, PM_PEAK_LONE);
+	source->moments = pmMomentsAlloc();
+	source->pixels = psImageAlloc(Nx, Ny, PS_TYPE_F32);
+	source->variance = psImageAlloc(Nx, Ny, PS_TYPE_F32);
+	
+	// CentroidWithGuessOffset(source, Io, sigma, Xc, Yc, 0.0, 0.0);
+	// CentroidWithGuessOffset(source, Io, sigma, Xc, Yc, 0.1, 0.0);
+	// CentroidWithGuessOffset(source, Io, sigma, Xc, Yc, 0.0, 0.1);
+	// CentroidWithGuessOffset(source, Io, sigma, Xc, Yc, 0.1, 0.1);
+	// CentroidWithGuessOffset(source, Io, sigma, Xc, Yc, 0.3, 0.3);
+	// CentroidWithGuessOffset(source, Io, sigma, Xc, Yc, 0.5, 0.5);
+
+	psVector *dX = psVectorAlloc(100, PS_TYPE_F32);
+	psVector *dY = psVectorAlloc(100, PS_TYPE_F32);
+
+	for (int i = 0; i < dX->n; i++) {
+	    CentroidWithGuessOffsetIterate(source, Io, sigma, Xc, Yc, 1.0, 0.0);
+	    dX->data.F32[i] = Xc - source->moments->Mx;
+	    dY->data.F32[i] = Yc - source->moments->My;
+	}
+	psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+
+	psVectorStats (stats, dX, NULL, 0, 0);
+	fprintf (stderr, "dX stats: %f +/- %f\n", stats->sampleMean, stats->sampleStdev);
+	psStatsInit(stats);
+	
+	psVectorStats (stats, dY, NULL, 0, 0);
+	fprintf (stderr, "dY stats: %f +/- %f\n", stats->sampleMean, stats->sampleStdev);
+	psStatsInit(stats);
+	
+	ok(status, "measured centroid");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // ----------------------------------------------------------------------
+    // pmSourceMomentsGetCentroid() tests (standard noise)
+    if (0) {
+        psMemId id = psMemGetId();
+
+	// generate a sample source (no noise)
+	pmSource *source = pmSourceAlloc();
+        ok(source, "source allocated");
+	
+	bool status = true;
+
+	// need to have: peak, pixels, variance, 
+	int Nx = 100;
+	int Ny = 100;
+	float Xc = 52.0;
+	float Yc = 48.0;
+	float Io = 100000.0;
+	float sigma = 4.0 / 2.35;
+
+	source->peak = pmPeakAlloc(Xc, Yc, Io, PM_PEAK_LONE);
+	source->moments = pmMomentsAlloc();
+	source->pixels = psImageAlloc(Nx, Ny, PS_TYPE_F32);
+	source->variance = psImageAlloc(Nx, Ny, PS_TYPE_F32);
+	
+	GaussiansWindowsAndTopHats(source, Io, sigma, Xc + 0.0, Yc + 0.0);
+
+	ok(status, "measured centroid (window)");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
+bool CentroidWithGuessOffsetIterate (pmSource *source, float Io, float sigma, float Xc, float Yc, float dX, float dY) {
+
+    // pmSourceMomentsGetCentroid(source, radius, sigma, minSN, maskVal)
+    source->peak->xf = Xc + dX;
+    source->peak->yf = Yc + dY;
+
+    // MakeGaussianNoNoise (source, Io, sigma, Xc, Yc);
+    MakeGaussian (source, Io, sigma, Xc, Yc);
+
+    // pmSourceMomentsGetCentroid(source, 10.0, 15.0 / 2.35, 0.0, 0);
+    pmSourceMomentsGetCentroid(source, 8.0, 0.0, 0.0, 0);
+    source->peak->xf = source->moments->Mx;
+    source->peak->yf = source->moments->My;
+    // fprintf (stderr, "%f,%f vs %f,%f : %f,%f -> %f,%f (SN = %f) ", Xc, Yc, source->moments->Mx, source->moments->My, dX, dY, Xc - source->moments->Mx, Yc - source->moments->My, source->moments->SN);
+
+    pmSourceMomentsGetCentroid(source, 10.0, 8.0/2.35, 0.0, 0);
+    // fprintf (stderr, "  ---> %f,%f\n", Xc - source->moments->Mx, Yc - source->moments->My);
+    return true;
+}
+
+bool CentroidWithGuessOffset (pmSource *source, float Io, float sigma, float Xc, float Yc, float dX, float dY) {
+
+    // pmSourceMomentsGetCentroid(source, radius, sigma, minSN, maskVal)
+    source->peak->xf = Xc + dX;
+    source->peak->yf = Yc + dY;
+
+    // MakeGaussianNoNoise (source, Io, sigma, Xc, Yc);
+    MakeGaussian (source, Io, sigma, Xc, Yc);
+
+    // pmSourceMomentsGetCentroid(source, 10.0, 15.0 / 2.35, 0.0, 0);
+    pmSourceMomentsGetCentroid(source, 8.0, 0.0, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f : %f,%f -> %f,%f (SN = %f)\n", Xc, Yc, source->moments->Mx, source->moments->My, dX, dY, Xc - source->moments->Mx, Yc - source->moments->My, source->moments->SN);
+    return true;
+}
+
+bool GaussiansWindowsAndTopHatsNoNoise (pmSource *source, float Io, float sigma, float Xc, float Yc) {
+
+    // pmSourceMomentsGetCentroid(source, radius, sigma, minSN, maskVal)
+    source->peak->xf = Xc;
+    source->peak->yf = Yc;
+
+    MakeGaussianNoNoise (source, Io, sigma, Xc, Yc);
+    pmSourceMomentsGetCentroid(source, 8.0, 0.0, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    pmSourceMomentsGetCentroid(source, 10.0, 0.0, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    pmSourceMomentsGetCentroid(source, 15.0, 0.0, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    pmSourceMomentsGetCentroid(source, 20.0, 0.0, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    ok(true, "measured centroid (tophat)");
+
+    pmSourceMomentsGetCentroid(source,  8.0, 8.0 / 2.35, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    pmSourceMomentsGetCentroid(source, 10.0, 8.0 / 2.35, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    pmSourceMomentsGetCentroid(source, 15.0, 8.0 / 2.35, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    pmSourceMomentsGetCentroid(source, 20.0, 8.0 / 2.35, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    ok(true, "measured centroid (window)");
+    return true;
+}
+
+bool GaussiansWindowsAndTopHats (pmSource *source, float Io, float sigma, float Xc, float Yc) {
+
+    // pmSourceMomentsGetCentroid(source, radius, sigma, minSN, maskVal)
+    source->peak->xf = Xc;
+    source->peak->yf = Yc;
+
+    MakeGaussian (source, Io, sigma, Xc, Yc);
+    pmSourceMomentsGetCentroid(source, 8.0, 0.0, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    pmSourceMomentsGetCentroid(source, 10.0, 0.0, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    pmSourceMomentsGetCentroid(source, 15.0, 0.0, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    pmSourceMomentsGetCentroid(source, 20.0, 0.0, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    ok(true, "measured centroid (tophat)");
+
+    pmSourceMomentsGetCentroid(source,  8.0, 8.0 / 2.35, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    pmSourceMomentsGetCentroid(source, 10.0, 8.0 / 2.35, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    pmSourceMomentsGetCentroid(source, 15.0, 8.0 / 2.35, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    pmSourceMomentsGetCentroid(source, 20.0, 8.0 / 2.35, 0.0, 0);
+    fprintf (stderr, "%f,%f vs %f,%f = %f,%f\n", Xc, Yc, source->moments->Mx, source->moments->My, Xc - source->moments->Mx, Yc - source->moments->My);
+
+    ok(true, "measured centroid (window)");
+    return true;
+}
+
+bool MakeGaussianNoNoise (pmSource *source, float Io, float sigma, float Xc, float Yc) {
+
+  psImageInit(source->pixels, 0.0);
+  psImageInit(source->variance, 0.0);
+
+  // populate the pixels with an object (Gaussian with Io, sigma)
+  for (int iy = 0; iy < source->pixels->numRows; iy++) {
+    for (int ix = 0; ix < source->pixels->numCols; ix++) {
+      source->pixels->data.F32[iy][ix] = Gaussian(Io * GAIN, sigma, Xc, Yc, ix, iy);
+      source->variance->data.F32[iy][ix] = source->pixels->data.F32[iy][ix] + PS_SQR(RDNOISE); // RDNOISE and initial signal in electrons
+      source->pixels->data.F32[iy][ix] /= GAIN;
+      source->variance->data.F32[iy][ix] /= GAIN;
+    }
+  }
+  return true;
+}
+
+bool MakeGaussian (pmSource *source, float Io, float sigma, float Xc, float Yc) {
+
+  psImageInit(source->pixels, 0.0);
+  psImageInit(source->variance, 0.0);
+
+  // populate the pixels with an object (Gaussian with Io, sigma)
+  for (int iy = 0; iy < source->pixels->numRows; iy++) {
+    for (int ix = 0; ix < source->pixels->numCols; ix++) {
+      source->pixels->data.F32[iy][ix] = Gaussian(Io * GAIN, sigma, Xc, Yc, ix, iy);
+      source->variance->data.F32[iy][ix] = source->pixels->data.F32[iy][ix] + PS_SQR(RDNOISE); // RDNOISE and initial signal in electrons
+      source->pixels->data.F32[iy][ix] += sqrtf(source->variance->data.F32[iy][ix]) * ppSimRandomGaussianNorm();
+      source->pixels->data.F32[iy][ix] /= GAIN;
+      source->variance->data.F32[iy][ix] /= GAIN;
+    }
+  }
+  return true;
+}
+
+float Gaussian(float Io, float sigma, float Xc, float Yc, int ix, int iy) {
+  
+  float radius2 = PS_SQR(ix + 0.5 - Xc) + PS_SQR(iy + 0.5 - Yc);
+  float exponent = -0.5 * radius2 / PS_SQR(sigma);
+
+  float value = Io * exp(exponent);
+  return value;
+}
+
+/// **** this stuff should be in psLib -- it is too useful...
+
+static int Ngaussint = 0;
+static double *gaussint = NULL;
+
+extern double drand48();
+
+double p_ppSimGaussian (double x, double mean, double sigma) {
+
+  double f;
+
+  f = exp (-0.5 * PS_SQR(x - mean) / PS_SQR(sigma)) / sqrt(2 * M_PI * PS_SQR(sigma));
+
+  return (f);
+
+}
+
+void ppSimRandomGaussianFree()
+{
+    psFree (gaussint);
+    return;
+}
+
+void ppSimRandomGaussianAlloc (int Nbin) {
+
+    gaussint = (double *) psAlloc(Nbin*sizeof(double));
+    return;
+}
+
+/* integrate a gaussian from -5 sigma to +5 sigma */
+void p_ppSimRandomGaussianInit (void) {
+
+  int i;
+  long A, B;
+  double val, x, dx, dx1, dx2, dx3, df;
+  double mean, sigma;
+
+  /* no need to generate this if it already exists */
+  if (gaussint) return;
+
+  A = time(NULL);
+  for (B = 0; A == time(NULL); B++);
+  srand48(B);
+
+  Ngaussint = 0x1000;
+  ppSimRandomGaussianAlloc (Ngaussint + 1);
+
+  val = 0;
+  dx = 1.0 / Ngaussint;
+  dx1 = dx / 3.0;
+  dx2 = 2.0*dx/3.0;
+  dx3 = dx;
+  mean = 0.0;
+  sigma = 1.0;
+
+  for (i = 0, x = -7.0; (i < Ngaussint) && (x < 7.0); x += dx)  {
+    df = (3.0*p_ppSimGaussian(x    , mean, sigma) +
+          9.0*p_ppSimGaussian(x+dx1, mean, sigma) +
+          9.0*p_ppSimGaussian(x+dx2, mean, sigma) +
+          3.0*p_ppSimGaussian(x+dx3, mean, sigma)) * (dx1/8.0);
+    val += df;
+    if (val > (i + 0.5) / (double) Ngaussint) {
+      gaussint[i] = x + dx / 2.0;
+      i++;
+    }
+  }
+}
+
+// XXX we are using drand48() rather than the random var supplied by rnd
+double ppSimRandomGaussian (double mean, double sigma) {
+
+  int i;
+  double y;
+
+  if (gaussint == NULL) {
+      p_ppSimRandomGaussianInit ();
+  }
+
+  y = drand48();
+  i = Ngaussint*y;
+  y = gaussint[i]*sigma + mean;
+
+  return (y);
+
+}
+
+// XXX we are using drand48() rather than the random var supplied by rnd
+double ppSimRandomGaussianNorm (void) {
+
+  int i;
+  double y;
+
+  if (gaussint == NULL) {
+      p_ppSimRandomGaussianInit ();
+  }
+
+  y = drand48();
+  i = Ngaussint*y;
+  y = gaussint[i];
+
+  return (y);
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourcePhotometry.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourcePhotometry.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourcePhotometry.c	(revision 42651)
@@ -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->apRadius = 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->psfMagErr,   0.001, "source psfMagErr is %f", source->psfMagErr);
+    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_branches/psModules.20240412/test/objects/tap_pmSourceSky.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceSky.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceSky.c	(revision 42651)
@@ -0,0 +1,267 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested.
+        pmSourceLocalSky(): needs more thorough testing with acceptable input params.
+        pmSourceLocalSkyVariance(): needs more thorough testing with acceptable input params.
+*/
+
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           (8)
+#define TEST_NUM_COLS           (16)
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+#define TEST_FLOATS_EQUAL(X, Y) (abs(X - Y) < 0.0001)
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(26);
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceLocalSky() tests
+    // Call pmSourceLocalSky() with NULL pmSource input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->pixels = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        for (int i =0 ; i < src->pixels->numRows ; i++) {
+            for (int j = 0 ; j < src->pixels->numCols ; j++) {
+                src->pixels->data.F32[i][j] = (float) (i + j);
+	    }
+	}
+        src->maskObj = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        bool rc = pmSourceLocalSky(NULL, PS_STAT_SAMPLE_MEAN, 10.0, 1, 2);
+        ok(rc == false, "pmSourceLocalSky() returned FALSE with NULL pmSource input parameter");
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceLocalSky() with NULL pmSource->pixels input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->maskObj = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        bool rc = pmSourceLocalSky(src, PS_STAT_SAMPLE_MEAN, 10.0, 1, 2);
+        ok(rc == false, "pmSourceLocalSky() returned FALSE with NULL pmSource->pixels input parameter");
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceLocalSky() with NULL pmSource->peak input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->pixels = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        for (int i =0 ; i < src->pixels->numRows ; i++) {
+            for (int j = 0 ; j < src->pixels->numCols ; j++) {
+                src->pixels->data.F32[i][j] = (float) (i + j);
+	    }
+	}
+        src->maskObj = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        bool rc = pmSourceLocalSky(src, PS_STAT_SAMPLE_MEAN, 10.0, 1, 2);
+        ok(rc == false, "pmSourceLocalSky() returned FALSE with NULL pmSource->peak input parameter");
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceLocalSky() with NULL pmSource->maskObj input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->pixels = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        for (int i =0 ; i < src->pixels->numRows ; i++) {
+            for (int j = 0 ; j < src->pixels->numCols ; j++) {
+                src->pixels->data.F32[i][j] = (float) (i + j);
+	    }
+	}
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        bool rc = pmSourceLocalSky(src, PS_STAT_SAMPLE_MEAN, -10.0, 1, 2);
+        ok(rc == false, "pmSourceLocalSky() returned FALSE with NULL pmSource->maskObj input parameter");
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceLocalSky() with negative input radius
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->pixels = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        for (int i =0 ; i < src->pixels->numRows ; i++) {
+            for (int j = 0 ; j < src->pixels->numCols ; j++) {
+                src->pixels->data.F32[i][j] = (float) (i + j);
+	    }
+	}
+        src->maskObj = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        bool rc = pmSourceLocalSky(src, PS_STAT_SAMPLE_MEAN, -10.0, 1, 2);
+        ok(rc == false, "pmSourceLocalSky() returned FALSE with negative input radius");
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    
+    // Call pmSourceLocalSky() with acceptable input parameters
+    // XX: Future Improvements:
+    //     Test more PS_STATS types
+    //     Test more psRegion values (region bigger than image, 0 region, etc.)
+    //     Test mask values
+    //
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->pixels = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        src->maskObj = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        psF32 mean = 0.0;
+        for (int i = 0 ; i < src->pixels->numRows ; i++) {
+            for (int j = 0 ; j < src->pixels->numCols ; j++) {
+                src->pixels->data.F32[i][j] = (float) (i + j);
+                mean+= (float) (i + j);
+                src->maskObj->data.U8[i][j] = 0;
+	    }
+	}
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        bool rc = pmSourceLocalSky(src, PS_STAT_SAMPLE_MEAN, 10.0, 0, 0);
+        ok(rc == true, "pmSourceLocalSky() returned TRUE with acceptable input parameters");
+        psF32 actualMean =  mean / (int) (TEST_NUM_ROWS * TEST_NUM_COLS);
+        psF32 testMean = src->moments->Sky;
+        ok(TEST_FLOATS_EQUAL(actualMean, testMean), "pmSourceLocalSky() calculated the mean correctly");
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceLocalSkyVariance() tests
+    // Call pmSourceLocalSkyVariance() with NULL pmSource input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->variance = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        for (int i =0 ; i < src->variance->numRows ; i++) {
+            for (int j = 0 ; j < src->variance->numCols ; j++) {
+                src->variance->data.F32[i][j] = (float) (i + j);
+	    }
+	}
+        src->maskObj = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        bool rc = pmSourceLocalSkyVariance(NULL, PS_STAT_SAMPLE_MEAN, 10.0, 1, 2);
+        ok(rc == false, "pmSourceLocalSkyVariance() returned FALSE with NULL pmSource input parameter");
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceLocalSkyVariance() with NULL pmSource->variance input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->maskObj = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        bool rc = pmSourceLocalSkyVariance(src, PS_STAT_SAMPLE_MEAN, 10.0, 1, 2);
+        ok(rc == false, "pmSourceLocalSkyVariance() returned FALSE with NULL pmSource->variance input parameter");
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceLocalSkyVariance() with NULL pmSource->maskObj input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->variance = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        for (int i =0 ; i < src->variance->numRows ; i++) {
+            for (int j = 0 ; j < src->variance->numCols ; j++) {
+                src->variance->data.F32[i][j] = (float) (i + j);
+	    }
+	}
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        bool rc = pmSourceLocalSkyVariance(src, PS_STAT_SAMPLE_MEAN, -10.0, 1, 2);
+        ok(rc == false, "pmSourceLocalSkyVariance() returned FALSE with NULL pmSource->maskObj input parameter");
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceLocalSkyVariance() with NULL pmSource->peak input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->variance = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        for (int i =0 ; i < src->variance->numRows ; i++) {
+            for (int j = 0 ; j < src->variance->numCols ; j++) {
+                src->variance->data.F32[i][j] = (float) (i + j);
+	    }
+	}
+        src->maskObj = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        bool rc = pmSourceLocalSkyVariance(src, PS_STAT_SAMPLE_MEAN, 10.0, 1, 2);
+        ok(rc == false, "pmSourceLocalSkyVariance() returned FALSE with NULL pmSource->peak input parameter");
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceLocalSkyVariance() with negative input radius
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->variance = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        for (int i =0 ; i < src->variance->numRows ; i++) {
+            for (int j = 0 ; j < src->variance->numCols ; j++) {
+                src->variance->data.F32[i][j] = (float) (i + j);
+	    }
+	}
+        src->maskObj = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        bool rc = pmSourceLocalSkyVariance(src, PS_STAT_SAMPLE_MEAN, -10.0, 1, 2);
+        ok(rc == false, "pmSourceLocalSkyVariance() returned FALSE with negative input radius");
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceLocalSkyVariance() with acceptable input parameters
+    // XX: Future Improvements:
+    //     Test more PS_STATS types
+    //     Test more psRegion values (region bigger than image, 0 region, etc.)
+    //     Test mask values
+    //
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->variance = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        src->maskObj = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        psF32 mean = 0.0;
+        for (int i = 0 ; i < src->variance->numRows ; i++) {
+            for (int j = 0 ; j < src->variance->numCols ; j++) {
+                src->variance->data.F32[i][j] = (float) (i + j);
+                mean+= (float) (i + j);
+                src->maskObj->data.U8[i][j] = 0;
+	    }
+	}
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        bool rc = pmSourceLocalSkyVariance(src, PS_STAT_SAMPLE_MEAN, 10.0, 0, 0);
+        ok(rc == true, "pmSourceLocalSkyVariance() returned TRUE with acceptable input parameters");
+        psF32 actualMean =  mean / (int) (TEST_NUM_ROWS * TEST_NUM_COLS);
+        psF32 testMean = src->moments->dSky;
+        ok(TEST_FLOATS_EQUAL(actualMean, testMean), "pmSourceLocalSky() calculated the mean correctly");
+        psFree(src);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceUtils.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceUtils.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmSourceUtils.c	(revision 42651)
@@ -0,0 +1,285 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS
+    All functions are tested.
+       pmSourceFromModel(): Must verify the pmSourceDefinePixels() set the
+           values correctly.
+*/
+
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           (8)
+#define TEST_NUM_COLS           (16)
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+#define TEST_FLOATS_EQUAL(X, Y) (abs((X) - (Y)) < 0.0001)
+#define NUM_SOURCES		100
+
+#define CELL_ALLOC_NAME        "CellName"
+#define NUM_READOUTS            3
+#define NUM_CELLS               10
+#define NUM_HDUS                5
+#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);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->variance = 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);
+    }
+
+    // First try to read data from ../dataFiles, then try dataFiles.
+    bool rc = pmConfigFileRead(&cell->hdu->format, "../dataFiles/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        rc = pmConfigFileRead(&cell->hdu->format, "dataFiles/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->variances = 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->variances->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->variances->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    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);
+    psTraceSetLevel("psModules.objects", 0);
+    plan_tests(23);
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceModelGuess() tests
+    // Call pmSourceModelGuess() with NULL pmSource input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        src->moments = pmMomentsAlloc();
+        pmModelType type = pmModelClassGetType ("PS_MODEL_GAUSS");
+        pmModel *model = pmSourceModelGuess(NULL, type);
+        ok(model == NULL, "pmSourceModelGuess() returned NULL with NULL pmSource input parameter");
+        psFree(src);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceModelGuess() with NULL pmSource->peak input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->moments = pmMomentsAlloc();
+        pmModelType type = pmModelClassGetType ("PS_MODEL_GAUSS");
+        pmModel *model = pmSourceModelGuess(NULL, type);
+        ok(model == NULL, "pmSourceModelGuess() returned NULL with NULL pmSource->peak input parameter");
+        psFree(src);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceModelGuess() with NULL pmSource->moments input parameter
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        pmModelType type = pmModelClassGetType ("PS_MODEL_GAUSS");
+        pmModel *model = pmSourceModelGuess(NULL, type);
+        ok(model == NULL, "pmSourceModelGuess() returned NULL with NULL pmSource->moments input parameter");
+        psFree(src);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmModel *pmSourceModelGuess(pmSource *source, pmModelType modelType)
+    // Call pmSourceModelGuess() with acceptable input parameters
+    // We only test a single model (PS_MODEL_GAUSS), but since this function is mostly
+    // a wrapper to the model functions, that will suffice.
+    {
+        psMemId id = psMemGetId();
+        pmSource *src = pmSourceAlloc();
+        src->peak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        src->moments = pmMomentsAlloc();
+
+        src->moments->Mx = 1.0;
+        src->moments->My = 1.0;
+        src->moments->Mxx = 2.0;
+        src->moments->Mxy= 2.0;
+        src->moments->Myy = 2.0;
+        src->moments->Mxxx = 3.0;
+        src->moments->Mxxy = 3.0;
+        src->moments->Mxyy = 3.0;
+        src->moments->Myyy = 3.0;
+        src->moments->Mxxxx = 4.0;
+        src->moments->Mxxxy = 4.0;
+        src->moments->Mxxyy = 4.0;
+        src->moments->Mxyyy = 4.0;
+        src->moments->Myyyy = 4.0;
+        src->moments->Sum = 2.0;
+        src->moments->Peak = 3.0;
+        src->moments->Sky = 4.0;
+        src->moments->dSky = 5.0;
+        src->moments->SN = 6.0;
+        src->moments->nPixels = 7.0;
+
+        pmModelType type = pmModelClassGetType("PS_MODEL_GAUSS");
+        pmModel *testModel = pmModelAlloc(type);
+        testModel->modelGuess(testModel, src);
+        pmModel *model = pmSourceModelGuess(src, type);
+        ok(model != NULL, "pmSourceModelGuess() returned non-NULL with acceptable input parameters");
+        psF32 *PAR  = model->params->data.F32;
+        psEllipseMoments emoments;
+        emoments.x2 = src->moments->Mx;
+        emoments.y2 = src->moments->My;
+        emoments.xy = src->moments->Mxy;
+        // force the axis ratio to be < 20.0
+        psEllipseAxes axes = psEllipseMomentsToAxes (emoments, 20.0);
+        psEllipseShape shape = psEllipseAxesToShape (axes);
+        ok(TEST_FLOATS_EQUAL(PAR[PM_PAR_SKY], src->moments->Sky), "pmSourceModelGuess() returned set model->params[PM_PAR_SKY] correctly");
+        ok(TEST_FLOATS_EQUAL(PAR[PM_PAR_I0], src->moments->Peak - src->moments->Sky), "pmSourceModelGuess() returned set model->params[PM_PAR_IO] correctly");
+        ok(TEST_FLOATS_EQUAL(PAR[PM_PAR_XPOS], src->moments->Mx), "pmSourceModelGuess() returned set model->params[PM_PAR_XPOS] correctly");
+        ok(TEST_FLOATS_EQUAL(PAR[PM_PAR_YPOS], src->moments->My), "pmSourceModelGuess() returned set model->params[PM_PAR_YPOS] correctly");
+        ok(TEST_FLOATS_EQUAL(PAR[PM_PAR_SXX], PS_MAX(0.5, M_SQRT2*shape.sx)), "pmSourceModelGuess() returned set model->params[PM_PAR_SXX] correctly");
+        ok(TEST_FLOATS_EQUAL(PAR[PM_PAR_SYY], PS_MAX(0.5, M_SQRT2*shape.sy)), "pmSourceModelGuess() returned set model->params[PM_PAR_SYY] correctly");
+        ok(TEST_FLOATS_EQUAL(PAR[PM_PAR_SXY], shape.sxy), "pmSourceModelGuess() returned set model->params[PM_PAR_SXY] correctly");
+        psFree(src);
+        psFree(testModel);
+        psFree(model);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmSourceModelGuess() tests
+    // Call pmSourceFromModel() with NULL pmModel input parameter
+    {
+        psMemId id = psMemGetId();
+        pmModelType type = pmModelClassGetType ("PS_MODEL_GAUSS");
+        pmModel *model = pmModelAlloc(type);
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *readout = cell->readouts->data[0];
+        pmSource *src = pmSourceFromModel(NULL, readout, 10.0, PM_SOURCE_TYPE_STAR);
+        ok(src == NULL, "pmSourceFromModel() returned NULL with NULL pmModel input parameter");
+        psFree(model);
+        psFree(src);
+        myFreeCell(cell);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceFromModel() with NULL pmReadout input parameter
+    {
+        psMemId id = psMemGetId();
+
+        pmModelType type = pmModelClassGetType ("PS_MODEL_GAUSS");
+        pmModel *model = pmModelAlloc(type);
+        pmCell *cell = generateSimpleCell(NULL);
+        pmSource *src = pmSourceFromModel(model, NULL, 10.0, PM_SOURCE_TYPE_STAR);
+        ok(src == NULL, "pmSourceFromModel() returned NULL with NULL pmReadout input parameter");
+        psFree(model);
+        psFree(src);
+        myFreeCell(cell);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmSourceFromModel() with acceptable input parameters
+    // XXX: Must verify the pmSourceDefinePixels() set the values correctly.
+    {
+        psMemId id = psMemGetId();
+
+        pmModel *model = pmModelAlloc(pmModelClassGetType("PS_MODEL_GAUSS"));
+        float Io    = model->params->data.F32[PM_PAR_I0] = 2.0;
+        float xChip = model->params->data.F32[PM_PAR_XPOS] = 3.0;
+        float yChip = model->params->data.F32[PM_PAR_YPOS] = 5.0;
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *readout = cell->readouts->data[0];
+        pmSource *src = pmSourceFromModel(model, readout, 10.0, PM_SOURCE_TYPE_STAR);
+        ok(src != NULL, "pmSourceFromModel() returned non-NULL with acceptable input parameters");
+        ok(src->modelPSF == model, "pmSourceFromModel() set pmSource->modelPSF correctly");
+
+        pmPeak *tmpPeak = pmPeakAlloc (xChip, yChip, Io, PM_PEAK_LONE);
+        ok(src->peak->x == xChip, "pmSourceFromModel() set pmSource->peak->x correctly (%.2f %.2f)", src->peak->x, xChip);
+
+        psFree(model);
+        // XXX: We get psMemory aborts if the following is not done.
+        // There is probably an issue with psMemIncrRefCounter() in pmSourceUtils.c
+
+        src->modelPSF = NULL;
+        src->modelEXT = NULL;
+        psFree(src);
+        psFree(tmpPeak);
+        myFreeCell(cell);
+        pmModelClassCleanup();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tap_pmTrend2D.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tap_pmTrend2D.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tap_pmTrend2D.c	(revision 42651)
@@ -0,0 +1,509 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* STATUS:
+    All functions are tested.
+        pmTrend2DFit(): Must test the PM_TREND_MAP case.
+*/
+
+#define NUM_ROWS 8
+#define NUM_COLS 16
+#define ERR_TRACE_LEVEL 0
+#define TEST_FLOATS_EQUAL(X, Y) (abs(X - Y) < 0.01)
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(91);
+
+    // ------------------------------------------------------------------------
+    // Test pmTrend2DAlloc()
+    // Call pmTrend2DAlloc() with NULL psImage input parameter (PM_TREND_MAP)
+    {
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+        pmTrend2D *trend = pmTrend2DAlloc(PM_TREND_MAP, NULL, 2, 2, stats);
+        ok(trend == NULL, "pmTrend2DAlloc() returned NULL with NULL psImage input parameter");
+        psFree(img);
+        psFree(stats);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmTrend2DAlloc() with NULL psImage input parameter (PM_TREND_POLY_ORD)
+    {
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+        pmTrend2D *trend = pmTrend2DAlloc(PM_TREND_POLY_ORD, NULL, 2, 2, stats);
+        ok(trend != NULL, "pmTrend2DAlloc() returned NULL with NULL psImage input parameter");
+        psFree(img);
+        psFree(stats);
+        psFree(trend);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmTrend2DAlloc() with NULL psStats input parameter
+    {
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+        pmTrend2D *trend = pmTrend2DAlloc(PM_TREND_POLY_ORD, img, 2, 2, NULL);
+        ok(trend == NULL, "pmTrend2DAlloc() returned NULL with NULL psStats input parameter");
+        psFree(img);
+        psFree(stats);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmTrend2DAlloc() with unallowed pmTrend2DMode
+    // XXX: We skip this test because pmTrend2DAlloc() aborts.
+    if (0) {
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+        pmTrend2D *trend = pmTrend2DAlloc(999, img, 2, 2, stats);
+        ok(trend == NULL, "pmTrend2DAlloc() returned NULL with unallowed pmTrend2DMode");
+        psFree(img);
+        psFree(stats);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmTrend2DAlloc() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+
+        // Call pmTrend2DAlloc() with PM_TREND_POLY_ORD
+        pmTrend2D *trend = pmTrend2DAlloc(PM_TREND_POLY_ORD, img, 2, 4, stats);
+        ok(trend != NULL && psMemCheckTrend2D(trend), 
+          "pmTrend2DAlloc() returned non-NULL with acceptable input parameters");
+        ok(trend->map == NULL, "pmTrend2DAlloc() set trend->map to NULL");
+        ok(trend->mode == PM_TREND_POLY_ORD, "pmTrend2DAlloc() set trend->mode correctly");
+        ok(trend->stats == stats, "pmTrend2DAlloc() set trend->stats correctly");
+        ok(trend->poly != NULL && trend->poly->type == PS_POLYNOMIAL_ORD, "pmTrend2DAlloc() set trend->poly->type correctly");
+        ok(trend->poly->type == PS_POLYNOMIAL_ORD, "pmTrend2DAlloc() set trend->poly->type correctly");
+        ok(trend->poly->nX == 2, "pmTrend2DAlloc() set trend->poly->nX correctly");
+        ok(trend->poly->nY == 4, "pmTrend2DAlloc() set trend->poly->nY correctly");
+        psFree(trend);
+
+        // Call pmTrend2DAlloc() with PM_TREND_POLY_CHEB
+        trend = pmTrend2DAlloc(PM_TREND_POLY_CHEB, img, 2, 4, stats);
+        ok(trend != NULL && psMemCheckTrend2D(trend), 
+          "pmTrend2DAlloc() returned non-NULL with acceptable input parameters");
+        ok(trend->map == NULL, "pmTrend2DAlloc() set trend->map to NULL");
+        ok(trend->mode == PM_TREND_POLY_CHEB, "pmTrend2DAlloc() set trend->mode correctly");
+        ok(trend->stats == stats, "pmTrend2DAlloc() set trend->stats correctly");
+        ok(trend->poly != NULL && trend->poly->type == PS_POLYNOMIAL_CHEB, "pmTrend2DAlloc() set trend->poly->type correctly");
+        ok(trend->poly->type == PS_POLYNOMIAL_CHEB, "pmTrend2DAlloc() set trend->poly->type correctly");
+        ok(trend->poly->nX == 2, "pmTrend2DAlloc() set trend->poly->nX correctly");
+        ok(trend->poly->nY == 4, "pmTrend2DAlloc() set trend->poly->nY correctly");
+        psFree(trend);
+
+        // Create a new pmTrend with PM_TREND_MAP
+        trend = pmTrend2DAlloc(PM_TREND_MAP, img, 2, 4, stats);
+        ok(trend->map != NULL && psMemCheckImageMap(trend->map), 
+           "pmTrend2DAlloc() set trend->map correctly");
+        psFree(trend);
+
+        psFree(img);
+        psFree(stats);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // Test pmTrend2DNoImageAlloc()
+    // Call pmTrend2DNoImageAlloc() with NULL psImageBinning input parameter
+    {
+        psMemId id = psMemGetId();
+        psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+        psImageBinning *binning = psImageBinningAlloc();
+        pmTrend2D *trend = pmTrend2DNoImageAlloc(PM_TREND_MAP, NULL, stats);
+        ok(trend == NULL, "pmTrend2DNoImageAlloc() returned NULL with NULL psImageBinning input parameter");
+        psFree(stats);
+        psFree(binning);
+        psFree(trend);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmTrend2DNoImageAlloc() with NULL psStats input parameter
+    {
+        psMemId id = psMemGetId();
+        psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+        psImageBinning *binning = psImageBinningAlloc();
+        pmTrend2D *trend = pmTrend2DNoImageAlloc(PM_TREND_MAP, binning, NULL);
+        ok(trend == NULL, "pmTrend2DNoImageAlloc() returned NULL with NULL psStats input parameter");
+        psFree(stats);
+        psFree(binning);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmTrend2DNoImageAlloc() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+        psImageBinning *binning = psImageBinningAlloc();
+        binning->nXruff = 2;
+        binning->nYruff = 4;
+
+        // Call pmTrend2DNoImageAlloc() with PM_TREND_POLY_ORD
+        pmTrend2D *trend = pmTrend2DNoImageAlloc(PM_TREND_POLY_ORD, binning, stats);
+        ok(trend != NULL && psMemCheckTrend2D(trend), "pmTrend2DNoImageAlloc() returned non-NULL with acceptable input parameters");
+        ok(trend->map == NULL, "pmTrend2DAlloc() set trend->map to NULL");
+        ok(trend->mode == PM_TREND_POLY_ORD, "pmTrend2DAlloc() set trend->mode correctly");
+        ok(trend->stats == stats, "pmTrend2DAlloc() set trend->stats correctly");
+        ok(trend->poly != NULL && trend->poly->type == PS_POLYNOMIAL_ORD, "pmTrend2DAlloc() set trend->poly->type correctly");
+        ok(trend->poly->type == PS_POLYNOMIAL_ORD, "pmTrend2DAlloc() set trend->poly->type correctly");
+        ok(trend->poly->nX == 2, "pmTrend2DAlloc() set trend->poly->nX correctly");
+        ok(trend->poly->nY == 4, "pmTrend2DAlloc() set trend->poly->nY correctly");
+        psFree(trend);
+
+        // Call pmTrend2DNoImageAlloc() with PM_TREND_POLY_CHEB
+        trend = pmTrend2DNoImageAlloc(PM_TREND_POLY_CHEB, binning, stats);
+        ok(trend != NULL && psMemCheckTrend2D(trend), "pmTrend2DNoImageAlloc() returned non-NULL with acceptable input parameters");
+        ok(trend->map == NULL, "pmTrend2DAlloc() set trend->map to NULL");
+        ok(trend->mode == PM_TREND_POLY_CHEB, "pmTrend2DAlloc() set trend->mode correctly");
+        ok(trend->stats == stats, "pmTrend2DAlloc() set trend->stats correctly");
+        ok(trend->poly != NULL && trend->poly->type == PS_POLYNOMIAL_CHEB, "pmTrend2DAlloc() set trend->poly->type correctly");
+        ok(trend->poly->type == PS_POLYNOMIAL_CHEB, "pmTrend2DAlloc() set trend->poly->type correctly");
+        ok(trend->poly->nX == 2, "pmTrend2DAlloc() set trend->poly->nX correctly");
+        ok(trend->poly->nY == 4, "pmTrend2DAlloc() set trend->poly->nY correctly");
+        psFree(trend);
+
+        // Call pmTrend2DNoImageAlloc() with PM_TREND_MAP
+        trend = pmTrend2DNoImageAlloc(PM_TREND_MAP, binning, stats);
+        ok(trend != NULL && psMemCheckTrend2D(trend), "pmTrend2DNoImageAlloc() returned non-NULL with acceptable input parameters");
+        ok(trend->map && psMemCheckImageMap(trend->map), "pmTrend2DNoImageAlloc() set the trend->map correctly");
+        psFree(trend);
+
+        psFree(stats);
+        psFree(binning);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // Test pmTrend2DFieldAlloc()
+    // Call pmTrend2DFieldAlloc() with NULL psStats input parameter
+    {
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+        pmTrend2D *trend = pmTrend2DFieldAlloc(PM_TREND_MAP, 1, 2, 3, 4, NULL);
+        ok(trend == NULL, "pmTrend2DFieldAlloc() returned NULL with NULL psStats input parameter");
+        psFree(img);
+        psFree(stats);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmTrend2DFieldAlloc() with acceptable input parameters
+    {
+        psMemId id = psMemGetId();
+        psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+        pmTrend2D *trend = pmTrend2DFieldAlloc(PM_TREND_POLY_ORD, 1, 2, 3, 4, stats);
+        ok(trend != NULL && psMemCheckTrend2D(trend), "pmTrend2DFieldAlloc() returned non-NULL with acceptable input parameters");
+        ok(trend->poly != NULL && trend->poly->type == PS_POLYNOMIAL_ORD, "pmTrend2DFieldAlloc() set trend->poly->type correctly");
+        ok(trend->poly->type == PS_POLYNOMIAL_ORD, "pmTrend2DFieldAlloc() set trend->poly->type correctly");
+        ok(trend->poly->nX == 3, "pmTrend2DFieldAlloc() set trend->poly->nX correctly");
+        ok(trend->poly->nY == 4, "pmTrend2DFieldAlloc() set trend->poly->nY correctly");
+        psFree(trend);
+        trend = NULL;
+
+        // Create a new pmTrend with PM_TREND_MAP
+        // XXX: This currently fails due to a big in pmTrend2DFieldAlloc():
+        if (0) {
+            trend = pmTrend2DFieldAlloc(PM_TREND_MAP, 1, 2, 3, 4, stats);
+            ok(trend->map != NULL && psMemCheckImageMap(trend->map), 
+               "pmTrend2DAlloc() set trend->map correctly");
+	}
+
+        psFree(stats);
+        psFree(trend);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // Test pmTrend2DModeToString()
+    // psString pmTrend2DModeToString (pmTrend2DMode mode)
+    // Call pmTrend2DModeToString() with unallowed pmTrend2DMode.
+    // XX: We comment this out because pmTrend2DModeToString() aborts.
+    if (0) {
+        psMemId id = psMemGetId();
+        psString str = pmTrend2DModeToString(99);
+        ok(str == NULL, "pmTrend2DModeToString() returned NULL with unallowed pmTrend2DMode");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmTrend2DModeToString() with unallowed pmTrend2DMode.
+    {
+        psMemId id = psMemGetId();
+        psString str = pmTrend2DModeToString(PM_TREND_NONE);
+        ok(!strcmp(str, "NONE"), "pmTrend2DModeToString(PM_TREND_NONE)");
+        psFree(str);
+
+        str = pmTrend2DModeToString(PM_TREND_POLY_ORD);
+        ok(!strcmp(str, "POLY_ORD"), "pmTrend2DModeToString(PM_TREND_POLY_ORD)");
+        psFree(str);
+
+        str = pmTrend2DModeToString(PM_TREND_POLY_CHEB);
+        ok(!strcmp(str, "POLY_CHEB"), "pmTrend2DModeToString(PM_TREND_POLY_CHEB)");
+        psFree(str);
+
+        str = pmTrend2DModeToString(PM_TREND_MAP);
+        ok(!strcmp(str, "MAP"), "pmTrend2DModeToString(PM_TREND_MAP)");
+        psFree(str);
+
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // Test pmTrend2DModeFromString()
+    // Call pmTrend2DModeFromString() with NULL input parameter
+    {
+        psMemId id = psMemGetId();
+        pmTrend2DMode mode = pmTrend2DModeFromString(NULL);
+        ok(PM_TREND_NONE == mode, "pmTrend2DModeFromString(NULL) returned PM_TREND_NONE");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmTrend2DModeFromString() with unallowed input string
+    {
+        psMemId id = psMemGetId();
+        pmTrend2DMode mode = pmTrend2DModeFromString("BOGUS");
+        ok(PM_TREND_NONE == mode, "pmTrend2DModeFromString(BOGUS) returned PM_TREND_NONE");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmTrend2DModeFromString() with acceptable input string
+    {
+        psMemId id = psMemGetId();
+        pmTrend2DMode mode = pmTrend2DModeFromString("NONE");
+        ok(PM_TREND_NONE == mode, "pmTrend2DModeFromString(NONE) returned PM_TREND_NONE");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmTrend2DModeFromString() with acceptable input string
+    {
+        psMemId id = psMemGetId();
+        pmTrend2DMode mode = pmTrend2DModeFromString("POLY_ORD");
+        ok(PM_TREND_POLY_ORD == mode, "pmTrend2DModeFromString(POLY_ORD) returned PM_TREND_NONE");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmTrend2DModeFromString() with acceptable input string
+    {
+        psMemId id = psMemGetId();
+        pmTrend2DMode mode = pmTrend2DModeFromString("POLY_CHEB");
+        ok(PM_TREND_POLY_CHEB == mode, "pmTrend2DModeFromString(POLY_CHEB) returned PM_TREND_NONE");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmTrend2DModeFromString() with acceptable input string
+    {
+        psMemId id = psMemGetId();
+        pmTrend2DMode mode = pmTrend2DModeFromString("MAP");
+        ok(PM_TREND_MAP == mode, "pmTrend2DModeFromString(MAP) returned PM_TREND_NONE");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // Test pmTrend2DFit()
+    // Call pmTrend2DFit() with bad input parameters
+    {
+        #define VEC_SIZE 9
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+        pmTrend2D *trend = pmTrend2DAlloc(PM_TREND_POLY_ORD, img, 4, 4, stats);
+        ok(trend != NULL && psMemCheckTrend2D(trend), 
+          "pmTrend2DAlloc() returned non-NULL with acceptable input parameters");
+        psVector *x = psVectorAlloc(VEC_SIZE, PS_TYPE_F32);
+        psVector *y = psVectorAlloc(VEC_SIZE, PS_TYPE_F32);
+        psVector *f = psVectorAlloc(VEC_SIZE, PS_TYPE_F32);
+        psVector *mask = psVectorAlloc(VEC_SIZE, PS_TYPE_U8);
+        psVector *df = psVectorAlloc(VEC_SIZE, PS_TYPE_F32);
+        for (int i = 0 ; i < VEC_SIZE ; i++) {
+            x->data.F32[i] = (float) (i);
+            y->data.F32[i] = (float) (2 * i);
+            f->data.F32[i] = x->data.F32[i] * y->data.F32[i];
+            mask->data.U8[i] = 0;
+            df->data.F32[i] = 0.0;
+        }
+
+        // NULL pmTrend2D input parameter
+        bool rc = pmTrend2DFit(NULL, mask, 0, x, y, f, df);
+        ok(rc == false, "pmTrend2DFit() returned FALSE with NULL pmTrend2D input parameter");
+
+        // NULL mask input parameter
+        rc = pmTrend2DFit(trend, NULL, 0, x, y, f, df);
+        ok(rc == false, "pmTrend2DFit() returned FALSE with NULL mask input parameter");
+
+        // NULL x psVector input parameter
+        rc = pmTrend2DFit(trend, mask, 0, NULL, y, f, df);
+        ok(rc == false, "pmTrend2DFit() returned FALSE with NULL x psVector input parameter");
+
+        // NULL y psVector input parameter
+        rc = pmTrend2DFit(trend, mask, 0, x, NULL, f, df);
+        ok(rc == false, "pmTrend2DFit() returned FALSE with NULL y psVector input parameter");
+
+        // NULL f psVector input parameter
+        rc = pmTrend2DFit(trend, mask, 0, x, y, NULL, df);
+        ok(rc == false, "pmTrend2DFit() returned FALSE with NULL f psVector input parameter");
+
+        // NULL df psVector input parameter
+        rc = pmTrend2DFit(trend, mask, 0, x, y, f, NULL);
+        ok(rc == true, "pmTrend2DFit() returned TRUE with NULL df psVector input parameter");
+
+        psFree(img);
+        psFree(stats);
+        psFree(trend);
+        psFree(x);
+        psFree(y);
+        psFree(f);
+        psFree(mask);
+        psFree(df);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Test pmTrend2DEval()
+    // Call pmTrend2DEval() with bad input parameters
+    {
+        psMemId id = psMemGetId();
+        psF64 tmpD = pmTrend2DEval(NULL, 0.0, 0.0);
+        ok(TEST_FLOATS_EQUAL(tmpD, 0.0), "pmTrend2DEval() returned 0.0 with NULL pmTrend2D input parameter");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+/**
+ * Needs to be rewritten to correlate with revised function prototype:
+ * psVector *pmTrend2DEvalVector(const pmTrend2D *trend, psVector *mask, psVectorMaskType maskValue, const psVector *x, const psVector *y)
+ */
+
+/*
+    // ------------------------------------------------------------------------
+
+    // ------------------------------------------------------------------------
+    // Test pmTrend2DEvalVector()
+    // psVector *pmTrend2DEvalVector (pmTrend2D *trend, psVector *x, psVector *y)
+    // Call pmTrend2DEvalVector() with bad input parameters
+    {
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+        pmTrend2D *trend = pmTrend2DAlloc(PM_TREND_POLY_ORD, img, 4, 4, stats);
+        psVector *x = psVectorAlloc(VEC_SIZE, PS_TYPE_F32);
+        psVector *y = psVectorAlloc(VEC_SIZE, PS_TYPE_F32);
+
+        // NULL pmTrend2D input parameter
+        psVector *f = pmTrend2DEvalVector(NULL, x, y);
+        ok(f == NULL, "pmTrend2DEvalVector() returned NULL with NULL pmTrend2D input parameter");
+
+        // NULL x psVector input parameter
+        f = pmTrend2DEvalVector(trend, NULL, y);
+        ok(f == NULL, "pmTrend2DEvalVector() returned NULL with NULL x psVector input parameter");
+
+        // NULL y psVector input parameter
+        f = pmTrend2DEvalVector(trend, x, NULL);
+        ok(f == NULL, "pmTrend2DEvalVector() returned NULL with NULL y psVector input parameter");
+
+        psFree(img);
+        psFree(stats);
+        psFree(trend);
+        psFree(x);
+        psFree(y);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmTrend2DFit(), pmTrend2DEval(), pmTrend2DEvalVector() with acceptable input parameters
+    // NOTE: We only test with a very simple 2D polynomial fit.  This is appropriate since the
+    // polynomial testing routines are tested extensively elsewhere.
+    // XXX: Must test the PM_TREND_MAP case.
+    {
+        #define VEC_SIZE 9
+        psMemId id = psMemGetId();
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+        pmTrend2D *trend = pmTrend2DAlloc(PM_TREND_POLY_ORD, img, 4, 4, stats);
+        ok(trend != NULL && psMemCheckTrend2D(trend), 
+          "pmTrend2DAlloc() returned non-NULL with acceptable input parameters");
+        psVector *x = psVectorAlloc(VEC_SIZE, PS_TYPE_F32);
+        psVector *y = psVectorAlloc(VEC_SIZE, PS_TYPE_F32);
+        psVector *f = psVectorAlloc(VEC_SIZE, PS_TYPE_F32);
+        psVector *mask = psVectorAlloc(VEC_SIZE, PS_TYPE_U8);
+        psVector *df = psVectorAlloc(VEC_SIZE, PS_TYPE_F32);
+
+        int cnt = 0;
+        for (int i = 0 ; i < 3 ; i++) {
+            for (int j = 0 ; j < 3 ; j++) {
+                x->data.F32[cnt] = (float) i;
+                y->data.F32[cnt] = (float) j;
+                f->data.F32[cnt] = x->data.F32[cnt] * y->data.F32[cnt];
+                mask->data.U8[cnt] = 0;
+                df->data.F32[cnt] = 0.0;
+                cnt++;
+            }
+        }
+
+        bool rc = pmTrend2DFit(trend, mask, 0, x, y, f, NULL);
+        ok(rc == true, "pmTrend2DFit() returned TRUE with acceptable input parameters");
+
+        // Test pmTrend2DFit, pmTrend2DEval()
+        bool errorFlag = false;
+        for (int i = 0 ; i < VEC_SIZE ; i++) {
+            if (!TEST_FLOATS_EQUAL(pmTrend2DEval(trend, x->data.F32[i], y->data.F32[i]), f->data.F32[i])) {
+                diag("ERROR: at (%.2f %.2f), eval is %.2f, should be %.2f\n", x->data.F32[i], y->data.F32[i],
+                      pmTrend2DEval(trend, x->data.F32[i], y->data.F32[i]), f->data.F32[i]);
+                errorFlag = true;
+            }
+        }
+        ok(!errorFlag, "pmTrend2DFit() and pmTrend2DEval() set and evaluated the 2DTrend polynomial correctly");
+
+        // Test pmTrend2DEvalVector()
+        psVector *fTest = pmTrend2DEvalVector(trend, x, y);
+        errorFlag = false;
+        for (int i = 0 ; i < VEC_SIZE ; i++) {
+            if (!TEST_FLOATS_EQUAL(fTest->data.F32[i], f->data.F32[i])) {
+                diag("ERROR: at (%.2f %.2f), eval is %.2f, should be %.2f\n", 
+                      x->data.F32[i], y->data.F32[i], fTest->data.F32[i], f->data.F32[i]);
+                errorFlag = true;
+            }
+        }
+        ok(!errorFlag, "pmTrend2DFit() and pmTrend2DEval() set and evaluated the 2DTrend polynomial correctly");
+
+
+        psFree(img);
+        psFree(stats);
+        psFree(trend);
+        psFree(x);
+        psFree(y);
+        psFree(f);
+        psFree(fTest);
+        psFree(mask);
+        psFree(df);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+*/
+}
Index: /branches/eam_branches/psModules.20240412/test/objects/tst_pmObjects01.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/objects/tst_pmObjects01.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/objects/tst_pmObjects01.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/pstap/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/pstap/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/pstap/Makefile.am	(revision 42651)
@@ -0,0 +1,1 @@
+SUBDIRS = src
Index: /branches/eam_branches/psModules.20240412/test/pstap/src/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/pstap/src/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/pstap/src/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/pstap/src/pstap.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/pstap/src/pstap.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/pstap/src/pstap.c	(revision 42651)
@@ -0,0 +1,6 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <tap.h>
+#include <pslib.h>
Index: /branches/eam_branches/psModules.20240412/test/pstap/src/pstap.h
===================================================================
--- /branches/eam_branches/psModules.20240412/test/pstap/src/pstap.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/pstap/src/pstap.h	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/INSTALL
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/INSTALL	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/INSTALL	(revision 42651)
@@ -0,0 +1,8 @@
+Quick Installation
+
+    ./configure
+    make
+    make check
+    make install
+
+Run "configure --help" for additional options.
Index: /branches/eam_branches/psModules.20240412/test/tap/LICENSE
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/LICENSE	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/LICENSE	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/Makefile.am	(revision 42651)
@@ -0,0 +1,5 @@
+SUBDIRS  = src
+#SUBDIRS += tests
+
+prove:
+	prove -v -r
Index: /branches/eam_branches/psModules.20240412/test/tap/README
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/README	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/README	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/bootstrap.sh
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/bootstrap.sh	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/bootstrap.sh	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/compile
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/compile	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/compile	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/configure.in
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/configure.in	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/configure.in	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/src/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/src/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/src/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/src/tap.3
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/src/tap.3	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/src/tap.3	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/src/tap.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/src/tap.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/src/tap.c	(revision 42651)
@@ -0,0 +1,433 @@
+/*-
+ * 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);
+        int status = vasprintf(&local_test_name, test_name, ap);
+	if (status) {/* warning? */}
+        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);
+    int status = asprintf(&skip_msg, fmt, ap);
+    if (status) {/* warning? */}
+    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);
+    int status = vasprintf(&todo_msg, fmt, ap);
+    if (status) {/* warning? */}
+    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_branches/psModules.20240412/test/tap/src/tap.h
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/src/tap.h	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/src/tap.h	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/Makefile.am	(revision 42651)
@@ -0,0 +1,7 @@
+SUBDIRS=	diag
+SUBDIRS+=	fail
+SUBDIRS+=	ok
+SUBDIRS+=	pass
+SUBDIRS+=	plan
+SUBDIRS+=	skip
+SUBDIRS+=	todo
Index: /branches/eam_branches/psModules.20240412/test/tap/tests/README
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/README	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/README	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/diag/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/diag/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/diag/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/diag/test.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/diag/test.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/diag/test.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/diag/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/diag/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/diag/test.pl	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/diag/test.t
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/diag/test.t	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/diag/test.t	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/fail/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/fail/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/fail/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/fail/test.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/fail/test.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/fail/test.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/fail/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/fail/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/fail/test.pl	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/fail/test.t
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/fail/test.t	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/fail/test.t	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/ok/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/ok/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/ok/Makefile.am	(revision 42651)
@@ -0,0 +1,3 @@
+SUBDIRS  =	ok
+SUBDIRS +=	ok-hash
+SUBDIRS +=	ok-numeric
Index: /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-hash/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-hash/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-hash/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/ok/ok-hash/test.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-hash/test.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-hash/test.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/ok/ok-hash/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-hash/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-hash/test.pl	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/ok/ok-hash/test.t
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-hash/test.t	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-hash/test.t	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/ok/ok-numeric/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-numeric/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-numeric/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/ok/ok-numeric/test.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-numeric/test.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-numeric/test.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/ok/ok-numeric/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-numeric/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-numeric/test.pl	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/ok/ok-numeric/test.t
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-numeric/test.t	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok-numeric/test.t	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/ok/ok/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/ok/ok/test.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok/test.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok/test.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/ok/ok/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok/test.pl	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/ok/ok/test.t
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok/test.t	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/ok/ok/test.t	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/pass/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/pass/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/pass/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/pass/test.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/pass/test.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/pass/test.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/pass/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/pass/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/pass/test.pl	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/pass/test.t
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/pass/test.t	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/pass/test.t	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/no-tests/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no-tests/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no-tests/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/no-tests/test.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no-tests/test.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no-tests/test.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/no-tests/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no-tests/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no-tests/test.pl	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/no-tests/test.t
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no-tests/test.t	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no-tests/test.t	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/no_plan/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no_plan/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no_plan/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/no_plan/test.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no_plan/test.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no_plan/test.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/no_plan/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no_plan/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no_plan/test.pl	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/no_plan/test.t
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no_plan/test.t	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/no_plan/test.t	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/not-enough-tests/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/not-enough-tests/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/not-enough-tests/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/not-enough-tests/test.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/not-enough-tests/test.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/not-enough-tests/test.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/not-enough-tests/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/not-enough-tests/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/not-enough-tests/test.pl	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/not-enough-tests/test.t
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/not-enough-tests/test.t	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/not-enough-tests/test.t	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/sane/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/sane/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/sane/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/sane/test.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/sane/test.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/sane/test.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/sane/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/sane/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/sane/test.pl	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/sane/test.t
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/sane/test.t	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/sane/test.t	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/skip_all/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/skip_all/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/skip_all/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/skip_all/plan.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/skip_all/plan.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/skip_all/plan.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/skip_all/test.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/skip_all/test.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/skip_all/test.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/skip_all/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/skip_all/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/skip_all/test.pl	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/skip_all/test.t
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/skip_all/test.t	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/skip_all/test.t	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/too-many-plans/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-plans/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-plans/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/too-many-plans/test.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-plans/test.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-plans/test.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/too-many-plans/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-plans/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-plans/test.pl	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/too-many-plans/test.t
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-plans/test.t	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-plans/test.t	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/too-many-tests/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-tests/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-tests/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/too-many-tests/test.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-tests/test.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-tests/test.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/too-many-tests/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-tests/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-tests/test.pl	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/plan/too-many-tests/test.t
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-tests/test.t	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/plan/too-many-tests/test.t	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/skip/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/skip/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/skip/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/skip/test.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/skip/test.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/skip/test.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/skip/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/skip/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/skip/test.pl	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/skip/test.t
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/skip/test.t	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/skip/test.t	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/todo/Makefile.am
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/todo/Makefile.am	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/todo/Makefile.am	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/todo/test.c
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/todo/test.c	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/todo/test.c	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/todo/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/todo/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/todo/test.pl	(revision 42651)
@@ -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_branches/psModules.20240412/test/tap/tests/todo/test.t
===================================================================
--- /branches/eam_branches/psModules.20240412/test/tap/tests/todo/test.t	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/tap/tests/todo/test.t	(revision 42651)
@@ -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_branches/psModules.20240412/test/test.pl
===================================================================
--- /branches/eam_branches/psModules.20240412/test/test.pl	(revision 42651)
+++ /branches/eam_branches/psModules.20240412/test/test.pl	(revision 42651)
@@ -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");
