Index: /branches/ccl_branches/ipponly-20191108/ppMerge/Doxyfile.in
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/Doxyfile.in	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/Doxyfile.in	(revision 41089)
@@ -0,0 +1,1310 @@
+# 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           = ppMerge
+
+# 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         = ipp-2.7.dev
+
+# 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    = NO
+
+# 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      = NO
+
+# 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         = NO
+
+# 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  = NO
+
+# 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            = YES
+
+# 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            = NO
+
+# 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  = YES
+
+# 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       = YES
+
+# 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           = 
+
+#---------------------------------------------------------------------------
+# 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
+
+# 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       = 
+
+# 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         = NO
+
+# 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    = YES
+
+# 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     = NO
+
+# 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             = NO
+
+# 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   = 4
+
+# 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          = NO
+
+# 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             = a4wide
+
+# 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         = 
+
+# 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         = NO
+
+# 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           = NO
+
+# 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              = NO
+
+#---------------------------------------------------------------------------
+# 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             = 
+
+# 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               = NO
+
+# 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               = NO
+
+# If set to YES, the inheritance and collaboration graphs will show the 
+# relations between templates and their instances.
+
+TEMPLATE_RELATIONS     = NO
+
+# 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        = NO
+
+# 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/ccl_branches/ipponly-20191108/ppMerge/Makefile.am
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/Makefile.am	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/Makefile.am	(revision 41089)
@@ -0,0 +1,3 @@
+SUBDIRS = src
+
+CLEANFILES = *~ core core.*
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/autogen.sh
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/autogen.sh	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/autogen.sh	(revision 41089)
@@ -0,0 +1,103 @@
+#!/bin/sh
+# Run this to generate all the initial makefiles, etc.
+
+srcdir=`dirname $0`
+test -z "$srcdir" && srcdir=.
+
+ORIGDIR=`pwd`
+cd $srcdir
+
+PROJECT=ppMerge
+TEST_TYPE=-f
+# change this to be a unique filename in the top level dir
+FILE=autogen.sh
+
+DIE=0
+
+LIBTOOLIZE=libtoolize
+ACLOCAL="aclocal $ACLOCAL_FLAGS"
+AUTOHEADER=autoheader
+AUTOMAKE=automake
+AUTOCONF=autoconf
+
+($LIBTOOLIZE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $LIBTOOLIZE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/libtool/"
+        DIE=1
+}
+
+($ACLOCAL --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $ACLOCAL installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOHEADER --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOHEADER installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+($AUTOMAKE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOMAKE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOCONF --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOCONF installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+if test "$DIE" -eq 1; then
+        exit 1
+fi
+
+test $TEST_TYPE $FILE || {
+        echo "You must run this script in the top-level $PROJECT directory"
+        exit 1
+}
+
+if test -z "$*"; then
+        echo "I am going to run ./configure with no arguments - if you wish "
+        echo "to pass any to it, please specify them on the $0 command line."
+fi
+
+$LIBTOOLIZE --copy --force || echo "$LIBTOOLIZE failed"
+$ACLOCAL || echo "$ACLOCAL failed"
+$AUTOHEADER || echo "$AUTOHEADER failed"
+$AUTOMAKE --add-missing --force-missing --copy || echo "$AUTOMAKE failed"
+$AUTOCONF || echo "$AUTOCONF failed"
+
+cd $ORIGDIR
+
+run_configure=true
+for arg in $*; do
+    case $arg in
+        --no-configure)
+            run_configure=false
+            ;;
+        *)
+            ;;
+    esac
+done
+
+if $run_configure; then
+    $srcdir/configure --enable-maintainer-mode "$@"
+    echo
+    echo "Now type 'make' to compile $PROJECT."
+else
+    echo
+    echo "Now run 'configure' and 'make' to compile $PROJECT."
+fi
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/configure.ac
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/configure.ac	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/configure.ac	(revision 41089)
@@ -0,0 +1,33 @@
+AC_PREREQ(2.61)
+
+AC_INIT([ppMerge], [1.1.0], [ipp-support@ifa.hawaii.edu])
+AC_CONFIG_SRCDIR([src])
+
+AM_INIT_AUTOMAKE([1.6 foreign dist-bzip2])
+AM_CONFIG_HEADER([src/config.h])
+AM_MAINTAINER_MODE
+
+IPP_STDLDFLAGS
+
+AC_LANG(C)
+AC_GNU_SOURCE
+AC_PROG_CC_C99
+AC_PROG_INSTALL
+AC_PROG_LIBTOOL
+AC_SYS_LARGEFILE
+
+PKG_CHECK_MODULES([PSLIB], [pslib >= 1.0.0])
+PKG_CHECK_MODULES([PSMODULE], [psmodules >= 1.0.0])
+PKG_CHECK_MODULES([PPSTATS], [ppStats >= 1.0.0]) 
+
+IPP_VERSION
+
+IPP_STDOPTS
+IPP_STDCFLAGS
+
+AC_CONFIG_FILES([
+  Makefile
+  src/Makefile
+  Doxyfile
+])
+AC_OUTPUT
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/Makefile.am
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/Makefile.am	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/Makefile.am	(revision 41089)
@@ -0,0 +1,36 @@
+bin_PROGRAMS = ppMerge
+
+# Force recompilation of ppMergeVersion.c, since it gets the version information
+ppMergeVersion.c: ppMergeVersionDefinitions.h
+ppMergeVersionDefinitions.h: ppMergeVersionDefinitions.h.in FORCE
+	pslib-setsvnversion.pl PPMERGE ppMergeVersionDefinitions.h.in ppMergeVersionDefinitions.h
+FORCE: ;
+
+ppMerge_CFLAGS = $(PPMERGE_CFLAGS) $(PPSTATS_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+ppMerge_LDFLAGS = $(PPMERGE_LIBS) $(PPSTATS_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+
+ppMerge_SOURCES =		\
+	ppMerge.c		\
+	ppMergeArguments.c	\
+	ppMergeCamera.c		\
+	ppMergeFiles.c		\
+	ppMergeScaleZero.c	\
+	ppMergeFileGroup.c	\
+	ppMergeReadChunk.c	\
+	ppMergeLoop.c		\
+	ppMergeSetThreads.c	\
+	ppMergeMask.c		\
+	ppMergeVersion.c
+
+#	ppMergeLoop.c		
+
+noinst_HEADERS =		\
+	ppMerge.h
+
+CLEANFILES = *~
+
+clean-local:
+	-rm -f TAGS
+# Tags for emacs
+tags:
+	etags `find . -name \*.[ch] -print`
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMerge.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMerge.c	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMerge.c	(revision 41089)
@@ -0,0 +1,162 @@
+/** @file ppMerge.c
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.26 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-01 21:43:05 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#include "ppMerge.h"
+
+// Yet to do:
+//
+// 1. Mask pixels with less than the minimum number of electrons
+// 2. Sampling factor for background measurement
+// 3. On/off pairs
+
+int main(int argc, char **argv)
+{
+    psLibInit(NULL);
+    psTimerStart(TIMERNAME);
+
+    psExit exitValue = PS_EXIT_SUCCESS; ///< Exit value for program
+
+    pmConfig *config = pmConfigRead(&argc, argv, PPMERGE_RECIPE); ///< Configuration
+    if (!config) {
+        psErrorStackPrint(stderr, "Error reading configuration.");
+        exitValue = PS_EXIT_CONFIG_ERROR;
+        goto die;
+    }
+
+    if (!ppMergeArguments(argc, argv, config)) {
+        psErrorStackPrint(stderr, "Error reading arguments.");
+        exitValue = PS_EXIT_CONFIG_ERROR;
+        goto die;
+    }
+
+    ppMergeVersionPrint();
+
+    ppMergeType type = psMetadataLookupS32(NULL, config->arguments, "TYPE"); ///< Type of frame
+    switch (type) {
+      case PPMERGE_TYPE_MASK:
+        if (!ppMergeMask(config)) {
+            psErrorStackPrint(stderr, "Error generating mask.");
+            exitValue = PS_EXIT_DATA_ERROR;
+            goto die;
+        }
+        break;
+      case PPMERGE_TYPE_BIAS:
+      case PPMERGE_TYPE_DARK:
+      case PPMERGE_TYPE_SHUTTER:
+      case PPMERGE_TYPE_FLAT:
+      case PPMERGE_TYPE_FRINGE:
+      case PPMERGE_TYPE_CTEMASK:
+      case PPMERGE_TYPE_NOISEMAP:
+        if (!ppMergeScaleZero(config)) {
+            psErrorStackPrint(stderr, "Error getting scale and zero-points.");
+            exitValue = PS_EXIT_DATA_ERROR;
+            goto die;
+        }
+        if (!ppMergeLoop(config)) {
+            psErrorStackPrint(stderr, "Error performing merge.");
+            exitValue = PS_EXIT_PROG_ERROR;
+            goto die;
+        }
+        break;
+      default:
+        psAbort("Invalid frame type: %x", type);
+    }
+
+
+    // Output the statistics
+    bool mdok;                          ///< Status of MD lookup
+    psString statsName = psMetadataLookupStr(&mdok, config->arguments, "STATS.NAME"); ///< Statistics file name
+    if (mdok && statsName && strlen(statsName) > 0) {
+        psString resolved = pmConfigConvertFilename(statsName, config, true, true); ///< Resolved filename
+        FILE *statsFile = fopen(resolved, "w"); ///< Output statistics file
+        if (!statsFile) {
+            psError(PS_ERR_IO, true, "Unable to open statistics file %s for writing.", resolved);
+            psFree(resolved);
+            exitValue = PS_EXIT_CONFIG_ERROR;
+            goto die;
+        }
+        psFree(resolved);
+        psMetadata *stats = psMetadataLookupMetadata(&mdok, config->arguments, "STATS.DATA"); ///< Statistics
+        if (!stats) {
+            psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find statistics");
+            exitValue = PS_EXIT_PROG_ERROR;
+            goto die;
+        }
+        psString statsOut = psMetadataConfigFormat(stats); ///< String to write out
+        fprintf(statsFile, "%s", statsOut);
+        psFree(statsOut);
+        fclose(statsFile);
+        pmConfigRunFilenameAddWrite(config, "STATS", statsName);
+    }
+
+
+
+
+#if 0
+    // Set various tasks (define optional operations)
+    ppMergeOptions *options = ppMergeOptionsParse(config);
+    if (!options) {
+        psErrorStackPrint(stderr, "Unable to parse options.");
+        exit(EXIT_FAILURE);
+    }
+
+    // Check the inputs
+    ppMergeData *data = ppMergeCheckInputs(options, config);
+    if (!data) {
+        psErrorStackPrint(stderr, "Not enough valid input files.");
+        exit(EXIT_FAILURE);
+    }
+
+    if (options->mask) {
+        // Generate a mask
+        ppMergeMask(data, options, config);
+    } else {
+
+        psImage *scale = NULL;              // The scalings
+        psImage *zero = NULL;               // The zeros
+        psArray *shutters = NULL;           // The shutter correction data
+
+        // Measure the background in each image
+        ppMergeScaleZero(&scale, &zero, &shutters, data, options, config);
+
+        // Do the combination and write
+        ppMergeCombine(scale, zero, shutters, data, options, config);
+
+        psFree(scale);
+        psFree(zero);
+        psFree(shutters);
+    }
+
+    // Output the statistics
+    if (data->statsFile && data->stats) {
+        psString statsOut = psMetadataConfigFormat(data->stats); // String to write out
+        fprintf(data->statsFile, "%s", statsOut);
+        psFree(statsOut);
+    }
+
+    // Cleaning up
+    pmDarkVisualCleanup();
+    psFree(data);
+    psFree(options);
+#endif
+
+ die:
+    psTrace("ppSub", 1, "Finished at %f sec\n", psTimerMark("ppSub"));
+    psTimerStop();
+
+    pmDarkVisualCleanup();
+    psFree(config);
+    pmConfigDone();
+    psLibFinalize();
+
+    exit(exitValue);
+}
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMerge.h
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMerge.h	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMerge.h	(revision 41089)
@@ -0,0 +1,190 @@
+/** @file ppMerge.h
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-01 21:43:05 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PP_MERGE_H
+#define PP_MERGE_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include <ppStats.h>
+
+/// @addtogroup ppArith
+/// @{
+
+#define TIMERNAME "ppMerge"             ///< Name for timer
+#define PPMERGE_RECIPE "PPMERGE"        ///< Recipe name
+#define THREADED 1                      ///< Compile with threads?
+
+/**
+ * Type of frame to merge
+ */
+typedef enum {
+    PPMERGE_TYPE_UNKNOWN,               ///< Unknown type
+    PPMERGE_TYPE_BIAS,                  ///< Bias frame
+    PPMERGE_TYPE_DARK,                  ///< (Multi-)Dark frame
+    PPMERGE_TYPE_MASK,                  ///< Mask frame
+    PPMERGE_TYPE_CTEMASK,		///< CTE Mask based on flat variance
+    PPMERGE_TYPE_SHUTTER,               ///< Shutter frame
+    PPMERGE_TYPE_FLAT,                  ///< Flat-field frame (dome or sky)
+    PPMERGE_TYPE_FRINGE,                ///< Fringe frame
+    PPMERGE_TYPE_NOISEMAP,              ///< Noise map
+} ppMergeType;
+
+/**
+ * Files, for activation
+ */
+typedef enum {
+    PPMERGE_FILES_ALL,                  ///< All files
+    PPMERGE_FILES_INPUT,                ///< Input files
+    PPMERGE_FILES_OUTPUT                ///< Output files
+} ppMergeFiles;
+
+/**
+ * \brief Group of files to read
+ *
+ * Each file contributes a readout, into which is read a chunk from that file
+ */
+typedef struct {
+    psArray *readouts;                  ///< Input readouts
+    bool read;                          ///< Has the scan been read?
+    bool busy;                          ///< Is the scan being processed?
+    int firstScan;                      ///< First row of the chunk to be read for this group
+    int lastScan;                       ///< Last row of the chunk to be read for this group
+} ppMergeFileGroup;
+
+/**
+ * Parse command-line arguments and recipe
+ */
+bool ppMergeArguments(int argc, char *argv[], ///< Command-line arguments
+                      pmConfig *config  ///< Configuration
+    );
+
+/**
+ * Set up camera files
+ */
+bool ppMergeCamera(pmConfig *config     ///< Configuration
+    );
+
+/**
+ * Measure scale and zero-points
+ */
+bool ppMergeScaleZero(pmConfig *config  ///< Configuration
+    );
+
+/**
+ * Main loop to do the merging
+ */
+bool ppMergeLoop(pmConfig *config       ///< Configuration
+    );
+
+/**
+ * Main loop for masks
+ */
+bool ppMergeMask(pmConfig *config       ///< Configuration
+    );
+
+/**
+ * Read nominated input file
+ */
+bool ppMergeFileReadInput(pmConfig *config, ///< Configuration
+                          pmReadout *readout, ///< Readout into which to read
+                          int num,      ///< Number of file in sequence
+                          int rows      ///< Number of rows to read at once
+    );
+
+/**
+ * Open nominated input file
+ */
+bool ppMergeFileOpenInput(pmConfig *config, ///< Configuration
+                          const pmFPAview *view, ///< View to open
+                          int num       ///< Number of file in sequence
+    );
+
+/**
+ * Set the data level for files specified by name; return an array of the files
+ */
+psArray *ppMergeFileDataLevel(const pmConfig *config, ///< Configuration
+                              const char *name, ///< Name of files
+                              pmFPALevel level ///< Level for file data level
+    );
+
+/**
+ * Activate/deactivate a list of files
+ */
+bool ppMergeFileActivate(const pmConfig *config, ///< Configuration
+                         ppMergeFiles files, ///< Files to turn on/off
+                         bool state     ///< Activation state
+    );
+
+/**
+ * Activate/deactivate a single element for a list; return array of files
+ */
+psArray *ppMergeFileActivateSingle(const pmConfig *config, ///< Configuration
+                                   ppMergeFiles files, ///< Files to turn on/off
+                                   bool state,   ///< Activation state
+                                   int num ///< Number of file in sequence
+    );
+
+/**
+ * Return name of output pmFPAfile
+ */
+psString ppMergeOutputFile(const pmConfig *config ///< Configuration
+    );
+
+
+/**
+ * Allocator for group of files
+ */
+ppMergeFileGroup *ppMergeFileGroupAlloc(void);
+
+/**
+ * Read chunk into the first available file group
+ */
+ppMergeFileGroup *ppMergeReadChunk(bool *status, ///< Status of read
+                                   psArray *fileGroups, ///< All groups
+                                   pmConfig *config, ///< Configuration
+                                   int numChunk ///< Chunk number (only for interest)
+    );
+
+/**
+ * Set up thread handling
+ */
+bool ppMergeSetThreads(void);
+
+
+/// Return software version
+psString ppMergeVersion(void);
+
+/// Return software source
+psString ppMergeSource(void);
+
+/// Return detailed software version information
+psString ppMergeVersionLong(void);
+
+/// Populate a FITS header with version information
+bool ppMergeVersionHeader(
+    psMetadata *header                  ///< Header to populate
+    );
+
+/// Print version information
+void ppMergeVersionPrint(void);
+
+
+///@}
+#endif
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeArguments.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeArguments.c	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeArguments.c	(revision 41089)
@@ -0,0 +1,455 @@
+/** @file ppMergeArguments.c
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.17 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-06 02:44:31 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#include "ppMerge.h"
+
+/**
+ * Print usage information and die
+ */
+static void usage(const char *program,  ///< Name of the program
+                  psMetadata *arguments ///< Command-line arguments
+    )
+{
+    fprintf(stderr, "\nPan-STARRS Detrend Merging\n\n");
+    fprintf(stderr, "Usage: %s INPUT.mdc OUTPUT_ROOT\n"
+            "where INPUTS.mdc contains various METADATAs, each with:\n"
+            "\tIMAGE(STR):     Image filename\n"
+            "\tMASK(STR):      Mask filename\n"
+            "\tVARIANCE(STR)   Variance map filename\n"
+            "where MASK and VARIANCE are optional.",
+            program);
+    fprintf(stderr, "\n");
+    psArgumentHelp(arguments);
+}
+
+/**
+ * Get a float-point value from the command-line or recipe, and add it to the arguments
+ */
+#define VALUE_ARG_RECIPE_FLOAT(ARGNAME, RECIPENAME, TYPE) { \
+    ps##TYPE value = psMetadataLookup##TYPE(NULL, arguments, ARGNAME); \
+    if (isnan(value)) { \
+        bool mdok; \
+        value = psMetadataLookup##TYPE(&mdok, recipe, RECIPENAME); \
+        if (!mdok) { \
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unable to find %s in recipe %s", \
+                RECIPENAME, PPMERGE_RECIPE); \
+            goto ERROR; \
+        } \
+    } \
+    psMetadataAdd##TYPE(config->arguments, PS_LIST_TAIL, RECIPENAME, 0, NULL, value); \
+}
+
+/**
+ * Get an integer value from the command-line or recipe, and add it to the arguments
+ */
+#define VALUE_ARG_RECIPE_INT(ARGNAME, RECIPENAME, TYPE, UNSET) { \
+    ps##TYPE value = psMetadataLookup##TYPE(NULL, arguments, ARGNAME); \
+    if (value == UNSET) { \
+        bool mdok; \
+        value = psMetadataLookup##TYPE(&mdok, recipe, RECIPENAME); \
+        if (!mdok) { \
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unable to find %s in recipe %s", \
+                RECIPENAME, PPMERGE_RECIPE); \
+            goto ERROR; \
+        } \
+    } \
+    psMetadataAdd##TYPE(config->arguments, PS_LIST_TAIL, RECIPENAME, 0, NULL, value); \
+}
+
+/**
+ * Get a boolean from the command-line or recipe, and add it to the arguments if either is set
+ */
+#define VALUE_ARG_RECIPE_BOOL(ARGNAME, RECIPENAME) { \
+    bool value = (psMetadataLookupBool(NULL, arguments, ARGNAME) || \
+                  psMetadataLookupBool(NULL, recipe, RECIPENAME)); \
+    psMetadataAddBool(config->arguments, PS_LIST_TAIL, RECIPENAME, 0, NULL, value); \
+}
+
+/**
+ * Get a statistic name from the command-line or recipe, and add the enum to the arguments
+ */
+#define VALUE_ARG_RECIPE_STAT(ARGNAME, RECIPENAME) { \
+    const char *stat = psMetadataLookupStr(NULL, arguments, ARGNAME); \
+    if (!stat) { \
+        stat = psMetadataLookupStr(NULL, recipe, RECIPENAME); \
+        if (!stat) { \
+            psError(PS_ERR_BAD_PARAMETER_VALUE, false, "Unable to find %s in recipe %s", \
+                    RECIPENAME, PPMERGE_RECIPE); \
+            goto ERROR; \
+        } \
+    } \
+    psMetadataAddS32(config->arguments, PS_LIST_TAIL, RECIPENAME, 0, NULL, psStatsOptionFromString(stat)); \
+}
+
+/**
+ * Get a string from the command-line or recipe, and add to the arguments
+ */
+#define VALUE_ARG_RECIPE_STR(ARGNAME, RECIPENAME) { \
+    const char *str = psMetadataLookupStr(NULL, arguments, ARGNAME); \
+    if (!str) { \
+        str = psMetadataLookupStr(NULL, recipe, RECIPENAME); \
+        if (!str) { \
+            psError(PS_ERR_BAD_PARAMETER_VALUE, false, "Unable to find %s in recipe %s", \
+                    RECIPENAME, PPMERGE_RECIPE); \
+            goto ERROR; \
+        } \
+    } \
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, RECIPENAME, 0, NULL, str); \
+}
+
+/**
+ * Get a string from the command-line or recipe, and add the appropriate mask value to the arguments
+ */
+#define VALUE_ARG_RECIPE_MASK(ARGNAME, RECIPENAME) { \
+    const char *str = psMetadataLookupStr(NULL, arguments, ARGNAME); \
+    if (!str) { \
+        str = psMetadataLookupStr(NULL, recipe, RECIPENAME); \
+        if (!str) { \
+            psError(PS_ERR_BAD_PARAMETER_VALUE, false, "Unable to find %s in recipe %s", \
+                    RECIPENAME, PPMERGE_RECIPE); \
+            goto ERROR; \
+        } \
+    } \
+    psImageMaskType mask = pmConfigMask(str, config); \
+    psMetadataAddImageMask(config->arguments, PS_LIST_TAIL, RECIPENAME, 0, NULL, mask); \
+}
+
+/**
+ * Get a string value from the command-line and add it to the target
+ */
+static bool valueArgStr(psMetadata *arguments, ///< Command-line arguments
+                        const char *argName, ///< Argument name in the command-line arguments
+                        const char *mdName, ///< Name for value in the metadata
+                        psMetadata *target ///< Target metadata to which to add value
+                        )
+{
+    psString value = psMetadataLookupStr(NULL, arguments, argName); ///< Value of interest
+    if (value && strlen(value) > 0) {
+        return psMetadataAddStr(target, PS_LIST_TAIL, mdName, 0, NULL, value);
+    }
+    return false;
+}
+
+bool ppMergeArguments(int argc, char *argv[], pmConfig *config)
+{
+    assert(config);
+
+    psMetadata *arguments = psMetadataAlloc(); ///< Command-line arguments
+    psMetadataAddStr(arguments, PS_LIST_TAIL, "-type", 0, "Type of calibration frame", NULL);
+    psMetadataAddStr(arguments, PS_LIST_TAIL, "-stats", 0, "MDC file to hold statistics ", NULL);
+    // Standard combination parameters
+    psMetadataAddS32(arguments, PS_LIST_TAIL, "-rows",     0, "Rows to read per scan", 0);
+    psMetadataAddS32(arguments, PS_LIST_TAIL, "-sample",   0, "Sampling factor for background", 0);
+    psMetadataAddS32(arguments, PS_LIST_TAIL, "-iter",     0, "Number of rejection iterations", 0);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-rej",      0, "Rejection threshold (sigma)", NAN);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-fraclow",  0, "Fraction of low pixels to discard", NAN);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-frachigh", 0, "Fraction of low pixels to discard", NAN);
+    psMetadataAddS32(arguments, PS_LIST_TAIL, "-nkeep",    0, "Minimum number of pixels in stack to keep", 0);
+    psMetadataAddBool(arguments, PS_LIST_TAIL, "-variances", 0, "Use image variances in combination?", false);
+    psMetadataAddBool(arguments, PS_LIST_TAIL, "-use-masks", 0, "Use image masks in combination?", false);
+
+    // XXX EAM : not clear this should be allowed on the command line.
+    // psMetadataAddStr(arguments, PS_LIST_TAIL, "-maskval",  0, "Mask value for input data", NULL);
+
+    psMetadataAddStr(arguments, PS_LIST_TAIL, "-combine",  0, "Statistic to use for combination", NULL);
+    psMetadataAddStr(arguments, PS_LIST_TAIL, "-mean",     0, "Statistic to use to measure the mean", NULL);
+    psMetadataAddStr(arguments, PS_LIST_TAIL, "-stdev",    0, "Statistic to use to measure the stdev", NULL);
+
+    /** Fringe construction parameters */
+    psMetadataAddS32(arguments, PS_LIST_TAIL, "-fringe-num",     0, "Number of fringe regions", 0);
+    psMetadataAddS32(arguments, PS_LIST_TAIL, "-fringe-size",    0, "Half-size of fringe regions", 0);
+    psMetadataAddS32(arguments, PS_LIST_TAIL, "-fringe-xsmooth", 0, "Number of smoothing regions in x", 0);
+    psMetadataAddS32(arguments, PS_LIST_TAIL, "-fringe-ysmooth", 0, "Number of smoothing regions in y", 0);
+    psMetadataAddBool(arguments, PS_LIST_TAIL, "-fringe-smooth", 0, "Smooth output image", false);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-fringe-smooth-sigma", 0, "Size of smoothing Gaussian", NAN);
+
+    /** CTEMASK construction parameters */
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-cte-min", 0, "min allowed value for good CTE", NAN);
+
+    /** Shutter construction parameters */
+    psMetadataAddS32(arguments, PS_LIST_TAIL, "-shutter-size", 0, "Size for shutter measurement regions", 0);
+    psMetadataAddS32(arguments, PS_LIST_TAIL, "-shutter-iter", 0, "Number of iterations for shutter", 0);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-shutter-rej",  0, "Rejection limit for shutter", NAN);
+
+    /** Mask construction parameters */
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-mask-suspect-sigma",  0, "Threshold for suspect pixels (sigma)", NAN);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-mask-suspect-min",  0, "Threshold for suspect pixels (sigma)", NAN);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-mask-suspect-max",  0, "Threshold for suspect pixels (sigma)", NAN);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-mask-bad",      0, "Threshold for bad pixels (sigma)", NAN);
+    psMetadataAddStr(arguments, PS_LIST_TAIL, "-mask-mode",     0, "Mode to identify bad pixels", NULL);
+    psMetadataAddS32(arguments, PS_LIST_TAIL, "-mask-grow",     0, "Number of pixels to grow final mask", 0);
+    psMetadataAddStr(arguments, PS_LIST_TAIL, "-mask-set-value",0, "Value to set for output mask pixels", NULL);
+    psMetadataAddBool(arguments, PS_LIST_TAIL, "-mask-chip",    0, "Measure mask statistics by chip?", false);
+
+    psMetadataAddBool(arguments, PS_LIST_TAIL, "-mask-smooth-suspect",  0, "smooth suspect pixels?", false);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-mask-smooth-scale",  0, "smoothing sigma for suspect pixels", NAN);
+
+    // chip & cell selections are used to limit chips to be processed
+    int argnum;
+    if ((argnum = psArgumentGet (argc, argv, "-chip"))) {
+        psArgumentRemove (argnum, &argc, argv);
+        psMetadataAddStr (config->arguments, PS_LIST_TAIL, "CHIP_SELECTIONS", PS_DATA_STRING, "", argv[argnum]);
+        psArgumentRemove (argnum, &argc, argv);
+    }
+    if ((argnum = psArgumentGet (argc, argv, "-cell"))) {
+        psArgumentRemove (argnum, &argc, argv);
+        psMetadataAddStr (config->arguments, PS_LIST_TAIL, "CELL_SELECTIONS", PS_DATA_STRING, "", argv[argnum]);
+        psArgumentRemove (argnum, &argc, argv);
+    }
+
+    if ((argnum = psArgumentGet (argc, argv, "-visual"))) {
+        psArgumentRemove (argnum, &argc, argv);
+	pmVisualSetVisual(true);
+    }
+
+    // Number of threads
+    if ((argnum = psArgumentGet(argc, argv, "-threads"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        int nThreads = atoi(argv[argnum]);
+        psMetadataAddS32(config->arguments, PS_LIST_TAIL, "NTHREADS", 0, "number of warp threads", nThreads);
+        psArgumentRemove(argnum, &argc, argv);
+
+        // create the thread pool with number of desired threads, supplying our thread launcher function
+        // XXX need to determine the number of threads from the config data
+        psThreadPoolInit (nThreads);
+    }
+    ppMergeSetThreads();
+
+    if (argc == 1 || !psArgumentParse(arguments, &argc, argv) || argc != 3) {
+        usage(argv[0], arguments);
+        goto ERROR;
+    }
+
+    unsigned int numBad = 0;                     ///< Number of bad lines
+    psMetadata *inputs = psMetadataConfigRead(NULL, &numBad, argv[1], false); ///< Information about inputs
+    if (!inputs || numBad > 0) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, false, "Unable to cleanly read MDC file with inputs.");
+        goto ERROR;
+    }
+    psMetadataAddMetadata(config->arguments, PS_LIST_TAIL, "INPUTS", 0,
+                          "Metadata with input details", inputs);
+    psFree(inputs);
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "OUTPUT", 0,
+                     "Root name of the output image list", argv[2]);
+
+    valueArgStr(arguments, "-stats", "STATS.NAME", config->arguments);
+
+
+    // Set the type of calibration frame
+    const char *typeStr = psMetadataLookupStr(NULL, arguments, "-type"); ///< Type of calibration
+    if (!typeStr || strlen(typeStr) <= 0) {
+      psError(PS_ERR_UNKNOWN, false, "No -type specified.");
+      goto ERROR;
+    }
+    ppMergeType type = PPMERGE_TYPE_UNKNOWN; ///< Enumerated type for frame type
+    if (strcasecmp(typeStr, "BIAS") == 0) {
+      type = PPMERGE_TYPE_BIAS;
+      goto VALID;
+    }
+    if (strcasecmp(typeStr, "DARK") == 0 ||
+        strcasecmp(typeStr, "DARKTEST") == 0 ||
+        strcasecmp(typeStr, "DARK_PREMASK") == 0) {
+      type = PPMERGE_TYPE_DARK;
+      goto VALID;
+    }
+    if (!strcasecmp(typeStr, "FLAT") ||
+        !strcasecmp(typeStr, "FLATTEST") ||
+        !strcasecmp(typeStr, "DOMEFLAT") ||
+        !strcasecmp(typeStr, "SKYFLAT") ||
+        !strcasecmp(typeStr, "FLAT_RAW") ||
+        !strcasecmp(typeStr, "SKYFLAT_RAW") ||
+	!strcasecmp(typeStr, "SKYFLATTEST_RAW") ||
+        !strcasecmp(typeStr, "DOMEFLAT_RAW") ||
+        !strcasecmp(typeStr, "FLAT_PREMASK") ||
+        !strcasecmp(typeStr, "SKYFLAT_PREMASK") ||
+        !strcasecmp(typeStr, "DOMEFLAT_PREMASK")) {
+      type = PPMERGE_TYPE_FLAT;
+      goto VALID;
+    }
+    if (strcasecmp(typeStr, "FRINGE") == 0) {
+      type = PPMERGE_TYPE_FRINGE;
+      goto VALID;
+    }
+    if (strcasecmp(typeStr, "SHUTTER") == 0) {
+      type = PPMERGE_TYPE_SHUTTER;
+      goto VALID;
+    }
+    if (strcasecmp(typeStr, "CTEMASK") == 0) {
+      type = PPMERGE_TYPE_CTEMASK;
+      goto VALID;
+    }
+    if (strcasecmp(typeStr, "MASK") == 0 ||
+        strcasecmp(typeStr, "DARKMASK") == 0 ||
+        strcasecmp(typeStr, "FLATMASK") == 0) {
+      type = PPMERGE_TYPE_MASK;
+      goto VALID;
+    }
+    if (strcasecmp(typeStr, "NOISEMAP") == 0) {
+      type = PPMERGE_TYPE_NOISEMAP;
+      goto VALID;
+    }
+    psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unrecognised image type: %s", typeStr);
+    goto ERROR;
+
+ VALID:
+    psMetadataAddS32(config->arguments, PS_LIST_TAIL, "TYPE", 0, "Type of calibration frame", type);
+
+    // Need to set the camera before the recipes can be read
+    if (!ppMergeCamera(config)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to setup cameras.");
+        goto ERROR;
+    }
+
+    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, PPMERGE_RECIPE); ///< Recipe for ppSim
+    if (!recipe) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find recipe %s", PPMERGE_RECIPE);
+        goto ERROR;
+    }
+
+    /** Standard combination parameters */
+    VALUE_ARG_RECIPE_INT("-rows",       "ROWS",     S32, 0);
+    VALUE_ARG_RECIPE_INT("-sample",     "SAMPLE",   S32, 0);
+    VALUE_ARG_RECIPE_INT("-iter",       "ITER",     S32, 0);
+    VALUE_ARG_RECIPE_FLOAT("-rej",      "REJ",      F32);
+    VALUE_ARG_RECIPE_FLOAT("-fraclow",  "FRACLOW",  F32);
+    VALUE_ARG_RECIPE_FLOAT("-frachigh", "FRACHIGH", F32);
+    VALUE_ARG_RECIPE_INT("-nkeep",      "NKEEP",    S32, 0);
+    VALUE_ARG_RECIPE_BOOL("-variances", "VARIANCES");
+    VALUE_ARG_RECIPE_BOOL("-use-masks", "INPUTS.MASKS.USE");
+
+    // XXX we do not supply this on the command line
+    // VALUE_ARG_RECIPE_MASK("-maskval",   "MASKVAL");
+
+    VALUE_ARG_RECIPE_STAT("-combine",   "COMBINE");
+    VALUE_ARG_RECIPE_STAT("-mean",      "MEAN");
+    VALUE_ARG_RECIPE_STAT("-stdev",     "STDEV");
+
+    /** Fringe construction parameters */
+    VALUE_ARG_RECIPE_INT("-fringe-num",     "FRINGE.NUM",     S32, 0);
+    VALUE_ARG_RECIPE_INT("-fringe-size",    "FRINGE.SIZE",    S32, 0);
+    VALUE_ARG_RECIPE_INT("-fringe-xsmooth", "FRINGE.XSMOOTH", S32, 0);
+    VALUE_ARG_RECIPE_INT("-fringe-ysmooth", "FRINGE.YSMOOTH", S32, 0);
+    VALUE_ARG_RECIPE_BOOL("-fringe-smooth", "FRINGE.SMOOTH");
+    VALUE_ARG_RECIPE_FLOAT("-fringe-smooth-sigma", "FRINGE.SMOOTH.SIGMA", F32);
+
+    /** CTEMASK construction parameters */
+    VALUE_ARG_RECIPE_FLOAT("-cte-min",      "CTE.MIN",        F32);
+
+    /** Shutter construction parameters */
+    VALUE_ARG_RECIPE_INT("-shutter-size",  "SHUTTER.SIZE", S32, 0);
+
+    /** Mask construction parameters */
+    VALUE_ARG_RECIPE_FLOAT("-mask-suspect-sigma", "MASK.SUSPECT.SIGMA", F32);
+    VALUE_ARG_RECIPE_FLOAT("-mask-suspect-min",   "MASK.SUSPECT.MIN", F32);
+    VALUE_ARG_RECIPE_FLOAT("-mask-suspect-max",   "MASK.SUSPECT.MAX", F32);
+    VALUE_ARG_RECIPE_STR("-mask-mode", "MASK.SUSPECT.MODE");
+    VALUE_ARG_RECIPE_FLOAT("-mask-bad",     "MASK.BAD",     F32);
+    VALUE_ARG_RECIPE_INT("-mask-grow",      "MASK.GROW",    S32, 0);
+    VALUE_ARG_RECIPE_STR("-mask-set-value", "MASK.SET.VALUE");
+    VALUE_ARG_RECIPE_BOOL("-mask-chip",     "MASK.CHIPSTATS");
+
+    VALUE_ARG_RECIPE_BOOL("-mask-smooth-suspect", "MASK.SMOOTH.SUSPECT");
+    VALUE_ARG_RECIPE_FLOAT("-mask-smooth-scale", "MASK.SMOOTH.SCALE", F32);
+
+    const char *maskModeStr = psMetadataLookupStr(NULL, arguments, "-mask-mode"); ///< Mode to identify bad pix
+    if (!maskModeStr) {
+        maskModeStr = psMetadataLookupStr(NULL, recipe, "MASK.MODE");
+        if (!maskModeStr) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, false, "No mask mode specified in recipe.");
+            goto ERROR;
+        }
+    }
+    pmMaskIdentifyMode maskMode = pmMaskIdentifyModeFromString(maskModeStr);
+    if (maskMode == PM_MASK_ID_NONE) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, false, "Invalid mask mode %s", maskModeStr);
+        goto ERROR;
+    }
+    psMetadataAddS32(config->arguments, PS_LIST_TAIL, "MASK.MODE", 0, "Mode for mask identification",
+                     maskMode);
+
+
+    if (type == PPMERGE_TYPE_DARK) {
+        psMetadata *ordinates = psMetadataLookupMetadata(NULL, recipe, "DARK.ORDINATES"); ///< Ordinates info
+        psArray *translated = psArrayAllocEmpty(psListLength(ordinates->list)); ///< Translated version
+
+        psMetadataIterator *iter = psMetadataIteratorAlloc(ordinates, PS_LIST_HEAD, NULL); ///< Iterator
+        psMetadataItem *item;           ///< Item from iteration
+        while ((item = psMetadataGetAndIncrement(iter))) {
+            int order = 0;              ///< Polynomial order
+            bool scale = false;         ///< Scale values?
+            float min = NAN, max = NAN; ///< Minimum and maximum values for scaling
+	    char *rule = NULL;
+            switch (item->type) {
+              case PS_TYPE_S32:
+                order = item->data.S32;
+                break;
+              case PS_DATA_METADATA:
+                order = psMetadataLookupS32(NULL, item->data.md, "ORDER");
+                bool mdok;                  ///< Status of MD lookup
+                scale = psMetadataLookupBool(&mdok, item->data.md, "SCALE");
+                min = psMetadataLookupF32(&mdok, item->data.md, "MIN");
+                max = psMetadataLookupF32(&mdok, item->data.md, "MAX");
+                rule = psMetadataLookupStr(&mdok, item->data.md, "RULE");
+                break;
+              default:
+                psError(PS_ERR_BAD_PARAMETER_TYPE, true,
+                        "Type of DARK.ORDINATES entry %s (%x) is not METADATA or S32",
+                        item->name, item->type);
+                return false;
+            }
+            if (order <= 0) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "ORDER not positive (%d) for DARK.ORDINATES %s",
+                        order, item->name);
+                return false;
+            }
+
+            pmDarkOrdinate *ord = pmDarkOrdinateAlloc(item->name, order);
+            ord->scale = scale;
+            ord->min = min;
+            ord->max = max;
+	    ord->rule = psStringCopy(rule);
+            psArrayAdd(translated, translated->n, ord);
+            psFree(ord);
+        }
+        psFree(iter);
+
+        psMetadataAddArray(config->arguments, PS_LIST_TAIL, "DARK.ORDINATES", 0,
+                           "Ordinates to fit for dark", translated);
+        psFree(translated);             ///< Drop reference
+
+        psString darkNorm = psMetadataLookupStr(NULL, recipe, "DARK.NORM"); ///<Normalisation concept
+        if (darkNorm && strcmp(darkNorm, "NONE") != 0) {
+            psMetadataAddStr(config->arguments, PS_LIST_TAIL, "DARK.NORM", 0,
+                             "Normalisation concept for dark", darkNorm);
+        }
+    }
+
+
+#if 0
+    // Add concepts for scale and zero
+    // XXX These have never been used
+    psMetadataItem *scaleItem = psMetadataItemAllocF32("PPMERGE.SCALE", "Scaling for ppMerge", NAN);
+    psMetadataItem *zeroItem = psMetadataItemAllocF32("PPMERGE.ZERO", "Zero offset for ppMerge", NAN);
+    pmConceptRegister(scaleItem, NULL, NULL, false, PM_FPA_LEVEL_CELL);
+    pmConceptRegister(zeroItem, NULL, NULL, false, PM_FPA_LEVEL_CELL);
+    psFree(scaleItem);
+    psFree(zeroItem);
+#endif
+
+    psFree(arguments);
+    return true;
+
+ERROR:
+    psFree(arguments);
+    return false;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeCamera.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeCamera.c	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeCamera.c	(revision 41089)
@@ -0,0 +1,385 @@
+/** @file ppMergeCamera.c
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-06 02:44:31 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#include "ppMerge.h"
+
+/**
+ * Define an output file, with its own FPA
+ */
+bool outputFile(pmConfig *config,       ///< Configuration
+                const char *name,       ///< Name of output file
+                pmFPAfileType type,     ///< Type of file
+                psMetadata *format,     ///< Camera format
+                pmFPAview *view         ///< View for PHU
+    )
+{
+    assert(config);
+    assert(name && strlen(name) > 0);
+    assert(view);
+
+    // Output image
+    pmFPA *fpa = pmFPAConstruct(config->camera, config->cameraName); ///< FPA to contain the output
+    if (!fpa) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to construct an FPA from camera configuration.");
+        return false;
+    }
+
+    // Chip selection: turn on only the chips specified.
+    bool status;
+    char *chipLine = psMetadataLookupStr(&status, config->arguments, "CHIP_SELECTIONS");
+    psArray *chips = psStringSplitArray (chipLine, ",", false);
+    if (chips->n > 0) {
+        pmFPASelectChip (fpa, -1, true); // deselect all chips
+        for (int i = 0; i < chips->n; i++) {
+            int chipNum = atoi(chips->data[i]);
+            if (! pmFPASelectChip(fpa, chipNum, false)) {
+                psError(PS_ERR_IO, false, "Chip number %d doesn't exist in camera.\n", chipNum);
+                psFree (chips);
+                return false;
+            }
+        }
+    }
+    psFree (chips);
+
+    // Cell selection: turn on only the cells specified.  Note that this affects the same cell
+    // for all chips
+    char *cellLine = psMetadataLookupStr(&status, config->arguments, "CELL_SELECTIONS");
+    psArray *cells = psStringSplitArray (cellLine, ",", false);
+    if (cells->n > 0) {
+        for (int i = 0; i < fpa->chips->n; i++) {
+            pmChip *chip = fpa->chips->data[i];
+            pmChipSelectCell (chip, -1, true); // deselect all cells
+            for (int j = 0; j < cells->n; j++) {
+                int cellNum = atoi(cells->data[j]);
+                if (! pmChipSelectCell(chip, cellNum, false)) {
+                    psError(PS_ERR_IO, false, "Cell number %d doesn't exist in camera.\n", cellNum);
+                    psFree (cells);
+                    return false;
+                }
+            }
+        }
+    }
+    psFree (cells);
+
+    pmFPAfile *output = pmFPAfileDefineOutput(config, fpa, name);
+    psFree(fpa);                        // Drop reference
+    if (!output) {
+        psError(PS_ERR_IO, false, "Unable to generate output file from %s", name);
+        return false;
+    }
+    if (output->type != type) {
+        psError(PS_ERR_IO, true, "%s is not of type %s", name, pmFPAfileStringFromType(type));
+        return false;
+    }
+    output->save = true;
+
+    if (!pmFPAAddSourceFromView(fpa, view, format)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to generate output FPA.");
+        return false;
+    }
+
+    return true;
+}
+
+
+bool ppMergeCamera(pmConfig *config)
+{
+    bool haveMasks = false;             // Do we have masks?
+    bool haveVariances = false;           // Do we have variance maps?
+
+    ppMergeType type = psMetadataLookupS32(NULL, config->arguments, "TYPE"); ///< Type of frame
+    psMetadata *inputs = psMetadataLookupMetadata(NULL, config->arguments, "INPUTS"); ///< The inputs info
+    psMetadataIterator *iter = psMetadataIteratorAlloc(inputs, PS_LIST_HEAD, NULL); ///< Iterator
+    psMetadataItem *item;               ///< Item from iteration
+    int numFiles = 0;                   ///< Number of files
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        if (item->type != PS_DATA_METADATA) {
+            psError(PS_ERR_BAD_PARAMETER_TYPE, true,
+                    "Component %s of the input metadata is not of type METADATA", item->name);
+            psFree(iter);
+            return false;
+        }
+
+        psMetadata *input = item->data.md; ///< The input metadata of interest
+
+        psString image = psMetadataLookupStr(NULL, input, "IMAGE"); ///< Name of image
+        if (!image || strlen(image) == 0) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Component %s lacks IMAGE of type STR", item->name);
+            psFree(iter);
+            return false;
+        }
+
+        bool mdok;
+        psString mask = psMetadataLookupStr(&mdok, input, "MASK"); // Name of mask
+        psString variance = psMetadataLookupStr(&mdok, input, "VARIANCE"); // Name of variance map
+
+        // Add the image file
+        psArray *imageFiles = psArrayAlloc(1); ///< Array of filenames for this FPA
+        imageFiles->data[0] = psMemIncrRefCounter(image);
+        psMetadataAddArray(config->arguments, PS_LIST_TAIL, "IMAGE.FILENAMES", PS_META_REPLACE,
+                           "Filenames of image files", imageFiles);
+        psFree(imageFiles);
+
+        bool found = false;             // Found the file?
+        pmFPAfile *imageFile = pmFPAfileDefineFromArgs(&found, config, "PPMERGE.INPUT", "IMAGE.FILENAMES");
+        if (!imageFile || !found) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to define file from image %d (%s)", numFiles, image);
+            return false;
+        }
+        if (imageFile->type != PM_FPA_FILE_IMAGE) {
+            psError(PS_ERR_IO, true, "PPMERGE.INPUT is not of type IMAGE");
+            return false;
+        }
+
+        // Optionally add the mask file
+        if (mask && strlen(mask) > 0) {
+            psArray *maskFiles = psArrayAlloc(1); ///< Array of filenames for this FPA
+            maskFiles->data[0] = psMemIncrRefCounter(mask);
+            psMetadataAddArray(config->arguments, PS_LIST_TAIL, "MASK.FILENAMES", PS_META_REPLACE,
+                               "Filenames of mask files", maskFiles);
+            psFree(maskFiles);
+
+            bool status;
+            pmFPAfile *maskFile = pmFPAfileBindFromArgs(&status, imageFile, config, "PPMERGE.INPUT.MASK",
+                                                        "MASK.FILENAMES");
+            if (!status) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to define file from mask %d (%s)", numFiles, mask);
+                return false;
+            }
+            if (maskFile->type != PM_FPA_FILE_MASK) {
+                psError(PS_ERR_IO, true, "PPMERGE.INPUT.MASK is not of type MASK");
+                return false;
+            }
+            haveMasks = true;
+        }
+
+        // Optionally add the variance file
+        if (variance && strlen(variance) > 0) {
+            haveVariances = true;
+            psArray *varianceFiles = psArrayAlloc(1); // Array of filenames for this FPA
+            varianceFiles->data[0] = psMemIncrRefCounter(variance);
+            psMetadataAddArray(config->arguments, PS_LIST_TAIL, "VARIANCE.FILENAMES", PS_META_REPLACE,
+                               "Filenames of variance files", varianceFiles);
+            psFree(varianceFiles);
+
+            bool status;
+            pmFPAfile *varianceFile = pmFPAfileBindFromArgs(&status, imageFile, config,
+                                                            "PPMERGE.INPUT.VARIANCE", "VARIANCE.FILENAMES");
+            if (!status) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to define file from variance %d (%s)",
+                        numFiles, variance);
+                return false;
+            }
+            if (varianceFile->type != PM_FPA_FILE_VARIANCE) {
+                psError(PS_ERR_IO, true, "PPMERGE.INPUT.VARIANCE is not of type VARIANCE");
+                return false;
+            }
+            haveVariances = true;
+        }
+
+        numFiles++;
+    }
+    psFree(iter);
+    psMetadataRemoveKey(config->arguments, "IMAGE.FILENAMES");
+    if (psMetadataLookup(config->arguments, "MASK.FILENAMES")) {
+        psMetadataRemoveKey(config->arguments, "MASK.FILENAMES");
+    }
+    if (psMetadataLookup(config->arguments, "VARIANCE.FILENAMES")) {
+        psMetadataRemoveKey(config->arguments, "VARIANCE.FILENAMES");
+    }
+    if (psMetadataLookup(config->arguments, "PSF.FILENAMES")) {
+        psMetadataRemoveKey(config->arguments, "PSF.FILENAMES");
+    }
+
+    psMetadataAddS32(config->arguments, PS_LIST_TAIL, "INPUTS.NUM", 0, "Number of input files", numFiles);
+    psMetadataAddBool(config->arguments, PS_LIST_TAIL, "INPUTS.MASKS", 0, "Got input masks?", haveMasks);
+    psMetadataAddBool(config->arguments, PS_LIST_TAIL, "INPUTS.VARIANCES", 0,
+                      "Got input variances?", haveVariances);
+
+
+    // Check that all the inputs are consistent
+
+// Check an FPA level to ensure the camera format and PHU are consistent across all input files
+#define CHECK_LEVEL(HDU, CHIP, CELL) { \
+    if (HDU) { \
+        if (!phuView) { \
+            phuView = pmFPAviewAlloc(0); \
+            phuView->chip = CHIP; \
+            phuView->cell = CELL; \
+        } else if ((phuView->chip != (CHIP)) && (phuView->cell != (CELL))) { \
+            psError(PS_ERR_UNKNOWN, true, "Differing PHU for input %d", i); \
+            psFree(phuView); \
+            return false; \
+        } \
+        if (!format) { \
+            format = (HDU)->format; \
+        } else if (format != (HDU)->format) { \
+	  psError(PS_ERR_UNKNOWN, true, "Camera format %d doesn't match: %p vs %p on %s %s", \
+		  i, format, (HDU)->format, (HDU)->extname, psMetadataLookupStr(NULL,(HDU)->header,"FILENAME")); \
+            psFree(phuView); \
+            return false; \
+        } \
+        continue; \
+    } \
+}
+    // CZW: 2012-04-10 Version of above to allow different camera formats to be processed together.
+/* #define CHECK_LEVEL(HDU, CHIP, CELL) { \ */
+/*     if (HDU) { \ */
+/*         if (!phuView) { \ */
+/*             phuView = pmFPAviewAlloc(0); \ */
+/*             phuView->chip = CHIP; \ */
+/*             phuView->cell = CELL; \ */
+/*         } else if ((phuView->chip != (CHIP)) && (phuView->cell != (CELL))) { \ */
+/*             psError(PS_ERR_UNKNOWN, true, "Differing PHU for input %d", i); \ */
+/*             psFree(phuView); \ */
+/*             return false; \ */
+/*         } \ */
+/*         if (!format) { \ */
+/*             format = (HDU)->format; \ */
+/*         } else if (format != (HDU)->format) { \ */
+/*         } \ */
+/*         continue; \ */
+/*     } \ */
+/* } */
+
+    psMetadata *format = NULL;          ///< Camera format
+    pmFPAview *phuView = NULL;          ///< View to PHU
+    for (int i = 0; i < numFiles; i++) {
+        pmFPAfile *input = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT", i); ///< File of interest
+        pmFPA *fpa = input->fpa;        ///< FPA of interest
+        CHECK_LEVEL(fpa->hdu, -1, -1);
+        psArray *chips = fpa->chips;    ///< Array of chips
+        for (int j = 0; j < chips->n; j++) {
+            pmChip *chip = chips->data[j]; ///< Chip of interest
+            CHECK_LEVEL(chip->hdu, j, -1);
+            psArray *cells = chip->cells;   ///< Array of cells
+            for (int k = 0; k < cells->n; k++) {
+                pmCell *cell = cells->data[k]; ///< Cell of interest
+                CHECK_LEVEL(cell->hdu, j, k);
+            }
+        }
+    }
+    if (!phuView || !format) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find PHU for input files.");
+        psFree(phuView);
+        return false;
+    }
+
+    // Cull chips and cells that don't have data
+    // Otherwise the abundance of metadata in the concepts (esp. for GPC) can overload the memory
+    for (int i = 0; i < numFiles; i++) {
+        pmFPAfile *input = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT", i); ///< File of interest
+        pmFPA *fpa = input->fpa;        ///< FPA of interest
+        psArray *chips = fpa->chips; ///< Array of chips in output
+        for (int i = 0; i < chips->n; i++) {
+            pmChip *chip = chips->data[i]; ///< Chip of interest
+            psArray *cells = chip->cells; ///< Array of cells
+            int culled = 0;             ///< Number of culled cells
+            for (int j = 0; j < cells->n; j++) {
+                pmCell *cell = cells->data[j];
+                pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); ///< HDU for cell
+                if (!hdu || hdu->blankPHU) {
+                    cell->data_exists = false;
+                    cell->file_exists = false;
+                    culled++;
+                    if (cell->concepts) {
+                        psFree(cell->concepts);
+                        cell->concepts = NULL;
+                    }
+                }
+            }
+            if (culled == cells->n) {
+                chip->data_exists = false;
+                chip->file_exists = false;
+                if (chip->concepts) {
+                    psFree(chip->concepts);
+                    chip->concepts = NULL;
+                }
+            }
+        }
+    }
+
+    // Count the cells
+    {
+        int numCells = 0;               ///< Number of cells
+        pmFPAfile *input = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT", 0); ///< Representative file
+        pmFPA *fpa = input->fpa;        ///< FPA for file
+        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
+                pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); ///< HDU that would have data
+                if (hdu && !hdu->blankPHU) {
+                    numCells++;
+                }
+            }
+        }
+
+        psMetadataAddS32(config->arguments, PS_LIST_TAIL, "INPUTS.CELLS", 0, "Number of cells in input",
+                         numCells);
+    }
+
+    psString outName = ppMergeOutputFile(config); ///< Name of output file
+
+    pmFPAfileType fileType = PM_FPA_FILE_NONE; ///< Type of output file
+    switch (type) {
+      case PPMERGE_TYPE_BIAS:
+        fileType = PM_FPA_FILE_IMAGE;
+        break;
+      case PPMERGE_TYPE_DARK:
+        fileType = PM_FPA_FILE_DARK;
+        break;
+      case PPMERGE_TYPE_MASK:
+        fileType = PM_FPA_FILE_MASK;
+        break;
+      case PPMERGE_TYPE_CTEMASK:
+        fileType = PM_FPA_FILE_MASK;
+        break;
+      case PPMERGE_TYPE_SHUTTER:
+        fileType = PM_FPA_FILE_IMAGE;
+        break;
+      case PPMERGE_TYPE_FLAT:
+        fileType = PM_FPA_FILE_IMAGE;
+        break;
+      case PPMERGE_TYPE_NOISEMAP:
+	fileType = PM_FPA_FILE_IMAGE;
+	break;
+      case PPMERGE_TYPE_FRINGE:
+        fileType = PM_FPA_FILE_FRINGE;
+        break;
+      default:
+        psAbort("Unknown frame type: %x", type);
+    }
+
+    if (!outputFile(config, outName, fileType, format, phuView)) {
+        psFree(outName);
+        psFree(phuView);
+        return false;
+    }
+    psFree(outName);
+
+    if (!outputFile(config, "PPMERGE.OUTPUT.SIGMA", PM_FPA_FILE_IMAGE, format, phuView)) {
+        psFree(phuView);
+        return false;
+    }
+
+    if (!outputFile(config, "PPMERGE.OUTPUT.COUNT", PM_FPA_FILE_IMAGE, format, phuView)) {
+        psFree(phuView);
+        return false;
+    }
+
+    psFree(phuView);
+
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeErrorCodes.c.in
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeErrorCodes.c.in	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeErrorCodes.c.in	(revision 41089)
@@ -0,0 +1,38 @@
+/** @file ppMergeErrorCodes.c.in
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-01 21:43:05 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#include "pslib.h"
+#include "ppMergeErrorCodes.h"
+
+/*
+ * The line
+    { PPMERGE_ERR_$X{ErrorCode}, "$X{ErrorDescription}"},
+ * (without the Xs)
+ * will be replaced by values from errorCodes.dat
+ */
+void ppMergeErrorRegister(void)
+{
+    static psErrorDescription errors[] = {
+       { PPMERGE_ERR_BASE, "First value we use; lower values belong to psLib" },
+       { PPMERGE_ERR_${ErrorCode}, "${ErrorDescription}"},
+    };
+    static int nerror = PPMERGE_ERR_NERROR - PPMERGE_ERR_BASE; ///< number of values in enum
+
+    for (int i = 0; i < nerror; i++) {
+       psErrorDescription *tmp = psAlloc(sizeof(psErrorDescription));
+       p_psMemSetPersistent(tmp, true);
+       *tmp = errors[i];
+       psErrorRegister(tmp, 1);
+       psFree(tmp);			// it's on the internal list
+    }
+    nerror = 0;			                // don't register more than once
+}
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeErrorCodes.dat
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeErrorCodes.dat	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeErrorCodes.dat	(revision 41089)
@@ -0,0 +1,10 @@
+#
+# This file is used to generate ppMergeErrorClasses.h
+#
+BASE = 6000		First value we use; lower values belong to psLib
+# 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/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeErrorCodes.h.in
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeErrorCodes.h.in	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeErrorCodes.h.in	(revision 41089)
@@ -0,0 +1,30 @@
+/** @file ppMergeErrorCodes.h.in
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-01 21:43:05 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#if !defined(PPMERGE_ERROR_CODES_H)
+#define PPMERGE_ERROR_CODES_H
+/*
+ * The line
+ *  PPMERGE_ERR_$X{ErrorCode},
+ * (without the X)
+ *
+ * will be replaced by values from errorCodes.dat
+ */
+typedef enum {
+    PPMERGE_ERR_BASE = 6000,
+    PPMERGE_ERR_${ErrorCode},
+    PPMERGE_ERR_NERROR
+} ppMergeErrorCode;
+
+void ppMergeErrorRegister(void);
+
+#endif
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeFileGroup.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeFileGroup.c	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeFileGroup.c	(revision 41089)
@@ -0,0 +1,30 @@
+/** @file ppMergeFileGroup.c
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-01 21:43:05 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "ppMerge.h"
+
+static void mergeFileGroupFree(ppMergeFileGroup *group)
+{
+    psFree(group->readouts);
+    return;
+}
+
+ppMergeFileGroup *ppMergeFileGroupAlloc(void)
+{
+    ppMergeFileGroup *group = psAlloc(sizeof(ppMergeFileGroup));
+    psMemSetDeallocator(group, (psFreeFunc)mergeFileGroupFree);
+
+    group->readouts = NULL;
+    group->read = false;
+    group->busy = false;
+    return group;
+}
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeFiles.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeFiles.c	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeFiles.c	(revision 41089)
@@ -0,0 +1,234 @@
+/** @file ppMergeFiles.c
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.8 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-06 02:44:31 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#include "ppMerge.h"
+
+const char *allFiles[] = { "PPMERGE.INPUT", "PPMERGE.INPUT.MASK", "PPMERGE.INPUT.VARIANCE",
+                           "PPMERGE.OUTPUT", "PPMERGE.OUTPUT.COUNT", "PPMERGE.OUTPUT.SIGMA",
+                           NULL };      // All files
+const char *inputFiles[] = { "PPMERGE.INPUT", "PPMERGE.INPUT.MASK", "PPMERGE.INPUT.VARIANCE",
+                             NULL };    // Input files
+const char *outputFiles[] = { "PPMERGE.OUTPUT", "PPMERGE.OUTPUT.COUNT", "PPMERGE.OUTPUT.SIGMA",
+                              NULL };   ///< Output files
+
+/**
+ * Select file list based on enum
+ */
+static const char **selectFiles(ppMergeFiles files)
+{
+    switch (files) {
+      case PPMERGE_FILES_ALL:    return allFiles;
+      case PPMERGE_FILES_INPUT:  return inputFiles;
+      case PPMERGE_FILES_OUTPUT: return outputFiles;
+      default:
+        psAbort("Invalid file option");
+    }
+    return NULL;
+}
+
+bool ppMergeFileReadInput(pmConfig *config, pmReadout *readout, int num, int rows)
+{
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT", num);
+    if (!pmReadoutReadChunk(readout, file->fits, 0, NULL, rows, 0, config)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read readout.");
+        return false;
+    }
+    bool mdok;          // Status of MD lookup
+    if (psMetadataLookupBool(&mdok, config->arguments, "INPUTS.MASKS")) {
+        pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT.MASK", num);
+        if (!pmReadoutReadChunkMask(readout, file->fits, 0, NULL, rows, 0, config)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to read readout mask.");
+            return false;
+        }
+    }
+    if (psMetadataLookupBool(&mdok, config->arguments, "INPUTS.VARIANCES")) {
+        pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT.VARIANCE", num);
+        if (!pmReadoutReadChunkVariance(readout, file->fits, 0, NULL, rows, 0, config)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to read readout variance.");
+            return false;
+        }
+    }
+    return true;
+}
+
+bool ppMergeFileOpenInput(pmConfig *config, const pmFPAview *view, int num)
+{
+    {
+        pmFPAfile *input = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT", num);
+        pmFPAview *fileView = pmFPAviewForLevel(input->fileLevel, view);
+        if (!pmFPAfileOpen(input, fileView, config)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to open image file %d", num);
+            psFree(fileView);
+            return false;
+        }
+        psFree(fileView);
+
+        // Read the headers, so we can have the concepts available
+        pmCell *cell = pmFPAviewThisCell(view, input->fpa); // Cell of interest
+        if (!pmCellReadHeaderSet(cell, input->fits, config)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to read headers for image %d", num);
+            return false;
+        }
+    }
+    bool mdok;          // Status of MD lookup
+    if (psMetadataLookupBool(&mdok, config->arguments, "INPUTS.MASKS")) {
+        pmFPAfile *mask = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT.MASK", num); // Mask file
+        pmFPAview *fileView = pmFPAviewForLevel(mask->fileLevel, view);
+        if (!pmFPAfileOpen(mask, fileView, config)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to open mask file %d", num);
+            psFree(fileView);
+            return false;
+        }
+        psFree(fileView);
+    }
+    if (psMetadataLookupBool(&mdok, config->arguments, "INPUTS.VARIANCES")) {
+        pmFPAfile *variance = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT.VARIANCE",
+                                                    num); // Variance file
+        pmFPAview *fileView = pmFPAviewForLevel(variance->fileLevel, view);
+        if (!pmFPAfileOpen(variance, fileView, config)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to open variance file %d", num);
+            psFree(fileView);
+            return false;
+        }
+        psFree(fileView);
+    }
+    return true;
+}
+
+psArray *ppMergeFileDataLevel(const pmConfig *config, const char *name, pmFPALevel level)
+{
+    assert(config);
+    assert(name);
+
+    int numFiles = psMetadataLookupS32(NULL, config->arguments, "INPUTS.NUM"); // Number of input files
+    assert(numFiles > 0);
+    psArray *files = psArrayAlloc(numFiles); // Files of interest
+    for (int i = 0; i < numFiles; i++) {
+        pmFPAfile *file = pmFPAfileSelectSingle(config->files, name, i); // Image file
+        file->dataLevel = level;
+        file->freeLevel = level;
+        files->data[i] = psMemIncrRefCounter(file);
+    }
+    return files;
+}
+
+bool ppMergeFileActivate(const pmConfig *config, ppMergeFiles files, bool state)
+{
+    assert(config);
+
+    bool mdok;                          // Status of MD lookup
+    bool haveMasks = psMetadataLookupBool(&mdok, config->arguments, "INPUTS.MASKS"); // Do we have masks?
+    bool haveVariances = psMetadataLookupBool(&mdok, config->arguments, "INPUTS.VARIANCES"); // Got variances?
+
+    const char **fileList = selectFiles(files); // Files to activate
+    for (int i = 0; fileList[i] != NULL; i++) {
+        if (!haveMasks && strcmp(fileList[i], "PPMERGE.INPUT.MASK") == 0) {
+            continue;
+        }
+        if (!haveVariances && strcmp(fileList[i], "PPMERGE.INPUT.VARIANCE") == 0) {
+            continue;
+        }
+        psString name = NULL;           // Name of file
+        if (strcmp(fileList[i], "PPMERGE.OUTPUT") == 0) {
+            name = ppMergeOutputFile(config);
+        } else {
+            name = psStringCopy(fileList[i]);
+        }
+
+        if (!pmFPAfileActivate(config->files, state, name)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to activate file %s", name);
+            psFree(name);
+            return false;
+        }
+        psFree(name);
+    }
+
+    return true;
+}
+
+
+
+psArray *ppMergeFileActivateSingle(const pmConfig *config, ppMergeFiles files, bool state, int num)
+{
+    assert(config);
+
+    bool mdok;                          // Status of MD lookup
+    bool haveMasks = psMetadataLookupBool(&mdok, config->arguments, "INPUTS.MASKS"); // Do we have masks?
+    bool haveVariances = psMetadataLookupBool(&mdok, config->arguments, "INPUTS.VARIANCES"); // Got variances?
+
+    psList *list = psListAlloc(NULL);   // List of files
+    const char **fileList = selectFiles(files); // Files to activate
+    for (int i = 0; fileList[i] != NULL; i++) {
+        if (!haveMasks && strcmp(fileList[i], "PPMERGE.INPUT.MASK") == 0) {
+            continue;
+        }
+        if (!haveVariances && strcmp(fileList[i], "PPMERGE.INPUT.VARIANCE") == 0) {
+            continue;
+        }
+
+        psString name = NULL;           // Name of file
+        if (strcmp(fileList[i], "PPMERGE.OUTPUT") == 0) {
+            name = ppMergeOutputFile(config);
+        } else {
+            name = psStringCopy(fileList[i]);
+        }
+
+        pmFPAfile *file = pmFPAfileActivateSingle(config->files, state, name, num); // Activated file
+        psFree(name);
+        psListAdd(list, PS_LIST_TAIL, file);
+    }
+
+    psArray *array = psListToArray(list);
+    psFree(list);
+
+    return array;
+}
+
+
+psString ppMergeOutputFile(const pmConfig *config)
+{
+    ppMergeType type = psMetadataLookupS32(NULL, config->arguments, "TYPE"); // Type of frame
+    const char *outSuffix = NULL;         // Suffix for output file
+    switch (type) {
+      case PPMERGE_TYPE_BIAS:
+        outSuffix = "BIAS";
+        break;
+      case PPMERGE_TYPE_DARK:
+        outSuffix = "DARK";
+        break;
+      case PPMERGE_TYPE_MASK:
+        outSuffix = "MASK";
+        break;
+      case PPMERGE_TYPE_CTEMASK:
+        outSuffix = "MASK";
+        break;
+      case PPMERGE_TYPE_SHUTTER:
+        outSuffix = "SHUTTER";
+        break;
+      case PPMERGE_TYPE_FLAT:
+        outSuffix = "FLAT";
+        break;
+      case PPMERGE_TYPE_FRINGE:
+        outSuffix = "FRINGE";
+	break;
+      case PPMERGE_TYPE_NOISEMAP:
+	outSuffix = "NOISEMAP";
+	break;
+      default:
+        psAbort("Unknown frame type: %x", type);
+    }
+
+    psString outName = NULL;            // Name of output file
+    psStringAppend(&outName, "PPMERGE.OUTPUT.%s", outSuffix);
+
+    return outName;
+}
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeLoop.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeLoop.c	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeLoop.c	(revision 41089)
@@ -0,0 +1,575 @@
+/** @file ppMergeLoop_Threaded.c
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-06 02:44:31 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+
+#include <pslib.h>
+#include <psmodules.h>
+#include <ppStats.h>
+
+#include "ppMerge.h"
+
+// XXX this function is now sufficiently different for the major types, it would make sense to just
+// split it into three: BASIC, SHUTTER, DARK
+
+bool ppMergeLoop(pmConfig *config)
+{
+    assert(config);
+
+    psMetadata *arguments = config->arguments; ///< Arguments
+    ppMergeType type = psMetadataLookupS32(NULL, config->arguments, "TYPE"); ///< Type of frame
+    int numFiles = psMetadataLookupS32(NULL, arguments, "INPUTS.NUM"); ///< Number of input files
+    bool mdok;                          ///< Status of MD lookup
+    bool haveMasks = psMetadataLookupBool(&mdok, arguments, "INPUTS.MASKS"); // Do we have masks?
+    bool useMasks = psMetadataLookupBool(&mdok, arguments, "INPUTS.MASKS.USE"); // Do we have masks?
+    bool haveVariances = psMetadataLookupBool(&mdok, arguments, "INPUTS.VARIANCES"); // Do we have variances?
+
+    psArray *inputs = ppMergeFileDataLevel(config, "PPMERGE.INPUT", PM_FPA_LEVEL_READOUT); // Input images
+    psArray *masks = NULL, *variances = NULL; // Input masks and variances
+    if (haveMasks) {
+        masks = ppMergeFileDataLevel(config, "PPMERGE.INPUT.MASK", PM_FPA_LEVEL_READOUT);
+    }
+    if (haveVariances) {
+        variances = ppMergeFileDataLevel(config, "PPMERGE.INPUT.VARIANCE", PM_FPA_LEVEL_READOUT);
+    }
+
+    int nThreads = psMetadataLookupS32 (&mdok, arguments, "NTHREADS");
+    if (!mdok) nThreads = 0;
+
+    // General combination parameters
+    int iter = psMetadataLookupS32(NULL, arguments, "ITER"); // Number of rejection iterations
+    float rej = psMetadataLookupF32(NULL, arguments, "REJ"); // Rejection level
+    float fraclow = psMetadataLookupF32(NULL, arguments, "FRACLOW"); // Reject fraction of low pixels
+    float frachigh = psMetadataLookupF32(NULL, arguments, "FRACHIGH"); // Reject fraction of hi pixels
+    int nKeep = psMetadataLookupS32(NULL, arguments, "NKEEP"); // Minimum number of values to keep
+    psStatsOptions combineStat = psMetadataLookupS32(NULL, arguments, "COMBINE"); // Combination statistic
+    bool useVariances = psMetadataLookupBool(NULL, arguments, "VARIANCES"); // Use variances?
+
+    // Fringe parameters
+    int fringeNum = psMetadataLookupS32(NULL, arguments, "FRINGE.NUM"); // Number of fringe points
+    int fringeSize = psMetadataLookupS32(NULL, arguments, "FRINGE.SIZE"); // Size of fringe regions
+    int fringeSmoothX = psMetadataLookupS32(NULL, arguments, "FRINGE.XSMOOTH"); // Smoothing regions in x
+    int fringeSmoothY = psMetadataLookupS32(NULL, arguments, "FRINGE.YSMOOTH"); // Smoothing regions in y
+    bool fringeSmooth = psMetadataLookupBool(NULL, arguments, "FRINGE.SMOOTH"); // Smooth the output image?
+    float fringeSmoothSigma = psMetadataLookupF32(NULL, arguments, "FRINGE.SMOOTH.SIGMA"); // Smooth the output image?
+
+    // set the mask and mark bit values based on the named masks
+    psImageMaskType maskVal, markVal;
+    if (!pmConfigMaskSetBits(&maskVal, &markVal, config)) {
+        psError (PS_ERR_UNKNOWN, true, "Unable to define the mask bit values");
+        return false;
+    }
+
+    pmCombineParams *combination = pmCombineParamsAlloc(combineStat); ///< Combination parameters
+    combination->maskVal = useMasks ? maskVal : 0;
+    combination->blank = pmConfigMaskGet("BLANK", config);
+    combination->nKeep = nKeep;
+    combination->fracHigh = frachigh;
+    combination->fracLow = fraclow;
+    combination->iter = iter;
+    combination->rej = rej;
+    combination->variances = useVariances;
+
+    psMetadata *stats = NULL;           ///< Statistics for output
+    if (psMetadataLookup(config->arguments, "STATS.NAME")) {
+        stats = psMetadataAlloc();
+        psMetadataAddMetadata(config->arguments, PS_LIST_TAIL, "STATS.DATA", 0, "Statistics output", stats);
+    }
+
+    pmFPAview *view = pmFPAviewAlloc(0); // View to component of interest
+
+    // Retrieve data placed on analysis
+    psVector *scales = NULL, *zeros = NULL; ///< Scale and zeroes for combination
+    psArray *shutters = NULL;           ///< Shutter correction data
+    psImage *zeroSet = NULL;
+    switch (type) {
+      case PPMERGE_TYPE_FRINGE:
+        zeroSet = psMetadataLookupPtr(NULL, arguments, "ZEROS");
+        if (!zeroSet) {
+            psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find ZEROS");
+            goto ERROR;
+        }
+        // the zeros vector is passed to pmReadoutCombine for each set of inputs per cell
+        zeros = psVectorAlloc(zeroSet->numRows, PS_TYPE_F32);
+        // Flow through
+      case PPMERGE_TYPE_FLAT:
+        scales = psMetadataLookupPtr(NULL, arguments, "SCALES");
+        if (!scales) {
+            psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find SCALES");
+            goto ERROR;
+        }
+        break;
+      case PPMERGE_TYPE_SHUTTER:
+        shutters = psMetadataLookupPtr(NULL, arguments, "SHUTTER");
+        if (!shutters) {
+            psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find SHUTTER");
+            goto ERROR;
+        }
+        break;
+      case PPMERGE_TYPE_MASK:
+      case PPMERGE_TYPE_BIAS:
+      case PPMERGE_TYPE_DARK:
+      case PPMERGE_TYPE_CTEMASK:
+      case PPMERGE_TYPE_NOISEMAP:
+        break;
+      default:
+        psAbort("Should never get here.");
+    }
+
+    // Dark parameters
+    psArray *darkOrdinates = psMetadataLookupPtr(NULL, arguments, "DARK.ORDINATES"); ///< Dark info
+    psString darkNorm = psMetadataLookupStr(&mdok, arguments, "DARK.NORM"); ///< Dark normalisation
+
+
+    if (!ppMergeFileActivate(config, PPMERGE_FILES_ALL, true)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to activate files.");
+        goto ERROR;
+    }
+    psString outName = ppMergeOutputFile(config); ///< Name of output file
+    pmFPAfile *output = psMetadataLookupPtr(NULL, config->files, outName); ///< Output file
+    psFree(outName);
+    assert(output && output->fpa);
+    pmFPA *outFPA = output->fpa;        ///< Output FPA
+    pmHDU *lastHDU = NULL;              // Last HDU that was updated
+
+    int cellNum = 0;                    ///< Index of cell
+    if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
+        goto ERROR;
+    }
+
+
+    // Average concepts across inputs
+    {
+        psList *inFPAs = psListAlloc(NULL); ///< List of FPAs
+        for (int i = 0; i < numFiles; i++) {
+            pmFPAfile *input = inputs->data[i]; ///< Input file
+            psListAdd(inFPAs, PS_LIST_TAIL, input->fpa);
+        }
+        if (!pmConceptsAverageFPAs(output->fpa, inFPAs)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to average FPA concepts.");
+            psFree(inFPAs);
+            goto ERROR;
+        }
+        psFree(inFPAs);
+    }
+
+    pmChip *outChip;                    ///< Chip of interest
+    while ((outChip = pmFPAviewNextChip(view, outFPA, 1))) {
+        if (!outChip->process || !outChip->file_exists) {
+            continue;
+        }
+        if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
+            goto ERROR;
+        }
+
+        // Average concepts across inputs
+        {
+            psList *inChips = psListAlloc(NULL);
+            for (int i=0; i < numFiles; i++) {
+                pmChip *chip = pmFPAviewThisChip(view, ((pmFPAfile *)inputs->data[i])->fpa);
+                psListAdd(inChips, PS_LIST_TAIL, chip);
+            }
+            if (!pmConceptsAverageChips(outChip, inChips, true)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to average Chip concepts.");
+                psFree(inChips);
+                goto ERROR;
+            }
+            psFree(inChips);
+        }
+
+        pmCell *outCell;                ///< Cell of interest
+
+        // XXX TEST : force a single loop
+        // outCell = pmFPAviewNextCell(view, outFPA, 1); {
+
+        while ((outCell = pmFPAviewNextCell(view, outFPA, 1))) {
+            if (!outCell->process || !outCell->file_exists) {
+                continue;
+            }
+            if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
+                goto ERROR;
+            }
+
+            pmHDU *hdu = pmHDUGetLowest(outFPA, outChip, outCell); ///< HDU for cell
+            if (!hdu || hdu->blankPHU) {
+                // No data here
+                continue;
+            }
+
+            // Update the header
+            {
+                pmHDU *hdu = pmHDUGetHighest(outFPA, outChip, outCell); // HDU for file
+                if (hdu && hdu != lastHDU) {
+                    if (!hdu->header) {
+                        hdu->header = psMetadataAlloc();
+                    }
+                    ppMergeVersionHeader(hdu->header);
+                    lastHDU = hdu;
+                }
+            }
+
+            float shutterRef = NAN;     ///< Reference shutter correction
+            pmReadout *pattern = NULL;
+            if (type == PPMERGE_TYPE_SHUTTER) {
+                shutterRef = pmShutterCorrectionReference(shutters->data[cellNum]);
+                pattern = pmReadoutAlloc(NULL);
+            }
+
+            pmReadout *outRO = pmReadoutAlloc(outCell);
+
+            // open the input files (we need to do the work ourselves)
+            for (int i = 0; i < numFiles; i++) {
+                if (!ppMergeFileOpenInput(config, view, i)) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to open file %d", i);
+                    goto ERROR;
+                }
+            }
+
+            if (zeroSet) {
+              for (int i = 0; i < zeroSet->numRows; i++) {
+                zeros->data.F32[i] = zeroSet->data.F32[i][cellNum];
+              }
+            }
+
+            int rows = psMetadataLookupS32(NULL, config->arguments, "ROWS"); // Number of rows to read per chunk
+            if (!rows && nThreads) {
+              psError(PS_ERR_UNKNOWN, false, "Invalid combination of threads > 0 and ROWS == 0 (ie, multiple threads working on the full array...)");
+              goto ERROR;
+            }
+
+            ppMergeFileGroup *fileGroup = NULL;
+            psArray *fileGroups = psArrayAlloc(nThreads + 1);
+
+            // Generate readouts for each input file in each file group
+            for (int i = 0; i < fileGroups->n; i++) {
+                psArray *readouts = psArrayAlloc(numFiles); ///< Input readouts
+                for (int j = 0; j < numFiles; j++) {
+                    pmFPAfile *input = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT", j);
+                    pmCell *inCell = pmFPAviewThisCell(view, input->fpa); ///< Input cell
+                    pmReadout *readout = pmReadoutAlloc(inCell);
+                    readout->process = true; // until proven otherwise, attempt to process this readout
+                    readouts->data[j] = readout;
+                }
+
+                fileGroup = ppMergeFileGroupAlloc();
+                fileGroup->readouts = readouts;
+                fileGroup->read = false;
+                fileGroup->busy = false;
+                fileGroup->lastScan = 0;
+                fileGroup->firstScan = 0;
+                fileGroups->data[i] = fileGroup;
+            }
+
+            // call the init functions
+            switch (type) {
+              case PPMERGE_TYPE_CTEMASK:
+              case PPMERGE_TYPE_BIAS:
+              case PPMERGE_TYPE_FLAT:
+              case PPMERGE_TYPE_FRINGE:
+  	      case PPMERGE_TYPE_NOISEMAP:
+                psAssert (fileGroups->n > 0, "no valid file groups defined");
+                fileGroup = fileGroups->data[0];
+                if (!pmReadoutCombinePrepare(outRO, fileGroup->readouts, combination)) {
+                    goto ERROR;
+                }
+                break;
+              case PPMERGE_TYPE_DARK:
+                psAssert (fileGroups->n > 0, "no valid file groups defined");
+                fileGroup = fileGroups->data[0];
+                if (!pmDarkCombinePrepare(outCell, fileGroup->readouts, darkOrdinates, darkNorm)) {
+                    goto ERROR;
+                }
+                break;
+              case PPMERGE_TYPE_SHUTTER:
+                psAssert (fileGroups->n > 0, "no valid file groups defined");
+                fileGroup = fileGroups->data[0];
+                if (!pmShutterCorrectionGeneratePrepare(outRO, pattern, fileGroup->readouts, combination->maskVal)) {
+                    goto ERROR;
+                }
+                break;
+              default:
+                psAbort("Should never get here.");
+            }
+
+            // Read input data by chunks
+            psTimerStart("ppMergeLoop");
+            for (int numChunk = 0; true; numChunk++) {
+
+                bool status = false;
+                fileGroup = ppMergeReadChunk(&status, fileGroups, config, numChunk);
+                if (!status) {
+                    // Something went wrong
+                    goto ERROR;
+                }
+                if (!fileGroup) {
+                    // Nothing more to read
+                    break;
+                }
+
+                // Start a job
+                switch (type) {
+                  case PPMERGE_TYPE_CTEMASK:
+                  case PPMERGE_TYPE_BIAS:
+                  case PPMERGE_TYPE_FLAT:
+  		  case PPMERGE_TYPE_NOISEMAP:
+                  case PPMERGE_TYPE_FRINGE: {
+                      psThreadJob *job = psThreadJobAlloc("PPMERGE_READOUT_COMBINE"); ///< Job to start
+
+                      // Construct the arguments for this job
+                      psArrayAdd(job->args, 1, outRO);
+                      psArrayAdd(job->args, 1, fileGroup);
+                      psArrayAdd(job->args, 1, zeros);
+                      psArrayAdd(job->args, 1, scales);
+                      psArrayAdd(job->args, 1, combination);
+
+                      // call: pmReadoutCombine(outRO, fileGroup->readouts, zeros, scales, combination);
+                      if (!psThreadJobAddPending(job)) {
+                          goto ERROR;
+                      }
+                      break;
+                  }
+                  case PPMERGE_TYPE_DARK: {
+                      psThreadJob *job = psThreadJobAlloc ("PPMERGE_DARK_COMBINE"); ///< Job to start
+
+                      // construct the arguments for this job
+                      psArrayAdd(job->args, 1, outCell);
+                      psArrayAdd(job->args, 1, fileGroup);
+                      psArrayAdd(job->args, 1, psScalarAlloc(iter, PS_TYPE_S32));
+                      psArrayAdd(job->args, 1, psScalarAlloc(rej, PS_TYPE_F32));
+                      psArrayAdd(job->args, 1, psScalarAlloc(maskVal, PS_TYPE_IMAGE_MASK));
+
+                      // call: pmDarkCombine(outCell, fileGroup->readouts, iter, rej, maskVal);
+                      if (!psThreadJobAddPending(job)) {
+			//                          goto ERROR;
+			// continue; // I don't care.
+                      }
+                      break;
+                  }
+                  case PPMERGE_TYPE_SHUTTER: {
+                      psThreadJob *job = psThreadJobAlloc ("PPMERGE_SHUTTER_CORRECTION");
+
+                      // construct the arguments for this job
+                      psArrayAdd(job->args, 1, outRO);
+                      psArrayAdd(job->args, 1, pattern);
+                      psArrayAdd(job->args, 1, fileGroup);
+                      psArrayAdd(job->args, 1, psScalarAlloc(shutterRef, PS_TYPE_F32));
+                      psArrayAdd(job->args, 1, shutters->data[cellNum]);
+                      psArrayAdd(job->args, 1, psScalarAlloc(iter, PS_TYPE_S32));
+                      psArrayAdd(job->args, 1, psScalarAlloc(rej, PS_TYPE_F32));
+                      psArrayAdd(job->args, 1, psScalarAlloc(maskVal, PS_TYPE_IMAGE_MASK));
+
+                      // call: pmShutterCorrectionGenerate(outRO, pattern, fileGroup->readouts, shutterRef,
+                      //                                   shutters->data[cellNum], iter, rej, maskVal);
+                      if (!psThreadJobAddPending (job)) {
+                          goto ERROR;
+                      }
+                      break;
+                  }
+                  default:
+                    psAbort("Should never get here.");
+                }
+            }
+
+            // Wait for the threads to finish and manage results
+            if (!psThreadPoolWait(false, true)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to combine images.");
+                return false;
+            }
+
+            // we don't care about the results, just dump the done queue jobs
+            psThreadJob *job = NULL;    // Job to dump
+            while ((job = psThreadJobGetDone())) {
+                psFree (job);
+            }
+
+            psFree(fileGroups);
+	    //            psFree(zeros);
+
+            // XXX eventually need to keep both the shutter and the pattern, as we do with dark
+            psFree(pattern);
+
+            // Get list of cells for concepts averaging
+            psList *inCells = psListAlloc(NULL); // List of cells
+            for (int i = 0; i < numFiles; i++) {
+                pmFPAfile *input = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT", i);
+                pmCell *inCell = pmFPAviewThisCell(view, input->fpa); ///< Input cell
+                psListAdd(inCells, PS_LIST_TAIL, inCell);
+            }
+            if (!pmConceptsAverageCells(outCell, inCells, NULL, NULL, true)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to average cell concepts.");
+                psFree(inCells);
+                psFree(outRO);
+                goto ERROR;
+            }
+            psFree(inCells);
+
+            // Plug supplementary images into their own FPAs
+            {
+                pmCell *countsCell = pmFPAfileThisCell(config->files, view, "PPMERGE.OUTPUT.COUNT");
+                pmReadout *countsRO = pmReadoutAlloc(countsCell); ///< Readout with count of inputs per pixel
+                psImage *counts = psMetadataLookupPtr(NULL, outRO->analysis, PM_READOUT_STACK_ANALYSIS_COUNT);
+                countsRO->image = psImageCopy(countsRO->image, counts, PS_TYPE_F32);
+                psMetadataRemoveKey(outRO->analysis, PM_READOUT_STACK_ANALYSIS_COUNT);
+                countsRO->data_exists = countsCell->data_exists = countsCell->parent->data_exists = true;
+                psFree(countsRO);
+
+                // XXX EAM 2009.01.18 : sigmaCell and countsCell need to have their concepts copied from outCell.
+                // This was causing segfaults for VYSOS5; Why did this ever work for SIMTEST?
+                if (!pmConceptsCopyCell(countsCell, outCell)) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to copy cell concepts.");
+                    psFree(outRO);
+                    goto ERROR;
+                }
+
+                pmCell *sigmaCell = pmFPAfileThisCell(config->files, view, "PPMERGE.OUTPUT.SIGMA");
+                pmReadout *sigmaRO = pmReadoutAlloc(sigmaCell); ///< Readout with stdev per pixel
+                psImage *sigma = psMetadataLookupPtr(NULL, outRO->analysis, PM_READOUT_STACK_ANALYSIS_SIGMA);
+                sigmaRO->image = psImageCopy(sigmaRO->image, sigma, PS_TYPE_F32);
+                psMetadataRemoveKey(outRO->analysis, PM_READOUT_STACK_ANALYSIS_SIGMA);
+                sigmaRO->data_exists = sigmaCell->data_exists = sigmaCell->parent->data_exists = true;
+                psFree(sigmaRO);
+
+                if (!pmConceptsCopyCell(sigmaCell, outCell)) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to copy cell concepts.");
+                    psFree(outRO);
+                    goto ERROR;
+                }
+            }
+
+            // Measure the fringes for this cell
+            //
+            // XXX Need to deal with multiple components: we will do this by building up the components
+            // Read the existing fringe measurements
+            // Use existing regions to measure fringe statistics
+            // Add the new fringe measurements to the existing fringe measurements.
+            // Write the appended fringe measurements.
+            // Read in the "output" file to get the existing components.
+            // Put the new readout into the cell after the existing readouts.
+            if (type == PPMERGE_TYPE_FRINGE && outRO) {
+                if (fringeSmooth) {
+                    if (outRO->mask) {
+                        psImage *smoothed = psImageSmoothMask (NULL, outRO->image, outRO->mask, maskVal, fringeSmoothSigma, 3, 0.2);
+                        psFree (outRO->image);
+                        outRO->image = smoothed;
+                    } else {
+                        psImageSmooth (outRO->image, fringeSmoothSigma, 3);
+                    }
+                }
+
+                pmFringeRegions *regions = pmFringeRegionsAlloc(fringeNum, fringeSize, fringeSize, fringeSmoothX, fringeSmoothY);
+                pmFringeStats *fringe = pmFringeStatsMeasure(regions, outRO, maskVal);
+                psFree(regions);
+                if (!fringe) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to measure fringe statistics.\n");
+                    psFree(outRO);
+                    goto ERROR;
+                }
+
+                psArray *fringes = psArrayAlloc(1); ///< Array of fringes
+                fringes->data[0] = fringe;
+
+                // XXX replaced this : pmFringesFormat(outCell, NULL, fringes);
+                if (!psMetadataAdd(outCell->analysis, PS_LIST_TAIL, "FRINGE.MEASUREMENTS", PS_DATA_ARRAY, "Fringes", fringes)) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to add fringe to analysis metadata\n");
+                    goto ERROR;
+                }
+                psFree(fringes);        // Drop reference
+            }
+
+            if (stats && !ppStatsFPA(stats, outFPA, view, maskVal, config)) {
+                psError(PS_ERR_UNKNOWN, true, "Unable to generate stats for image.");
+                goto ERROR;
+            }
+
+            // calculate CTEMASK after stats so stats reflect median image
+            if (type == PPMERGE_TYPE_CTEMASK && outRO) {
+                // need to apply range cuts on the output image
+                psAssert (outRO->mask, "mask is not defined");
+                psAssert (outRO->mask->numCols == outRO->image->numCols, "mismatch between image and mask");
+                psAssert (outRO->mask->numRows == outRO->image->numRows, "mismatch between image and mask");
+
+                // CTEMASK parameters
+                float cteMin = psMetadataLookupF32(NULL, arguments, "CTE.MIN"); // Number of fringe points
+
+                char *cteMaskName = psMetadataLookupStr (&mdok, config->arguments, "MASK.SET.VALUE");
+                psImageMaskType cteMaskValue = pmConfigMaskGet(cteMaskName, config);
+
+                if (0) {
+                  psFits *fits = NULL;
+                  fits = psFitsOpen ("combine.fits", "w");
+                  psFitsWriteImage (fits, NULL, outRO->image, 0, NULL);
+                  psFitsClose (fits);
+
+                  fits = psFitsOpen ("inmask.fits", "w");
+                  psFitsWriteImage (fits, NULL, outRO->mask, 0, NULL);
+                  psFitsClose (fits);
+                }
+
+                psF32 **outputImage = outRO->image->data.F32;
+                psImageMaskType **outputMask = outRO->mask->data.PS_TYPE_IMAGE_MASK_DATA;
+                for (int iy = 0; iy < outRO->image->numRows; iy++) {
+                    for (int ix = 0; ix < outRO->image->numCols; ix++) {
+                        if (outputImage[iy][ix] < cteMin) {
+                            outputMask[iy][ix] |= cteMaskValue;
+                        }
+                    }
+                }
+
+                if (0) {
+                  psFits *fits = NULL;
+                  fits = psFitsOpen ("otmask.fits", "w");
+                  psFitsWriteImage (fits, NULL, outRO->mask, 0, NULL);
+                  psFitsClose (fits);
+                }
+
+            }
+
+            psFree(outRO);
+            cellNum++;
+
+            if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
+                goto ERROR;
+            }
+        }
+
+        if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
+            goto ERROR;
+        }
+    }
+
+    if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
+        goto ERROR;
+    }
+
+    psFree(view);
+    psFree(combination);
+    psFree(inputs);
+    psFree(masks);
+    psFree(variances);
+    psFree(stats);
+    return true;
+
+ERROR:
+    psFree(view);
+    psFree(combination);
+    psFree(inputs);
+    psFree(masks);
+    psFree(variances);
+    psFree(stats);
+    return false;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeMask.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeMask.c	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeMask.c	(revision 41089)
@@ -0,0 +1,528 @@
+/** @file ppMergeMask.c
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.18 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-06 02:44:31 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#include "ppMerge.h"
+
+static bool mergeMask(pmConfig *config, ///< Configuration
+                      const pmFPAview *view, ///< View to chip
+                      bool writeOut,     ///< Write output?
+                      pmHDU **lastHDU,   ///< HDU last updated
+                      psRandom *rng,    ///< Random number generator
+                      psMetadata *stats ///< Statistics output
+    )
+{
+    assert(config);
+    assert(view);
+    assert(view->chip != -1 && view->cell == -1 && view->readout == -1);
+
+    bool mdok;                          ///< Status of MD lookup
+    int numFiles = psMetadataLookupS32(NULL, config->arguments, "INPUTS.NUM"); ///< Number of input files
+    psStatsOptions meanStat = psMetadataLookupS32(NULL, config->arguments, "MEAN"); ///< Statistic for mean
+    psStatsOptions stdevStat = psMetadataLookupS32(NULL, config->arguments, "STDEV"); ///< Statistic for stdev
+    int sample = psMetadataLookupS32(NULL, config->arguments, "SAMPLE"); ///< Size of sample for statistics
+    bool chipStats = psMetadataLookupBool(&mdok, config->arguments, "MASK.CHIPSTATS"); ///< Statistics on chip?
+
+    char *maskSuspectMode  = psMetadataLookupStr(NULL, config->arguments, "MASK.SUSPECT.MODE"); ///< Threshold for suspect pixels
+    if (strcasecmp(maskSuspectMode, "SIGMA") && strcasecmp(maskSuspectMode, "VALUE")) {
+        psError (PS_ERR_UNKNOWN, true, "Invalid choice for MASK.SUSPECT.MODE: %s\n", maskSuspectMode);
+        return false;
+    }
+    float maskSuspectSigma = psMetadataLookupF32(NULL, config->arguments, "MASK.SUSPECT.SIGMA"); ///< Threshold for suspect pixels
+    float maskSuspectMin   = psMetadataLookupF32(NULL, config->arguments, "MASK.SUSPECT.MIN"); ///< Threshold for suspect pixels
+    float maskSuspectMax   = psMetadataLookupF32(NULL, config->arguments, "MASK.SUSPECT.MAX"); ///< Threshold for suspect pixels
+
+    float maskBad = psMetadataLookupF32(NULL, config->arguments, "MASK.BAD"); ///< Threshold for bad pixels
+    pmMaskIdentifyMode maskMode = psMetadataLookupS32(NULL, config->arguments, "MASK.MODE"); ///< Mode for identifying bad pixels
+    int maskGrow = psMetadataLookupS32(NULL, config->arguments, "MASK.GROW"); ///< Radius to grow mask
+
+    bool smoothSuspect = psMetadataLookupBool(&mdok, config->arguments, "MASK.SMOOTH.SUSPECT"); ///< Radius to grow mask
+    float smoothScale = psMetadataLookupF32(&mdok, config->arguments, "MASK.SMOOTH.SCALE"); ///< Radius to grow mask
+
+    psImageMaskType markVal, maskValRaw;
+    if (!pmConfigMaskSetBits(&maskValRaw, &markVal, config)) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to define the mask bit values");
+        return false;
+    }
+
+    char *maskOutName = psMetadataLookupStr (&mdok, config->arguments, "MASK.SET.VALUE");
+    psImageMaskType maskValOut = pmConfigMaskGet (maskOutName, config);
+    if (!maskValOut) {
+        psError (PS_ERR_UNKNOWN, true, "Undefined output mask bit value");
+        return false;
+    }
+
+    psStats *statistics = psStatsAlloc(meanStat | stdevStat); // Statistics for background
+
+    psString outName = ppMergeOutputFile(config); ///< Name of output file
+    pmChip *outChip = pmFPAfileThisChip(config->files, view, outName); ///< Output chip
+    psFree(outName);
+
+    int numCells = 1;
+    if (chipStats) {
+        // count the number of active cells for this chip:
+        numCells = 0;
+        for (int i = 0; i < outChip->cells->n; i++) {
+            pmCell *cell = outChip->cells->data[i];
+            if (!cell->process) continue;
+            numCells ++;
+        }
+    }
+
+    // For each input file, get the statistics, which can be calculated at the chip or cell levels
+    psVector *values = psVectorAlloc(sample, PS_TYPE_F32); ///< Pixel values for statistics
+    pmFPAview *inView = pmFPAviewAlloc(0); ///< View for input
+    for (int i = 0; i < numFiles; i++) {
+        pmFPAfileActivate(config->files, false, NULL);
+        psArray *files = ppMergeFileActivateSingle(config, PPMERGE_FILES_INPUT, true, i); ///< Input files
+        pmFPAfile *input = files->data[0]; ///< Input file
+        psFree(files);
+        pmFPA *inFPA = input->fpa;  ///< Input FPA
+        *inView = *view;
+
+        int valueIndex = 0;             ///< Index for vector of pixel values
+
+        pmCell *inCell;                 ///< Input cell
+        while ((inCell = pmFPAviewNextCell(inView, inFPA, 1))) {
+
+            // the output FPA structure carries the information about which cells to process
+            pmCell *outCell = pmFPAfileThisCell(config->files, inView, "PPMERGE.OUTPUT.MASK"); // Output cell
+            if (!outCell->process) continue;
+
+            pmHDU *hdu = pmHDUFromCell(inCell); ///< HDU for cell
+            if (!hdu || hdu->blankPHU) {
+                // No data here
+                continue;
+            }
+            psTrace("ppMerge", 1, "Getting suspect pixels for file %d chip %d cell %d",
+                    i, inView->chip, inView->cell);
+
+            // Update the header
+            {
+                pmHDU *hdu = pmHDUGetHighest(outCell->parent->parent, outCell->parent, outCell); // File HDU
+                if (hdu && hdu != *lastHDU) {
+                    if (!hdu->header) {
+                        hdu->header = psMetadataAlloc();
+                    }
+                    ppMergeVersionHeader(hdu->header);
+                    *lastHDU = hdu;
+                }
+            }
+
+            if (!pmFPAfileIOChecks(config, inView, PM_FPA_BEFORE)) {
+                psFree(inView);
+                goto MERGE_MASK_ERROR;
+            }
+
+            if (!ppMergeFileOpenInput(config, inView, i)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to open file %d", i);
+                psFree(inView);
+                goto MERGE_MASK_ERROR;
+            }
+
+            if (inCell->readouts->n > 1) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                        "File %d chip %d cell %d contains more than one readout (%ld)",
+                        i, inView->chip, inView->cell, inCell->readouts->n);
+                psFree(inView);
+                goto MERGE_MASK_ERROR;
+            }
+
+            pmReadout *readout;
+            if (inCell->readouts && inCell->readouts->n == 1) {
+                readout = psMemIncrRefCounter(inCell->readouts->data[0]); ///< Input readout
+            } else {
+                readout = pmReadoutAlloc(inCell);
+            }
+
+            if (!ppMergeFileReadInput(config, readout, i, 0)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to read readout %d", i);
+                psFree(inView);
+                psFree(readout);
+                goto MERGE_MASK_ERROR;
+            }
+
+            pmReadout *outRO = NULL;    ///< Output readout
+            if (outCell->readouts && outCell->readouts->n == 1) {
+                outRO = psMemIncrRefCounter(outCell->readouts->data[0]);
+            } else {
+                outRO = pmReadoutAlloc(outCell);
+            }
+            psImage *outMask = outRO->mask;    ///< Output mask image (for iterative generation of mask)
+
+            psImage *image = readout->image, *mask = readout->mask; ///< Image and mask
+            int numCols = readout->image->numCols, numRows = readout->image->numRows; ///< Image size
+            int numPix = numCols * numRows; ///< Number of pixels
+            int num = PS_MIN(numPix, sample / numCells); ///< Number of values to add
+            if (!chipStats) {
+                valueIndex = 0;
+            }
+
+	    if (!strcasecmp(maskSuspectMode, "VALUE")) {
+		// this function increments the count for each suspect pixel in each input plane
+		// maskValRaw is used to test for valid input pixels
+		if (!pmMaskFlagSuspectPixelsByValue(outRO, readout, maskSuspectMin, maskSuspectMax, maskValRaw)) {
+		    psError(PS_ERR_UNKNOWN, false, "Unable to find suspect values in file %d", i);
+		    psFree(inView);
+		    psFree(readout);
+		    goto MERGE_MASK_ERROR;
+		}
+		pmCellFreeData(inCell);
+
+		if (!pmFPAfileIOChecks(config, inView, PM_FPA_AFTER)) {
+		    psFree(inView);
+		    psFree(readout);
+		    goto MERGE_MASK_ERROR;
+		}
+	    } else {
+		// extract a subset of pixels for stats measurement -- don't use pixels which are masked for this calculation
+		for (int i = 0; i < num; i++) {
+		    int pixel = numPix * psRandomUniform(rng);
+		    int x = pixel % numCols;
+		    int y = pixel / numCols;
+		    if (mask && (mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskValRaw)) continue;
+		    if (outMask && (outMask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskValOut)) continue;
+		    if (!isfinite(image->data.F32[y][x])) continue;
+
+		    values->data.F32[valueIndex] = image->data.F32[y][x];
+		    valueIndex++;
+		}
+
+		// for per-readout stats, measure the stats and find the suspect pixels
+		if (!chipStats) {
+		    values->n = valueIndex;
+		    if (!psVectorStats(statistics, values, NULL, NULL, 0)) {
+			psError(PS_ERR_UNKNOWN, false, "Unable to do statistics on readout.");
+			psFree(inView);
+			psFree(readout);
+			goto MERGE_MASK_ERROR;
+		    }
+		    float mean = psStatsGetValue(statistics, meanStat);
+		    float stdev = psStatsGetValue(statistics, stdevStat);
+
+		    // this function increments the count for each suspect pixel in each input plane
+		    // maskValRaw is used to test for valid input pixels
+		    if (!pmMaskFlagSuspectPixelsBySigma(outRO, readout, mean, stdev, maskSuspectSigma, maskValRaw)) {
+			psError(PS_ERR_UNKNOWN, false, "Unable to find suspect values in file %d", i);
+			psFree(inView);
+			psFree(readout);
+			goto MERGE_MASK_ERROR;
+		    }
+		    pmCellFreeData(inCell);
+
+		    if (!pmFPAfileIOChecks(config, inView, PM_FPA_AFTER)) {
+			psFree(inView);
+			psFree(readout);
+			goto MERGE_MASK_ERROR;
+		    }
+		}
+	    }
+            psFree(readout);
+            psFree(outRO);
+        }
+
+        // Additional run through cells if we want chip-level statistics
+	// only used for MASK.SUSPECT.MODE == SIGMA
+        if (!strcasecmp(maskSuspectMode, "SIGMA") && chipStats && valueIndex > 0) {
+            values->n = valueIndex;
+            if (!psVectorStats(statistics, values, NULL, NULL, 0)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to do statistics on chip.");
+                goto MERGE_MASK_ERROR;
+            }
+            inView->cell = -1;
+            while ((inCell = pmFPAviewNextCell(inView, inFPA, 1))) {
+                // the output FPA structure carries the information about which cells to process
+                pmCell *outCell = pmFPAfileThisCell(config->files, inView, "PPMERGE.OUTPUT.MASK"); // Output cell
+                if (!outCell->process) continue;
+
+                pmHDU *hdu = pmHDUFromCell(inCell); ///< HDU for cell
+                if (!hdu || hdu->blankPHU) continue;
+
+                pmReadout *readout = inCell->readouts->data[0]; ///< Readout of interest
+
+                inView->readout = 0;
+                pmReadout *outRO = pmFPAfileThisReadout(config->files, inView, "PPMERGE.OUTPUT.MASK");
+
+                float mean = psStatsGetValue(statistics, meanStat);
+                float stdev = psStatsGetValue(statistics, stdevStat);
+
+                if (!pmMaskFlagSuspectPixelsBySigma(outRO, readout, mean, stdev, maskSuspectSigma, maskValRaw)) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to find suspect values in file %d", i);
+                    goto MERGE_MASK_ERROR;
+                }
+                pmCellFreeData(inCell);
+
+                inView->readout = -1;
+                if (!pmFPAfileIOChecks(config, inView, PM_FPA_AFTER)) {
+                    psFree(inView);
+                    goto MERGE_MASK_ERROR;
+                }
+            }
+        }
+    }
+    psFree(inView);
+    psFree(statistics); statistics = NULL;
+    psFree(values); values = NULL;
+
+
+    // Another run through the chip to threshold on the suspects
+    pmFPAfileActivate(config->files, false, NULL);
+    if (writeOut) {
+        ppMergeFileActivate(config, PPMERGE_FILES_OUTPUT, true);
+    }
+    pmFPA *outFPA = outChip->parent;    ///< Output FPA
+    pmCell *outCell;                    ///< Output cell
+    pmFPAview *outView = pmFPAviewAlloc(0); ///< View into output FPA
+    *outView = *view;
+    while ((outCell = pmFPAviewNextCell(outView, outFPA, 1))) {
+
+        // skip inactive cells
+        if (!outCell->process) continue;
+
+        // pmHDU *hdu = pmHDUFromCell(outCell); ///< HDU for cell
+	pmHDU *hdu = pmHDUGetLowest(outFPA, outChip, outCell); ///< HDU for cell
+        if (!hdu || hdu->blankPHU) continue;
+
+        psTrace("ppMerge", 1, "Getting bad pixels for chip %d cell %d", outView->chip, outView->cell);
+
+        assert(outCell->readouts && outCell->readouts->n == 1);
+        pmReadout *outRO = outCell->readouts->data[0]; ///< Output readout
+
+        if (smoothSuspect) {
+            // XXX test output of suspect pixel image
+            psImage *suspects = psMetadataLookupPtr(NULL, outRO->analysis, PM_MASK_ANALYSIS_SUSPECT); // Suspect img
+            assert (suspects);
+            psImageSmooth (suspects, smoothScale, 3); // extend smoothing region to 3-sigma
+        }
+
+        // set the bad pixels to the value 'maskVal'
+        if (!pmMaskIdentifyBadPixels(outRO, maskValOut, maskBad, maskMode)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to mask bad pixels");
+            goto MERGE_MASK_ERROR;
+        }
+
+        // Supplementary outputs
+	pmCell *countsCell = NULL;
+	pmCell *sigmaCell = NULL;
+        {
+            // The counts image is fairly useless, but it preserves the model
+            countsCell = pmFPAfileThisCell(config->files, outView, "PPMERGE.OUTPUT.COUNT");
+            pmReadout *countsRO = pmReadoutAlloc(countsCell); ///< Readout with count of inputs per pixel
+            countsRO->image = psImageAlloc(outRO->mask->numCols, outRO->mask->numRows, PS_TYPE_F32);
+            psImageInit(countsRO->image, numFiles);
+            countsRO->data_exists = countsCell->data_exists = countsCell->parent->data_exists = true;
+            psFree(countsRO);
+
+            sigmaCell = pmFPAfileThisCell(config->files, outView, "PPMERGE.OUTPUT.SIGMA");
+            pmReadout *sigmaRO = pmReadoutAlloc(sigmaCell); ///< Readout with suspect image
+            psImage *suspect = psMetadataLookupPtr(NULL, outRO->analysis, PM_MASK_ANALYSIS_SUSPECT);
+            sigmaRO->image = psImageCopy(sigmaRO->image, suspect, PS_TYPE_F32);
+            psMetadataRemoveKey(outRO->analysis, PM_MASK_ANALYSIS_SUSPECT);
+            sigmaRO->data_exists = sigmaCell->data_exists = sigmaCell->parent->data_exists = true;
+            psFree(sigmaRO);
+
+        }
+
+        if (maskGrow > 0) {
+            psImage *grown = psImageGrowMask(NULL, outRO->mask, maskValOut, maskGrow, maskValOut); ///< Grown mask
+            psFree(outRO->mask);
+            outRO->mask = grown;
+        }
+
+        if (writeOut) {
+            if (!pmFPAfileIOChecks(config, outView, PM_FPA_BEFORE)) {
+                psFree(outView);
+                goto MERGE_MASK_ERROR;
+            }
+
+            outRO->data_exists = outCell->data_exists = outChip->data_exists = true;
+
+            // Average concepts
+            psList *cells = psListAlloc(NULL); ///< List of cells, for concept averaging
+            for (int i = 0; i < numFiles; i++) {
+                pmFPAfile *inFile = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT", i); ///< Input file
+                pmCell *inCell = pmFPAviewThisCell(outView, inFile->fpa); ///< Input cell
+                psListAdd(cells, PS_LIST_TAIL, inCell);
+            }
+            if (!pmConceptsAverageCells(outCell, cells, NULL, NULL, true)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to average cell concepts.");
+                psFree(cells);
+                psFree(outRO);
+                psFree(outView);
+                return false;
+            }
+            psFree(cells);
+
+	    // XXX EAM 2016.03.29 : sigmaCell and countsCell need to have their concepts copied from outCell.
+	    // This was causing segfaults for VYSOS5; Why did this ever work for SIMTEST?
+	    if (!pmConceptsCopyCell(countsCell, outCell)) {
+	      psError(PS_ERR_UNKNOWN, false, "Unable to copy cell concepts.");
+	      goto MERGE_MASK_ERROR;
+	    }
+
+	    if (!pmConceptsCopyCell(sigmaCell, outCell)) {
+	      psError(PS_ERR_UNKNOWN, false, "Unable to copy cell concepts.");
+	      goto MERGE_MASK_ERROR;
+	    }
+
+            // Statistics on the merged cell using a fake image
+            outRO->image = psImageAlloc(outRO->mask->numCols, outRO->mask->numRows, PS_TYPE_F32);
+            psImageInit(outRO->image, 1.0);
+            if (!ppStatsFPA(stats, outRO->parent->parent->parent, outView, maskValOut, config)) {
+                psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to generate stats for image.");
+                psFree(outRO);
+                psFree(outView);
+                return false;
+            }
+            psFree(outRO->image);
+            outRO->image = NULL;
+
+            // Write
+            if (!pmFPAfileIOChecks(config, outView, PM_FPA_AFTER)) {
+                psFree(outView);
+                goto MERGE_MASK_ERROR;
+            }
+        }
+    }
+    psFree(outView);
+
+    ppMergeFileActivate(config, PPMERGE_FILES_ALL, true);
+
+    return true;
+
+
+MERGE_MASK_ERROR:
+    psFree(statistics);
+    psFree(values);
+    return false;
+}
+
+bool ppMergeMask(pmConfig *config)
+{
+    assert(config);
+
+    bool mdok;                          // Status of MD lookup
+    int numFiles = psMetadataLookupS32(NULL, config->arguments, "INPUTS.NUM"); // Number of inputs
+    bool haveMasks = psMetadataLookupBool(&mdok, config->arguments, "INPUTS.MASKS"); // Do we have masks?
+    bool haveVariances = psMetadataLookupBool(&mdok, config->arguments, "INPUTS.VARIANCES"); // Got variances?
+    int iter = psMetadataLookupS32(NULL, config->arguments, "ITER"); // Number of rejection iterations
+
+    PS_ASSERT_INT_POSITIVE(iter, false);
+
+    pmFPAview *view = pmFPAviewAlloc(0); ///< View to component of interest
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS); ///< Random number generator
+
+    psMetadata *stats = NULL;           ///< Statistics for output
+    if (psMetadataLookup(config->arguments, "STATS.NAME")) {
+        stats = psMetadataAlloc();
+        psMetadataAddMetadata(config->arguments, PS_LIST_TAIL, "STATS.DATA", 0, "Statistics output", stats);
+    }
+
+    psArray *inputs = ppMergeFileDataLevel(config, "PPMERGE.INPUT", PM_FPA_LEVEL_READOUT); ///< Input images
+    psFree(inputs);
+    if (haveMasks) {
+        psArray *masks = ppMergeFileDataLevel(config, "PPMERGE.INPUT.MASK", PM_FPA_LEVEL_READOUT);
+        psFree(masks);
+    }
+    if (haveVariances) {
+        psArray *variances = ppMergeFileDataLevel(config, "PPMERGE.INPUT.VARIANCE", PM_FPA_LEVEL_READOUT);
+        psFree(variances);
+    }
+
+    if (!ppMergeFileActivate(config, PPMERGE_FILES_INPUT, true)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to activate files.");
+        goto PPMERGE_MASK_ERROR;
+    }
+    if (!ppMergeFileActivate(config, PPMERGE_FILES_OUTPUT, true)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to activate files.");
+        goto PPMERGE_MASK_ERROR;
+    }
+
+    // XXX this function should use pmConfigMaskReadHeader () to get the named values defined
+    // for the input masks.
+
+    psString outName = ppMergeOutputFile(config); ///< Name of output file
+    pmFPAfile *output = psMetadataLookupPtr(NULL, config->files, outName); ///< Output file
+    psFree(outName);
+    assert(output && output->fpa);
+    pmFPA *outFPA = output->fpa;        ///< Output FPA
+    pmHDU *lastHDU = NULL;              // Last HDU updated
+
+    if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
+        goto PPMERGE_MASK_ERROR;
+    }
+    pmChip *outChip;                    ///< Chip of interest
+    while ((outChip = pmFPAviewNextChip(view, outFPA, 1))) {
+
+        if (!outChip->process) continue;
+
+        if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
+            goto PPMERGE_MASK_ERROR;
+        }
+
+        for (int i = 0; i < iter; i++) {
+            if (!mergeMask(config, view, (i == iter - 1), &lastHDU, rng, stats)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to merge chip %d", view->chip);
+                goto PPMERGE_MASK_ERROR;
+            }
+        }
+
+        if (outChip->data_exists) {
+            psList *inChips = psListAlloc(NULL);
+            for (int i=0; i < numFiles; i++) {
+                pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT", i); // Input file
+                pmChip *chip = pmFPAviewThisChip(view, file->fpa);
+                psListAdd(inChips, PS_LIST_TAIL, chip);
+            }
+
+            // XXX I need to call pmConfigMaskWriteHeader for the PHU somewhere, after it is created!
+
+            if (!pmConceptsAverageChips(outChip, inChips, true)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to average Chip concepts.");
+                psFree(inChips);
+                goto PPMERGE_MASK_ERROR;
+            }
+            psFree(inChips);
+        }
+        if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
+            goto PPMERGE_MASK_ERROR;
+        }
+    }
+
+    psList *fpaList = psListAlloc(NULL);///< List of FPAs for concept averaging
+    for (int i = 0; i < numFiles; i++) {
+        pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT", i); ///< Input file
+        psListAdd(fpaList, PS_LIST_TAIL, file->fpa);
+    }
+    if (!pmConceptsAverageFPAs(outFPA, fpaList)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to average FPA concepts.");
+        psFree(fpaList);
+        goto PPMERGE_MASK_ERROR;
+    }
+    psFree(fpaList);
+
+    if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
+        goto PPMERGE_MASK_ERROR;
+    }
+
+    psFree(stats);
+    psFree(view);
+    psFree(rng);
+
+    return true;
+
+PPMERGE_MASK_ERROR:
+    psFree(stats);
+    psFree(view);
+    psFree(rng);
+    return false;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeReadChunk.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeReadChunk.c	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeReadChunk.c	(revision 41089)
@@ -0,0 +1,160 @@
+/** @file ppMergeReadChunk.c
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-06 02:44:31 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "ppMerge.h"
+
+#define THREAD_WAIT 10000               ///< Microseconds to wait if thread is not available
+
+ppMergeFileGroup *ppMergeReadChunk(bool *status, psArray *fileGroups, pmConfig *config, int numChunk)
+{
+    *status = true;
+
+    bool mdok;
+    bool haveMasks = psMetadataLookupBool(&mdok, config->arguments, "INPUTS.MASKS"); // Do we have masks?
+    bool haveVariances = psMetadataLookupBool(&mdok, config->arguments,
+                                              "INPUTS.VARIANCES");// Do we have variances?
+    int rows = psMetadataLookupS32(NULL, config->arguments, "ROWS"); // Number of rows to read per chunk
+    int rejectedInputs = 0;
+    // select an available fileGroup
+    while (1) {
+        // check for any fileGroups which can read data
+        for (int j = 0; j < fileGroups->n; j++) {
+            ppMergeFileGroup *fileGroup = fileGroups->data[j];
+            if (fileGroup->read) {
+                continue;
+            }
+
+            // find max last scan so far
+            int lastScan = 0;
+            for (int i = 0; i < fileGroups->n; i++) {
+                ppMergeFileGroup *fileGroup = fileGroups->data[i];
+                lastScan = PS_MAX(fileGroup->lastScan, lastScan);
+            }
+            fileGroup->firstScan = lastScan;
+            fileGroup->lastScan = lastScan + rows;
+
+            psArray *readouts = fileGroup->readouts;
+
+            psTimerStart ("ppMergeReadChunk");
+
+            psTrace("ppStack", 2, "Reading data for chunk %d into fileGroup %d....n", numChunk, j);
+            for (int i = 0; i < readouts->n; i++) {
+                pmReadout *inRO = readouts->data[i]; ///< Input readout
+		if (!inRO->process) continue;
+
+                // override the recorded last scan
+                inRO->thisImageScan  = fileGroup->firstScan;
+                inRO->thisVarianceScan = fileGroup->firstScan;
+                inRO->thisMaskScan   = fileGroup->firstScan;
+                inRO->forceScan      = true;
+                // inRO->process        = true;
+
+		// char *cellname = psMetadataLookupStr(&mdok, inRO->parent->concepts, "CELL.NAME");
+		// fprintf (stderr, "cell: %s, file %d, process: %d, image, mask, var: %lx, %lx, %lx -> ", 
+		// 	 cellname, i, inRO->process, (long int) inRO->image, (long int) inRO->mask, (long int) inRO->variance);
+
+                // Read a chunk from a file
+                pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT", i);
+
+		int zMax = 0;
+                bool keepReading = false;
+                if (pmReadoutMore(inRO, file->fits, 0, &zMax, rows, config)) {
+		    // skip video cells
+		    if (zMax > 1) {
+			psWarning ("skipping video cell (1: %ld)", (long) pthread_self());
+			inRO->process = false;
+			rejectedInputs++;
+			continue;
+		    }
+                    keepReading = true;
+                    if (!pmReadoutReadChunk(inRO, file->fits, 0, &zMax, rows, 0, config)) {
+                        psError(PS_ERR_IO, false, "Unable to read chunk %d for file PPMERGE.INPUT %d",
+                                numChunk, i);
+                        *status = false;
+                        return NULL;
+                    }
+		    // skip video cells
+		    if (zMax > 1) {
+			psWarning ("skipping video cell (2: %ld)", (long) pthread_self());
+			inRO->process = false;
+			rejectedInputs++;
+			continue;
+		    }
+                }
+		// skip video cells
+		// XXX this could be more efficient if we identified the cells to skip before calling the function...
+		if (zMax > 1) {
+		    psWarning ("skipping video cell (3: %ld)", (long) pthread_self());
+		    inRO->process = false;
+		    rejectedInputs++;
+		    continue;
+		}
+
+                if (haveMasks && pmReadoutMoreMask(inRO, file->fits, 0, &zMax, rows, config)) {
+                    keepReading = true;
+                    pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT.MASK", i);
+                    if (!pmReadoutReadChunkMask(inRO, file->fits, 0, &zMax, rows, 0, config)) {
+                        psError(PS_ERR_IO, false, "Unable to read chunk %d for file PPMERGE.INPUT.MASK %d",
+                                numChunk, i);
+                        *status = false;
+                        return NULL;
+                    }
+                }
+
+                if (haveVariances && pmReadoutMoreVariance(inRO, file->fits, 0, &zMax, rows, config)) {
+                    keepReading = true;
+                    pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PPMERGE.INPUT.VARIANCE", i);
+                    if (!pmReadoutReadChunkVariance(inRO, file->fits, 0, &zMax, rows, 0, config)) {
+                        psError(PS_ERR_IO, false,
+                                "Unable to read chunk %d for file PPMERGE.INPUT.VARIANCE %d",
+                                numChunk, i);
+                        *status = false;
+                        return NULL;
+                    }
+                }
+		
+		// fprintf (stderr, "%lx, %lx, %lx\n", (long int) inRO->image, (long int) inRO->mask, (long int) inRO->variance);
+		if (!keepReading) {
+		  return NULL;
+		}
+		
+		
+            }
+	    psLogMsg("ppMerge",PS_LOG_INFO,"%d %ld %d %d %d",
+		     j,readouts->n,rejectedInputs,fileGroup->read,fileGroup->busy);
+	    if (rejectedInputs >= readouts->n) {
+	      fileGroup->read = false;
+	      fileGroup->busy = false;
+	      return NULL;
+	    }
+	    
+            fileGroup->read = fileGroup->busy = true;
+            return fileGroup;
+        }
+
+        // Check for threads that are ready to read
+        bool wait = true;
+        for (int j = 0; j < fileGroups->n; j++) {
+            ppMergeFileGroup *fileGroup = fileGroups->data[j];
+            if (fileGroup->busy) {
+                continue;
+            }
+            fileGroup->read = false;
+            wait = false;
+        }
+        if (wait) {
+            // No threads currently available
+            usleep(THREAD_WAIT);
+        }
+    }
+    return NULL;
+}
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeScaleZero.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeScaleZero.c	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeScaleZero.c	(revision 41089)
@@ -0,0 +1,248 @@
+/** @file ppMergeScaleZero.c
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.31 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-06 02:44:31 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#include "ppMerge.h"
+
+/**
+ * Get the scale and zero for each chip of each input
+ */
+bool ppMergeScaleZero(pmConfig *config)
+{
+    assert(config);
+
+    ppMergeType type = psMetadataLookupS32(NULL, config->arguments, "TYPE"); ///< Type of frame
+    int numInputs = psMetadataLookupS32(NULL, config->arguments, "INPUTS.NUM"); ///< Number of inputs
+    int numCells = psMetadataLookupS32(NULL, config->arguments, "INPUTS.CELLS"); ///< Number of cells
+    psStatsOptions meanStat = psMetadataLookupS32(NULL, config->arguments, "MEAN"); ///< Statistic for mean
+    psStatsOptions stdevStat = psMetadataLookupS32(NULL, config->arguments, "STDEV"); ///< Statistic for stdev
+    int shutterSize = psMetadataLookupS32(NULL, config->arguments, "SHUTTER.SIZE"); ///< Size of shutter region
+
+    psVector *gains = NULL;             ///< Gains for each cell
+    psArray *shutters = NULL;           ///< Shutter data for each cell
+    psStats *stats = NULL;              ///< Statistics for background
+    psImage *background = NULL;         ///< Background measurements per cell per file
+
+    switch (type) {
+      case PPMERGE_TYPE_BIAS:
+      case PPMERGE_TYPE_DARK:
+      case PPMERGE_TYPE_CTEMASK:
+      case PPMERGE_TYPE_NOISEMAP:
+        // Nothing to measure
+        return true;
+      case PPMERGE_TYPE_FLAT:
+      case PPMERGE_TYPE_FRINGE:
+        gains = psVectorAlloc(numCells, PS_TYPE_F32);
+        background = psImageAlloc(numCells, numInputs, PS_TYPE_F32);
+        psImageInit(background, NAN);
+        stats = psStatsAlloc(meanStat);
+        break;
+      case PPMERGE_TYPE_SHUTTER:
+        shutters = psArrayAlloc(numCells);
+        break;
+      case PPMERGE_TYPE_MASK:
+      default:
+        break;
+    }
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS); ///< Random number generator
+    pmFPAview *view = NULL;             ///< View into FPA
+
+    for (int i = 0; i < numInputs; i++) {
+        pmFPAfileActivate(config->files, false, NULL);
+        psArray *files = ppMergeFileActivateSingle(config, PPMERGE_FILES_INPUT, true, i); // Activated files
+        pmFPAfile *input = files->data[0]; // Representative file; should be the image (not mask or variance)
+        pmFPA *fpa = input->fpa;        // FPA of interest
+        view = pmFPAviewAlloc(0);       // View to component of interest
+        int cellNum = 0;                // Index for cell
+        if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
+            goto ERROR;
+        }
+        pmChip *chip;                   ///< Chip of interest
+        while ((chip = pmFPAviewNextChip(view, fpa, 1))) {
+            if (!chip->process || !chip->file_exists) {
+                continue;
+            }
+            if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
+                goto ERROR;
+            }
+
+            pmCell *cell;               ///< Cell of interest
+            while ((cell = pmFPAviewNextCell(view, fpa, 1))) {
+                if (!cell->process || !cell->file_exists) {
+                    continue;
+                }
+                if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
+                    goto ERROR;
+                }
+
+                if (!cell->data_exists) {
+                    continue;
+                }
+
+                // skip cells with video data
+                if (cell->readouts->n > 1) {
+                    // psError(PS_ERR_BAD_PARAMETER_VALUE, true, "File %d chip %d cell %d contains more than one readout (%ld)", i, view->chip, view->cell, cell->readouts->n);
+                    // goto ERROR;
+                  psWarning("File %d chip %d cell %d contains more than one readout (%ld), skipping",
+                            i, view->chip, view->cell, cell->readouts->n);
+                  continue;
+                }
+                pmReadout *readout = cell->readouts->data[0]; ///< Readout of interest
+
+                psImageMaskType maskVal = pmConfigMaskGet("MASK.VALUE", config); ///< Value to mask
+
+                switch (type) {
+                  case PPMERGE_TYPE_FLAT:
+                  case PPMERGE_TYPE_FRINGE: {
+                      // Extract the gain
+                      float gain = psMetadataLookupF32(NULL, cell->concepts, "CELL.GAIN"); ///< Cell gain
+                      if (!isfinite(gain)) {
+                          psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes,
+                                                                        PPMERGE_RECIPE); // Recipe
+                          psAssert(recipe, "Should be there!");
+                          bool override = psMetadataLookupBool(NULL, recipe,
+                                                               "GAIN.OVERRIDE"); // Override the bad gain?
+                          if (override) {
+                              psWarning("CELL.GAIN is not set for readout (%d,%d,%d) on file %d "
+                                        "--- setting to unity.",
+                                        view->chip, view->cell, view->readout, i);
+                              psMetadataItem *item = psMetadataLookup(cell->concepts, "CELL.GAIN"); // Item with gain
+                              psAssert(item, "Should be there!");
+                              item->data.F32 = 1.0;
+			      
+			      // for unity gain, there is no modification for the readnoise, note that it has (effectively) been updated
+			      psMetadataRemoveKey(cell->concepts, "CELL.READNOISE.UPDATE");
+                          } else {
+                              // psError(PS_ERR_BAD_PARAMETER_VALUE, false,
+                              // "CELL.GAIN for file %d chip %d cell %d is not set.",
+                              // i, view->chip, view->cell);
+                              // goto ERROR;
+                              psWarning("CELL.GAIN for file %d chip %d cell %d is NaN",
+                                        i, view->chip, view->cell);
+                          }
+                      }
+                      gains->data.F32[cellNum] = gain;
+
+                      // Measure the background
+                      if (!psImageBackground(stats, NULL, readout->image, readout->mask, maskVal, rng)) {
+                        // psError(PS_ERR_UNKNOWN, false,
+                        // "Unable to get statistics for file %d chip %d cell %d",
+                        // i, view->chip, view->cell);
+                        // goto ERROR;
+                        psWarning ("Unable to get statistics for file %d chip %d cell %d",
+                                   i, view->chip, view->cell);
+                        background->data.F32[i][cellNum] = NAN;
+                      } else {
+                        background->data.F32[i][cellNum] = psStatsGetValue(stats, meanStat);
+                      }
+                      break;
+                  }
+                  case PPMERGE_TYPE_SHUTTER: {
+                      pmShutterCorrectionData *shutter = shutters->data[cellNum]; ///< Shutter correction data
+                      if (!shutter) {
+                          shutter = pmShutterCorrectionDataAlloc(readout->image->numCols,
+                                                                 readout->image->numRows,
+                                                                 shutterSize);
+                          shutters->data[cellNum] = shutter;
+                      }
+                      if (!pmShutterCorrectionAddReadout(shutter, readout, meanStat, stdevStat,
+                                                         maskVal, rng)) {
+                          psError(PS_ERR_UNKNOWN, false,
+                                  "Can't add file %d chip %d cell %d to shutter correction.",
+                                  i, view->chip, view->cell);
+                          goto ERROR;
+                      }
+                      break;
+                  }
+                  case PPMERGE_TYPE_MASK:
+                  default:
+                    psAbort("Should never get here.");
+                }
+
+                cellNum++;
+
+                if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
+                    goto ERROR;
+                }
+            }
+
+            if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
+                goto ERROR;
+            }
+        }
+
+        if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
+            goto ERROR;
+        }
+
+        psFree(view);
+
+#if 0
+        // Reset files for reading again
+        for (int i = 0; i < files->n; i++) {
+            pmFPAfile *file = files->data[i]; // File of interest
+        }
+#endif
+        psFree(files);
+    }
+
+    psFree(rng); rng = NULL;
+    psFree(stats); stats = NULL;
+
+    // Store results
+    switch (type) {
+      case PPMERGE_TYPE_FRINGE:
+        psMetadataAddImage(config->arguments, PS_LIST_TAIL, "ZEROS", 0,
+                           "Zero to subtract from each input cell", background);
+        // Flow through
+      case PPMERGE_TYPE_FLAT: {
+          // Need to normalize over the focal plane
+          if (psTraceGetLevel("ppMerge") > 9) {
+              for (int i = 0; i < gains->n; i++) {
+                  psTrace("ppMerge", 10, "Gain for cell %d is %f\n", i, gains->data.F32[i]);
+              }
+          }
+          psVector *fluxes = NULL;        ///< Solution to fluxes
+          if (!pmFlatNormalize(&fluxes, &gains, background)) {
+              psError(PS_ERR_UNKNOWN, false, "Normalisation failed to converge --- continuing anyway.");
+              psFree(fluxes);
+              goto ERROR;
+          }
+
+          psMetadataAddVector(config->arguments, PS_LIST_TAIL, "SCALES", 0,
+                              "Scale to divide into each input file", fluxes);
+          psFree(fluxes);               // Drop reference
+          break;
+      }
+      case PPMERGE_TYPE_SHUTTER:
+        psMetadataAddArray(config->arguments, PS_LIST_TAIL, "SHUTTER", 0,
+                           "Shutter data", shutters);
+        break;
+      case PPMERGE_TYPE_MASK:
+      default:
+        psAbort("Should never get here.");
+    }
+
+    psFree(background);
+    psFree(shutters);
+    return true;
+
+ERROR:
+    // Common path for errors
+    psFree(gains);
+    psFree(background);
+    psFree(shutters);
+    psFree(rng);
+    psFree(stats);
+    psFree(view);
+    return false;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeScaleZero.h
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeScaleZero.h	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeScaleZero.h	(revision 41089)
@@ -0,0 +1,33 @@
+/** @file ppMergeScaleZero.h
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-01 21:43:05 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PP_MERGE_SCALE_ZERO_H
+#define PP_MERGE_SCALE_ZERO_H
+
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "ppMergeData.h"
+#include "ppMergeOptions.h"
+
+/**
+ * Get the scale and zero for each chip of each input
+ */
+bool ppMergeScaleZero(psImage **scales, ///< The scales for each integration/cell
+                      psImage **zeros, ///< The zeroes for each integration/cell
+                      psArray **shutter,///< The shutter correction data for each cell
+                      ppMergeData *data,///< The data
+                      const ppMergeOptions *options, ///< The options
+                      const pmConfig *config ///< The configuration
+    );
+
+#endif
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeSetThreads.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeSetThreads.c	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeSetThreads.c	(revision 41089)
@@ -0,0 +1,95 @@
+/** @file ppMergeSetThreads.c
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-01 21:43:05 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "ppMerge.h"
+
+// "PPMERGE_READOUT_COMBINE", 5
+bool ppMergeThread_pmReadoutCombine(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    pmReadout *output           = job->args->data[0];
+    ppMergeFileGroup *fileGroup = job->args->data[1];
+    psVector *zero              = job->args->data[2];
+    psVector *scale             = job->args->data[3];
+    pmCombineParams *params     = job->args->data[4];
+
+    bool status = pmReadoutCombine (output, fileGroup->readouts, zero, scale, params);
+
+    // after we are done, tell the I/O system that this file group is done
+    fileGroup->busy = false;
+    return status;
+}
+
+bool ppMergeThread_pmDarkCombine(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    pmCell *outCell             = job->args->data[0];
+    ppMergeFileGroup *fileGroup = job->args->data[1];
+    psScalar *iter              = job->args->data[2];
+    psScalar *rej               = job->args->data[3];
+    psScalar *maskVal           = job->args->data[4];
+
+    bool status = pmDarkCombine(outCell, fileGroup->readouts, iter->data.S32, rej->data.F32, maskVal->data.PS_TYPE_IMAGE_MASK_DATA);
+
+    // after we are done, tell the I/O system that this file group is done
+    fileGroup->busy = false;
+    return status;
+}
+
+bool ppMergeThread_pmShutterCorrectionGenerate(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    pmReadout *output             = job->args->data[0];
+    pmReadout *pattern            = job->args->data[1];
+    ppMergeFileGroup *fileGroup   = job->args->data[2];
+    psScalar *shutterRef          = job->args->data[3];
+    pmShutterCorrectionData *data = job->args->data[4];
+    psScalar *iter                = job->args->data[5];
+    psScalar *rej                 = job->args->data[6];
+    psScalar *maskVal             = job->args->data[7];
+
+    bool status = pmShutterCorrectionGenerate(output, pattern, fileGroup->readouts, shutterRef->data.F32, data, iter->data.S32, rej->data.F32, maskVal->data.PS_TYPE_IMAGE_MASK_DATA);
+
+    // after we are done, tell the I/O system that this file group is done
+    fileGroup->busy = false;
+    return status;
+}
+
+bool ppMergeSetThreads(void)
+{
+
+    {
+        psThreadTask *task = psThreadTaskAlloc("PPMERGE_READOUT_COMBINE", 5);
+        task->function = &ppMergeThread_pmReadoutCombine;
+        psThreadTaskAdd(task);
+        psFree(task);
+    }
+
+    {
+        psThreadTask *task = psThreadTaskAlloc("PPMERGE_DARK_COMBINE", 5);
+        task->function = &ppMergeThread_pmDarkCombine;
+        psThreadTaskAdd(task);
+        psFree(task);
+    }
+
+    {
+        psThreadTask *task = psThreadTaskAlloc("PPMERGE_SHUTTER_CORRECTION", 8);
+        task->function = &ppMergeThread_pmShutterCorrectionGenerate;
+        psThreadTaskAdd(task);
+        psFree(task);
+    }
+
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeThreadLauncher.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeThreadLauncher.c	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeThreadLauncher.c	(revision 41089)
@@ -0,0 +1,108 @@
+/** @file ppMergeThreadLauncher.c
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-01 21:43:05 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "ppMerge.h"
+
+/**
+ * each thread runs this function, starting a new job when it finished with an old one
+ * it is called with a (void *) pointer to its own thread pointer
+ */
+void *ppMergeThreadLauncher (void *data) {
+
+    psThread *self = data;
+    psThreadJob *job = NULL;
+
+    while (1) {
+
+	// if we get an error, just wait until we are cleared or killed
+	while (self->fault) {
+	    usleep (10000);
+	}
+
+	// request a new job, if there are none available, sleep a bit
+	// we have to lock here so the job queue cannot be empty yet no threads busy
+	psThreadLock();
+	while ((job = psThreadJobGetPending ()) == NULL) {
+	    psThreadUnlock();
+	    usleep (10000);
+	}
+	self->busy = true;
+	psThreadUnlock();
+
+	// list all allowed job types here
+
+	// pmReadoutCombine
+	if (!strcmp (job->type, "PPMERGE_READOUT_COMBINE")) {
+	    psAssert (job->args->n == 5, "invalid number of arguments to pmReadoutCombine");
+
+	    pmReadout *output           = job->args->data[0];
+	    ppMergeFileGroup *fileGroup = job->args->data[1];
+	    psVector *zero              = job->args->data[2];
+	    psVector *scale             = job->args->data[3];
+	    pmCombineParams *params     = job->args->data[4];
+
+	    bool status = pmReadoutCombine (output, fileGroup->readouts, zero, scale, params);
+	    if (!status) {
+		self->fault = true;
+	    }
+
+	    // we do not have to lock here because this transition is not tied to the job queue
+	    fileGroup->busy = false;
+	    self->busy = false;  
+	    continue;
+	}
+
+	// pmDarkCombine
+	if (!strcmp (job->type, "PPMERGE_DARK_COMBINE")) {
+	    psAssert (job->args->n == 7, "invalid number of arguments to pmDarkCombine");
+
+	    pmCell *outCell             = job->args->data[0];
+	    ppMergeFileGroup *fileGroup = job->args->data[1];
+	    psArray *darkOrdinates      = job->args->data[2];
+	    psString darkNorm           = job->args->data[3];
+	    psScalar *iter     	        = job->args->data[4];
+	    psScalar *rej     	        = job->args->data[5];
+	    psScalar *maskVal     	= job->args->data[6];
+
+	    bool status = pmDarkCombine(outCell, fileGroup->readouts, darkOrdinates, darkNorm, iter->data.S32, rej->data.F32, maskVal->data.PS_TYPE_IMAGE_MASK_DATA);
+	    if (!status) {
+		self->fault = true;
+	    }
+	    // we do not have to lock here because this transition is not tied to the job queue
+	    fileGroup->busy = false;
+	    self->busy = false;  
+	    continue;
+	}
+
+	// pmShutterCorrectionGenerate
+	if (!strcmp (job->type, "PPMERGE_SHUTTER_CORRECTION")) {
+	    psAssert (job->args->n == 7, "invalid number of arguments to pmDarkCombine");
+
+	    pmReadout *output             = job->args->data[0];
+	    ppMergeFileGroup *fileGroup   = job->args->data[1];
+	    psScalar *shutterRef          = job->args->data[2];
+	    pmShutterCorrectionData *data = job->args->data[3];
+	    psScalar *iter     	          = job->args->data[4];
+	    psScalar *rej     	          = job->args->data[5];
+	    psScalar *maskVal     	  = job->args->data[6];
+
+	    bool status = pmShutterCorrectionGenerate(output, NULL, fileGroup->readouts, shutterRef->data.F32, data, iter->data.S32, rej->data.F32, maskVal->data.PS_TYPE_IMAGE_MASK_DATA);
+	    if (!status) {
+		self->fault = true;
+	    }
+	    // we do not have to lock here because this transition is not tied to the job queue
+	    fileGroup->busy = false;
+	    self->busy = false;  
+	    continue;
+	}
+    }  
+}
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeVersion.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeVersion.c	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeVersion.c	(revision 41089)
@@ -0,0 +1,123 @@
+/** @file ppMergeVersion.c
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-01 21:43:05 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include <ppStats.h>
+
+#include "ppMergeVersion.h"
+#include "ppMergeVersionDefinitions.h"
+
+#ifndef PPMERGE_VERSION
+#error "PPMERGE_VERSION is not set"
+#endif
+#ifndef PPMERGE_BRANCH
+#error "PPMERGE_BRANCH is not set"
+#endif
+#ifndef PPMERGE_SOURCE
+#error "PPMERGE_SOURCE is not set"
+#endif
+
+psString ppMergeVersion(void)
+{
+    char *value = NULL;
+    psStringAppend(&value, "%s@%s", PPMERGE_BRANCH, PPMERGE_VERSION);
+    return value;
+}
+
+psString ppMergeSource(void)
+{
+    return psStringCopy(PPMERGE_SOURCE);
+}
+
+psString ppMergeVersionLong(void)
+{
+    psString version = ppMergeVersion();  // Version, to return
+    psString source = ppMergeSource();    // Source
+
+    psStringPrepend(&version, "ppMerge ");
+    psStringAppend(&version, " from %s, built %s, %s", source, __DATE__, __TIME__);
+    psFree(source);
+
+#ifdef __OPTIMIZE__
+    psStringAppend(&version, " optimised");
+#else
+    psStringAppend(&version, " unoptimised");
+#endif
+
+    return version;
+};
+
+bool ppMergeVersionHeader(psMetadata *header)
+{
+    PS_ASSERT_METADATA_NON_NULL(header, false);
+
+    psTime *time = psTimeGetNow(PS_TIME_TAI); // The time now
+    psString timeString = psTimeToISO(time); // The time in an ISO string
+    psFree(time);
+    psString history = NULL;               // History string
+    psStringAppend(&history, "ppMerge at %s", timeString);
+    psFree(timeString);
+    psMetadataAddStr(header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, NULL, history);
+    psFree(history);
+
+    psLibVersionHeader(header);
+    psModulesVersionHeader(header);
+    ppStatsVersionHeader(header);
+
+    psString version = ppMergeVersion(); // Software version
+    psString source  = ppMergeSource();  // Software source
+
+    psMetadataAddStr(header, PS_LIST_TAIL, "MERGE_V", PS_META_REPLACE, NULL, PPMERGE_VERSION);
+    
+    psStringPrepend(&version, "ppMerge version: ");
+    psStringPrepend(&source, "ppMerge 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;
+}
+
+void ppMergeVersionPrint(void)
+{
+    psTime *time = psTimeGetNow(PS_TIME_TAI); // The time now
+    psString timeString = psTimeToISO(time); // The time in an ISO string
+    psFree(time);
+    psLogMsg("ppMerge", PS_LOG_INFO, "ppMerge at %s", timeString);
+    psFree(timeString);
+
+    psString pslib = psLibVersionLong();// psLib version
+    psString psmodules = psModulesVersionLong(); // psModules version
+    psString ppStats = ppStatsVersionLong(); // ppStats version
+    psString ppMerge = ppMergeVersionLong(); // ppMerge version
+
+    psLogMsg("ppImage", PS_LOG_INFO, "%s", pslib);
+    psLogMsg("ppImage", PS_LOG_INFO, "%s", psmodules);
+    psLogMsg("ppImage", PS_LOG_INFO, "%s", ppStats);
+    psLogMsg("ppImage", PS_LOG_INFO, "%s", ppMerge);
+
+    psFree(pslib);
+    psFree(psmodules);
+    psFree(ppStats);
+    psFree(ppMerge);
+
+    return;
+}
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeVersion.h
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeVersion.h	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeVersion.h	(revision 41089)
@@ -0,0 +1,37 @@
+/** @file ppMergeVersion.h
+ *
+ *  @brief
+ *
+ *  @ingroup ppMerge
+ *
+ *  @author IfA
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-01 21:43:05 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PP_MERGE_VERSION_H
+#define PP_MERGE_VERSION_H
+
+/**
+ * Return short version information
+ */
+psString ppMergeVersion(void);
+
+/**
+ * Return software source
+ */
+psString ppMergeSource(void);
+
+/**
+ * Return long version information
+ */
+psString ppMergeVersionLong(void);
+
+/**
+ * Update the metadata with version information for all dependencies
+ */
+void ppMergeVersionMetadata(psMetadata *metadata ///< Metadata to update with version information
+    );
+
+#endif
Index: /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeVersionDefinitions.h.in
===================================================================
--- /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeVersionDefinitions.h.in	(revision 41089)
+++ /branches/ccl_branches/ipponly-20191108/ppMerge/src/ppMergeVersionDefinitions.h.in	(revision 41089)
@@ -0,0 +1,8 @@
+#ifndef PPMERGE_VERSION_DEFINITIONS_H
+#define PPMERGE_VERSION_DEFINITIONS_H
+
+#define PPMERGE_VERSION @PPMERGE_VERSION@ // SVN version
+#define PPMERGE_BRANCH  @PPMERGE_BRANCH@  // SVN branch
+#define PPMERGE_SOURCE  @PPMERGE_SOURCE@  // SVN source
+
+#endif
