Index: /branches/ccl_branches/ipponly-20191108/psastro/Doxyfile.in
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/Doxyfile.in	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/Doxyfile.in	(revision 41084)
@@ -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           = psastro
+
+# 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/psastro/Makefile.am
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/Makefile.am	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/Makefile.am	(revision 41084)
@@ -0,0 +1,10 @@
+SUBDIRS = src
+
+CLEANFILES = *.pyc *~ core core.*
+
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA= psastro.pc
+
+EXTRA_DIST = \
+	psastro.pc.in \
+	autogen.sh
Index: /branches/ccl_branches/ipponly-20191108/psastro/autogen.sh
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/autogen.sh	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/autogen.sh	(revision 41084)
@@ -0,0 +1,103 @@
+#!/bin/sh
+# Run this to generate all the initial makefiles, etc.
+
+srcdir=`dirname $0`
+test -z "$srcdir" && srcdir=.
+
+ORIGDIR=`pwd`
+cd $srcdir
+
+PROJECT=psastro
+TEST_TYPE=-f
+# change this to be a unique filename in the top level dir
+FILE=autogen.sh
+
+DIE=0
+
+LIBTOOLIZE=libtoolize
+ACLOCAL="aclocal $ACLOCAL_FLAGS"
+AUTOHEADER=autoheader
+AUTOMAKE=automake
+AUTOCONF=autoconf
+
+($LIBTOOLIZE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $LIBTOOLIZE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/libtool/"
+        DIE=1
+}
+
+($ACLOCAL --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $ACLOCAL installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOHEADER --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOHEADER installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+($AUTOMAKE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOMAKE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOCONF --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOCONF installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+if test "$DIE" -eq 1; then
+        exit 1
+fi
+
+test $TEST_TYPE $FILE || {
+        echo "You must run this script in the top-level $PROJECT directory"
+        exit 1
+}
+
+if test -z "$*"; then
+        echo "I am going to run ./configure with no arguments - if you wish "
+        echo "to pass any to it, please specify them on the $0 command line."
+fi
+
+$LIBTOOLIZE --copy --force || echo "$LIBTOOLIZE failed"
+$ACLOCAL || echo "$ACLOCAL failed"
+$AUTOHEADER || echo "$AUTOHEADER failed"
+$AUTOMAKE --add-missing --force-missing --copy || echo "$AUTOMAKE  failed"
+$AUTOCONF || echo "$AUTOCONF failed"
+
+cd $ORIGDIR
+
+run_configure=true
+for arg in $*; do
+    case $arg in
+        --no-configure)
+            run_configure=false
+            ;;
+        *)
+            ;;
+    esac
+done
+
+if $run_configure; then
+    $srcdir/configure --enable-maintainer-mode "$@"
+    echo
+    echo "Now type 'make' to compile $PROJECT."
+else
+    echo
+    echo "Now run 'configure' and 'make' to compile $PROJECT."
+fi
Index: /branches/ccl_branches/ipponly-20191108/psastro/configure.ac
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/configure.ac	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/configure.ac	(revision 41084)
@@ -0,0 +1,208 @@
+dnl Process this file with autoconf to produce a configure script.
+AC_PREREQ(2.61)
+
+AC_INIT([psastro], [0.9.0], [ipp-support@ifa.hawaii.edu])
+AC_CONFIG_SRCDIR([src])
+
+AM_INIT_AUTOMAKE([1.6 foreign dist-bzip2])
+AM_CONFIG_HEADER([src/config.h])
+AM_MAINTAINER_MODE
+
+IPP_STDLDFLAGS
+
+AC_LANG(C)
+AC_GNU_SOURCE
+AC_PROG_CC_C99
+AC_PROG_INSTALL
+AC_PROG_LIBTOOL
+AC_SYS_LARGEFILE
+
+dnl ------------------------------------------------------------
+
+AC_PATH_PROG([ERRORCODES], [psParseErrorCodes], [missing])
+if test "$ERRORCODES" = "missing" ; then
+  AC_MSG_ERROR([psParseErrorCodes is required])
+fi
+
+dnl ------------------ kapa,libkapa options -------------------------
+dnl -- libkapa implies the requirement for libpng, libjpeg as well --
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+dnl test for command-line options: use ohana-config if not supplied
+KAPA_CFLAGS_CONFIG="true"
+KAPA_LIBS_CONFIG="true"
+AC_ARG_WITH(kapa,
+[AS_HELP_STRING(--with-kapa=DIR,Specify location of libkapa)],
+[KAPA_CFLAGS="-I$withval/include" KAPA_LIBS="-L$withval/lib" 
+ KAPA_CFLAGS_CONFIG="false"       KAPA_LIBS_CONFIG="false"])
+AC_ARG_WITH(kapa-include,
+[AS_HELP_STRING(--with-kapa-include=DIR,Specify libkapa include directory.)],
+[KAPA_CFLAGS="-I$withval" KAPA_CFLAGS_CONFIG="false"])
+AC_ARG_WITH(kapa-lib,
+[AS_HELP_STRING(--with-kapa-lib=DIR,Specify libkapa library directory.)],
+[KAPA_LIBS="-L$withval" KAPA_LIBS_CONFIG="false"])
+
+echo "KAPA_CFLAGS_CONFIG: $KAPA_CFLAGS_CONFIG"
+echo "KAPA_LIBS_CONFIG: $KAPA_LIBS_CONFIG"
+echo "KAPA_CFLAGS: $KAPA_CFLAGS"
+echo "KAPA_LIBS: $KAPA_LIBS"
+
+dnl HAVE_KAPA is set to false if any of the tests fail
+HAVE_KAPA="true"
+AC_MSG_NOTICE([checking for libkapa])
+if test "$KAPA_CFLAGS_CONFIG" = "true" -o "$KAPA_LIBS_CONFIG" = "true"; then
+  AC_MSG_NOTICE([kapa info supplied by ohana-config])
+  KAPA_CONFIG=`which ohana-config`
+  AC_CHECK_FILE($KAPA_CONFIG,[],
+    [HAVE_KAPA="false"; AC_MSG_WARN([libkapa is not found: output plots disabled.  Obtain libkapa at http://kiawe.ifa.hawaii.edu/Elixir/Ohana or use --with-kapa to specify location])])
+  
+  echo "HAVE_KAPA: $HAVE_KAPA"
+  echo "KAPA_CFLAGS_CONFIG: $KAPA_CFLAGS_CONFIG"
+
+  if test "$HAVE_KAPA" = "true" -a "$KAPA_CFLAGS_CONFIG" = "true" ; then
+   AC_MSG_NOTICE([libkapa cflags info supplied by ohana-config])
+   AC_MSG_CHECKING([libkapa cflags])
+   KAPA_CFLAGS="`${KAPA_CONFIG} --cflags`"
+   AC_MSG_RESULT([${KAPA_CFLAGS}])
+  fi
+
+  if test "$HAVE_KAPA" = "true" -a "$KAPA_LIBS_CONFIG" = "true" ; then
+   AC_MSG_NOTICE([libkapa ldflags info supplied by ohana-config])
+   AC_MSG_CHECKING([libkapa ldflags])
+   KAPA_LIBS="`${KAPA_CONFIG} --libs` -lX11"
+   AC_MSG_RESULT([${KAPA_LIBS}])
+  fi
+fi
+
+if test "$HAVE_KAPA" = "true" ; then
+ AC_MSG_NOTICE([libkapa supplied])
+ PSASTRO_CFLAGS="${PSASTRO_CFLAGS} ${KAPA_CFLAGS}"
+ PSASTRO_LIBS="${PSASTRO_LIBS} ${KAPA_LIBS}"
+else
+ AC_MSG_NOTICE([libkapa ignored])
+fi
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------ libjpeg options ---------------------
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+AC_ARG_WITH(jpeg,
+[AS_HELP_STRING(--with-jpeg=DIR,Specify location of libjpeg.)],
+[JPEG_CFLAGS="-I$withval/include"
+ JPEG_LDFLAGS="-L$withval/lib"])
+AC_ARG_WITH(jpeg-include,
+[AS_HELP_STRING(--with-jpeg-include=DIR,Specify libjpeg include directory.)],
+[JPEG_CFLAGS="-I$withval"])
+AC_ARG_WITH(jpeg-lib,
+[AS_HELP_STRING(--with-jpeg-lib=DIR,Specify libjpeg library directory.)],
+[JPEG_LDFLAGS="-L$withval"])
+
+CFLAGS="${CFLAGS} ${JPEG_CFLAGS}"
+CPPFLAGS=${CFLAGS}
+LDFLAGS="${LDFLAGS} ${JPEG_LDFLAGS}"
+
+AC_CHECK_HEADERS([jpeglib.h],
+  [PSASTRO_CFLAGS="$PSASTRO_CFLAGS $JPEG_CFLAGS" AC_SUBST(JPEG_CFLAGS)],
+  [HAVE_KAPA=false; AC_MSG_WARN([libjpeg headers not found: output plots disabled.  Obtain libjpeg from http://www.ijg.org/ or use --with-jpeg to specify location.])]
+)
+
+AC_CHECK_LIB(jpeg,jpeg_CreateCompress,
+  [PSASTRO_LIBS="$PSASTRO_LIBS $JPEG_LDFLAGS -ljpeg"],
+  [HAVE_KAPA=false; AC_MSG_WARN([libjpeg library not found: output plots disabled.  Obtain libjpeg from http://www.ijg.org/ or use --with-jpeg to specify location.])]
+)
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------ libpng options ---------------------
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+AC_ARG_WITH(png,
+[AS_HELP_STRING(--with-png=DIR,Specify location of libpng.)],
+[PNG_CFLAGS="-I$withval/include"
+ PNG_LDFLAGS="-L$withval/lib"])
+AC_ARG_WITH(png-include,
+[AS_HELP_STRING(--with-png-include=DIR,Specify libpng include directory.)],
+[PNG_CFLAGS="-I$withval"])
+AC_ARG_WITH(png-lib,
+[AS_HELP_STRING(--with-png-lib=DIR,Specify libpng library directory.)],
+[PNG_LDFLAGS="-L$withval"])
+
+CFLAGS="${CFLAGS} ${PNG_CFLAGS}"
+CPPFLAGS=${CFLAGS}
+LDFLAGS="${LDFLAGS} ${PNG_LDFLAGS}"
+
+AC_CHECK_HEADERS([png.h],
+  [PSASTRO_CFLAGS="$PSASTRO_CFLAGS $PNG_CFLAGS" AC_SUBST(PNG_CFLAGS)],
+  [HAVE_KAPA=false; AC_MSG_WARN([libpng headers not found: output plots disabled.  Obtain libpng from http://www.ijg.org/ or use --with-png to specify location.])]
+)
+
+AC_CHECK_LIB(png,png_init_io,
+  [PSASTRO_LIBS="$PSASTRO_LIBS $PNG_LDFLAGS -lpng"],
+  [HAVE_KAPA=false; AC_MSG_WARN([libpng library not found: output plots disabled.  Obtain libpng from http://www.ijg.org/ or use --with-png to specify location.])]
+)
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------ use kapa or not? ---------------------
+
+if test "$HAVE_KAPA" == "true" ; then
+  AC_MSG_RESULT([including plotting functions])
+  AC_DEFINE([HAVE_KAPA],[1],[enable use of libkapa])
+else
+  AC_MSG_RESULT([skipping plotting functions])
+  AC_DEFINE([HAVE_KAPA],[0],[disable use of libkapa])
+fi
+
+dnl ------------- psLib, psModules ---------------
+PKG_CHECK_MODULES([PSLIB],    [pslib >= 1.0.0])
+PKG_CHECK_MODULES([PSMODULE], [psmodules >= 1.0.0])
+PKG_CHECK_MODULES([PPSTATS],  [ppStats >= 1.0.0]) 
+
+dnl Set CFLAGS for build
+IPP_STDOPTS
+IPP_STDCFLAGS
+
+echo "PSASTRO_CFLAGS: $PSASTRO_CFLAGS"
+echo "PSASTRO_LIBS: $PSASTRO_LIBS"
+
+IPP_VERSION
+
+AC_SUBST([PSASTRO_CFLAGS])
+AC_SUBST([PSASTRO_LIBS])
+
+AC_CONFIG_FILES([
+  Makefile
+  src/Makefile
+  psastro.pc
+  Doxyfile
+])
+
+AC_OUTPUT
Index: /branches/ccl_branches/ipponly-20191108/psastro/doc/mktest.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/doc/mktest.txt	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/doc/mktest.txt	(revision 41084)
@@ -0,0 +1,31 @@
+
+the program, psastro-mktest, will generate a set of fake test data for
+psastro.  the config file 'psastro-mktest.conf' gives an example setup
+for this program.  It generates 4 output files:
+
+- cmpfile : this is a basic elixir-style cmp file (stars plus header)
+  which can be taken as input to psastro
+
+- catfile : this is a basic text reference catalog, which the current
+  psastro loads as a reference
+
+- rawfile : this is a text listing of all astrometry stages for the
+  generated stars: readout, cell, chip, fpa, tpa, sky, with the last
+  two columns being the magnitudes.  this file is generated by
+  applying the transformations starting at the readout and going
+  step-by-step to the sky.
+
+- reffile : this is the inverse of the preceeding file, with the
+  resulting sources transformed from the sky coordinates down
+  step-by-step to the readout pixels.  If the transformations are
+  functioning, these two files (rawfile & reffile) should be identical
+  within floating point errors.
+
+examples:
+
+build a fake dataset
+# psastro-mktest doc/psastro-mktest.conf test.cmp test.cat test.raw test.ref
+
+run psastrom on the fake data:
+# psastro doc/psastro.conf test.cmp test.dat
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/doc/notes.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/doc/notes.txt	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/doc/notes.txt	(revision 41084)
@@ -0,0 +1,39 @@
+
+2006.12.26
+
+ work still to be done for psastro:
+
+ - test suite
+   - create a DVO database from a fake population
+   - create fake images with range of errors drawn from DVO
+
+ - 
+
+
+we have a few different astrometry circumstances for our complete
+image/chip hierarchy structure:
+
+- ChipAstrom with one readout, one cell per chip:
+
+  whether or not there is more than one chip, we need to have two
+  stages
+  
+    - in the first stage, the per-chip results are applied to the
+      chip.toFPA parameter set.  
+
+    - in the second stage, the chip results are collectively used to
+      modify the fpa.toSky terms.
+
+      - the average offsets of the chip positions relative to starting
+	coordinates are used to calculate a single average offset to
+	the boresite. 
+
+      - the average rotations of the chips relative to their starting
+        rotations are used to calculate a single average rotation of
+        the boresite
+
+
+- ChipAstrom with more than one readout and/or cell:
+
+  - match all stars up relative to first readout?
+   
Index: /branches/ccl_branches/ipponly-20191108/psastro/doc/outline.txt
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/doc/outline.txt	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/doc/outline.txt	(revision 41084)
@@ -0,0 +1,48 @@
+
+psastro outline:
+
+  psastroParseCamera
+    psastroDefineFiles
+  psastroDataLoad
+    psastroConvertReadout (saves PSASTRO.RAWSTARS on readout->analysis)
+      psastroCorrectKH
+  psastroAnalysis
+    psastroUseModel
+    psastroAstromGuess (plots PSASTRO.RAWSTARS)
+    psastroRemoveClumpsRawstars (saves PSASTRO.RAWSTARS.SUBSET on readout->analysis)
+    psastroLoadRefstars (loads full reference star set)
+    psastroChooseRefstars (save PSASTRO.REFSTARS & PSASTRO.REFSTARS.SUBSET on readout->analysis)
+      psastroRefstarSubset (adjust PSASTRO.REFSTARS.SUBSET on readout->analysis)
+    psastroChooseGlintStars (uses full reference star set)
+    psastroChipAstrom (uses PSASTRO.REFSTARS.SUBSET & PSASTRO.RAWSTARS.SUBSET)
+      psastroOneChipGrid
+      psastroOneChipFit
+    psastroMosaicAstrom
+      psastroFixChips
+      psastroFixChipsTest
+      psastroMosaicFit
+        psastroMosaicSetMatch (uses PSASTRO.REFSTARS.SUBSET & PSASTRO.RAWSTARS.SUBSET)
+        psastroMosaicCommonScale
+        psastroMosaicDistortion
+        psastroMosaicChipAstrom
+    psastroZeroPoint
+    psastroAstromGuessCheck
+    psastroMaskUpdates
+  psastroDataSave
+
+psastroExtract outline:
+  psastroExtractParseCamera
+  psastroExtractDataLoad
+  psastroExtractAnalysis
+    psastroLoadRefstars (loads full reference star set)
+    psastroChooseRefstars (save PSASTRO.REFSTARS & PSASTRO.REFSTARS.SUBSET on readout->analysis)
+      psastroRefstarSubset (adjust PSASTRO.REFSTARS.SUBSET on readout->analysis)
+    psastroExtractGhosts
+    psastroExtractStars
+  psastroDataSave
+
+
+psastroModel outline:
+
+psastroModelFit outline:
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/doc/psastro-mktest.conf
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/doc/psastro-mktest.conf	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/doc/psastro-mktest.conf	(revision 41084)
@@ -0,0 +1,28 @@
+
+NX            S32  2000    	 # chip dimensions
+NY            S32  2000    	 # chip dimensions
+
+NSTARS        S32  20    	 # number of fake stars to generate
+
+# the current test output file only has the basic WCS info.  
+READOUT.COL.0    F32  0		 # cell coord for readout (0,0)
+READOUT.ROW.0    F32  0		 # cell coord for readout (0,0)
+READOUT.COL.BIN  F32  1		 # readout col binning factor
+READOUT.ROW.BIN  F32  1		 # readout row binning factor
+
+CELL.COL.0    	 F32  0		 # chip coord for cell (0,0)
+CELL.ROW.0    	 F32  0		 # chip coord for cell (0,0)
+
+CHIP.COL.0    	 F32  0		 # FPA coord for chip (0,0)
+CHIP.ROW.0    	 F32  0		 # FPA coord for chip (0,0)
+CHIP.THETA    	 F32  0	         # cw rotation from chip to FPA
+CHIP.PARITY.X 	 F32  1		 # x flip 
+CHIP.PARITY.Y 	 F32  1		 # y flip 
+
+FPA.THETA     	 F32  0		 # fpa row offset
+FPA.COL.0  	 F32  0		 # TPA coord for FPA (0,0)
+FPA.ROW.0  	 F32  0		 # TPA coord for FPA (0,0)
+
+PLATE.SCALE	 F32  1	         # plate scale in arcsec/pixel
+RA		 F32  10	 # celestial coord for TPA (0,0)
+DEC     	 F32  20	 # celestial coord for TPA (0,0)
Index: /branches/ccl_branches/ipponly-20191108/psastro/doc/psastro.conf
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/doc/psastro.conf	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/doc/psastro.conf	(revision 41084)
@@ -0,0 +1,17 @@
+
+PSASTRO.MATCH.RADIUS 	F32 2.0    # fitting radius in pixels
+
+PSASTRO.CHIP.NX      	S32 1      # chip-to-fpa tranformation order
+PSASTRO.CHIP.NY	     	S32 1      # chip-to-fpa tranformation order
+
+PSASTRO.CHIP.NSIGMA  	F32 3.0    # Nsigma clip for matched-fit 
+PSASTRO.CHIP.NITER   	S32 3      # number of clipping interations for matched-fit 
+
+PSASTRO.GRID.OFFSET  	F32 100.0  # maximum allowed offset for grid match
+PSASTRO.GRID.SCALE   	F32  50.0  # binning scale for grid match
+
+PSASTRO.GRID.MIN.ANGLE  F32   0.0  # starting angle
+PSASTRO.GRID.MAX.ANGLE  F32   0.0  # ending angle
+PSASTRO.GRID.DEL.ANGLE  F32   0.5  # steps between angle attempts
+
+PSASTRO.STARS.MAX       S32 300    # use no more than this number of sources
Index: /branches/ccl_branches/ipponly-20191108/psastro/psastro.pc.in
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/psastro.pc.in	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/psastro.pc.in	(revision 41084)
@@ -0,0 +1,12 @@
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+
+Name: libpsastro
+Description: Pan-STARRS Photometry Library
+Version: @VERSION@
+Requires: pslib psmodules
+Libs: -L${libdir} -lpsastro
+Libs.private: @PSASTRO_LIBS@
+Cflags: -I${includedir} @PSASTRO_CFLAGS@
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/Makefile.am
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/Makefile.am	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/Makefile.am	(revision 41084)
@@ -0,0 +1,139 @@
+lib_LTLIBRARIES = libpsastro.la
+
+# Force recompilation of psastroVersion.c, since it gets the version information
+psastroVersion.c: psastroVersionDefinitions.h
+psastroVersionDefinitions.h: psastroVersionDefinitions.h.in FORCE
+	pslib-setsvnversion.pl PSASTRO psastroVersionDefinitions.h.in psastroVersionDefinitions.h
+FORCE: ;
+
+libpsastro_la_CFLAGS = $(PSASTRO_CFLAGS) $(PPSTATS_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+libpsastro_la_LDFLAGS = $(PSASTRO_LIBS) $(PPSTATS_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+
+bin_PROGRAMS = psastro psastroExtract psastroModel psastroModelFit gpcModel
+
+psastro_CFLAGS = $(PSASTRO_CFLAGS) $(PPSTATS_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+psastro_LDFLAGS = $(PSASTRO_LIBS) $(PPSTATS_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+psastro_LDADD = libpsastro.la
+
+psastroExtract_CFLAGS = $(PSASTRO_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+psastroExtract_LDFLAGS = $(PSASTRO_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+psastroExtract_LDADD = libpsastro.la
+
+psastroModel_CFLAGS = $(PSASTRO_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+psastroModel_LDFLAGS = $(PSASTRO_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+psastroModel_LDADD = libpsastro.la
+
+psastroModelFit_CFLAGS = $(PSASTRO_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+psastroModelFit_LDFLAGS = $(PSASTRO_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+psastroModelFit_LDADD = libpsastro.la
+
+gpcModel_CFLAGS = $(PSASTRO_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+gpcModel_LDFLAGS = $(PSASTRO_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+gpcModel_LDADD = libpsastro.la
+
+psastro_SOURCES = \
+	psastro.c		    \
+        psastroArguments.c	    \
+	psastroParseCamera.c   	    \
+	psastroDataLoad.c           \
+	psastroDataSave.c           \
+	psastroMetadataStats.c      \
+	psastroCleanup.c
+
+psastroExtract_SOURCES = \
+	psastroExtract.c		    \
+        psastroExtractArguments.c	    \
+	psastroExtractParseCamera.c   \
+	psastroExtractDataLoad.c      \
+	psastroExtractAnalysis.c      \
+	psastroExtractStar.c      \
+	psastroExtractStars.c      \
+	psastroExtractGhosts.c      \
+	psastroCleanup.c
+
+psastroModel_SOURCES = \
+	psastroModel.c		    \
+        psastroModelArguments.c	    \
+	psastroModelParseCamera.c   \
+	psastroModelDataLoad.c      \
+	psastroModelAnalysis.c      \
+	psastroModelBoresite.c      \
+	psastroModelFitBoresite.c   \
+	psastroModelAdjust.c        \
+	psastroModelDataSave.c      \
+	psastroCleanup.c
+
+psastroModelFit_SOURCES = \
+	psastroModelFit.c \
+	psastroModelBoresite.c      \
+	psastroModelFitBoresite.c
+
+gpcModel_SOURCES = gpcModel.c
+
+libpsastro_la_SOURCES = \
+	psastroGalaxyShapeErrors.c  \
+	psastroMaskUpdates.c        \
+	psastroMaskUtils.c          \
+	psastroChooseGlintStars.c   \
+	psastroLoadCrosstalk.c \
+	psastroLoadGlints.c   \
+	psastroLoadGhosts.c         \
+	psastroGhostUtils.c         \
+	psastroErrorCodes.c         \
+	psastroVersion.c            \
+	psastroDefineFiles.c        \
+	psastroAnalysis.c           \
+	psastroAstromGuess.c        \
+	psastroLoadRefstars.c       \
+	psastroChooseRefstars.c     \
+	psastroConvert.c	    \
+	psastroChipAstrom.c         \
+	psastroOneChipGrid.c	    \
+	psastroOneChipFit.c	    \
+	psastroRemoveClumps.c	    \
+	psastroUtils.c	       	    \
+	psastroTestFuncs.c          \
+	psastroLuminosityFunction.c \
+	psastroRefstarSubset.c      \
+	psastroFixChips.c           \
+	psastroFixChipsTest.c       \
+	psastroUseModel.c           \
+	psastroMosaicAstrom.c       \
+	psastroMosaicDistortion.c   \
+	psastroMosaicFPtoTP.c       \
+	psastroMosaicGradients.c    \
+	psastroMosaicCorrectDistortion.c   \
+	psastroMosaicChipAstrom.c   \
+	psastroMosaicOneChip.c      \
+	psastroMosaicSetAstrom.c    \
+	psastroMosaicSetMatch.c     \
+	psastroFindChip.c           \
+	psastroZeroPoint.c    	    \
+	psastroDemoDump.c           \
+	psastroDemoPlot.c
+
+include_HEADERS = \
+	psastro.h \
+	psastroErrorCodes.h
+
+noinst_HEADERS = \
+	psastroInternal.h \
+	psastroStandAlone.h
+
+clean-local:
+	-rm -f TAGS
+
+# Tags for emacs
+tags:
+	etags `find . -name \*.[ch] -print`
+
+### Error codes.
+BUILT_SOURCES = psastroErrorCodes.h psastroErrorCodes.c psastroVersionDefinitions.h
+CLEANFILES = psastroErrorCodes.h psastroErrorCodes.c
+EXTRA_DIST = psastroErrorCodes.dat psastroErrorCodes.h.in psastroErrorCodes.c.in
+
+psastroErrorCodes.h : psastroErrorCodes.dat psastroErrorCodes.h.in
+	$(ERRORCODES) --data=psastroErrorCodes.dat --outdir=. psastroErrorCodes.h
+
+psastroErrorCodes.c : psastroErrorCodes.dat psastroErrorCodes.c.in psastroErrorCodes.h
+	$(ERRORCODES) --data=psastroErrorCodes.dat --outdir=. psastroErrorCodes.c
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/gpcModel.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/gpcModel.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/gpcModel.c	(revision 41084)
@@ -0,0 +1,178 @@
+/** @file gpcModel.c
+ *
+ *  @brief
+ *
+ *  @ingroup gpcModel
+ *
+ *  @author IfA
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+
+/**
+ * generate model for gpc based on input model for all but chips
+ * USAGE: gpcModel (input) (output)
+ */
+int main (int argc, char **argv) {
+
+    if (argc != 3) {
+	fprintf (stderr, "USAGE: gpcModel (input) (output)\n");
+	exit (2);
+    }
+
+    psFits *input = psFitsOpen (argv[1], "r");
+    psFits *output = psFitsOpen (argv[2], "w");
+
+    { // PHU
+	psMetadata *header = psFitsReadHeader (NULL, input);
+	psFitsWriteBlank (output, header, "");
+    }
+
+    { // CHIPS
+
+	// read initial chips
+	if (!psFitsMoveExtName (input, "CHIPS")) {
+	    psError(PS_ERR_IO, false, "missing CHIPS extension in astrometry table\n");
+	    return false;
+	}
+	psArray *chips = psFitsReadTable (input);
+	if (!chips) psAbort("cannot read chips");
+	fprintf (stderr, "read %ld rows from CHIPS\n", chips->n);
+
+	psMetadata *header = psMetadataAlloc();
+	psMetadataAddStr(header, PS_LIST_TAIL, "COORD",    PS_META_REPLACE, "name of this layer",	"CHIPS");
+	psMetadataAddStr(header, PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",     	"FOCAL_PLANE");
+	psMetadataAddStr(header, PS_LIST_TAIL, "BOUNDARY", PS_META_REPLACE, "validity region",   	"RECTANGLE");
+	psMetadataAddStr(header, PS_LIST_TAIL, "TRANSFRM", PS_META_REPLACE, "mapping to parent", 	"POLYNOMIAL");
+
+	float coeff[2][2];
+	char coeffMask[2][2];
+	coeff[0][0] = 0.0;
+	coeff[1][0] = 1.0;
+	coeff[0][1] = 0.0;
+	coeff[1][1] = 0.0;
+
+	coeffMask[0][0] = 0;
+	coeffMask[1][0] = 0;
+	coeffMask[0][1] = 0;
+	coeffMask[1][1] = 1;
+
+	psArray *table = psArrayAllocEmpty (1);
+	for (int i = 0; i < 8; i++) {
+	    for (int j = 0; j < 8; j++) {
+		if ((i == 0) && (j == 0)) continue;
+		if ((i == 0) && (j == 7)) continue;
+		if ((i == 7) && (j == 0)) continue;
+		if ((i == 7) && (j == 7)) continue;
+
+		float Xo;
+		float Yo;
+		if (i < 4) {
+		    Xo = (3 - i)*4970.0 +  60.0;
+		    Yo = (3 - j)*5133.0 + 125.0;
+		    coeff[1][0] = +1.0;
+		} else {
+		    Xo = (4 - i)*4970.0 -  60.0;
+		    Yo = (4 - j)*5133.0 - 150.0;
+		    coeff[1][0] = -1.0;
+		}	    
+
+		for (int ix = 0; ix < 2; ix++) {
+		    for (int iy = 0; iy < 2; iy++) {
+
+			psMetadata *row = psMetadataAlloc ();
+		
+			char chipname[80];
+			sprintf (chipname, "XY%d%d", i, j);
+			psMetadataAddStr(row,    PS_LIST_TAIL, "SEGMENT",  PS_META_REPLACE, "name of this segment", chipname);
+			psMetadataAddStr(row,    PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "FOCAL_PLANE");
+			psMetadataAddF32(row,    PS_LIST_TAIL, "MINX",     PS_META_REPLACE, "range", 0.0);
+			psMetadataAddF32(row,    PS_LIST_TAIL, "MAXX",     PS_META_REPLACE, "range", 4846.0);
+			psMetadataAddF32(row,    PS_LIST_TAIL, "MINY",     PS_META_REPLACE, "range", 0.0);
+			psMetadataAddF32(row,    PS_LIST_TAIL, "MAXY",     PS_META_REPLACE, "range", 4868.0);
+
+			psMetadataAddS32(row,    PS_LIST_TAIL, "XORDER",   PS_META_REPLACE, "", ix);
+			psMetadataAddS32(row,    PS_LIST_TAIL, "YORDER",   PS_META_REPLACE, "", iy);
+			psMetadataAddS32(row,    PS_LIST_TAIL, "NXORDER",  PS_META_REPLACE, "", 1);
+			psMetadataAddS32(row,    PS_LIST_TAIL, "NYORDER",  PS_META_REPLACE, "", 1);
+			if ((ix == 0) && (iy == 0)) {
+			    psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_X",   PS_META_REPLACE, "", Xo);
+			    psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_Y",   PS_META_REPLACE, "", Yo);
+			} else {
+			    psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_X",   PS_META_REPLACE, "", coeff[ix][iy]);
+			    psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_Y",   PS_META_REPLACE, "", coeff[iy][ix]);
+			}
+			psMetadataAddF32(row,    PS_LIST_TAIL, "ERROR_X",  PS_META_REPLACE, "", 0.0);
+			psMetadataAddF32(row,    PS_LIST_TAIL, "ERROR_Y",  PS_META_REPLACE, "", 0.0);
+			psMetadataAddU8 (row,    PS_LIST_TAIL, "MASK_X",   PS_META_REPLACE, "", coeffMask[ix][iy]);
+			psMetadataAddU8 (row,    PS_LIST_TAIL, "MASK_Y",   PS_META_REPLACE, "", coeffMask[ix][iy]);
+			psArrayAdd (table, 100, row);
+			psFree (row);
+		    }
+		}
+	    }
+	}
+    
+	if (!psFitsWriteTable (output, header, table, "CHIPS")) {
+	    psError(PS_ERR_IO, false, "writing sky data\n");
+	    psFree(table);
+	    return false;
+	}
+    }
+
+    { // FP
+	if (!psFitsMoveExtName (input, "FP")) {
+	    psError(PS_ERR_IO, false, "missing FP extension in astrometry table\n");
+	    return false;
+	}
+	psMetadata *header = psFitsReadHeader (NULL, input);
+	psArray *table = psFitsReadTable (input);
+	if (!table) psAbort("cannot read fp");
+	fprintf (stderr, "read %ld rows from FP\n", table->n);
+
+	if (!psFitsWriteTable (output, header, table, "FP")) {
+	    psError(PS_ERR_IO, false, "writing sky data\n");
+	    psFree(table);
+	    return false;
+	}
+    }
+
+    { // TP
+	if (!psFitsMoveExtName (input, "TP")) {
+	    psError(PS_ERR_IO, false, "missing TP extension in astrometry table\n");
+	    return false;
+	}
+	psMetadata *header = psFitsReadHeader (NULL, input);
+	psArray *table = psFitsReadTable (input);
+	if (!table) psAbort("cannot read tp");
+	fprintf (stderr, "read %ld rows from TP\n", table->n);
+	if (!psFitsWriteTable (output, header, table, "TP")) {
+	    psError(PS_ERR_IO, false, "writing sky data\n");
+	    psFree(table);
+	    return false;
+	}
+    }
+
+    { // SKY
+	if (!psFitsMoveExtName (input, "SKY")) {
+	    psError(PS_ERR_IO, false, "missing SKY extension in astrometry table\n");
+	    return false;
+	}
+	psMetadata *header = psFitsReadHeader (NULL, input);
+	psArray *sky = psFitsReadTable (input);
+	if (!sky) psAbort("cannot read sky");
+	fprintf (stderr, "read %ld rows from SKY\n", sky->n);
+	if (!psFitsWriteTable (output, header, sky, "SKY")) {
+	    psError(PS_ERR_IO, false, "writing sky data\n");
+	    psFree(sky);
+	    return false;
+	}
+    }
+
+    psFitsClose (input);
+    psFitsClose (output);
+    exit (0);
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastro.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastro.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastro.c	(revision 41084)
@@ -0,0 +1,77 @@
+/** @file psastro.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.28 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+
+static void usage(void) {
+    fprintf(stderr, "USAGE: psastro [-file image(s)] [-list imagelist] (output)\n");
+    exit(PS_EXIT_CONFIG_ERROR);
+}
+
+int main (int argc, char **argv)
+{
+    psTimerStart ("complete");
+
+    psastroErrorRegister();              // register our error codes/messages
+
+    // model inits are needed in pmSourceIO
+    // models defined in psphot/src/models are not available in psastro
+    pmModelClassInit();
+
+    // load configuration information
+    pmConfig *config = config = psastroArguments(argc, argv);
+    if (!config) {
+        usage();
+    }
+
+    psastroVersionPrint();
+
+    // load identify the data sources
+    if (!psastroParseCamera (config)) {
+        psErrorStackPrint(stderr, "error setting up the camera\n");
+        psFree(config);
+        exit(PS_EXIT_CONFIG_ERROR);
+    }
+
+    // load the raw detection data (using PSPHOT.SOURCES filerule)
+    // select subset of stars for astrometry
+    if (!psastroDataLoad (config)) {
+        psErrorStackPrint(stderr, "error loading input data\n");
+        psFree(config);
+        exit(PS_EXIT_DATA_ERROR);
+    }
+
+    psMetadata *stats = psMetadataAlloc(); // Statistics, for output
+    psMetadataAddS32(stats, PS_LIST_TAIL, "QUALITY", 0, "No problems", 0);
+
+    // run the full astrometry analysis (chip and/or mosaic)
+    if (!psastroAnalysis(config, stats)) {
+        psErrorStackPrint(stderr, "failure in psastro analysis\n");
+        psFree(config);
+        psFree(stats);
+        exit(PS_EXIT_SYS_ERROR);
+    }
+
+    // write out the results
+    if (!psastroDataSave(config, stats)) {
+        psErrorStackPrint(stderr, "failed to write out data\n");
+        psFree(config);
+        psFree(stats);
+        exit(PS_EXIT_DATA_ERROR);
+    }
+
+    psFree (stats);
+    psLogMsg("psastro", 3, "complete psastro run: %f sec\n", psTimerMark ("complete"));
+
+    psastroCleanup(config);
+    exit(PS_EXIT_SUCCESS);
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastro.h
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastro.h	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastro.h	(revision 41084)
@@ -0,0 +1,203 @@
+/** @file psastro.h
+ *
+ *  @brief This file defines the library functions available to external
+ *  programs.
+ *
+ *  It must be included by programs which are compiled against
+ *  psphot functions.
+ *
+ *  @ingroup psastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.49 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-09 21:25:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# ifndef PSASTRO_H
+# define PSASTRO_H
+
+# include "psastroErrorCodes.h"
+
+/// @addtogroup psastro
+/// @{
+
+# define PSASTRO_RECIPE "PSASTRO" ///< Name of the recipe to use
+
+# define psMemCopy(A)(psMemIncrRefCounter((A)))
+# define DEG_RAD 57.295779513082322
+# define RAD_DEG  0.017453292519943
+# define SIGN(X)  (((X) == 0) ? 0 : ((fabs((double)(X))) / (X)))
+
+#if 0
+/**
+ * this structure represents a fit to the logN / logS curve for a set of stars
+ * logN = offset + slope * logS
+ */
+typedef struct {
+    double mMin;                        ///< minimum magnitude bin with data
+    double mMax;                        ///< maximum magnitude bin with data
+    double offset;                      ///< fitted line offset
+    double slope;                       ///< fitted line slope
+    double mPeak;                       ///< mag of peak bin
+    int nPeak;                          ///< # of stars in peak bin
+    int sPeak;                          ///< sum of stars to peak bin
+} pmLumFunc;
+#endif
+
+/**
+ * this structure defines the parameters to describe a ghost
+ */
+typedef struct {
+    psPlane *srcFP;			///< location in FPA coords of the source star
+    psPlane *FP;			///< location in FPA coords of the ghost center
+    psPlane *chip;			///< location in chip coords of the ghost center
+    double Mag;				///< instrumental magnitude of source star
+    psEllipseAxes inner;		///< inner elliptical annulus boundary
+    psEllipseAxes outer;		///< outer elliptical annulus boundary
+} psastroGhost;
+
+psastroGhost     *psastroGhostAlloc (void);
+bool              psastroLoadGhosts (pmConfig *config);
+bool              psastroLoadGhostsHSC (pmConfig *config);
+
+bool              psastroDataSave (pmConfig *config, psMetadata *stats);
+bool              psastroDefineFiles (pmConfig *config, pmFPAfile *input);
+bool              psastroDefineFile (pmConfig *config, pmFPA *input, char *filerule, char *argname, pmFPAfileType fileType, pmDetrendType detrendType);
+bool              psastroAnalysis (pmConfig *config, psMetadata *stats);
+
+bool              psastroConvertFPA (pmConfig *config, pmFPA *fpa, psMetadata *recipe);
+bool              psastroConvertReadout (pmConfig *config, pmFPAview *view, pmReadout *readout, psMetadata *recipe);
+bool              psastroCorrectKH (pmConfig *config, pmFPAview *view, pmReadout *readout, psMetadata *recipe, psArray *inStars);
+
+psArray          *pmSourceToAstromObj (psArray *sources, float MagOffset);
+bool              psastroAstromGuess (int *nStars, pmConfig *config);
+bool              psastroAstromGuessCheck (pmConfig *config);
+bool              psastroMaskUpdates (pmConfig *config, psMetadata *stats);
+
+bool              psastroLoadGlints (pmConfig *config);
+bool              psastroChooseGlintStars (pmConfig *config, psArray *refs, const char *source);
+
+bool              psastroLoadCrosstalk (pmConfig *config);
+
+psArray          *psastroLoadRefstars (pmConfig *config, const char *source);
+bool              psastroChipAstrom (pmConfig *config);
+bool              psastroOneChip (pmFPA *fpa, pmChip *chip, psArray *refset, psArray *rawset, psMetadata *recipe, psMetadata *updates);
+bool              psastroOneChipGrid (pmFPA *fpa, pmChip *chip, psArray *refset, psArray *rawset, psMetadata *recipe, psMetadata *updates);
+bool              psastroOneChipFit (pmFPA *fpa, pmChip *chip, pmReadout *readout, psArray *refset, psArray *rawset, psMetadata *recipe, psMetadata *updates);
+bool              psastroChooseRefstars (pmConfig *config, psArray *refs, const char *source, bool saveExistingMatchedRefs);
+bool              psastroRefstarSubset (pmReadout *readout);
+pmLumFunc        *psastroLuminosityFunction (psArray *stars, pmLumFunc *rawFunc);
+bool              psastroLuminosityFunctionPlot(psVector *lnMag, psVector *Mag, pmLumFunc *lumFunc, pmLumFunc *rawFunc);
+psArray          *psastroRemoveClumps (psArray *input, int scale);
+psArray          *psastroRemoveClumpsIterate (psArray *input, int scale, int nIter);
+bool              psastroRemoveClumpsRawstars (pmConfig *config);
+
+bool              psastroChipFailureHeader (psMetadata *updates);
+
+
+// utility functions:
+bool              psastroUpdateChipToFPA (pmFPA *fpa, pmChip *chip);
+bool              psastroWriteStars (char *filename, psArray *sources);
+bool              psastroWriteTransform (psPlaneTransform *map);
+
+// mosaic fitting functions
+bool              psastroMosaicDistortion (pmFPA *fpa, psMetadata *recipe, int pass);
+psPlaneTransform *psastroMosaicFitRotAndScale (pmFPA *fpa);
+bool              psastroMosaicApplyRotAndScale (pmFPA *fpa, psPlaneTransform *TPtoFP);
+bool              psastroMosaicDistortionFromGradients (pmFPA *fpa, psMetadata *recipe);
+bool              psastroMosaicCorrectDistortion (pmFPA *fpa, psPlaneTransform *TPtoFP);
+bool              psastroMosaicCommonScale (pmFPA *fpa, psMetadata *recipe);
+bool              psastroMosaicAstrom (pmConfig *config);
+bool              psastroMosaicChipAstrom (pmFPA *fpa, psMetadata *recipe, int iteration);
+bool              psastroMosaicSetMatch (pmFPA *fpa, psMetadata *recipe, int iteration);
+bool              psastroMosaicSetAstrom (pmFPA *fpa);
+bool              psastroMosaicHeaders (pmConfig *config);
+bool              psastroMosaicRescaleChips (pmFPA *fpa);
+bool              psastroMosaicOneChip (pmChip *chip, pmReadout *readout, psMetadata *recipe, psMetadata *updates, int iteration);
+
+// Return version strings.
+psString          psastroVersion(void);
+psString          psastroSource(void);
+psString          psastroVersionLong(void);
+bool              psastroVersionHeader(psMetadata *header);
+bool              psastroVersionHeaderFull(psMetadata *header);
+void              psastroVersionPrint(void);
+
+// demo plots
+bool              psastroPlotRawstars (psArray *rawstars, pmFPA *fpa, pmChip *chip, psMetadata *recipe);
+bool              psastroPlotRefstars (psArray *refstars, psMetadata *recipe);
+bool              psastroPlotOneChipFit (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe);
+
+bool              psastroDumpRawstars (psArray *rawstars, pmFPA *fpa, pmChip *chip);
+bool              psastroDumpRefstars (psArray *refstars, char *filename);
+bool              psastroDumpMatches (pmFPA *fpa, char *filename);
+bool              psastroDumpStars (psArray *stars, char *filename);
+bool              psastroDumpMatchedStars (char *filename, psArray *rawstars, psArray *refstars, psArray *match);
+bool              psastroDumpGradients (psArray *gradients, char *filename);
+
+bool              psastroMosaicSetAstrom_tmp (pmFPA *fpa);
+int               psastroSortByMag (const void *a, const void *b);
+
+bool              psastroFixChips (pmConfig *config, psMetadata *recipe);
+bool              psastroFixChipsTest (pmConfig *config, psMetadata *recipe);
+bool              psastroUseModel (pmConfig *config, psMetadata *recipe);
+bool              psastroDumpCorners (char *filenameU, char *filenameD, pmFPA *fpa);
+
+char             *psastroSetMagLimit (float *minMag, float *maxRho, pmConfig *config, const char *source);
+
+psArray          *psastroReadGetstarCatalog (psFits *fits);
+psArray          *psastroReadGetstar_PS1_DEV_0 (psFits *fits);
+
+bool              psastroAstromGuessSetChip (pmFPA *fpa, pmChip *chip, const pmFPAview *view, double pixelScale, bool bilevelAstrometry);
+bool              psastroAstromGuessSetFPA (pmFPA *fpa, bool *bilevelAstrometry);
+bool              psastroMetadataStats (pmConfig *config, psMetadata *stats);
+
+bool 		  psastroZeroPoint (pmConfig *config);
+psVector         *psastroZeroPointReadoutAccum(psVector *dMag, pmReadout *readout, float exptime, float refminMag, float refmaxMag);
+bool              psastroZeroPointAnalysis (psMetadata *header, psVector *dMag, float zeropt, psMetadata *recipe);
+bool 		  psastroZeroPointFromRecipe (float *zeropt, float *exptime, float *ghostMaxMag, pmFPA *fpa, psMetadata *recipe);
+bool              psastroZeroPointRefMagLimitFromRecipe (float *refminMag, float *refmaxMag, pmFPA *fpa, psMetadata *recipe);
+
+psStats          *psastroStatsPercentile (psVector *myVector, psMetadata *recipe);
+float             psastroStatsPercentileValue (psHistogram *histogram, psHistogram *cumulative, psVector *myVector, float flimit);
+
+// masking functions
+pmCell           *pmCellInChip (pmChip *chip, float x, float y);
+bool 		  pmCellCoordsForChip (float *xCell, float *yCell, pmCell *cell, float xChip, float yChip);
+bool 		  pmChipCoordsForCell (float *xChip, float *yChip, pmCell *cell, float xCell, float yCell);
+		  
+bool 		  psastroMaskCircle (psImage *mask, psImageMaskType value, float x0, float y0, float dX, float dY);
+bool 		  psastroMaskEllipse (psImage *mask, psImageMaskType value, float x0, float y0, psEllipseAxes axes);
+bool 		  psastroMaskEllipticalAnnulus (psImage *mask, psImageMaskType value, float x0, float y0, psEllipseAxes eInner, psEllipseAxes eOuter);
+		  
+bool 		  psastroMaskBox (psImage *mask, psImageMaskType value, float x0, float y0, float dL, float dW, float theta);
+void 		  psastroMaskLine (psImage *mask, psImageMaskType value, double x1, double y1, double x2, double y2, int dW);
+void 		  psastroMaskLineBresen (psImage *mask, psImageMaskType value, int X1, int Y1, int X2, int Y2, int dW, int swapcoords);
+void 		  psastroMaskRectangle (psImage *mask, psImageMaskType value, int x0, int y0, int x1, int y1);
+
+pmChip           *psastroFindChip (double *xChip, double *yChip, pmFPA *fpa, double xFPA, double yFPA);
+bool 		  psastroChipBounds (pmFPA *fpa);
+bool              psastroFindChipInXrange (pmFPA *fpa, int nChip, double xFPA, double yFPA);
+bool              psastroFindChipInYrange (pmFPA *fpa, int nChip, double xFPA, double yFPA);
+bool 		  psastroFindChipYedges (double *yFPAs, double *yFPAe, pmFPA *fpa, int nChip);
+bool 		  psastroFindChipXedges (double *yFPAs, double *yFPAe, pmFPA *fpa, int nChip);
+bool 		  psastroFPAtoChip (double *xChip, double *yChip, pmFPA *fpa, int nChip, double xFPA, double yFPA);
+
+//bool              psastroMaskStats(pmConfig *config, psMetadata *stats);
+
+// psastroExtract functions
+bool 		  psastroExtractAnalysis (pmConfig *config);
+bool 		  psastroExtractStars (pmConfig *config);
+bool 		  psastroExtractGhosts (pmConfig *config);
+
+psImage          *psastroExtractStar (psImage *input, double x, double y, double dX, double dY);
+bool 		  psastroExtractParseCamera (pmConfig *config);
+bool 		  psastroExtractDataLoad (pmConfig *config);
+pmConfig         *psastroExtractArguments (int argc, char **argv);
+bool              psastroExtractFreeChipBounds(void);
+
+bool              psastroGalaxyShapeErrors (psMetadata *recipe, pmReadout *readout);
+
+///@}
+# endif /* PSASTRO_H */
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroAnalysis.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroAnalysis.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroAnalysis.c	(revision 41084)
@@ -0,0 +1,171 @@
+/** @file psastroAnalysis.c
+ *
+ *  @brief
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.15 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+/// Turn save on/off for a file
+static void fileSave(pmConfig *config,  // Configuration
+                     const char *name,  // Name of file
+                     bool save          // Save file?
+    )
+{
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, name, 0); // File of interest
+    if (!file) {
+        psErrorClear();
+        return;
+    }
+    file->save = save;
+    return;
+}
+
+
+bool psastroAnalysis (pmConfig *config, psMetadata *stats) {
+
+    bool status;
+    int nStars;
+
+    // measure the total elapsed time in psastroAnalysis.
+    psTimerStart ("psastroAnalysis");
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+        return false;
+    }
+
+    if (!psastroUseModel (config, recipe)) {
+        psError (PSASTRO_ERR_UNKNOWN, false, "failed to set model astrometry\n");
+        return false;
+    }
+
+    // interpret the available initial astrometric information
+    // apply the initial guess
+    if (!psastroAstromGuess (&nStars, config)) {
+        psError (PSASTRO_ERR_UNKNOWN, false, "failed to determine initial astrometry guess\n");
+        return false;
+    }
+    if (nStars == 0) {
+        // This is likely a data quality issue
+        psWarning("No stars for astrometry analysis --- suspect bad data quality");
+        if (stats && psMetadataLookupS32(NULL, stats, "QUALITY") == 0) {
+            psMetadataAddS32(stats, PS_LIST_TAIL, "QUALITY", PS_META_REPLACE,
+                             "No stars for astrometry", PSASTRO_ERR_DATA);
+        }
+        fileSave(config, "PSASTRO.OUTPUT", false);
+        fileSave(config, "PSASTRO.OUTPUT.MASK", false);
+        fileSave(config, "PSASTRO.OUT.REFSTARS", false);
+        psErrorClear();
+        return true;
+    }
+
+    if (!psastroRemoveClumpsRawstars(config)) {
+        psError (PSASTRO_ERR_UNKNOWN, false, "failed to remove RAWSTAR clumps\n");
+        return false;
+    }
+
+    // load the reference stars overlapping the data stars
+    psArray *refs = psastroLoadRefstars(config, "PSASTRO.INPUT");
+    if (!refs) {
+        psError (PSASTRO_ERR_UNKNOWN, false, "failed to load reference data\n");
+        return false;
+    }
+    if (refs->n == 0) {
+        psError(PSASTRO_ERR_REFSTARS, true, "no reference stars found");
+        psFree(refs);
+        return false;
+    }
+
+    bool skipastro = psMetadataLookupBool (&status, config->arguments, "PSASTRO.SKIP.ASTRO");
+
+    if (!psastroChooseRefstars (config, refs, "PSASTRO.INPUT", skipastro)) {
+        psError (PSASTRO_ERR_UNKNOWN, false, "failed to select reference data for chips\n");
+        psFree(refs);
+        return false;
+    }
+
+    if (!psastroChooseGlintStars (config, refs, "PSASTRO.INPUT")) {
+        psError (PSASTRO_ERR_UNKNOWN, false, "failed to select glint stars from reference star list\n");
+        psFree(refs);
+        return false;
+    }
+    psFree (refs);  // refs of interest are saved on readout->analysis
+
+    // check the command-line arguments first
+    bool chipastro = psMetadataLookupBool (&status, config->arguments, "PSASTRO.CHIP.MODE");
+    if (!status) {
+        chipastro = psMetadataLookupBool (&status, recipe, "PSASTRO.CHIP.MODE");
+    }
+    bool mosastro  = psMetadataLookupBool (&status, config->arguments, "PSASTRO.MOSAIC.MODE");
+    if (!status) {
+        mosastro  = psMetadataLookupBool (&status, recipe, "PSASTRO.MOSAIC.MODE");
+    }
+
+    if (skipastro) {
+        chipastro = false;
+        mosastro = false;
+        psLogMsg ("psastro", 3, "skip astrometry mode, accepting input astrometry\n");
+    } else if (!chipastro && !mosastro) {
+        psLogMsg ("psastro", 3, "no astrometry mode selected, assuming chip astrometry\n");
+        chipastro = true;
+    }
+
+    if (chipastro) {
+        if (!psastroChipAstrom (config)) {
+            // This is likely a data quality issue
+            psWarning("Failed single chip astrometry --- suspect bad data quality");
+            if (stats && psMetadataLookupS32(NULL, stats, "QUALITY") == 0) {
+                psMetadataAddS32(stats, PS_LIST_TAIL, "QUALITY", PS_META_REPLACE,
+                                 "Single chip astrometry failed", PSASTRO_ERR_DATA);
+            }
+            fileSave(config, "PSASTRO.OUTPUT", false);
+            fileSave(config, "PSASTRO.OUTPUT.MASK", false);
+            fileSave(config, "PSASTRO.OUT.REFSTARS", false);
+            psErrorClear();
+            return true;
+        }
+    }
+    if (mosastro) {
+        if (!psastroMosaicAstrom (config)) {
+            // This is likely a data quality issue
+            psWarning("Failed mosaic astrometry --- suspect bad data quality");
+            if (stats && psMetadataLookupS32(NULL, stats, "QUALITY") == 0) {
+                psMetadataAddS32(stats, PS_LIST_TAIL, "QUALITY", PS_META_REPLACE,
+                                 "Mosaic astrometry failed", PSASTRO_ERR_DATA);
+            }
+            fileSave(config, "PSASTRO.OUTPUT", false);
+            fileSave(config, "PSASTRO.OUTPUT.MASK", false);
+            fileSave(config, "PSASTRO.OUT.REFSTARS", false);
+            psErrorClear();
+            return true;
+        }
+    }
+
+    if (!skipastro) {
+        if (!psastroZeroPoint (config)) {
+            psError(psErrorCodeLast(), false, "Failed to calculate zero point.");
+            return false;
+        }
+
+        if (!psastroAstromGuessCheck (config)) {
+            psError(psErrorCodeLast(), false, "Failed to check astrometry guess.");
+            return false;
+        }
+    }
+
+    if (!psastroMaskUpdates (config, stats)) {
+        psError(psErrorCodeLast(), false, "Failed to generate dynamic masks.");
+        return false;
+    }
+
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroArguments.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroArguments.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroArguments.c	(revision 41084)
@@ -0,0 +1,149 @@
+/** @file psastroArguments.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.34.2.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-19 17:59:50 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+
+pmConfig *psastroArguments (int argc, char **argv) {
+
+    bool status;
+    int N;
+
+    if (argc == 1) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "No arguments supplied");
+        psErrorStackPrint(stderr, "exit");
+        return NULL;
+    }
+
+    // load config data from default locations
+    pmConfig *config = pmConfigRead(&argc, argv, PSASTRO_RECIPE);
+    if (config == NULL) {
+        psError(PSASTRO_ERR_CONFIG, false, "Can't read site configuration");
+        psErrorStackPrint(stderr, "exit");
+        return NULL;
+    }
+
+    // save the following additional recipe values based on command-line options
+    // these options override the PSASTRO recipe values loaded from recipe files
+    psMetadata *options = pmConfigRecipeOptions (config, PSASTRO_RECIPE);
+
+    // photcode : used in output to supplement header data (argument or recipe?)
+    if ((N = psArgumentGet (argc, argv, "-photcode"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (options, PS_LIST_TAIL, "PHOTCODE", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    // chip selection is used to limit chips to be processed
+    if ((N = psArgumentGet (argc, argv, "-chip"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (config->arguments, PS_LIST_TAIL, "CHIP_SELECTIONS", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    // apply the chip correction based on the reference astrometry?
+    if ((N = psArgumentGet (argc, argv, "-fixchips"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.FIX.CHIPS", PS_META_REPLACE, "", true);
+    }
+    // no valid header WCS: supply from astrom model
+    if ((N = psArgumentGet (argc, argv, "-use-astrommodel"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.USE.MODEL", PS_META_REPLACE, "", true);
+    }
+    // define the reference astrometry file (add a container ASTROM.MODEL to config->arguments if -astrommodel (file) is found)
+    status = pmConfigFileSetsMD (config->arguments, &argc, argv, "ASTROM.MODEL", "-astrommodel", "-astrommodellist");
+
+    // define the koppenhoefer correction file (add a container KH.CORRECT to config->arguments if -kh-correct (file) is found)
+    status = pmConfigFileSetsMD (config->arguments, &argc, argv, "KH.CORRECT", "-kh-correct", NULL);
+
+    // define the reference astrometry file
+    status = pmConfigFileSetsMD (config->arguments, &argc, argv, "INPUT.MASK", "-mask", "-masklist");
+    status = pmConfigFileSetsMD (config->arguments, &argc, argv, "REFMASK", "-refmask", "-refmasklist");
+
+    if ((N = psArgumentGet(argc, argv, "-stats"))) {
+        psArgumentRemove(N, &argc, argv);
+        psMetadataAddStr(config->arguments, PS_LIST_TAIL, "STATS", PS_META_REPLACE, "Filename for summary statistics", argv[N]);
+        psArgumentRemove(N, &argc, argv);
+    }
+
+    // apply mosastro mode?
+    bool mosastro = false;
+    if ((N = psArgumentGet (argc, argv, "-mosastro"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.MOSAIC.MODE", PS_META_REPLACE, "", false);
+    }
+    if ((N = psArgumentGet (argc, argv, "+mosastro"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.MOSAIC.MODE", PS_META_REPLACE, "", true);
+        mosastro = true;
+    }
+
+    // apply chipastro mode?
+    bool chipastro = false;
+    if ((N = psArgumentGet (argc, argv, "-chipastro"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.CHIP.MODE", PS_META_REPLACE, "", false);
+    }
+    if ((N = psArgumentGet (argc, argv, "+chipastro"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.CHIP.MODE", PS_META_REPLACE, "", true);
+        chipastro = true;
+    }
+    if ((N = psArgumentGet (argc, argv, "-skipastro"))) {
+        psArgumentRemove (N, &argc, argv);
+        if (mosastro) {
+            psError(PSASTRO_ERR_ARGUMENTS, true, "cannot specify +mosastro with -skipastro");
+            psErrorStackPrint(stderr, "exit");
+            return false;
+        }
+        if (chipastro) {
+            psError(PSASTRO_ERR_ARGUMENTS, true, "cannot specify +chipastro with -skipastro");
+            return false;
+        }
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.SKIP.ASTRO", PS_META_REPLACE, "", true);
+
+	// SKIP.ASTRO and USE.MODEL are fundamentally incompatible
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.USE.MODEL", PS_META_REPLACE, "", false);
+    }
+
+    // run in visual mode?
+    if ((N = psArgumentGet (argc, argv, "-visual"))) {
+        psArgumentRemove (N, &argc, argv);
+        pmVisualSetVisual (true);
+    }
+
+    // dump the configuration to a file?
+    if ((N = psArgumentGet (argc, argv, "-dumpconfig"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (config->arguments, PS_LIST_TAIL, "DUMP_CONFIG", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    status = pmConfigFileSetsMD (config->arguments, &argc, argv, "INPUT", "-file", "-list");
+    if (!status) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "Missing -file (input) or -list (input)");
+        psErrorStackPrint(stderr, "exit");
+        return NULL;
+    }
+
+    if (argc != 2) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "Incorrect arguments supplied");
+        psErrorStackPrint(stderr, "exit");
+        return NULL;
+    }
+
+    // output positions is fixed
+    psMetadataAddStr (config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "", argv[1]);
+
+    psTrace("psastro", 1, "Done with psastroArguments...\n");
+    return (config);
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroAstromGuess.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroAstromGuess.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroAstromGuess.c	(revision 41084)
@@ -0,0 +1,484 @@
+/** @file psastroAstromGuess.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.35 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-09 21:25:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+# define DEBUG 0
+
+/**
+ * \brief This function loads the header WCS astrometry terms into the fpa 
+ * terms and applies the astrometry to the detected objects.
+ * 
+ * This function assumes the initial astrometry arrives in the form of WCS 
+ * keywords in the headers corresponding to the chips.
+ */
+bool psastroAstromGuess (int *nStars, pmConfig *config) {
+
+    bool newFPA = true;
+    bool status = false;
+    double RAmin  = +FLT_MAX;
+    double RAmax  = -FLT_MAX;
+    double DECmin = +FLT_MAX;
+    double DECmax = -FLT_MAX;
+
+    double RAminSky = NAN;
+    double RAmaxSky = NAN;
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    *nStars = 0;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!");
+        return false;
+    }
+
+    // have we already supplied the astrometry from the model?
+    bool useModel = psMetadataLookupBool (&status, config->arguments, "PSASTRO.USE.MODEL");
+    if (!status) {
+        useModel = psMetadataLookupBool (&status, recipe, "PSASTRO.USE.MODEL");
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+        return false;
+    }
+
+    // physical pixel scale in microns per pixel
+    double pixelScale = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.SCALE");
+    if (!status) {
+        psError(PS_ERR_IO, true, "Failed to lookup pixel scale");
+        return false;
+    }
+
+    psVector *cornerL = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerM = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerP = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerQ = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerR = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerD = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+    pmFPA *fpa = input->fpa;
+
+    // this call only works if we have loaded a model : seg fault if not
+    if (DEBUG && useModel) psastroDumpCorners ("corners.up.guess1.dat", "corners.dn.guess1.dat", fpa);
+
+    // load mosaic-level astrometry?
+    bool bilevelAstrometry = false;
+    if (!useModel) {
+        psastroAstromGuessSetFPA (fpa, &bilevelAstrometry);
+    }
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists || !chip->data_exists) { continue; }
+
+        if (!useModel) {
+            if (!psastroAstromGuessSetChip (fpa, chip, view, pixelScale, bilevelAstrometry)) continue;
+        }
+
+	if (!chip->toFPA || !chip->fromFPA) {
+	  char *name = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+	  fprintf (stderr, "no astrom model for %s, skipping\n", name);
+	  continue;
+	}
+
+        if (newFPA) {
+            newFPA = false;
+            while (fpa->toSky->R <        0) fpa->toSky->R += 2.0*M_PI;
+            while (fpa->toSky->R > 2.0*M_PI) fpa->toSky->R -= 2.0*M_PI;
+            RAminSky = fpa->toSky->R - M_PI;
+            RAmaxSky = fpa->toSky->R + M_PI;
+        }
+
+        // report and save the current best guess for the chip 0,0 pixel coordinates
+        {
+            psPlane ptCH, ptFP, ptTP;
+            psSphere ptSky;
+
+            ptCH.x = 0;
+            ptCH.y = 0;
+            psPlaneTransformApply (&ptFP, chip->toFPA, &ptCH);
+            psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+            psDeproject (&ptSky, &ptTP, fpa->toSky);
+            psLogMsg ("psastro", 3, "0,0 pix for chip %3d = %f,%f\n", view->chip, DEG_RAD*ptSky.r, DEG_RAD*ptSky.d);
+
+            psVectorAppend (cornerL, ptFP.x);
+            psVectorAppend (cornerM, ptFP.y);
+            psVectorAppend (cornerP, ptTP.x);
+            psVectorAppend (cornerQ, ptTP.y);
+            psVectorAppend (cornerR, ptSky.r);
+            psVectorAppend (cornerD, ptSky.d);
+        }
+
+        // apply the new WCS guess data to all of the data in the readouts
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+                if (rawstars == NULL) { continue; }
+
+                *nStars += rawstars->n;
+                for (int i = 0; i < rawstars->n; i++) {
+                    pmAstromObj *raw = rawstars->data[i];
+
+                    psPlaneTransformApply (raw->FP, chip->toFPA, raw->chip);
+                    psPlaneTransformApply (raw->TP, fpa->toTPA, raw->FP);
+                    psDeproject (raw->sky, raw->TP, fpa->toSky);
+
+                    // rationalize ra to sky range centered on boresite
+                    while (raw->sky->r < RAminSky) raw->sky->r += 2.0*M_PI;
+                    while (raw->sky->r > RAmaxSky) raw->sky->r -= 2.0*M_PI;
+
+                    RAmin = PS_MIN (raw->sky->r, RAmin);
+                    RAmax = PS_MAX (raw->sky->r, RAmax);
+
+                    DECmin = PS_MIN (raw->sky->d, DECmin);
+                    DECmax = PS_MAX (raw->sky->d, DECmax);
+                }
+
+                // dump or plot the resulting projected positions
+                if (psTraceGetLevel("psastro.dump") > 0) {
+                    psastroDumpRawstars (rawstars, fpa, chip);
+                }
+
+                pmAstromVisualPlotRawStars(rawstars, fpa, chip, recipe);
+
+                if (psTraceGetLevel("psastro.plot") > 0) {
+                    psastroPlotRawstars (rawstars, fpa, chip, recipe);
+                }
+
+                // Next if we are using a different set of stars for grid search fill out their pmAstromObjs
+                psArray *grid_rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.GRID.RAWSTARS");
+                if (grid_rawstars == NULL || grid_rawstars == rawstars) { continue; }
+
+                for (int i = 0; i < grid_rawstars->n; i++) {
+                    pmAstromObj *raw = grid_rawstars->data[i];
+
+                    psPlaneTransformApply (raw->FP, chip->toFPA, raw->chip);
+                    psPlaneTransformApply (raw->TP, fpa->toTPA, raw->FP);
+                    psDeproject (raw->sky, raw->TP, fpa->toSky);
+
+                    // rationalize ra to sky range centered on boresite
+                    while (raw->sky->r < RAminSky) raw->sky->r += 2.0*M_PI;
+                    while (raw->sky->r > RAmaxSky) raw->sky->r -= 2.0*M_PI;
+
+                    RAmin = PS_MIN (raw->sky->r, RAmin);
+                    RAmax = PS_MAX (raw->sky->r, RAmax);
+
+                    DECmin = PS_MIN (raw->sky->d, DECmin);
+                    DECmax = PS_MAX (raw->sky->d, DECmax);
+                }
+                // XXX: should we plot grid_rawstars?
+            }
+        }
+    }
+
+    if (DEBUG) psastroDumpCorners ("corners.up.guess2.dat", "corners.dn.guess2.dat", fpa);
+
+    // how many total sources are available to us?
+    psMetadataAddS32 (recipe, PS_LIST_TAIL, "NTOTSTAR",  PS_META_REPLACE, "", *nStars);
+    if (*nStars == 0) {
+        psLogMsg ("psastro", 2, "no sources available for astrometry\n");
+        psFree (view);
+        return true;
+    }
+
+    psLogMsg ("psastro", 2, "loaded raw data from %f,%f to %f,%f\n",
+              DEG_RAD*RAmin, DEG_RAD*DECmin,
+              DEG_RAD*RAmax, DEG_RAD*DECmax);
+
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "RA_MIN",  PS_META_REPLACE, "", RAmin);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "RA_MAX",  PS_META_REPLACE, "", RAmax);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "DEC_MIN", PS_META_REPLACE, "", DECmin);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "DEC_MAX", PS_META_REPLACE, "", DECmax);
+
+    psMetadataAddVector (input->fpa->analysis, PS_LIST_TAIL, "CORNER.L", PS_META_REPLACE, "corner pixel", cornerL);
+    psMetadataAddVector (input->fpa->analysis, PS_LIST_TAIL, "CORNER.M", PS_META_REPLACE, "corner pixel", cornerM);
+    psMetadataAddVector (input->fpa->analysis, PS_LIST_TAIL, "CORNER.P", PS_META_REPLACE, "corner pixel", cornerP);
+    psMetadataAddVector (input->fpa->analysis, PS_LIST_TAIL, "CORNER.Q", PS_META_REPLACE, "corner pixel", cornerQ);
+    psMetadataAddVector (input->fpa->analysis, PS_LIST_TAIL, "CORNER.R", PS_META_REPLACE, "corner pixel", cornerR);
+    psMetadataAddVector (input->fpa->analysis, PS_LIST_TAIL, "CORNER.D", PS_META_REPLACE, "corner pixel", cornerD);
+
+    psFree (cornerL);
+    psFree (cornerM);
+    psFree (cornerP);
+    psFree (cornerQ);
+    psFree (cornerR);
+    psFree (cornerD);
+
+    psFree (view);
+    return true;
+}
+
+/* coordinate frame hierachy
+   pixels (on a given readout)
+   cell
+   chip
+   FP (focal plane)
+   TP (tangent plane)
+   sky (ra, dec)
+*/
+
+bool psastroAstromGuessSetChip (pmFPA *fpa, pmChip *chip, const pmFPAview *view, double pixelScale, bool bilevelAstrometry) {
+
+    // read WCS data from the corresponding header
+    pmHDU *hdu = pmFPAviewThisHDU (view, fpa);
+    if (bilevelAstrometry) {
+        if (!pmAstromReadBilevelChip (chip, hdu->header)) {
+            psWarning("Could not get WCS information from header for chip %d, skipping", view->chip);
+            return false;
+        }
+    } else {
+        if (!pmAstromReadWCS (fpa, chip, hdu->header, pixelScale)) {
+            psWarning("Could not get WCS information from header for chip %d, skipping", view->chip);
+            return false;
+        }
+    }
+    return true;
+}
+
+bool psastroAstromGuessSetFPA (pmFPA *fpa, bool *bilevelAstrometry) {
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmHDU *phu = pmFPAviewThisPHU (view, fpa);
+
+    *bilevelAstrometry = false;
+
+    // load mosaic-level astrometry?
+    if (phu) {
+        char *ctype = psMetadataLookupStr (NULL, phu->header, "CTYPE1");
+        if (ctype) {
+            *bilevelAstrometry = !strcmp (&ctype[4], "-DIS");
+        }
+    }
+    if (*bilevelAstrometry) {
+        pmAstromReadBilevelMosaic (fpa, phu->header);
+    }
+    psFree (view);
+    return true;
+}
+
+// we made a guess at the beginning; how does the guess compare with the result?
+bool psastroAstromGuessCheck (pmConfig *config) {
+
+    bool status;
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+        return false;
+    }
+
+    pmFPA *fpa = input->fpa;
+
+    psVector *cornerLo = psMetadataLookupPtr (&status, input->fpa->analysis, "CORNER.L");
+    psVector *cornerMo = psMetadataLookupPtr (&status, input->fpa->analysis, "CORNER.M");
+    psVector *cornerPo = psMetadataLookupPtr (&status, input->fpa->analysis, "CORNER.P");
+    psVector *cornerQo = psMetadataLookupPtr (&status, input->fpa->analysis, "CORNER.Q");
+    psVector *cornerRo = psMetadataLookupPtr (&status, input->fpa->analysis, "CORNER.R");
+    psVector *cornerDo = psMetadataLookupPtr (&status, input->fpa->analysis, "CORNER.D");
+
+    if (cornerLo->n < 3) return true;
+
+    psVector *cornerLn = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerMn = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerPn = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerQn = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerRn = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerDn = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+    psVector *cornerMK = psVectorAllocEmpty (100, PS_TYPE_VECTOR_MASK);
+
+    if (DEBUG) psastroDumpCorners ("corners.up.guess3.dat", "corners.dn.guess3.dat", fpa);
+
+    pmChip *chip = NULL;
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        if (!chip->process || !chip->file_exists || !chip->data_exists) { continue; }
+
+	if (!chip->toFPA || !chip->fromFPA) {
+	  char *name = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+	  fprintf (stderr, "no astrom model for %s, skipping\n", name);
+	  continue;
+	}
+
+        // XXX we are currently inconsistent with marking the good vs the bad data
+        // psastroChipAstrom sets data_exists to false if the fit is bad.  this is
+        // probably wrong since it implies there is no data!
+
+        // skip chips for which the astrometry failed (NASTRO == 0)
+        if (!chip->cells->n) goto skip_chip;
+        pmCell *cell = chip->cells->data[0];
+        if (!cell) goto skip_chip;
+
+        if (!cell->readouts->n) goto skip_chip;
+        pmReadout *readout = cell->readouts->data[0];
+        if (!readout) goto skip_chip;
+
+        psMetadata *updates = psMetadataLookupMetadata (&status, readout->analysis, "PSASTRO.HEADER");
+        if (!updates) goto skip_chip;
+
+        int nAstro = psMetadataLookupS32 (&status, updates, "NASTRO");
+        if (!nAstro) goto skip_chip;
+
+        float astError = psMetadataLookupF32 (&status, updates, "CERROR");
+        if (fabs(astError) < 1e-6) goto skip_chip;
+
+        psPlane ptCH, ptFP, ptTP;
+        psSphere ptSky;
+
+        ptCH.x = 0;
+        ptCH.y = 0;
+        psPlaneTransformApply (&ptFP, chip->toFPA, &ptCH);
+        psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+        psDeproject (&ptSky, &ptTP, fpa->toSky);
+        psLogMsg ("psastro", 3, "0,0 pix for chip %3d = %f,%f\n", view->chip, DEG_RAD*ptSky.r, DEG_RAD*ptSky.d);
+
+        // new corner locations based on the calibrated astrometry
+        psVectorAppend (cornerLn, ptFP.x);
+        psVectorAppend (cornerMn, ptFP.y);
+        psVectorAppend (cornerPn, ptTP.x);
+        psVectorAppend (cornerQn, ptTP.y);
+        psVectorAppend (cornerRn, ptSky.r);
+        psVectorAppend (cornerDn, ptSky.d);
+        psVectorAppend (cornerMK, 0);
+        continue;
+
+    skip_chip:
+        // new corner locations based on the calibrated astrometry
+        psVectorAppend (cornerLn, 0.0);
+        psVectorAppend (cornerMn, 0.0);
+        psVectorAppend (cornerPn, 0.0);
+        psVectorAppend (cornerQn, 0.0);
+        psVectorAppend (cornerRn, 0.0);
+        psVectorAppend (cornerDn, 0.0);
+        psVectorAppend (cornerMK, 1);
+    }
+
+    // compare the old R,D values projected to the same tangent plane as the new R,D values:
+
+    psVector *cornerPs = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerQs = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+    for (int i = 0; i < cornerRo->n; i++) {
+
+        psPlane ptTP;
+        psSphere ptSky;
+
+        ptSky.r = cornerRo->data.F32[i];
+        ptSky.d = cornerDo->data.F32[i];
+
+        psProject (&ptTP, &ptSky, fpa->toSky);
+        psVectorAppend (cornerPs, ptTP.x);
+        psVectorAppend (cornerQs, ptTP.y);
+    }
+
+    psPlaneTransform *map = psPlaneTransformAlloc (1, 1);
+    map->x->coeffMask[1][1] = PS_POLY_MASK_SET;
+    map->y->coeffMask[1][1] = PS_POLY_MASK_SET;
+
+    // fit the valid chips, mask the invalid chips
+    psVectorFitPolynomial2D (map->x, cornerMK, 1, cornerPn, NULL, cornerPs, cornerQs);
+    psVectorFitPolynomial2D (map->y, cornerMK, 1, cornerQn, NULL, cornerPs, cornerQs);
+
+    // apply the linear fit...
+    psVector *cornerPf = psPolynomial2DEvalVector (map->x, cornerPs, cornerQs);
+    psVector *cornerQf = psPolynomial2DEvalVector (map->y, cornerPs, cornerQs);
+
+    // ...and calculate the residual between Pn,Qn and Pf,Qf
+    psVector *cornerPd = (psVector *) psBinaryOp (NULL, cornerPn, "-", cornerPf);
+    psVector *cornerQd = (psVector *) psBinaryOp (NULL, cornerQn, "-", cornerQf);
+
+    pmAstromVisualPlotAstromGuessCheck (cornerPo, cornerQo, cornerPn, cornerQn, cornerPd, cornerQd);
+
+    psStats *statsP = psStatsAlloc (PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+    psStats *statsQ = psStatsAlloc (PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+
+    if (!psVectorStats (statsP, cornerPd, NULL, cornerMK, 1)) {
+	psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	return false;
+    }
+    if (!psVectorStats (statsQ, cornerQd, NULL, cornerMK, 1)) {
+	psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	return false;
+    }
+
+    float angle = atan2 (map->y->coeff[1][0], map->x->coeff[1][0]);
+    float scale = hypot (map->y->coeff[1][0], map->x->coeff[1][0]);
+
+    psLogMsg ("psastro", 3, "boresite offset  : %f,%f\n", map->x->coeff[0][0], map->y->coeff[0][0]);
+    psLogMsg ("psastro", 3, "boresite angle   : %f, scale: %f", angle*PS_DEG_RAD, scale);
+    psLogMsg ("psastro", 3, "boresite scatter : %f,%f\n", statsP->sampleStdev, statsQ->sampleStdev);
+
+    // write the elapsed time here; this will be updated in psastroMosaicAstrometry, if called
+    psMetadata *header = psMetadataLookupMetadata (&status, input->fpa->analysis, "PSASTRO.HEADER");
+    if (!header) {
+        header = psMetadataAlloc();
+        psMetadataAddMetadata (input->fpa->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", header);
+        psFree (header);  // drop this reference
+    }
+
+    psMetadataAddF32 (header, PS_LIST_TAIL, "AST_R0", PS_META_REPLACE, "boresite offset in RA (TP units)", map->x->coeff[0][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "AST_D0", PS_META_REPLACE, "boresite offset in DEC (TP units)", map->y->coeff[0][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "AST_T0", PS_META_REPLACE, "boresite angle (degrees)", angle*PS_DEG_RAD);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "AST_S0", PS_META_REPLACE, "boresite scale correction", scale);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "AST_RS", PS_META_REPLACE, "boresite scatter in RA (TP units)", statsP->sampleStdev);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "AST_DS", PS_META_REPLACE, "boresite scatter in DEC (TP units)", statsQ->sampleStdev);
+
+    if (DEBUG) {
+        FILE *f = fopen ("corners.dat", "w");
+        for (int i = 0; i < cornerRo->n; i++) {
+            fprintf (f, "%10.6f %10.6f  %9.2f %9.2f  %9.2f %9.2f  |  %10.6f %10.6f  %9.2f %9.2f  %9.2f %9.2f\n",
+                     cornerRn->data.F32[i], cornerDn->data.F32[i], cornerPn->data.F32[i], cornerQn->data.F32[i], cornerLn->data.F32[i], cornerMn->data.F32[i],
+                     cornerRo->data.F32[i], cornerDo->data.F32[i], cornerPo->data.F32[i], cornerQo->data.F32[i], cornerLo->data.F32[i], cornerMo->data.F32[i]);
+        }
+        fclose (f);
+    }
+
+    psFree (cornerPf);
+    psFree (cornerQf);
+    psFree (cornerPd);
+    psFree (cornerQd);
+
+    psFree (statsP);
+    psFree (statsQ);
+
+    psFree (cornerMK);
+
+    psFree (cornerLn);
+    psFree (cornerMn);
+    psFree (cornerPn);
+    psFree (cornerQn);
+    psFree (cornerRn);
+    psFree (cornerDn);
+    psFree (cornerPs);
+    psFree (cornerQs);
+    psFree (map);
+    psFree (view);
+
+
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroChipAstrom.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroChipAstrom.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroChipAstrom.c	(revision 41084)
@@ -0,0 +1,207 @@
+/** @file psastroChipAstrom.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.29 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+# define NONLIN_TOL 0.001 ///< tolerance in pixels
+
+bool psastroChipAstrom (pmConfig *config) {
+
+    bool status;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+        return false;
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (&status, config->files, "PSASTRO.INPUT");
+    if (!input) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+        return false;
+    }
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmFPA *fpa = input->fpa;
+
+    int numGoodChips = 0;               // Number of chips for which astrometry succeeds
+
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+
+        int numGoodCells = 0;           // Number of cells for which astrometry succeeds
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+            if (!chip->fromFPA) { continue; }
+
+            // process each of the readouts
+            int numGoodRO = 0;          // Number of readouts for which astrometry succeeds
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                // select the raw objects for this readout
+                psArray *rawstars = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.RAWSTARS.SUBSET");
+                if (rawstars == NULL) { continue; }
+
+                // select the raw objects for this readout
+                psArray *gridrawstars = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.GRID.RAWSTARS.SUBSET");
+                if (gridrawstars == NULL) {
+                    gridrawstars = rawstars;
+                } else {
+                    // the absolute minimum number of stars is 4 (for order = 1)
+                    if (gridrawstars->n < 4) {
+                        readout->data_exists = false;
+                        psLogMsg ("psastro", 3, "insufficient gird rawstars (%ld)", gridrawstars->n);
+                        continue;
+                    }
+                }
+
+                psArray *refstars = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.REFSTARS.SUBSET");
+                if (refstars == NULL) { continue; }
+
+# if (0)
+		static int Nchip = 0;
+		if (1) {
+		  // XXX test
+		  char filename[64];
+		  snprintf (filename, 64, "refstars.%02d.dat", Nchip);
+		  FILE *outfile = fopen (filename, "w");
+		  assert (outfile);
+		  for (int nn = 0; nn < refstars->n; nn++) {
+		    pmAstromObj *ref = refstars->data[nn];
+		    fprintf (outfile, "%lf %lf  %lf %lf  %lf %lf  %lf %lf : %f %f\n", 
+			     ref->sky->r*PS_DEG_RAD, ref->sky->d*PS_DEG_RAD,
+			     ref->TP->x, ref->TP->y, 
+			     ref->FP->x, ref->FP->y, 
+			     ref->chip->x, ref->chip->y, ref->Mag, ref->magCal);
+		  }
+		  fclose (outfile);
+		}
+		if (1) {
+		  // XXX test
+		  char filename[64];
+		  snprintf (filename, 64, "rawstars.%02d.dat", Nchip);
+		  FILE *outfile = fopen (filename, "w");
+		  assert (outfile);
+		  for (int nn = 0; nn < gridrawstars->n; nn++) {
+		    pmAstromObj *ref = gridrawstars->data[nn];
+		    fprintf (outfile, "%lf %lf  %lf %lf  %lf %lf  %lf %lf : %f %f\n", 
+			     ref->sky->r*PS_DEG_RAD, ref->sky->d*PS_DEG_RAD,
+			     ref->TP->x, ref->TP->y, 
+			     ref->FP->x, ref->FP->y, 
+			     ref->chip->x, ref->chip->y, ref->Mag, ref->magCal);
+		  }
+		  fclose (outfile);
+		  Nchip ++;
+		}
+# endif
+
+                // the absolute minimum number of stars is 4 (for order = 1)
+                if ((rawstars->n < 4) || (refstars->n < 4)) {
+                    readout->data_exists = false;
+                    psLogMsg ("psastro", 3, "insufficient rawstars (%ld) or refstars (%ld)", rawstars->n, refstars->n);
+                    continue;
+                }
+
+                // save WCS and analysis metadata in update header
+		// (pull or create local view to entry on readout->analysis)
+		psMetadata *updates = psMetadataLookupMetadata (&status, readout->analysis, "PSASTRO.HEADER");
+		if (!updates) {
+		    updates = psMetadataAlloc ();
+		    psMetadataAddMetadata (readout->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", updates);
+		    psFree (updates);
+		}
+
+                // XXX update the header with info to reflect the failure
+                if (!psastroOneChipGrid (fpa, chip, refstars, gridrawstars, recipe, updates)) {
+                    readout->data_exists = false;
+                    psLogMsg ("psastro", 3, "failed to find a solution\n");
+                    continue;
+                }
+                // XXX update the header with info to reflect the failure
+                if (!psastroOneChipFit (fpa, chip, readout, refstars, rawstars, recipe, updates)) {
+                    readout->data_exists = false;
+                    psLogMsg ("psastro", 3, "failed to find a solution\n");
+                    continue;
+                }
+
+                numGoodRO++;
+
+                psU64 astrom_chip_val = 1;
+                astrom_chip_val <<= view->chip;
+                psMetadataAddU64 (updates, PS_LIST_TAIL, "ASTROM_CHIPS", PS_META_REPLACE, 
+                    "chips that passed astrometry", astrom_chip_val);
+
+                // write the elapsed time here; this will be updated in psastroMosaicAstrometry, if called
+                psMetadataAddF32 (updates, PS_LIST_TAIL, "DT_ASTR", PS_META_REPLACE, "elapsed psastro time", psTimerMark ("psastroAnalysis"));
+		
+		fpa->wcsCDkeys = psMetadataLookupBool(&status, recipe , "PSASTRO.WCS.USECDKEYS");
+                pmAstromWriteWCS (updates, fpa, chip, NONLIN_TOL);
+
+                if (psTraceGetLevel("psastro.dump") > 0) {
+
+                    char *filename = NULL;
+                    char *chipname = psMetadataLookupStr (&status, chip->concepts, "CHIP.NAME");
+
+                    psStringAppend (&filename, "rawstars.ch.%s.dat", chipname);
+                    psastroDumpStars (rawstars, filename);
+                    psFree (filename);
+                    filename = NULL;
+
+                    psStringAppend (&filename, "refstars.ch.%s.dat", chipname);
+                    psastroDumpStars (refstars, filename);
+                    psFree (filename);
+                    filename = NULL;
+                }
+            }
+            if (numGoodRO > 0) {
+                numGoodCells++;
+            }
+        }
+        if (numGoodCells > 0) {
+            numGoodChips++;
+        }
+    }
+
+    if (fpa->chips->n == 1 && numGoodChips == 0) {
+        psError(PSASTRO_ERR_UNKNOWN, false, "Failed to fit single chip.");
+        return false;
+    }
+
+    if (numGoodChips == 0) {
+        psError(PSASTRO_ERR_UNKNOWN, false, "Failed to fit any chips");
+        return false;
+    }
+
+    if (!psastroFixChips (config, recipe)) {
+        psError(PSASTRO_ERR_UNKNOWN, false, "failed to align problematic chips");
+        return false;
+    }
+
+    psFree (view);
+    return true;
+}
+
+/* coordinate frame hierachy
+   pixels (on a given readout)
+   cell
+   chip
+   FP (focal plane)
+   TP (tangent plane)
+   sky (ra, dec)
+*/
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroChooseGlintStars.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroChooseGlintStars.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroChooseGlintStars.c	(revision 41084)
@@ -0,0 +1,73 @@
+/** @file psastroChooseGlintStars.c
+ *
+ *  @brief: Select stars by magnitude which are likely glint sources
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.21 $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+bool psastroChooseGlintStars (pmConfig *config, psArray *refs, const char *source) {
+
+    bool status;
+    float zeropt, exptime;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+        return false;
+    }
+
+    bool REFSTAR_MASK_GLINTS = psMetadataLookupBool (&status, recipe, "REFSTAR_MASK_GLINTS");
+    if (!REFSTAR_MASK_GLINTS) return true;
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, source);
+    if (!input) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data!\n");
+        return false;
+    }
+    pmFPA *fpa = input->fpa;
+
+    // really error-out here?  or just skip?
+    if (!psastroZeroPointFromRecipe (&zeropt, &exptime, NULL, fpa, recipe)) {
+        psLogMsg ("psastro", PS_LOG_INFO, "failed to load zeropt data from recipe");
+        return false;
+    }
+
+    // select the limiting magnitude
+    double GLINT_MAX_MAG = psMetadataLookupF32 (&status, recipe, "GLINT_MAX_MAG");
+
+    // recipe values are given in instrumental magnitudes
+    // use the zero point and exposure time to convert to apparent mags: M_ap = M_inst + C_0 + 2.5*log(exptime)
+    float MagOffset = zeropt + 2.5*log10(exptime);
+    GLINT_MAX_MAG += MagOffset;
+
+    // There is no selection based on the location of the star. We need the full astrometry
+    // solution before we can trust the star positions.
+
+    // the refstars is a subset within range of this chip
+    psArray *glintStars = psArrayAllocEmpty (100);
+
+    // select the reference objects brighter than the cutoff magnitude within range of the FPA
+    for (int i = 0; i < refs->n; i++) {
+	pmAstromObj *ref = refs->data[i];
+	if (ref->Mag > GLINT_MAX_MAG) continue;
+
+	pmAstromObj *glint = pmAstromObjCopy(ref);
+	psArrayAdd (glintStars, 100, glint);
+	psFree (glint);
+    }
+    psTrace ("psastro", 4, "Added %ld glint stars\n", glintStars->n);
+
+    psMetadataAdd (fpa->analysis, PS_LIST_TAIL, "PSASTRO.GLINT.STARS", PS_DATA_ARRAY, "possible glint stars", glintStars);
+    psFree (glintStars);
+
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroChooseRefstars.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroChooseRefstars.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroChooseRefstars.c	(revision 41084)
@@ -0,0 +1,213 @@
+/** @file psastroChooseRefstars.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.21 $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "Failure in psastroChooseRefstars"); \
+  psFree (index); \
+  psFree (view); \
+  return false; \
+}
+
+bool psastroChooseRefstars (pmConfig *config, psArray *refs, const char *source, bool saveExistingMatchedRefs) {
+
+    bool status;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+        return false;
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, source);
+    if (!input) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data!\n");
+        return false;
+    }
+
+    // extra field fraction to add
+    double fieldPadding = psMetadataLookupF32 (&status, recipe, "PSASTRO.FIELD.PADDING");
+    if (!status) fieldPadding = 0.0;
+
+    bool matchLumFunc = psMetadataLookupBool (&status, recipe, "PSASTRO.MATCH.LUMFUNC");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmFPA *fpa = input->fpa;
+
+    // XXX kind of a hack -- think this through a bit more clearly:
+    if (psMetadataLookupPtr (&status, fpa->analysis, "MATCHED_REFS")) {
+	// we loaded a set of matched references from an earlier astrometry
+	// analysis.  however, we are re-doing the astrometry here, so remove
+	// that prior set of matched references
+        if (!saveExistingMatchedRefs) {
+            psMetadataRemoveKey (fpa->analysis, "MATCHED_REFS");
+        }
+    }
+
+    // sort by mag
+    psVector *index = psArraySortIndex (NULL, refs, psastroSortByMag);
+
+    int nMax = psMetadataLookupS32 (&status, recipe, "PSASTRO.MAX.NREF");
+    psF32 clampMagMin = psMetadataLookupF32 (&status, recipe, "REFSTAR_CLAMP_MAG_MIN");
+    if (!status) clampMagMin = -5.0;
+    //MEH adding max option as well -- really should not be hardcoded..
+    psF32 clampMagMax = psMetadataLookupF32 (&status, recipe, "REFSTAR_CLAMP_MAG_MAX");
+    if (!status) clampMagMax = 30.0;
+    psWarning("Will skip/clamp refstar with brighter/fainter magnitude %f/%f\n",clampMagMin,clampMagMax); 
+
+
+    // de-activate all files except PSASTRO.REFSTARS
+    pmFPAfileActivate (config->files, false, NULL);
+    pmFPAfileActivate (config->files, true, "PSASTRO.OUT.REFSTARS");
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!chip->fromFPA) { continue; }
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            // XXX there can only be one readout per chip in astrometry, right?
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+		psRegion *extent = pmReadoutExtent (readout);
+		if (!extent) {
+		    psError(PSASTRO_ERR_CONFIG, true, "Can't find readout size!\n");
+		    return NULL;
+		}
+
+		int Nx = abs(extent->x1 - extent->x0);
+		int Ny = abs(extent->y1 - extent->y0);
+
+                float minX = -fieldPadding*Nx;
+                float maxX = (1+fieldPadding)*Nx;
+                float minY = -fieldPadding*Ny;
+                float maxY = (1+fieldPadding)*Ny;
+
+                // the refstars is a subset within range of this chip
+                psArray *refstars = psArrayAllocEmpty (100);
+
+                // select the reference objects within range of this readout
+                // project the reference objects to this chip
+                for (int i = 0; i < refs->n; i++) {
+                    pmAstromObj *ref = pmAstromObjCopy(refs->data[index->data.S32[i]]);
+
+                    if (ref->Mag < clampMagMin) {
+                        //psWarning("Skipping refstar with abusrd magnitude %f ra: %f dec: %f\n",
+                        //    ref->Mag, RAD_TO_DEG(ref->sky->r), RAD_TO_DEG(ref->sky->d));
+                        psTrace ("psastro", 6, "Skipping by clamp refstar with bright magnitude %f ra: %f dec: %f\n",
+			         ref->Mag, RAD_TO_DEG(ref->sky->r), RAD_TO_DEG(ref->sky->d));
+                        goto skip;
+                    }
+		    //MEH option for max mag clamp also -- need to change psWarning (to log) to trace and above for bright..
+                    if (ref->Mag > clampMagMax) {
+                        //psWarning("Skipping by clamp refstar with faint magnitude %f ra: %f dec: %f\n",
+                        //    ref->Mag, RAD_TO_DEG(ref->sky->r), RAD_TO_DEG(ref->sky->d));
+			psTrace ("psastro", 6, "Skipping by clamp refstar with faint magnitude %f ra: %f dec: %f\n",
+			         ref->Mag, RAD_TO_DEG(ref->sky->r), RAD_TO_DEG(ref->sky->d));
+                        goto skip;
+                    }
+
+                    psProject (ref->TP, ref->sky, fpa->toSky);
+                    psPlaneTransformApply (ref->FP, fpa->fromTPA, ref->TP);
+                    psPlaneTransformApply (ref->chip, chip->fromFPA, ref->FP);
+
+                    // limit the X,Y range of the refs to the selected chip
+                    if (ref->chip->x < minX) goto skip;
+                    if (ref->chip->x > maxX) goto skip;
+                    if (ref->chip->y < minY) goto skip;
+                    if (ref->chip->y > maxY) goto skip;
+
+                    psArrayAdd (refstars, 100, ref);
+                skip:
+                    psFree (ref);
+
+		    if (nMax && (refstars->n >= nMax)) break;
+                }
+                psTrace ("psastro", 4, "Added %ld refstars\n", refstars->n);
+
+# if (0)
+		if (1) {
+		  // test output block, not used in normal ops
+		  char filename[64];
+		  snprintf (filename, 64, "refstars.%02d.dat", Nchip);
+		  FILE *outfile = fopen (filename, "w");
+		  assert (outfile);
+		  for (int nn = 0; nn < refstars->n; nn++) {
+		    pmAstromObj *ref = refstars->data[nn];
+		    fprintf (outfile, "%lf %lf\n", ref->sky->r*PS_DEG_RAD, ref->sky->d*PS_DEG_RAD);
+		  }
+		  fclose (outfile);
+		}
+		if (1) {
+		  // test output block, not used in normal ops
+		  char filename[64];
+		  snprintf (filename, 64, "refstars.%02d.dat", Nchip);
+		  FILE *outfile = fopen (filename, "w");
+		  assert (outfile);
+		  for (int nn = 0; nn < refstars->n; nn++) {
+		    pmAstromObj *ref = refstars->data[nn];
+		    fprintf (outfile, "%lf %lf  %lf %lf  %lf %lf\n", ref->sky->r*PS_DEG_RAD, ref->sky->d*PS_DEG_RAD, );
+		  }
+		  fclose (outfile);
+		}
+# endif
+
+		psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.REFSTARS", PS_DATA_ARRAY, "astrometry matches", refstars);
+
+		// generate a reduced subset excluding the clumps
+		// XXX do we need both REFSTARS and SUBSET? 
+		psArray *subset = psastroRemoveClumpsIterate(refstars, 150, 3);
+		psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.REFSTARS.SUBSET", PS_DATA_ARRAY, "astrometry objects", subset);
+
+		psFree (refstars);
+		psFree (subset);
+		psFree (extent);
+
+		if (matchLumFunc) {
+		    // limit the total magnitude range of PSASTRO.REFSTARS.SUBSET based on
+		    // overlapping luminosity functions
+		    if (!psastroRefstarSubset (readout)) {
+			psError(PSASTRO_ERR_DATA, false, "Can't determine an appropriate refstar subset\n");
+			psFree (index);
+			psFree (view);
+			return false;
+		    }
+		}
+            }
+        }
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+    }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+
+    // activate all files except PSASTRO.OUTPUT
+    pmFPAfileActivate (config->files, true, NULL);
+    pmFPAfileActivate (config->files, false, "PSASTRO.OUT.REFSTARS");
+
+    bool onlyRefstars = psMetadataLookupBool (&status, recipe, "PSASTRO.ONLY.REFSTARS");
+    if (onlyRefstars) exit (0);
+
+    psFree (index);
+    psFree (view);
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroCleanup.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroCleanup.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroCleanup.c	(revision 41084)
@@ -0,0 +1,31 @@
+/** @file psastroCleanup.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.8.2.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-19 17:59:50 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+
+void psastroCleanup (pmConfig *config) {
+
+    psFree (config);
+    pmVisualClose ();
+    pmVisualCleanup ();
+
+    psTimerStop ();
+    psMemCheckCorruption (stderr, true);
+    pmModelClassCleanup ();
+    psTimeFinalize ();
+    pmConceptsDone ();
+    pmConfigDone ();
+    fprintf (stderr, "found %d leaks at %s\n", psMemCheckLeaks (0, NULL, stdout, false), "psastro");
+    // fprintf (stderr, "found %d leaks at %s\n", psMemCheckLeaks (0, NULL, NULL, false), "psastro");
+
+    return;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroConvert.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroConvert.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroConvert.c	(revision 41084)
@@ -0,0 +1,502 @@
+/** @file psastroConvert.c
+ *
+ *  @brief
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.25 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+// leak free 2006.04.27
+
+static psArray *chooseStars(psArray *inStars, char *listName, psArray *sources, psVector *index, int nMax, float iMagMin, float iMagMax, pmSourceMode skip);
+
+bool psastroConvertFPA (pmConfig *config, pmFPA *fpa, psMetadata *recipe) {
+
+    pmChip *chip;
+    pmCell *cell;
+    pmReadout *readout;
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                if (!psastroConvertReadout (config, view, readout, recipe)) {
+		  psError(PSASTRO_ERR_CONFIG, true, "problem converting readout\n");
+		  return false;
+		}
+            }
+        }
+    }
+    psFree (view);
+    return true;
+}
+
+// pass/apply the WCS information?
+bool psastroConvertReadout (pmConfig *config, pmFPAview *view, pmReadout *readout, psMetadata *recipe) {
+
+    bool status;
+
+    // XXX I want to make this optional
+    float MagOffset = 0.0;
+    if (1) {
+      // select the input data sources
+      pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+      if (!input) {
+	psError(PSASTRO_ERR_CONFIG, true, "failed to find PSASTRO.INPUT\n");
+	return false;
+      }
+      pmFPA *fpa = input->fpa;
+
+      float zeropt, exptime;
+
+      // really error-out here?  or just skip?
+      if (!psastroZeroPointFromRecipe (&zeropt, &exptime, NULL, fpa, recipe)) {
+        psLogMsg ("psastro", PS_LOG_INFO, "failed to load zeropt data from recipe");
+	zeropt = 0.0;
+	exptime = 1.0;
+      }
+
+      // recipe values are given in instrumental magnitudes
+      // use the zero point and exposure time to convert to apparent mags: M_ap = M_inst + C_0 + 2.5*log(exptime)
+      MagOffset = zeropt + 2.5*log10(exptime);
+    }
+
+    // PSPHOT.SOURCES carries the pmSource objects (from psphot analysis or loaded externally)
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *sources = detections->allSources;
+    psAssert (sources, "missing sources?");
+
+    // convert the pmSource objects into pmAstromObj objects (drop !STAR and SATSTAR?)
+    psArray *inStars = pmSourceToAstromObj (sources, MagOffset);
+
+    // apply Koppenhoefer correction if needed
+    if (!psastroCorrectKH (config, view, readout, recipe, inStars)){
+      psError(PSASTRO_ERR_CONFIG, true, "failed to correct Koppenhoefer Effect\n");
+      return false;
+    }
+
+    // sort in ascending magnitude order
+    // psArraySort (inStars, psastroSortByMag);
+    // psVector *index = psArraySortIndex (sources, pmSourceSortByFlux);
+    psVector *index = psArraySortIndex (NULL, inStars, psastroSortByMag);
+
+    // XXX need to exit gracefully is inStars->n is 0 (or 1?)
+
+    // we are going to select the brighter Nmax subset for astrometry
+    int nMax = psMetadataLookupS32 (&status, recipe, "PSASTRO.MAX.NRAW");
+    if (!status || !nMax) nMax = inStars->n;
+
+    // we are going to select the brighter Nmax subset for astrometry
+    float iMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.MAX.INST.MAG.RAW");
+    float iMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.MIN.INST.MAG.RAW");
+
+    // we are going to select the brighter Nmax subset for astrometry
+    pmSourceMode skip = PM_SOURCE_MODE_DEFAULT;
+    char *ignoreList = psMetadataLookupStr (&status, recipe, "PSASTRO.IGNORE");
+    if (ignoreList != NULL) {
+      psArray *list = psStringSplitArray (ignoreList, ",", false);
+      for (int i = 0; i < list->n; i++) {
+        pmSourceMode mode = pmSourceModeFromString (list->data[i]);
+        if (mode == PM_SOURCE_MODE_DEFAULT) {
+          psWarning ("unknown source mode in PSASTRO.IGNORE, skipping");
+          continue;
+        }
+        skip |= mode;
+      }
+      psFree (list);
+    }
+
+    psArray *rawStars = chooseStars(inStars, "", sources, index, PS_MIN(nMax, inStars->n), iMagMin, iMagMax, skip);
+    psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.RAWSTARS", PS_DATA_ARRAY, "astrometry objects", rawStars);
+
+    bool gridSearch = psMetadataLookupBool (&status, recipe, "PSASTRO.GRID.SEARCH");
+    if (gridSearch) {
+        // See if different magnitude limits have been specified for grid search. If so, create a separate list of stars to use.
+        float iGridMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.GRID.MAX.INST.MAG.RAW");
+        float iGridMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.GRID.MIN.INST.MAG.RAW");
+        int   nMaxGrid = psMetadataLookupS32 (&status, recipe, "PSASTRO.GRID.NRAW.MAX");
+    
+        // XXX Should we check PSASTRO.GRID.NRAW.MAX != PSASTRO.MAX.NRAW as well? It usually is smaller so that would cause
+        // us to always create a separate list. So I won't check.
+
+        if ((iGridMagMax != iMagMax) || (iGridMagMin != iMagMin)) {
+            psArray *gridStars = chooseStars(inStars, "grid search ", sources, index, PS_MIN(nMaxGrid, inStars->n), iGridMagMin, iGridMagMax, skip);
+            psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.GRID.RAWSTARS", PS_DATA_ARRAY, "astrometry objects for grid search", gridStars);
+            psFree(gridStars);
+        }
+    }
+
+    psFree (index);
+    psFree (inStars);
+    psFree (rawStars);
+
+    return true;
+}
+
+
+psArray * chooseStars(psArray *inStars, char *listName, psArray *sources, psVector *index, int nMax, float iMagMin, float iMagMax, pmSourceMode skip) {
+    // choose the first nMax sources
+    int j = 0;
+    psArray *rawStars = psArrayAlloc(nMax);
+
+    float mMin = +100.0;
+    float mMax = -100.0;
+    int nModeSkip = 0;
+    int nFaintSkip = 0;
+    int nBrightSkip = 0;
+    int nInfSkip = 0;
+
+    for (int i = 0; (i < inStars->n) && (j < rawStars->n); i++) {
+        int n = index->data.S32[i];
+        pmSource *source = sources->data[n];
+
+        psTrace ("psastro", 6, "mag: %f +/- %f, mode: %x, skip: %x\n", source->psfMag, source->psfMagErr, source->mode, skip);
+
+        if (source->mode & skip) {
+          nModeSkip ++;
+          continue;
+        }
+
+        if ((iMagMax != 0.0) && (source->psfMag > iMagMax)) {
+          nFaintSkip ++;
+          continue;
+        }
+        if ((iMagMin != 0.0) && (source->psfMag < iMagMin)) {
+          nBrightSkip ++;
+          continue;
+        }
+        if (!isfinite(source->psfMag)) {
+          nInfSkip ++;
+          continue;
+        }
+        mMin = PS_MIN (mMin, source->psfMag);
+        mMax = PS_MAX (mMax, source->psfMag);
+        rawStars->data[j] = psMemIncrRefCounter (inStars->data[n]);
+        j++;
+    }
+    rawStars->n = j;
+
+    psLogMsg ("psastro", 4, "loaded %ld %ssources, using %ld of %ld good sources (inst mag: %f to %f)\n", sources->n, listName, rawStars->n, inStars->n, mMin, mMax);
+    psLogMsg ("psastro", 4, "skip reasons: mode: %d, faint: %d, bright: %d, inf: %d\n", nModeSkip, nFaintSkip, nBrightSkip, nInfSkip);
+
+    return rawStars;
+}
+
+// select a magnitude range?
+psArray *pmSourceToAstromObj (psArray *sources, float MagOffset) {
+
+    psArray *objects = psArrayAllocEmpty (sources->n);
+
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+
+        // only accept the PSF sources?
+        // XXX drop SATSTAR?
+        // if (source->type != PM_SOURCE_TYPE_STAR) continue;
+
+        pmModel *model = source->modelPSF;
+        if (model == NULL) continue;
+
+        psF32 *PAR = model->params->data.F32;
+        psF32 *dPAR = model->dparams->data.F32;
+
+	// Note from EAM: I measured the KH correction from the nightly science database
+	// The KH effect was active until 2011/05/11 when the camera voltages were modified
+	// Between 2011/09/06 and 2013/04/30, there was a bug in pmPSF_ModelToAxes with the 
+	// result that the reported values were too large by a factor of sqrt(2).  Fortunately,
+	// this did not affect the data in the nightly science DVO used to measure the
+	// effect (all affected data was processed BEFORE the bug was introduced), and it
+	// does not affect any of the PV2 or PV3 data processed AFTER the bug was fixed.
+	psEllipseAxes axes = pmPSF_ModelToAxes (PAR, model->class->useReff);
+
+        pmAstromObj *obj = pmAstromObjAlloc ();
+
+        // is the source magnitude calibrated in any sense?
+        obj->pix->x    = PAR[PM_PAR_XPOS];
+        obj->pix->y    = PAR[PM_PAR_YPOS];
+        obj->pix->xErr = dPAR[PM_PAR_XPOS];
+        obj->pix->yErr = dPAR[PM_PAR_YPOS];
+        obj->Mag = source->psfMag;
+        obj->dMag = source->psfMagErr;
+        obj->SBinst = source->psfMag + 5.0*log10(axes.major);
+        obj->magCal = obj->Mag + MagOffset;
+
+        // XXX do we have the information giving the readout and cell offset?
+        // for the moment, assume chip == cell == readout
+        *obj->cell = *obj->pix;
+        *obj->chip = *obj->cell;
+
+        psArrayAdd (objects, 100, obj);
+        psFree (obj);
+    }
+    return objects;
+}
+
+/**
+ * sort by Mag (ascending)
+ */
+int psastroSortByMag (const void *a, const void *b)
+{
+    const pmAstromObj *A = a;
+    const pmAstromObj *B = b;
+
+    psF32 mA = (isfinite(A->Mag)) ? A->Mag : FLT_MAX;
+    psF32 mB = (isfinite(B->Mag)) ? B->Mag : FLT_MAX;
+
+    psF32 diff = mA - mB;
+    if (diff > +FLT_EPSILON) return (+1);
+    if (diff < -FLT_EPSILON) return (-1);
+    return (0);
+}
+
+static float pltScale[] = {
++0.0,
+-0.255347177386,
+-0.255585625172,
+-0.255733260512,
+-0.255747352242,
+-0.255611001253,
+-0.255311933756,
+ +0.0,
+ +0.0,
+ +0.0,
+-0.255296201766,
+-0.255688636720,
+-0.256161080003,
+-0.256464074850,
+-0.256324860930,
+-0.256147363186,
+-0.255734743476,
+-0.255422729909,
+ +0.0,
+ +0.0,
+-0.255508323163,
+-0.256142899513,
+-0.255493895173,
+-0.256997376740,
+-0.256935591102,
+-0.256644588679,
+-0.256123321533,
+-0.255515242517,
+ +0.0,
+ +0.0,
+-0.255633033872,
+-0.256356916189,
+-0.256929253668,
+-0.257250442505,
+-0.257260403216,
+-0.256962024927,
+-0.256387320280,
+-0.255681164920,
+ +0.0,
+ +0.0,
+-0.255671992302,
+-0.256371312678,
+-0.256936249495,
+-0.257237892330,
+-0.257248004675,
+-0.256911942035,
+-0.256365648448,
+-0.255632205397,
+ +0.0,
+ +0.0,
+-0.255455072403,
+-0.256080205411,
+-0.256570428520,
+-0.256908982217,
+-0.256899240971,
+-0.256607972383,
+-0.256107793808,
+-0.255482232273,
+ +0.0,
+ +0.0,
+-0.255237901688,
+-0.255669494152,
+-0.256101093233,
+-0.256372770309,
+-0.256373448133,
+-0.256118197262,
+-0.255689391643,
+-0.255279636681,
+ +0.0,
+ +0.0,
+ +0.0,
+-0.255286350489,
+-0.255517200172,
+-0.255689459741,
+-0.255668706447,
+-0.255539349556,
+-0.255363308907,
+};
+
+bool psastroCorrectKH (pmConfig *config, pmFPAview *view, pmReadout *readout, psMetadata *recipe, psArray *inStars) {
+
+  bool status;
+
+  psAssert (readout, "missing readout");
+  psAssert (readout->parent, "missing cell");
+  psAssert (readout->parent->parent, "missing chip");
+
+  // should we stay or should we go?
+  bool apply = psMetadataLookupBool (&status, recipe, "KH.CORRECT.APPLY.EXP");
+  if (!apply) return true;
+
+  // get the chip ID from the chip name:
+  pmChip *chip = readout->parent->parent;
+  char *chipName = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+
+  // skip stacks
+  if (!strcmp(chipName, "SkyChip")) {
+    psLogMsg ("psastro.correctKH", PS_LOG_DETAIL, "skipping KH correction: not a gpc1 chip\n");
+    return true;
+  }
+
+  // only try to address gpc1 chips (should probably check the camera)
+  if (strncmp(chipName, "XY", 2)) {
+    psLogMsg ("psastro.correctKH", PS_LOG_DETAIL, "skipping KH correction: not a gpc1 chip\n");
+    return true;
+  }
+
+  psAssert (strlen(chipName) == 4, "error in chip name");
+  psAssert (chipName[0] == 'X', "error in chip name");
+  psAssert (chipName[1] == 'Y', "error in chip name");
+  
+  int chipID = atoi (&chipName[2]);
+
+  // XXX hardwired list of chips to correct
+  // some notes:
+  // XY24 surpisingly does not need correction
+  // XY27 has poor astrometry with a weird correction for all mags
+  // XY36 surpisingly does not need correction
+  // XY67 has poor astrometry with a weird correction for all mags
+
+  // correct this chip? (raise an error for unexpected chipID values)
+  switch (chipID) {
+    case  1: // do not correct
+    case  2: // do not correct
+    case  3: // do not correct
+    case 10: // do not correct
+    case 11: // do not correct
+    case 12: // do not correct
+    case 13: // do not correct
+    case 20: // do not correct
+    case 21: // do not correct
+    case 22: // do not correct
+    case 23: // do not correct
+    case 24: // do not correct
+    case 27: // do not correct
+    case 30: // do not correct
+    case 31: // do not correct
+    case 32: // do not correct
+    case 33: // do not correct
+    case 36: // do not correct
+    case 44: // do not correct
+    case 45: // do not correct
+    case 46: // do not correct
+    case 47: // do not correct
+    case 54: // do not correct
+    case 55: // do not correct
+    case 56: // do not correct
+    case 57: // do not correct
+    case 64: // do not correct
+    case 65: // do not correct
+    case 66: // do not correct
+    case 67: // do not correct
+    case 74: // do not correct
+    case 75: // do not correct
+    case 76: // do not correct
+      return true;
+    case  4: // correct
+    case  5: // correct
+    case  6: // correct
+    case 14: // correct
+    case 15: // correct
+    case 16: // correct
+    case 17: // correct
+    case 25: // correct
+    case 26: // correct
+    case 34: // correct
+    case 35: // correct
+    case 37: // correct
+    case 40: // correct
+    case 41: // correct
+    case 42: // correct
+    case 43: // correct
+    case 50: // correct
+    case 51: // correct
+    case 52: // correct
+    case 53: // correct
+    case 60: // correct
+    case 61: // correct
+    case 62: // correct
+    case 63: // correct
+    case 71: // correct
+    case 72: // correct
+    case 73: // correct
+      break;
+    default:
+      psAbort ("chipID is invalid");
+  }
+
+  // grab the KH correction file
+  pmFPAfile *KHfile = psMetadataLookupPtr (NULL, config->files, "PSASTRO.KH.CORRECT");
+  if (!KHfile) {
+    psError(PM_ERR_CONFIG, false, "KH correction file not found");
+    return false;
+  }
+
+  // grab the corresponding chip
+  pmChip *KHchip = pmFPAviewThisChip (view, KHfile->fpa);
+  psAssert (KHchip, "found KH file, but not chip?");
+
+  // grab the correction spline
+  KHcorrectData *spline = psMetadataLookupPtr (&status, KHchip->analysis, "KH.CORRECT");
+  if (!spline) {
+    psError(PM_ERR_CONFIG, false, "KH correction not found");
+    return false;
+  }
+
+  // the hard-wired array of plate-scales per chip above come from 
+  // a single smf.  in there, a negative plate scale means parity is flipped
+  // on the sky.
+  float myPltScale = fabs(pltScale[chipID]);
+
+  // apply the correction to the detections
+  for (int i = 0; i < inStars->n; i++) {
+    pmAstromObj *obj = inStars->data[i];
+
+    float Xraw = obj->pix->x;
+    float SBinst = obj->SBinst;
+
+    float dX = KHcorrectApply (spline, SBinst);
+
+    float Xfix = Xraw + dX / myPltScale;
+
+    // note that we carry around pix, cell, chip but the real analysis only operates on chip
+    // if in the future we add transformations between chip->cell->pix, then we will need to
+    // make these consistent as well.
+    obj->pix->x = Xfix;
+    obj->cell->x = Xfix;
+    obj->chip->x = Xfix;
+  }
+
+  return true;
+
+}
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroDataLoad.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroDataLoad.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroDataLoad.c	(revision 41084)
@@ -0,0 +1,96 @@
+/** @file psastroDataLoad.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.12 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "Failure in psastroDataLoad"); \
+  psFree (view); \
+  return false; \
+}
+  
+/**
+ * \brief this loop loads the data from the input files and selects the
+ * brighter stars for astrometry
+ *
+ * at the end of this function, the complete stellar data is loaded
+ * into the correct fpa structure locations (readout.analysis:PSPHOT.SOURCES)
+ * all of the different astrometry analysis modes use the same data load loop
+ */
+bool psastroDataLoad (pmConfig *config) {
+
+    pmChip *chip;
+    pmCell *cell;
+    pmReadout *readout;
+
+    psTimerStart ("psastro");
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find input data!\n");
+	return false;
+    }
+
+    // de-activate all files except PSASTRO.INPUT
+    pmFPAfileActivate (config->files, false, NULL);
+    pmFPAfileActivate (config->files, true, "PSASTRO.INPUT");
+
+    // if we request KH Correction, activate that file as well
+    bool applyKH = psMetadataLookupBool (NULL, recipe, "KH.CORRECT.APPLY.EXP");
+    if (applyKH) {
+	pmFPAfileActivate (config->files, true, "PSASTRO.KH.CORRECT");
+    }
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // files associated with the science image
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+    while ((chip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+	while ((cell = pmFPAviewNextCell (view, input->fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+	    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+	    // process each of the readouts
+	    while ((readout = pmFPAviewNextReadout (view, input->fpa, 1)) != NULL) {
+		if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+		if (!readout->data_exists) { continue; }
+
+		if (!psastroConvertReadout (config, view, readout, recipe)) ESCAPE;
+
+		if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+	    }
+	    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+	}
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+    }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+
+    psLogMsg ("psastro", 3, "load data : %f sec\n", psTimerMark ("psastro"));
+
+    psFree (view);
+    return true;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroDataSave.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroDataSave.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroDataSave.c	(revision 41084)
@@ -0,0 +1,116 @@
+/** @file psastroDataSave.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.15 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "Failure in psastroDataSave"); \
+  psFree (view); \
+  return false; \
+}
+
+/**
+ * this loop saves the photometry/astrometry data files
+ */
+bool psastroDataSave (pmConfig *config, psMetadata *stats) {
+
+    pmChip *chip;
+    pmCell *cell;
+    pmReadout *readout;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+        return false;
+    }
+
+    // select the output data sources
+    pmFPAfile *output = psMetadataLookupPtr (NULL, config->files, "PSASTRO.OUTPUT");
+    if (!output) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find or interpret output file rule PSASTRO.OUTPUT!\n");
+        return false;
+    }
+
+    // de-activate all files except PSASTRO.OUTPUT, PSASTRO.OUT.ASTROM, and PSPHOT.OUTPUT.CFF
+    pmFPAfileActivate (config->files, false, NULL);
+    pmFPAfileActivate (config->files, true, "PSASTRO.OUTPUT");
+    pmFPAfileActivate (config->files, true, "PSASTRO.OUT.ASTROM");
+    pmFPAfileActivate (config->files, true, "PSPHOT.OUTPUT.CFF");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmHDU *lastHDU = NULL;              // Last HDU updated
+
+    // open/load files as needed
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+    while ((chip = pmFPAviewNextChip (view, output->fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+        if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+        while ((cell = pmFPAviewNextCell (view, output->fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+            if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+            // process each of the readouts
+            while ((readout = pmFPAviewNextReadout (view, output->fpa, 1)) != NULL) {
+                if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+                if (!readout->data_exists) { continue; }
+
+		psastroGalaxyShapeErrors (recipe, readout);
+
+                // Put version information into the header
+                pmHDU *hdu = pmHDUGetHighest(output->fpa, chip, cell);
+                if (hdu && hdu != lastHDU) {
+                    psastroVersionHeaderFull(hdu->header);
+		    // Append teh reference catalog to the header as well.
+		    psMetadata *recipe = psMetadataLookupPtr(NULL, config->recipes, PSASTRO_RECIPE);
+		    psMetadataAddStr(hdu->header,PS_LIST_TAIL,"PSREFCAT",PS_META_REPLACE,NULL,
+				     psMetadataLookupStr(NULL,recipe,"PSASTRO.CATDIR"));
+                    lastHDU = hdu;
+                }
+
+                if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+            }
+            if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+        }
+        if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+    }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+
+    // Write out summary statistics
+    if (!psastroMetadataStats (config, stats)) ESCAPE;
+
+    bool status;
+    psString dump_file = psMetadataLookupStr(&status, config->arguments, "DUMP_CONFIG");
+    if (dump_file) {
+        pmConfigCamerasCull(config, NULL);
+        pmConfigRecipesCull(config, "PPIMAGE,PPSTATS,PSPHOT,MASKS,PSASTRO");
+
+        if (!pmConfigDump(config, dump_file)) {
+            psError(psErrorCodeLast(), false, "Unable to dump configuration.");
+            psFree(view);
+            return false;
+        }
+    }
+
+    // activate all files except PSASTRO.OUTPUT, and PSPHOT.OUTPUT.CFF
+    pmFPAfileActivate (config->files, true, NULL);
+    pmFPAfileActivate (config->files, false, "PSASTRO.OUTPUT");
+    pmFPAfileActivate (config->files, false, "PSPHOT.OUTPUT.CFF");
+
+    psFree (view);
+
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroDefineFiles.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroDefineFiles.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroDefineFiles.c	(revision 41084)
@@ -0,0 +1,215 @@
+/** @file psastroDefineFiles.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.10 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+bool psastroDefineFiles (pmConfig *config, pmFPAfile *input) {
+
+    // these calls bind the I/O handle to the specified fpa
+    bool status;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, false, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+    pmFPAfile *output = pmFPAfileDefineOutput (config, input->fpa, "PSASTRO.OUTPUT");
+    if (!output) {
+	psError(PSASTRO_ERR_CONFIG, false, "Failed to build FPA from PSASTRO.INPUT");
+	return false;
+    }
+    output->save = true;
+
+    bool fixChips = psMetadataLookupBool (&status, config->arguments, "PSASTRO.FIX.CHIPS");
+    if (!status) {
+	fixChips = psMetadataLookupBool (&status, recipe, "PSASTRO.FIX.CHIPS");
+    }
+    bool useModel = psMetadataLookupBool (&status, config->arguments, "PSASTRO.USE.MODEL");
+    if (!status) {
+	useModel = psMetadataLookupBool (&status, recipe, "PSASTRO.USE.MODEL");
+    }
+    if (fixChips || useModel) {
+        if (!psastroDefineFile (config, input->fpa, "PSASTRO.MODEL", "ASTROM.MODEL", PM_FPA_FILE_ASTROM_MODEL, PM_DETREND_TYPE_ASTROM)) {
+            psError (PS_ERR_IO, false, "Can't find an astrometry model file");
+            return false;
+        }
+    }
+
+    bool KHapply = psMetadataLookupBool (&status, recipe, "KH.CORRECT.APPLY");
+    if (KHapply) {
+	// Get the time from FPA.TIME
+	psTime *time = psMetadataLookupPtr(NULL, input->fpa->concepts, "FPA.TIME");
+	if (time->sec == 0 && time->nsec == 0) {
+	    psError (PM_ERR_CONFIG, false, "KH.CORRECT.APPLY requested, but date/time of exposure not found");
+	    return false;
+	}
+	// what is the appropriate date range for the correction
+	char *KHendDate = psMetadataLookupStr (&status, recipe, "KH.CORRECT.ENDDATE");
+	if (!KHendDate) {
+	    psError (PM_ERR_CONFIG, false, "KH.CORRECT.APPLY requested, but KH.CORRECT.ENDDATE not in recipe");
+	    return false;
+	}
+	psTime *endtime = psTimeFromString (KHendDate, PS_TIME_TAI);
+	if (!endtime) {
+	    psError (PM_ERR_CONFIG, false, "KH.CORRECT.APPLY requested, KH.CORRECT.ENDDATE has an invalid date/time");
+	    return false;
+	}
+	if (psTimeDelta (endtime, time) < 0) {
+	    KHapply = false;
+	    psLogMsg ("psastro", PS_LOG_INFO, "Koppenhoefer correction requested, but exposure after end date, correction skipped");
+	} else {
+	    psLogMsg ("psastro", PS_LOG_INFO, "Koppenhoefer correction requested, exposure in valid date range, correction may be applied");
+	}   
+	psFree (endtime);
+    }
+    if (KHapply) {
+        if (!psastroDefineFile (config, input->fpa, "PSASTRO.KH.CORRECT", "KH.CORRECT", PM_FPA_FILE_KH_CORRECT, PM_DETREND_TYPE_KH_CORRECT)) {
+            psError (PS_ERR_IO, false, "Can't find a Koppenhoefer Correction file");
+            return NULL;
+        }
+    }
+    // the bool above in the recipe says "apply generally for this camera".  here we are asserting that
+    // we can and should apply for this exposure XXX change the name?
+    psMetadataAddBool (recipe, PS_LIST_TAIL, "KH.CORRECT.APPLY.EXP", PS_META_REPLACE, "", KHapply);
+
+    // this step is optional
+    bool REFSTAR_MASK = psMetadataLookupBool (&status, recipe, "REFSTAR_MASK");
+    if (REFSTAR_MASK) {
+
+        if (!psastroDefineFile (config, input->fpa, "PSASTRO.REFMASK", "REFMASK", PM_FPA_FILE_MASK, PM_DETREND_TYPE_MASK)) {
+            psError (PS_ERR_IO, false, "Can't find an input mask file");
+            return NULL;
+        }
+
+        if (!psastroDefineFile (config, input->fpa, "PSASTRO.INPUT.MASK", "INPUT.MASK", PM_FPA_FILE_MASK, PM_DETREND_TYPE_MASK)) {
+            psError (PS_ERR_IO, false, "Can't find an input mask file");
+            return NULL;
+        }
+
+	pmFPAfile *inMask = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT.MASK");
+	if (!inMask) {
+            psError (PS_ERR_IO, false, "Can't find an input mask file");
+            return NULL;
+        }
+
+	// XXX not yet sure if we need to mosaic or not...
+	pmFPAfile *outMask = pmFPAfileDefineOutputFromFile  (config, inMask, "PSASTRO.OUTPUT.MASK");
+	// pmFPAfile *outMask = pmFPAfileDefineChipMosaic(config, inMask->fpa, "PSASTRO.OUTPUT.MASK");
+	if (!outMask) {
+	    psError (PS_ERR_IO, false, "Can't find the astrometry refstars file definition");
+	    return NULL;
+	}
+	if (outMask->type != PM_FPA_FILE_MASK) {
+	    psError(PS_ERR_IO, true, "%s is not of type %s", "PSASTRO.OUTPUT.MASK", pmFPAfileStringFromType (PM_FPA_FILE_MASK));
+	    return NULL;
+	}
+	outMask->save = true;
+	inMask->freeLevel = outMask->dataLevel;
+    }
+
+    bool saveRefstars = psMetadataLookupBool (&status, config->arguments, "PSASTRO.SAVE.REFSTARS");
+    if (!status) {
+      saveRefstars = psMetadataLookupBool (&status, recipe, "PSASTRO.SAVE.REFSTARS");
+    }
+    if (saveRefstars) {
+	// look for the file in the camera config table
+	pmFPAfile *file = pmFPAfileDefineOutputFromFile  (config, input, "PSASTRO.OUT.REFSTARS");
+	if (!file) {
+	    psError (PS_ERR_IO, false, "Can't find the astrometry refstars file definition");
+	    return NULL;
+	}
+	if (file->type != PM_FPA_FILE_ASTROM_REFSTARS) {
+	    psError(PS_ERR_IO, true, "%s is not of type %s", "PSASTRO.OUT.REFSTARS", pmFPAfileStringFromType (PM_FPA_FILE_ASTROM_REFSTARS));
+	    return NULL;
+	}
+	file->save = true;
+    }
+
+    bool writeCff = psMetadataLookupBool (&status, recipe, "PSASTRO.SAVE.CFF");
+    if (writeCff) {
+	pmFPAfile *file = pmFPAfileDefineOutputFromFile  (config, input, "PSPHOT.OUTPUT.CFF");
+	if (!file) {
+	    psError (PS_ERR_IO, false, "Can't find the output cff file definition");
+	    return NULL;
+	}
+	if (file->type != PM_FPA_FILE_CFF) {
+	    psError(PS_ERR_IO, true, "%s is not of type %s", "PSPHOT.OUTPUT.CFF", pmFPAfileStringFromType (PM_FPA_FILE_CFF));
+	    return NULL;
+	}
+	file->save = true;
+    }
+
+
+# if (0)
+    // optionally save output plots
+    if (!pmFPAfileDefineOutput (config, input->fpa, "SOURCE.PLOT.MOMENTS")) {
+	psTrace ("psphot", 3, "Cannot find a rule for SOURCE.PLOT.MOMENTS");
+    }
+    if (!pmFPAfileDefineOutput (config, input->fpa, "SOURCE.PLOT.PSFMODEL")) {
+	psTrace ("psphot", 3, "Cannot find a rule for SOURCE.PLOT.PSFMODEL");
+    }
+# endif
+
+    return true;
+}
+
+bool psastroDefineFile (pmConfig *config, pmFPA *input, char *filerule, char *argname, pmFPAfileType fileType, pmDetrendType detrendType) {
+
+    bool status;
+    pmFPAfile *file;
+
+    // look for the file on the argument list
+    file = pmFPAfileDefineFromArgs  (&status, config, filerule, argname);
+    if (!status) {
+	psError (PS_ERR_UNKNOWN, false, "failed to load find definition");
+	return false;
+    }
+    if (file) {
+	if (file->type != fileType) {
+	    psError(PS_ERR_IO, true, "%s is not of type %s", filerule, pmFPAfileStringFromType (fileType));
+	    return false;
+	}
+	return true;
+    }
+
+    // look for the file in the camera config table
+    file = pmFPAfileDefineFromConf  (&status, config, filerule);
+    if (!status) {
+	psError (PS_ERR_UNKNOWN, false, "failed to load find definition");
+	return false;
+    }
+    if (file) {
+	if (file->type != fileType) {
+	    psError(PS_ERR_IO, true, "%s is not of type %s", filerule, pmFPAfileStringFromType (fileType));
+	    return false;
+	}
+	return true;
+    }
+
+    // look for the file to be loaded from the detrend database
+    file = pmFPAfileDefineFromDetDB (&status, config, filerule, input, detrendType);
+    if (!status) {
+	psError (PS_ERR_UNKNOWN, false, "failed to load file definition");
+	return false;
+    }
+    if (file) {
+	if (file->type != fileType) {
+	    psError(PS_ERR_IO, true, "%s is not of type %s", filerule, pmFPAfileStringFromType (fileType));
+	    return false;
+	}
+	return true;
+    }
+    return false;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroDemoDump.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroDemoDump.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroDemoDump.c	(revision 41084)
@@ -0,0 +1,325 @@
+/** @file psastroMosaicDemoDump.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.11 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+/**
+ * this function is used for test purposes 
+ * (-trace psastro.dump.psastroAstromGuess 1)
+ */
+bool psastroDumpStars (psArray *stars, char *filename) {
+
+    FILE *f = fopen (filename, "w");
+
+    for (int i = 0; i < stars->n; i++) {
+	pmAstromObj *obj = stars->data[i];
+
+	// write out the upward projections
+	fprintf (f, "%d  %f %f  %f  %f %f  %f %f  %f %f\n", i,
+		 obj->sky->r, obj->sky->d, obj->Mag, 
+		 obj->TP->x, obj->TP->y, 
+		 obj->FP->x, obj->FP->y, 
+		 obj->chip->x, obj->chip->y);
+    }
+    fclose (f);
+    return true;
+}
+
+/** this function is used for test purposes 
+ * (-trace psastro.dump.psastroAstromGuess 1)
+ */
+bool psastroDumpRawstars (psArray *rawstars, pmFPA *fpa, pmChip *chip) {
+
+    char *filename = NULL;
+    char *chipname = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+
+    psStringAppend (&filename, "rawstars.up.%s.dat", chipname);
+    FILE *f1 = fopen (filename, "w");
+    psFree (filename);
+    filename = NULL;
+
+    psStringAppend (&filename, "rawstars.dn.%s.dat", chipname);
+    FILE *f2 = fopen (filename, "w");
+    psFree (filename);
+    filename = NULL;
+
+    for (int i = 0; i < rawstars->n; i++) {
+	pmAstromObj *raw = rawstars->data[i];
+
+	psPlane *fp = psPlaneAlloc();
+	psPlane *tp = psPlaneAlloc();
+	psPlane *ch = psPlaneAlloc();
+			
+	psProject (tp, raw->sky, fpa->toSky);
+	psPlaneTransformApply (fp, fpa->fromTPA, tp);
+	psPlaneTransformApply (ch, chip->fromFPA, fp);
+			
+	// write out the upward projections
+	fprintf (f1, "%d  %f %f  %f  %f %f  %f %f  %f %f\n", i,
+		 raw->sky->r, raw->sky->d, raw->Mag, 
+		 raw->TP->x, raw->TP->y, 
+		 raw->FP->x, raw->FP->y, 
+		 raw->chip->x, raw->chip->y);
+		
+	// write out the downward projections
+	fprintf (f2, "%d  %f %f  %f  %f %f  %f %f  %f %f\n", i,
+		 raw->sky->r, raw->sky->d, raw->Mag, 
+		 tp->x, tp->y, 
+		 fp->x, fp->y, 
+		 ch->x, ch->y);
+		
+	psFree (fp);
+	psFree (tp);
+	psFree (ch);
+    }
+
+    fclose (f1);
+    fclose (f2);
+    return true;
+}
+
+bool psastroDumpMatchedStars (char *filename, psArray *rawstars, psArray *refstars, psArray *match) {
+    
+    FILE *f = fopen (filename, "w");
+
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+
+	fprintf (f, "%f %f  %f %f  %f %f  %f %f  %f   |   ",  
+		 DEG_RAD*raw->sky->r, DEG_RAD*raw->sky->d, 
+		 raw->TP->x, raw->TP->y, 
+		 raw->FP->x, raw->FP->y, 
+		 raw->chip->x, raw->chip->y, raw->Mag);
+
+	fprintf (f, "%f %f  %f %f  %f %f  %f %f  %f\n", 
+		 DEG_RAD*ref->sky->r, DEG_RAD*ref->sky->d, 
+		 ref->TP->x, ref->TP->y, 
+		 ref->FP->x, ref->FP->y, 
+		 ref->chip->x, ref->chip->y, ref->Mag);
+    }
+    fclose (f);
+
+    return true;
+}
+
+/**
+ * this function is used for test purposes 
+ * (-trace psastro.dump.psastroLoadRefstars 1)
+ */
+bool psastroDumpRefstars (psArray *refstars, char *filename) {
+
+    FILE *f = fopen (filename, "w");
+
+    for (int i = 0; i < refstars->n; i++) {
+	pmAstromObj *ref = refstars->data[i];
+
+	// write out the refstar data
+	fprintf (f, "%d  %f %f  %f\n", i,
+		 ref->sky->r, ref->sky->d, ref->Mag);
+    }
+
+    fclose (f);
+    return true;
+}
+
+bool psastroDumpMatches (pmFPA *fpa, char *filename) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    FILE *f = fopen (filename, "w");
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) continue;
+	
+	char *chipName = psMetadataLookupStr(NULL, chip->concepts, "CHIP.NAME");
+
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) continue;
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) continue;
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS.SUBSET");
+		if (rawstars == NULL) continue;
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS.SUBSET");
+		if (refstars == NULL) continue;
+		psTrace ("psastro", 4, "Trying %ld refstars\n", refstars->n);
+
+		psArray *matches = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+		if (matches == NULL) continue;
+
+		for (int i = 0; i < matches->n; i++) {
+		    pmAstromMatch *match = matches->data[i];
+
+		    pmAstromObj *raw = rawstars->data[match->raw];
+		    fprintf (f, "%s  %f %f  %f %f  %f %f  %f %f  %f   |   ",  
+			     chipName, DEG_RAD*raw->sky->r, DEG_RAD*raw->sky->d, 
+			     raw->TP->x, raw->TP->y, 
+			     raw->FP->x, raw->FP->y, 
+			     raw->chip->x, raw->chip->y, raw->Mag);
+
+		    pmAstromObj *ref = refstars->data[match->ref];
+		    fprintf (f, "%f %f  %f %f  %f %f  %f %f  %f\n", 
+			     DEG_RAD*ref->sky->r, DEG_RAD*ref->sky->d, 
+			     ref->TP->x, ref->TP->y, 
+			     ref->FP->x, ref->FP->y, 
+			     ref->chip->x, ref->chip->y, ref->Mag);
+		}
+	    }
+	}
+    }
+    fclose (f);
+    psFree (view);
+    return true;
+}
+
+/**
+ * this function is used for test purposes (-trace psastro.dump 1)
+ */
+bool psastroDumpGradients (psArray *gradients, char *filename) {
+
+    FILE *f = fopen (filename, "w");
+
+    for (int i = 0; i < gradients->n; i++) {
+	pmAstromGradient *gradient = gradients->data[i];
+
+	// write out the refstar data
+	fprintf (f, "%d  %f %f   %f %f  %f %f\n", i,
+		 gradient->FP.x, gradient->FP.y, 
+		 gradient->dTPdL.x, gradient->dTPdL.y, 
+		 gradient->dTPdM.x, gradient->dTPdM.y);
+    }
+
+    fclose (f);
+    return true;
+}
+
+bool psastroDumpCorners (char *filenameU, char *filenameD, pmFPA *fpa) {
+
+  // XXX test output of chip corners based on model
+  FILE *fu = fopen (filenameU, "w");
+  FILE *fd = fopen (filenameD, "w");
+
+  pmFPAview *view = pmFPAviewAlloc (0);
+
+  float fpaAngle = PM_DEG_RAD * atan2 (fpa->toTPA->y->coeff[1][0], fpa->toTPA->x->coeff[1][0]);
+
+  fprintf (fu, "# boresite: %f, %f @ %f\n", fpa->toSky->R*PS_DEG_RAD, fpa->toSky->D*PS_DEG_RAD, fpaAngle);
+  fprintf (fd, "# boresite: %f, %f @ %f\n", fpa->toSky->R*PS_DEG_RAD, fpa->toSky->D*PS_DEG_RAD, fpaAngle);
+
+  pmChip *chip = NULL;
+  while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        if (!chip->process || !chip->file_exists || !chip->data_exists) { continue; }
+
+	if (!chip->toFPA || !chip->fromFPA) {
+	  char *name = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+	  fprintf (stderr, "no astrom model for %s, skipping\n", name);
+	  continue;
+	}
+
+	// XXX write out the four corners for a test
+	psRegion *region = pmChipPixels (chip);
+	psPlane ptCP, ptFP, ptTP;
+	psSphere ptSky;
+
+	// UP 0,0
+	ptCP.x = region->x0; ptCP.y = region->y0;
+	psPlaneTransformApply (&ptFP, chip->toFPA, &ptCP);
+	psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+	psDeproject (&ptSky, &ptTP, fpa->toSky);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// DOWN 0,0
+	psProject (&ptTP, &ptSky, fpa->toSky);
+	psPlaneTransformApply (&ptFP, fpa->fromTPA, &ptTP);
+	psPlaneTransformApply (&ptCP, chip->fromFPA, &ptFP);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// UP 1,0
+	ptCP.x = region->x1; ptCP.y = region->y0;
+	psPlaneTransformApply (&ptFP, chip->toFPA, &ptCP);
+	psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+	psDeproject (&ptSky, &ptTP, fpa->toSky);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// DOWN 1,0
+	psProject (&ptTP, &ptSky, fpa->toSky);
+	psPlaneTransformApply (&ptFP, fpa->fromTPA, &ptTP);
+	psPlaneTransformApply (&ptCP, chip->fromFPA, &ptFP);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// UP 1,1
+	ptCP.x = region->x1; ptCP.y = region->y1;
+	psPlaneTransformApply (&ptFP, chip->toFPA, &ptCP);
+	psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+	psDeproject (&ptSky, &ptTP, fpa->toSky);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// DOWN 1,1
+	psProject (&ptTP, &ptSky, fpa->toSky);
+	psPlaneTransformApply (&ptFP, fpa->fromTPA, &ptTP);
+	psPlaneTransformApply (&ptCP, chip->fromFPA, &ptFP);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// UP 0,1
+	ptCP.x = region->x0; ptCP.y = region->y1;
+	psPlaneTransformApply (&ptFP, chip->toFPA, &ptCP);
+	psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+	psDeproject (&ptSky, &ptTP, fpa->toSky);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// DOWN 0,1
+	psProject (&ptTP, &ptSky, fpa->toSky);
+	psPlaneTransformApply (&ptFP, fpa->fromTPA, &ptTP);
+	psPlaneTransformApply (&ptCP, chip->fromFPA, &ptFP);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// UP 0,0
+	ptCP.x = region->x0; ptCP.y = region->y0;
+	psPlaneTransformApply (&ptFP, chip->toFPA, &ptCP);
+	psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+	psDeproject (&ptSky, &ptTP, fpa->toSky);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// DOWN 0,0
+	psProject (&ptTP, &ptSky, fpa->toSky);
+	psPlaneTransformApply (&ptFP, fpa->fromTPA, &ptTP);
+	psPlaneTransformApply (&ptCP, chip->fromFPA, &ptFP);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	psFree (region);
+  }
+
+  fclose (fu);
+  fclose (fd);
+  psFree (view);
+  return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroDemoPlot.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroDemoPlot.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroDemoPlot.c	(revision 41084)
@@ -0,0 +1,526 @@
+/** @file psastroMosaicDemoPlot.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.12 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+# if (HAVE_KAPA)
+# include <kapa.h>
+
+bool pmKapaPlotVectorTriple_AutoLimitsZscale_OpenGraph (int kapa, Graphdata *graphdata, psVector *xVec, psVector *yVec, psVector *zVec, bool increasing);
+
+bool psastroPlotRawstars (psArray *rawstars, pmFPA *fpa, pmChip *chip, psMetadata *recipe)
+{
+    Graphdata graphdata;
+    KapaSection section;
+
+    int kapa = pmKapaOpen (true);
+    if (kapa == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    bool status = false;
+    float iMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MIN");
+    float iMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MAX");
+
+    KapaResize (kapa, 1000, 1000);
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 7;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+
+    section.dx = 0.5;
+    section.dy = 0.5;
+
+    psVector *xVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+    psVector *zVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+
+    section.x = 0.0;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a0");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    int n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->x;
+        yVec->data.F32[n] = raw->chip->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    section.x = 0.5;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a1");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+
+        xVec->data.F32[n] = raw->FP->x;
+        yVec->data.F32[n] = raw->FP->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    section.x = 0.0;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a2");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+
+        xVec->data.F32[n] = raw->TP->x;
+        yVec->data.F32[n] = raw->TP->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    section.x = 0.5;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a3");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+
+        xVec->data.F32[n] = DEG_RAD*raw->sky->r;
+        yVec->data.F32[n] = DEG_RAD*raw->sky->d;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // flip x (East increase to left)
+    SWAP (graphdata.xmin, graphdata.xmax);
+    KapaSetLimits (kapa, &graphdata);
+
+    // pause and wait for user input:
+    // continue, save (provide name), ??
+    char key[10], name[80];
+    fprintf (stdout, "(s)ave plot or [c]ontinue? ");
+    if (!fgets(key, 8, stdin)) {
+        psWarning("Unable to read option");
+    } else if (key[0] == 's') {
+        fprintf (stdout, "enter plot name [rawstars.png]: ");
+        if (fscanf(stdin, "%s", name) != 1) {
+            psWarning("Unable to read plot name");
+        } else if (!strcmp (name, "")) {
+            strcpy (name, "rawstars.png");
+        }
+        KapaPNG (kapa, name);
+    }
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+    return true;
+}
+
+bool psastroPlotRefstars (psArray *refstars, psMetadata *recipe)
+{
+    Graphdata graphdata;
+
+    int kapa = pmKapaOpen (true);
+    if (kapa == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    bool status = false;
+    float rMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MIN");
+    float rMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MAX");
+
+    KapaResize (kapa, 1000, 1000);
+    KapaInitGraph (&graphdata);
+    KapaClearSections (kapa);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 7;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+
+    psVector *xVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    psVector *zVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+
+    int n = 0;
+    for (int i = 0; i < refstars->n; i++) {
+        pmAstromObj *ref = refstars->data[i];
+        if (!isfinite(ref->Mag)) continue;
+        if (ref->Mag > rMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+
+        xVec->data.F32[n] = DEG_RAD*ref->sky->r;
+        yVec->data.F32[n] = DEG_RAD*ref->sky->d;
+        zVec->data.F32[n] = ref->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // flip x (East increase to left)
+    SWAP (graphdata.xmin, graphdata.xmax);
+    KapaSetLimits (kapa, &graphdata);
+
+    // pause and wait for user input:
+    // continue, save (provide name), ??
+    char key[10], name[80];
+    fprintf (stdout, "(s)ave plot or [c]ontinue? ");
+    if (!fgets(key, 8, stdin)) {
+        psWarning("Couldn't read anything.");
+    } else if (key[0] == 's') {
+        fprintf (stdout, "enter plot name [refstars.png]: ");
+        if (fscanf (stdin, "%s", name) != 1) {
+            psWarning("Unable to read name");
+        } else if (!strcmp (name, "")) {
+            strcpy (name, "refstars.png");
+        }
+        KapaPNG (kapa, name);
+    }
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+    return true;
+}
+
+bool psastroPlotOneChipFit (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe) {
+
+    Graphdata graphdata;
+    KapaSection section;
+
+    int kapa = pmKapaOpen (true);
+    if (kapa == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    bool status = false;
+    float iMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MIN");
+    float iMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MAX");
+    float rMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MIN");
+    float rMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MAX");
+
+    KapaResize (kapa, 1000, 1000);
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 7;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+
+    section.dx = 0.5;
+    section.dy = 0.5;
+
+    psVector *xVec = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *zVec = psVectorAlloc (match->n, PS_TYPE_F32);
+
+    // X vs dX
+    section.x = 0.0;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a0");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    int n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->x;
+        yVec->data.F32[n] = raw->chip->x - ref->chip->x;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // X vs dY
+    section.x = 0.5;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a1");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->x;
+        yVec->data.F32[n] = raw->chip->y - ref->chip->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // Y vs dX
+    section.x = 0.0;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a2");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->y;
+        yVec->data.F32[n] = raw->chip->x - ref->chip->x;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // Y vs dY
+    section.x = 0.5;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a3");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->y;
+        yVec->data.F32[n] = raw->chip->y - ref->chip->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // *** X vs Y plot (different window)
+    int kapa2 = KapaOpenNamedSocket ("kapa", "XvsY");
+    if (kapa2 == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    KapaResize (kapa2, 1000, 1000);
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa2);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 2;
+    graphdata.style = 2;
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+
+    xVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+    yVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+    zVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+
+    // X vs Y by mag (raw)
+    n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->x;
+        yVec->data.F32[n] = raw->chip->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa2, &graphdata, xVec, yVec, zVec, false);
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 7;
+    graphdata.style = 2;
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+
+    xVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    yVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    zVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+
+    // X vs Y by mag (raw)
+    n = 0;
+    for (int i = 0; i < refstars->n; i++) {
+        pmAstromObj *ref = refstars->data[i];
+        if (!isfinite(ref->Mag)) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = ref->chip->x;
+        yVec->data.F32[n] = ref->chip->y;
+        zVec->data.F32[n] = ref->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimitsZscale_OpenGraph (kapa2, &graphdata, xVec, yVec, zVec, false);
+
+    // pause and wait for user input:
+    // continue, save (provide name), ??
+    char key[10], name[80];
+    fprintf (stdout, "(s)ave plot or [c]ontinue? ");
+    if (!fgets (key, 8, stdin)) {
+        psWarning("Couldn't read anything");
+    } else if (key[0] == 's') {
+        fprintf (stdout, "enter plot name [chipfit.png]: ");
+        if (fscanf (stdin, "%s", name) != 1) {
+            psWarning("Couldn't read name");
+        } else if (!strcmp (name, "")) {
+            strcpy (name, "chipfit.png");
+        }
+        KapaPNG (kapa, name);
+    }
+
+    close (kapa2);
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+    return true;
+}
+
+bool pmKapaPlotVectorTriple_AutoLimitsZscale_OpenGraph (int kapa, Graphdata *graphdata, psVector *xVec, psVector *yVec, psVector *zVec, bool increasing)
+{
+
+    // set limits based on data values
+    float zmin = +FLT_MAX;
+    float zmax = -FLT_MAX;
+    for (int i = 0; i < xVec->n; i++) {
+        zmin = PS_MIN (zmin, zVec->data.F32[i]);
+        zmax = PS_MAX (zmax, zVec->data.F32[i]);
+    }
+
+    // set the scale vector
+    psVector *zScale = psVectorAlloc (zVec->n, PS_DATA_F32);
+
+    float range = zmax - zmin;
+    if (range == 0.0) {
+        psVectorInit (zScale, 1.0);
+    } else {
+        for (int i = 0; i < zVec->n; i++) {
+            if (increasing) {
+                zScale->data.F32[i] = PS_MIN (1.5, PS_MAX(0.05, 1.5*(zVec->data.F32[i] - zmin)/range));
+            } else {
+                zScale->data.F32[i] = PS_MIN (1.5, PS_MAX(0.05, 1.5*(zmax - zVec->data.F32[i])/range));
+            }
+        }
+    }
+
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaBox (kapa, graphdata);
+
+    // the point size will be scaled from the z vector
+    graphdata->size = -1;
+    KapaPrepPlot (kapa, xVec->n, graphdata);
+    KapaPlotVector (kapa, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapa, yVec->n, yVec->data.F32, "y");
+    KapaPlotVector (kapa, zVec->n, zScale->data.F32, "z");
+    psFree (zScale);
+    return true;
+}
+
+# else
+
+bool psastroPlotRawstars (psArray *rawstars, pmFPA *fpa, pmChip *chip, psMetadata *recipe)
+{
+    return false;
+}
+
+bool psastroPlotRefstars (psArray *refstars, psMetadata *recipe)
+{
+    return false;
+}
+
+bool psastroPlotOneChipFit (psArray *rawstars, psArray *refstars, psArray *match,
+    psMetadata *recipe)
+{
+    return false;
+}
+
+# endif
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroErrorCodes.c.in
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroErrorCodes.c.in	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroErrorCodes.c.in	(revision 41084)
@@ -0,0 +1,37 @@
+/** @file psastroErrorCodes.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#include "pslib.h"
+#include "psastroErrorCodes.h"
+
+/*
+ * The line
+    { PSASTRO_ERR_$X{ErrorCode}, "$X{ErrorDescription}"},
+ * (without the Xs)
+ * will be replaced by values from errorCodes.dat
+ */
+void psastroErrorRegister(void)
+{
+    static psErrorDescription errors[] = {
+       { PSASTRO_ERR_BASE, "First value we use; lower values belong to psLib" },
+       { PSASTRO_ERR_${ErrorCode}, "${ErrorDescription}"},
+    };
+    static int nerror = PSASTRO_ERR_NERROR - PSASTRO_ERR_BASE; // number of values in enum
+
+    for (int i = 0; i < nerror; i++) {
+       psErrorDescription *tmp = psAlloc(sizeof(psErrorDescription));
+       *tmp = errors[i];
+       psErrorRegister(tmp, 1);
+       psFree(tmp);			/* it's on the internal list */
+    }
+    nerror = 0;			                // don't register more than once
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroErrorCodes.dat
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroErrorCodes.dat	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroErrorCodes.dat	(revision 41084)
@@ -0,0 +1,12 @@
+#
+# This file is used to generate pmErrorClasses.h
+#
+BASE = 4000		First value we use; lower values belong to psLib
+UNKNOWN			Unknown psastro error code
+NOT_IMPLEMENTED		Desired feature is not yet implemented
+ARGUMENTS		Incorrect arguments
+CONFIG			Problem in configure files
+IO			Problem in FITS I/O
+WCS      		Error interpreting FITS WCS information
+DATA                    Problem in data values
+REFSTARS                Problem in data values
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroErrorCodes.h.in
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroErrorCodes.h.in	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroErrorCodes.h.in	(revision 41084)
@@ -0,0 +1,30 @@
+/** @file psastroErrorCodes.h
+ *
+ *  @brief
+ *
+ *  @ingroup psastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#if !defined(PSASTRO_ERROR_CODES_H)
+#define PSASTRO_ERROR_CODES_H
+/*
+ * The line
+ *  PSASTRO_ERR_$X{ErrorCode},
+ * (without the X)
+ *
+ * will be replaced by values from errorCodes.dat
+ */
+typedef enum {
+    PSASTRO_ERR_BASE = 4000,
+    PSASTRO_ERR_${ErrorCode},
+    PSASTRO_ERR_NERROR
+} psastroErrorCode;
+
+void psastroErrorRegister(void);
+
+#endif
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtract.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtract.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtract.c	(revision 41084)
@@ -0,0 +1,59 @@
+/** @file psastroExtract.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastroExtract
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+
+int main (int argc, char **argv) {
+
+    pmConfig *config = NULL;
+
+    psTimerStart ("complete");
+
+    psastroErrorRegister();              // register our error codes/messages
+
+    // model inits are needed in pmSourceIO
+    // models defined in psphot/src/models are not available in psastro
+    pmModelClassInit ();
+
+    // load configuration information
+    config = psastroExtractArguments (argc, argv);
+
+    // load identify the data sources
+    if (!psastroExtractParseCamera (config)) {
+	psErrorStackPrint(stderr, "error setting up the camera\n");
+	exit (1);
+    }
+
+    // load the raw pixel data (from PSPHOT.SOURCES)
+    // select subset of stars for astrometry
+    if (!psastroExtractDataLoad (config)) {
+	psErrorStackPrint(stderr, "error loading input data\n");
+	exit (1);
+    }
+
+    // run the full astrometry analysis (chip and/or mosaic)
+    if (!psastroExtractAnalysis (config)) {
+	psErrorStackPrint(stderr, "failure in psastro model analysis\n");
+	exit (1);
+    }
+    
+    // XXX save the results; is this needed?
+    // if (!psastroExtractDataSave (config)) {
+    // psErrorStackPrint(stderr, "error saving output data\n");
+    // exit (1);
+    // }
+
+    psLogMsg ("psastro", 3, "complete psastro run: %f sec\n", psTimerMark ("complete"));
+
+    psastroCleanup (config);
+    exit (EXIT_SUCCESS);
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractAnalysis.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractAnalysis.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractAnalysis.c	(revision 41084)
@@ -0,0 +1,58 @@
+/** @file psastroExtractAnalysis.c
+ *
+ *  @brief 
+ *
+ *  @ingroup psastroExtract
+ *
+ *  @author IfA
+ *  @version $Revision: 1.7 $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+/**
+ * create a mask or mask regions based on the collection of reference stars that * are in the vicinity of each chip
+ */
+bool psastroExtractAnalysis (pmConfig *config) {
+
+    bool status;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+        return false;
+    }
+
+    // force this recipe value to be false (would cause an error in psastroChooseRefstars -- no rawstars!)
+    psMetadataAddBool (recipe, PS_LIST_TAIL, "PSASTRO.MATCH.LUMFUNC", PS_META_REPLACE, "do not match luminosity functions", false);
+
+    psLogMsg ("psastro", PS_LOG_INFO, "loading reference stars");
+
+    // load the reference stars overlapping the data stars
+    psArray *refs = psastroLoadRefstars(config, "PSASTRO.EXTRACT.ASTROM");
+    if (!refs) {
+        psError (PSASTRO_ERR_UNKNOWN, false, "failed to load reference data\n");
+        return false;
+    }
+    if (refs->n == 0) {
+        psError(PSASTRO_ERR_REFSTARS, true, "no reference stars found");
+        psFree(refs);
+        return false;
+    }
+
+    if (!psastroChooseRefstars (config, refs, "PSASTRO.EXTRACT.ASTROM", false)) {
+        psError (PSASTRO_ERR_UNKNOWN, false, "failed to select reference data for chips\n");
+        psFree(refs);
+        return false;
+    }
+    psFree(refs);
+
+    // convert star positions to ghost positions and add to the readout->analysis data
+    psastroExtractGhosts (config);
+
+    psastroExtractStars (config);
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractArguments.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractArguments.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractArguments.c	(revision 41084)
@@ -0,0 +1,78 @@
+/** @file psastroExtractArguments.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastroExtract
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+
+static char *usage = "USAGE: psastroExtract -file (input) -astrom (cmffiles) -outroot (outroot) [-chip]";
+
+pmConfig *psastroExtractArguments (int argc, char **argv) {
+
+    int N;
+
+    if (argc == 1) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "No arguments supplied");
+        psErrorStackPrint(stderr, "%s", usage);
+	exit (1);
+    }
+
+    // load config data from default locations
+    pmConfig *config = pmConfigRead(&argc, argv, PSASTRO_RECIPE);
+    if (config == NULL) {
+        psError(PSASTRO_ERR_CONFIG, false, "Can't read site configuration");
+        psErrorStackPrint(stderr, "%s", usage);
+	exit (1);
+    }
+
+    // save the following additional recipe values based on command-line options
+    // these options override the PSASTRO recipe values loaded from recipe files
+    // psMetadata *options = pmConfigRecipeOptions (config, PSASTRO_RECIPE);
+
+    // define the image pixel data
+    if (!pmConfigFileSetsMD (config->arguments, &argc, argv, "INPUT", "-file", "-list")) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "Missing -file (input) or -list (input)");
+        psErrorStackPrint(stderr, "%s", usage);
+	exit (1);
+    }
+
+    // define the astrometry data (XXX make this optional and use the image header?)
+    if (!pmConfigFileSetsMD(config->arguments, &argc, argv, "ASTROM",   "-astrom", "-astromlist")) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "Missing -astrom (smf) or -astromlist (smf)");
+        psErrorStackPrint(stderr, "%s", usage);
+	exit (1);
+    }
+
+    // define the output filename
+    N = psArgumentGet (argc, argv, "-output");
+    if (!N) {
+	psError(PSASTRO_ERR_ARGUMENTS, true, "Missing -output (root)");
+        psErrorStackPrint(stderr, "%s", usage);
+	exit (1);
+    }
+    psArgumentRemove (N, &argc, argv);
+    psMetadataAddStr (config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "", argv[N]);
+    psArgumentRemove (N, &argc, argv);
+
+    // chip selection is used to limit chips to be processed
+    if ((N = psArgumentGet (argc, argv, "-chip"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (config->arguments, PS_LIST_TAIL, "CHIP_SELECTIONS", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+    
+    if (argc < 1) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "Incorrect arguments supplied");
+        psErrorStackPrint(stderr, "exit");
+	exit (1);
+    }
+
+    return (config);
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractDataLoad.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractDataLoad.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractDataLoad.c	(revision 41084)
@@ -0,0 +1,165 @@
+/** @file psastroExtractDataLoad.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastroExtract
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "Failure in psastroExtractDataLoad"); \
+  psFree (view); \
+  return false; \
+}
+  
+/**
+ * this loop loads the header data from the input files, using the output 
+ * pmFPAfile to guide the chip selection and related issues
+ * all of the different astrometry analysis modes use the same data load loop
+ */
+bool psastroExtractDataLoad (pmConfig *config) {
+
+    bool status;
+    pmChip *chip;
+
+    bool newFPA = true;
+    double RAmin  = +FLT_MAX;
+    double RAmax  = -FLT_MAX;
+    double DECmin = +FLT_MAX;
+    double DECmax = -FLT_MAX;
+
+    double RAminSky = NAN;
+    double RAmaxSky = NAN;
+
+    psTimerStart ("psastro");
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+    // physical pixel scale in microns per pixel
+    double pixelScale = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.SCALE");
+    if (!status) {
+	psError(PS_ERR_IO, true, "Failed to lookup pixel scale"); 
+	return false; 
+    } 
+
+    // select the input data sources
+    pmFPAfile *astrom = psMetadataLookupPtr (NULL, config->files, "PSASTRO.EXTRACT.ASTROM");
+    if (!astrom) psAbort ("PSASTRO.EXTRACT.ASTROM not listed in config->files");
+    pmFPA *fpa = astrom->fpa;
+
+    pmFPAfileActivate(config->files, false, NULL);
+    pmFPAfileActivate(config->files, true, "PSASTRO.EXTRACT.ASTROM");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // files associated with the science image
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+    // load the headers from the astrometry files
+    while ((chip = pmFPAviewNextChip (view, astrom->fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process) { continue; }
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+    }
+    psLogMsg ("psastro", 3, "load headers : %f sec\n", psTimerMark ("psastro"));
+
+    // check PHU header to see if we are using mosaic-level or per-chip astrometry
+    bool bilevelAstrometry = false;
+    pmHDU *phu = pmFPAviewThisPHU (view, astrom->fpa);
+    if (phu) {
+      char *ctype = psMetadataLookupStr (NULL, phu->header, "CTYPE1");
+      if (ctype) bilevelAstrometry = !strcmp (&ctype[4], "-DIS");
+    }
+    if (bilevelAstrometry) {
+      pmAstromReadBilevelMosaic (astrom->fpa, phu->header);
+    } 
+
+    // apply the header astrometry to the astrometry structures
+    while ((chip = pmFPAviewNextChip (view, astrom->fpa, 1)) != NULL) {
+      psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+      if (!chip->process || !chip->file_exists || !chip->data_exists) { continue; }
+
+      if (newFPA) {
+	newFPA = false;
+	while (fpa->toSky->R <        0) fpa->toSky->R += 2.0*M_PI;
+	while (fpa->toSky->R > 2.0*M_PI) fpa->toSky->R -= 2.0*M_PI;
+	RAminSky = fpa->toSky->R - M_PI;
+	RAmaxSky = fpa->toSky->R + M_PI;
+      }
+
+      // read WCS data from the corresponding header
+      pmHDU *hdu = pmFPAviewThisHDU (view, astrom->fpa);
+      int nAstro = psMetadataLookupS32 (&status, hdu->header, "NASTRO");
+      if (!nAstro) continue;
+
+      if (bilevelAstrometry) {
+	if (!pmAstromReadBilevelChip (chip, hdu->header)) {
+	  psWarning("Could not get WCS information from header for chip %d, skipping", view->chip); 
+	  continue;
+	} 
+      } else {
+	if (!pmAstromReadWCS (astrom->fpa, chip, hdu->header, pixelScale)) {
+	  psWarning("Could not get WCS information from header for chip %d, skipping", view->chip); 
+	  continue;
+	} 
+      }
+      
+      // determine RA,DEC of 4 corners, use to find RA_MIN,MAX, DEC_MIN,MAX
+      psRegion *region = pmChipPixels (chip);
+      psPlane ptCH[4], ptFP, ptTP;
+      psSphere ptSky;
+      ptCH[0].x = region->x0;
+      ptCH[0].y = region->y0;
+      ptCH[1].x = region->x1;
+      ptCH[1].y = region->y0;
+      ptCH[2].x = region->x1;
+      ptCH[2].y = region->y1;
+      ptCH[3].x = region->x0;
+      ptCH[3].y = region->y1;
+      psFree (region);
+      
+      for (int i = 0; i < 4; i++) {
+	psPlaneTransformApply (&ptFP, chip->toFPA, &ptCH[i]);
+	psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+	psDeproject (&ptSky, &ptTP, fpa->toSky);
+
+	// rationalize ra to sky range centered on boresite
+	while (ptSky.r < RAminSky) ptSky.r += 2.0*M_PI;
+	while (ptSky.r > RAmaxSky) ptSky.r -= 2.0*M_PI;
+
+	RAmin = PS_MIN (ptSky.r, RAmin);
+	RAmax = PS_MAX (ptSky.r, RAmax);
+	
+	DECmin = PS_MIN (ptSky.d, DECmin);
+	DECmax = PS_MAX (ptSky.d, DECmax);
+
+	psLogMsg ("psastro", 2, "chip %d (corner %d) : %f %f  -> %f %f -> %f %f -> %f %f\n",
+		  view->chip, i, ptCH[i].x, ptCH[i].y, ptFP.x, ptFP.y, ptTP.x, ptTP.y, 
+		  DEG_RAD*ptSky.r, DEG_RAD*ptSky.d);
+      }
+    }
+    psLogMsg ("psastro", 3, "convert wcs terms to internal format : %f sec\n", psTimerMark ("psastro"));
+
+    psLogMsg ("psastro", 2, "loaded raw data from %f,%f to %f,%f\n",
+              DEG_RAD*RAmin, DEG_RAD*DECmin,
+              DEG_RAD*RAmax, DEG_RAD*DECmax);
+
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "RA_MIN",  PS_META_REPLACE, "", RAmin);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "RA_MAX",  PS_META_REPLACE, "", RAmax);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "DEC_MIN", PS_META_REPLACE, "", DECmin);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "DEC_MAX", PS_META_REPLACE, "", DECmax);
+
+    psFree (view);
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractDataSave.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractDataSave.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractDataSave.c	(revision 41084)
@@ -0,0 +1,52 @@
+/** @file psastroModelDataSave.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastroModel
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+# define NONLIN_TOL 0.001 ///< tolerance in pixels 
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "Failure in psastroModelDataSave"); \
+  psFree (view); \
+  return false; \
+}
+  
+bool psastroModelDataSave (pmConfig *config) {
+
+    bool status;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+	return false;
+    }
+
+    pmFPAfile *output = psMetadataLookupPtr (&status, config->files, "PSASTRO.OUT.MODEL");
+    if (!status) psAbort ("Can't find output pmFPAfile PSASTRO.OUT.MODEL");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmChip *chip = NULL;
+
+    while ((chip = pmFPAviewNextChip (view, output->fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process) { continue; }
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+    }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+
+    psLogMsg ("psastro", 3, "save headers : %f sec\n", psTimerMark ("psastro"));
+
+    return true;
+}
+
+
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractGhosts.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractGhosts.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractGhosts.c	(revision 41084)
@@ -0,0 +1,136 @@
+/** @file psastroExtractGhosts.c
+ *
+ *  @brief calculate ghost FPA and Chip positions for the stars loaded on the FPA
+ *
+ *  @ingroup psastroExtract
+ *
+ *  @author IfA
+ *  @version $Revision: 1.7 $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+# define ESCAPE(MSG) {							\
+	psError(PS_ERR_UNKNOWN, false, "I/O failure in psastroMaskUpdate: %s", MSG); \
+	psFree (view);							\
+	return false;							\
+    }
+
+/**
+ * calculate ghost FPA and Chip positions for the stars loaded on the FPA
+ */
+bool psastroExtractGhosts (pmConfig *config) {
+
+    bool status;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    float zeropt, exptime;
+
+    psLogMsg ("psastro", PS_LOG_INFO, "determine ghost positions");
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+        return false;
+    }
+
+    double EXTRACT_MAX_MAG = psMetadataLookupF32 (&status, recipe, "EXTRACT_MAX_MAG");
+
+    // we have two input pmFPAfiles: PSASTRO.EXTRACT.INPUT and PSASTRO.EXTRACT.ASTROM.  both are in chip-mosaic or fpa format
+    // we have already loaded the headers from PSASTRO.EXTRACT.ASTROM and determined the astrometry terms
+
+    // select the input astrometry data (also carries the refstars)
+    pmFPAfile *astrom = psMetadataLookupPtr (NULL, config->files, "PSASTRO.EXTRACT.ASTROM");
+    if (!astrom) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+        return false;
+    }
+    pmFPA *fpa = astrom->fpa;
+
+    // really error-out here?  or just skip?
+    if (!psastroZeroPointFromRecipe (&zeropt, &exptime, NULL, fpa, recipe)) {
+        psLogMsg ("psastro", PS_LOG_INFO, "failed to load zeropt data from recipe");
+        return false;
+    }
+
+    // recipe values are given in instrumental magnitudes
+    // use the zero point and exposure time to convert to apparent mags: M_ap = M_inst + C_0 + 2.5*log(exptime)
+    float MagOffset = zeropt + 2.5*log10(exptime);
+    EXTRACT_MAX_MAG += MagOffset;
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+        if (!chip->fromFPA) { continue; }
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                // select the raw objects for this readout (loaded in psastroExtract.c)
+                psArray *refstars = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.REFSTARS");
+                if (refstars == NULL) { continue; }
+
+                // identify the bright stars of interest
+                for (int i = 0; i < refstars->n; i++) {
+                    pmAstromObj *ref = refstars->data[i];
+                    if (ref->Mag > EXTRACT_MAX_MAG) continue;
+
+		    // XXX can make a more clever model...
+		    double xFPA = -1*ref->FP->x;
+		    double yFPA = -1*ref->FP->y;
+
+		    double xChip, yChip;
+		    pmChip *ghostChip = psastroFindChip (&xChip, &yChip, fpa, xFPA, yFPA);
+		    if (!ghostChip) continue;
+		    if (!ghostChip->cells) continue;
+		    if (!ghostChip->cells->n) continue;
+		    pmCell *ghostCell = ghostChip->cells->data[0];
+		    if (!ghostCell) continue;
+		    if (!ghostCell->readouts) continue;
+		    if (!ghostCell->readouts->n) continue;
+		    pmReadout *ghostReadout = ghostCell->readouts->data[0];
+		    if (!ghostReadout) continue;
+
+		    psArray *ghosts = psMetadataLookupPtr (&status, ghostReadout->analysis, "PSASTRO.GHOSTS");
+		    if (ghosts == NULL) { 
+			ghosts = psArrayAllocEmpty (100);
+			if (!psMetadataAdd (ghostReadout->analysis, PS_LIST_TAIL, "PSASTRO.GHOSTS", PS_DATA_ARRAY, "astrometry matches", ghosts)) {
+			  psError(PSASTRO_ERR_CONFIG, false, "failure to add ghosts to readout");
+			  return false;
+			}
+			psFree (ghosts);
+		    }
+
+		    pmAstromObj *ghost = pmAstromObjAlloc ();
+		    ghost->FP->x   = xFPA;
+		    ghost->FP->y   = yFPA;
+		    ghost->chip->x = xChip;
+		    ghost->chip->y = yChip;
+		    ghost->TP->x   = ref->FP->x; // XXX this is a bit of sleazy over-loading
+		    ghost->TP->y   = ref->FP->y;
+		    ghost->Mag     = ref->Mag;
+		    
+		    psArrayAdd (ghosts, 100, ghost);
+		    psFree (ghost);
+                }
+            }
+        }
+    }
+
+    psastroExtractFreeChipBounds();
+
+    psFree (view);
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractParseCamera.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractParseCamera.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractParseCamera.c	(revision 41084)
@@ -0,0 +1,68 @@
+/** @file psastroExtractParseCamera.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastroExtract
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ 
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+bool psastroExtractParseCamera (pmConfig *config) {
+
+    bool status = false;
+
+    // the input image(s) are required arguments; they define the camera
+    status = false;
+    pmFPAfile *input = pmFPAfileDefineFromArgs(&status, config, "PSASTRO.EXTRACT.INPUT", "INPUT");
+    if (!input || !status) {
+        psError(PSASTRO_ERR_CONFIG, false, "Failed to build FPA from PSASTRO.EXTRACT.INPUT");
+        return false;
+    }
+
+    // the input image(s) are required arguments; they define the camera
+    status = false;
+    pmFPAfile *astrom = pmFPAfileDefineFromArgs(&status, config, "PSASTRO.EXTRACT.ASTROM", "ASTROM");
+    if (!astrom || !status) {
+        psError (PS_ERR_UNKNOWN, false, "failed to load astrometry definition");
+        return NULL;
+    }
+
+# if (0)    
+    // XXX for now, don't use the pmFPAfile methods
+    // set up an output fpa structure & file based on the last input file
+    pmFPAfile *output = pmFPAfileDefineOutput (config, input->fpa, "PSASTRO.OUT.MODEL");
+    if (!output) {
+	psError(PSASTRO_ERR_CONFIG, false, "Failed to build FPA for PSASTRO.OUT.MODEL from input");
+	return false;
+    }
+    output->save = true;
+# endif
+
+    // Chip selection: turn on only the chips specified (option is not required)
+    char *chipLine = psMetadataLookupStr(&status, config->arguments, "CHIP_SELECTIONS"); 
+    psArray *chips = psStringSplitArray (chipLine, ",", false);
+    if (chips->n > 0) {
+	pmFPASelectChip (input->fpa, -1, true); // deselect all chips
+	pmFPASelectChip (astrom->fpa, -1, true); // deselect all chips
+	for (int i = 0; i < chips->n; i++) {
+	    int chipNum = atoi(chips->data[i]);
+	    if (! pmFPASelectChip(input->fpa, chipNum, false)) {
+		psError(PSASTRO_ERR_CONFIG, true, "Chip number %d doesn't exist in camera.\n", chipNum);
+		return false;
+	    }
+	    if (! pmFPASelectChip(astrom->fpa, chipNum, false)) {
+		psError(PSASTRO_ERR_CONFIG, true, "Chip number %d doesn't exist in camera.\n", chipNum);
+		return false;
+	    }
+        }
+    }
+    psFree (chips);
+
+    psTrace("psastro", 1, "Done with psastroExtractParseCamera...\n");
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractStar.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractStar.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractStar.c	(revision 41084)
@@ -0,0 +1,54 @@
+/** @file psastroExtractStar.c
+ *
+ *  @brief 
+ *
+ *  @ingroup psastroExtract
+ *
+ *  @author IfA
+ *  @version $Revision: 1.7 $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+psImage *psastroExtractStar (psImage *input, double x, double y, double dX, double dY) {
+
+    // skip if no pixels are on the image
+    // skip if center is not on the image
+    // if bounds fall off image, paste into a full-size image.
+
+    if (x < 0) return NULL;
+    if (y < 0) return NULL;
+    if (x >= input->numCols) return NULL;
+    if (y >= input->numRows) return NULL;
+
+    int xo = x; // index of center pixel (eg, 6.00 to 6.99 -> 6)
+    int yo = y;
+
+    // build an output image of size 2dX + 1, 2dY + 1
+    // psRegion fullRegion = psRegionSet (xo - dX, x + dX + 1, y - dY, y + dY + 1);
+    // psRegion realRegion = psRegionForImage (input, fullRegion);
+    // psImage *subset = psImageSubset (input, realRegion);
+
+    psImage *subset = psImageAlloc (2*dX+1, 2*dY+1, PS_TYPE_F32);
+    psImageInit (subset, 0.0);
+
+    // fill in the subset image with the values from the subset
+    // I must already have done this elsewhere...
+
+    for (int iy = yo - dY; iy < yo + dY + 1; iy++) {
+	for (int ix = xo - dX; ix < xo + dX + 1; ix++) {
+
+	    if (ix < input->col0) continue;
+	    if (iy < input->row0) continue;
+	    if (ix >= input->numCols) continue;
+	    if (iy >= input->numRows) continue;
+	
+	    int jx = ix + dX - xo; // runs from 0 to 2dX
+	    int jy = iy + dY - yo;
+	    subset->data.F32[jy][jx] = input->data.F32[iy][ix];
+	}
+    }
+    return subset;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractStars.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractStars.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroExtractStars.c	(revision 41084)
@@ -0,0 +1,212 @@
+/** @file psastroExtractAnalysis.c
+ *
+ *  @brief 
+ *
+ *  @ingroup psastroExtract
+ *
+ *  @author IfA
+ *  @version $Revision: 1.7 $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+# define ESCAPE(MSG) {							\
+	psError(PS_ERR_UNKNOWN, false, "I/O failure in psastroMaskUpdate: %s", MSG); \
+	psFree (view);							\
+	return false;							\
+    }
+
+/**
+ * create a mask or mask regions based on the collection of reference stars that * are in the vicinity of each chip
+ */
+bool psastroExtractStars (pmConfig *config) {
+
+    bool status;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    float zeropt, exptime;
+    char extname[81];
+
+    psLogMsg ("psastro", PS_LOG_INFO, "extracting bright-star subrasters");
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+        return false;
+    }
+
+    double EXTRACT_MAX_MAG = psMetadataLookupF32 (&status, recipe, "EXTRACT_MAX_MAG");
+
+    // we have two input pmFPAfiles: PSASTRO.EXTRACT.INPUT and PSASTRO.EXTRACT.ASTROM.  both are in chip-mosaic or fpa format
+    // we have already loaded the headers from PSASTRO.EXTRACT.ASTROM and determined the astrometry terms
+
+    // select the input astrometry data (also carries the refstars)
+    pmFPAfile *astrom = psMetadataLookupPtr (NULL, config->files, "PSASTRO.EXTRACT.ASTROM");
+    if (!astrom) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+        return false;
+    }
+    pmFPA *fpa = astrom->fpa;
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.EXTRACT.INPUT");
+    if (!input) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+        return false;
+    }
+
+    // only load data from INPUT
+    pmFPAfileActivate(config->files, false, NULL);
+    pmFPAfileActivate(config->files, true, "PSASTRO.EXTRACT.INPUT");
+
+    // really error-out here?  or just skip?
+    if (!psastroZeroPointFromRecipe (&zeropt, &exptime, NULL, fpa, recipe)) {
+        psLogMsg ("psastro", PS_LOG_INFO, "failed to load zeropt data from recipe");
+        return false;
+    }
+
+    // recipe values are given in instrumental magnitudes
+    // use the zero point and exposure time to convert to apparent mags: M_ap = M_inst + C_0 + 2.5*log(exptime)
+    float MagOffset = zeropt + 2.5*log10(exptime);
+    EXTRACT_MAX_MAG += MagOffset;
+
+    int nExt = 0;
+    int nExtGhost = 0;
+
+    // open the output file handle: we are just saving a series of extensions to this file
+    char *fileroot = psMetadataLookupStr (&status, config->arguments, "OUTPUT");
+    if (!fileroot) {
+	psLogMsg ("psastro", PS_LOG_INFO, "output fileroot not supplied");
+	return false;
+    }
+
+    char *filename = NULL;
+    psStringAppend (&filename, "%s.st.fits", fileroot);
+    psFits *outStars = psFitsOpen (filename, "w");
+    if (!outStars) {
+	psError(PS_ERR_IO, false, "error opening file %s\n", filename);
+	return false;
+    }
+    psFree (filename);
+
+    filename = NULL;
+    psStringAppend (&filename, "%s.gh.fits", fileroot);
+    psFits *outGhosts = psFitsOpen (filename, "w");
+    if (!outGhosts) {
+	psError(PS_ERR_IO, false, "error opening file %s\n", filename);
+	return false;
+    }
+    psFree (filename);
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // generate a (very) basic header:
+    psMetadata *phu = psMetadataAlloc ();
+    // select the filter; default to fixed photcode and mag limit otherwise
+    char *filter = psMetadataLookupStr (&status, fpa->concepts, "FPA.FILTERID");
+    if (!status) ESCAPE("missing FPA.FILTER in concepts");
+
+    psMetadataAddStr (phu, PS_LIST_TAIL, "FILTER",  0, "filter", filter);
+    psMetadataAddF32 (phu, PS_LIST_TAIL, "EXPTIME", 0, "exptime", exptime);
+    psMetadataAddF32 (phu, PS_LIST_TAIL, "ZEROPT",  0, "zeropt", zeropt);
+
+    psFitsWriteBlank (outStars, phu, NULL);
+    psFitsWriteBlank (outGhosts, phu, NULL);
+    psFree (phu);
+
+    // open/load files as needed
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE("failed on FPA BEFORE");
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+        if (!chip->fromFPA) { continue; }
+
+        if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE("failed on Chip BEFORE");
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                // select the raw objects for this readout (loaded in psastroExtract.c)
+                psArray *refstars = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.REFSTARS");
+                if (refstars == NULL) { continue; }
+
+                // identify the bright stars of interest
+                for (int i = 0; i < refstars->n; i++) {
+                    pmAstromObj *ref = refstars->data[i];
+                    if (ref->Mag > EXTRACT_MAX_MAG) continue;
+
+		    pmReadout *inReadout = pmFPAviewThisReadout (view, input->fpa);
+		    psImage *subraster = psastroExtractStar (inReadout->image, ref->chip->x, ref->chip->y, 200.0, 200.0);
+		    if (!subraster) continue;
+		    
+                    // generate a (very) basic header:
+                    psMetadata *header = psMetadataAlloc ();
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "CHIP_X",  0, "chip coordinate",        ref->chip->x);
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "CHIP_Y",  0, "chip coordinate",        ref->chip->y);
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "FPA_X",   0, "focal-plane coordinate", ref->FP->x);
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "FPA_Y",   0, "focal-plane coordinate", ref->FP->y);
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "MAG",     0, "magnitude",              ref->Mag);
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "INST_MAG",0, "instrumental magnitude", ref->Mag - MagOffset);
+
+		    snprintf (extname, 80, "extname.%05d", nExt);
+		    psFitsWriteImage (outStars, header, subraster, 0, extname);
+		    nExt ++;
+		    
+		    psFree (header);
+		    psFree (subraster);
+                }
+
+                // select the raw objects for this readout (loaded in psastroExtract.c)
+                psArray *ghosts = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.GHOSTS");
+                if (ghosts == NULL) { continue; }
+
+                // identify the bright stars of interest
+                for (int i = 0; i < ghosts->n; i++) {
+                    pmAstromObj *ghost = ghosts->data[i];
+
+		    pmReadout *inReadout = pmFPAviewThisReadout (view, input->fpa);
+		    psImage *subraster = psastroExtractStar (inReadout->image, ghost->chip->x, ghost->chip->y, 400.0, 400.0);
+		    if (!subraster) continue;
+		    
+                    // generate a (very) basic header:
+                    psMetadata *header = psMetadataAlloc ();
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "CHIP_X",  0, "chip coordinate",        ghost->chip->x);
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "CHIP_Y",  0, "chip coordinate",        ghost->chip->y);
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "FPA_X",   0, "focal-plane coordinate", ghost->FP->x);
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "FPA_Y",   0, "focal-plane coordinate", ghost->FP->y);
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "FPA_X0",  0, "star focal-plane coordinate", ghost->TP->x); // note : over-loaded value 
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "FPA_Y0",  0, "star focal-plane coordinate", ghost->TP->y); // note : over-loaded value 
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "MAG",     0, "magnitude",              ghost->Mag);
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "INST_MAG",0, "instrumental magnitude", ghost->Mag - MagOffset);
+
+		    snprintf (extname, 80, "extname.%05d", nExtGhost);
+		    psFitsWriteImage (outGhosts, header, subraster, 0, extname);
+		    nExtGhost ++;
+		    
+		    psFree (header);
+		    psFree (subraster);
+                }
+                if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE("failed on Readout AFTER");;
+            }
+            if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE("failed on Cell AFTER");;
+        }
+        if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE("failed on Chip AFTER");;
+    }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE("failed on FPA AFTER");;
+
+    psFitsClose (outStars);
+    psFitsClose (outGhosts);
+    psFree (view);
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroFindChip.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroFindChip.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroFindChip.c	(revision 41084)
@@ -0,0 +1,174 @@
+/** @file psastroFindChip.c
+ *
+ *  @brief calculate chip position for the given FPA coords
+ *
+ *  @ingroup psastroExtract
+ *
+ *  @author IfA
+ *  @version $Revision: 1.7 $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+static psVector *chipXmin = NULL;
+static psVector *chipXmax = NULL;
+static psVector *chipYmin = NULL;
+static psVector *chipYmax = NULL;
+
+bool psastroChipBounds (pmFPA *fpa) {
+
+    chipXmin = psVectorAlloc (fpa->chips->n, PS_TYPE_F32);
+    chipXmax = psVectorAlloc (fpa->chips->n, PS_TYPE_F32);
+    chipYmin = psVectorAlloc (fpa->chips->n, PS_TYPE_F32);
+    chipYmax = psVectorAlloc (fpa->chips->n, PS_TYPE_F32);
+
+    // this loop selects the matched stars for all chips
+    for (int i = 0; i < fpa->chips->n; i++) {
+
+      pmChip *chip = fpa->chips->data[i];
+      if (!chip->process || !chip->file_exists) { continue; }
+      if (!chip->fromFPA) { continue; }
+
+      // determine RA,DEC of 4 corners, use to find RA_MIN,MAX, DEC_MIN,MAX
+      psRegion *region = pmChipPixels (chip);
+      psPlane ptCH[4], ptFP;
+
+      ptCH[0].x = region->x0;
+      ptCH[0].y = region->y0;
+      ptCH[1].x = region->x1;
+      ptCH[1].y = region->y0;
+      ptCH[2].x = region->x1;
+      ptCH[2].y = region->y1;
+      ptCH[3].x = region->x0;
+      ptCH[3].y = region->y1;
+      psFree (region);
+      
+      double Xmin = +FLT_MAX;
+      double Xmax = -FLT_MAX;
+      double Ymin = +FLT_MAX;
+      double Ymax = -FLT_MAX;
+
+      for (int j = 0; j < 4; j++) {
+	psPlaneTransformApply (&ptFP, chip->toFPA, &ptCH[j]);
+	Xmin = PS_MIN (ptFP.x, Xmin);
+	Xmax = PS_MAX (ptFP.x, Xmax);
+	Ymin = PS_MIN (ptFP.y, Ymin);
+	Ymax = PS_MAX (ptFP.y, Ymax);
+      }
+
+      // fpa-range for the given chip
+      chipXmin->data.F32[i] = Xmin;
+      chipXmax->data.F32[i] = Xmax;
+      chipYmin->data.F32[i] = Ymin;
+      chipYmax->data.F32[i] = Ymax;
+    }
+
+    return true;
+}
+
+pmChip *psastroFindChip (double *xChip, double *yChip, pmFPA *fpa, double xFPA, double yFPA) {
+
+    *xChip = NAN;
+    *yChip = NAN;
+
+    if (!chipXmin) {
+	psastroChipBounds (fpa);
+    }
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+
+	if (xFPA <  chipXmin->data.F32[i]) continue;
+	if (xFPA >= chipXmax->data.F32[i]) continue;
+	if (yFPA <  chipYmin->data.F32[i]) continue;
+	if (yFPA >= chipYmax->data.F32[i]) continue;
+
+	pmChip *chip = fpa->chips->data[i];
+	psRegion *region = pmChipPixels (chip);
+
+	psPlane ptCH, ptFP;
+	ptFP.x = xFPA;
+	ptFP.y = yFPA;
+	psPlaneTransformApply (&ptCH, chip->fromFPA, &ptFP);
+
+	if (ptCH.x <  region->x0) goto next_chip;
+	if (ptCH.x >= region->x1) goto next_chip;
+	if (ptCH.y <  region->y0) goto next_chip;
+	if (ptCH.y >= region->y1) goto next_chip;
+	psFree (region);
+
+	*xChip = ptCH.x;
+	*yChip = ptCH.y;
+	return chip;
+
+    next_chip:
+	psFree (region);
+    }
+
+    return NULL;
+}
+
+// identify chips which land on this column (FP coords)
+bool psastroFindChipInXrange (pmFPA *fpa, int nChip, double xFPA, double yFPA) {
+
+    if (!chipXmin || !chipXmax) {
+	psAbort ("chip bounds not set");
+    }
+
+    if (xFPA <  chipXmin->data.F32[nChip]) return false;
+    if (xFPA >= chipXmax->data.F32[nChip]) return false;
+    return true;
+}
+
+// identify chips which land on this row (FP coords)
+bool psastroFindChipInYrange (pmFPA *fpa, int nChip, double xFPA, double yFPA) {
+
+    if (!chipYmin || !chipYmax) {
+	psAbort ("chip bounds not set");
+    }
+
+    if (yFPA <  chipYmin->data.F32[nChip]) return false;
+    if (yFPA >= chipYmax->data.F32[nChip]) return false;
+    return true;
+}
+
+// return the FPA coordinates of the Y edges of the chip
+bool psastroFindChipYedges (double *yFPAs, double *yFPAe, pmFPA *fpa, int nChip) {
+
+    *yFPAs = chipYmin->data.F32[nChip];
+    *yFPAe = chipYmax->data.F32[nChip];
+    return true;
+}
+
+// return the FPA coordinates of the X edges of the chip
+bool psastroFindChipXedges (double *yFPAs, double *yFPAe, pmFPA *fpa, int nChip) {
+
+    *yFPAs = chipXmin->data.F32[nChip];
+    *yFPAe = chipXmax->data.F32[nChip];
+    return true;
+}
+
+// convert FPA to Chip coordinates
+bool psastroFPAtoChip (double *xChip, double *yChip, pmFPA *fpa, int nChip, double xFPA, double yFPA) {
+
+    pmChip *chip = fpa->chips->data[nChip];
+
+    psPlane ptCH, ptFP;
+    ptFP.x = xFPA;
+    ptFP.y = yFPA;
+    psPlaneTransformApply (&ptCH, chip->fromFPA, &ptFP);
+
+    *xChip = ptCH.x;
+    *yChip = ptCH.y;
+    return true;
+}
+
+bool psastroExtractFreeChipBounds () {
+  
+  psFree (chipXmin);
+  psFree (chipXmax);
+  psFree (chipYmin);
+  psFree (chipYmax);
+  return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroFixChips.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroFixChips.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroFixChips.c	(revision 41084)
@@ -0,0 +1,347 @@
+/** @file psastroFixChips.c
+ *
+ *  @brief
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-09 21:25:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+# define NONLIN_TOL 0.001 ///< tolerance in pixels
+# define DEBUG 0
+
+// XXX I think the 'badAstrom' tests may need to be adjusted: see eg the nominal rotation of
+// the chips
+bool psastroFixChips (pmConfig *config, psMetadata *recipe) {
+
+    bool status;
+    FILE *f;
+    char *chipName;
+
+    bool fixChips = psMetadataLookupBool (&status, config->arguments, "PSASTRO.FIX.CHIPS");
+    if (!status) {
+        fixChips = psMetadataLookupBool (&status, recipe, "PSASTRO.FIX.CHIPS");
+    }
+    if (!fixChips) return true;
+
+    // identify reference astrometry table
+    // if not defined, correction was not requested; skip step
+    pmFPAfile *astrom = psMetadataLookupPtr (NULL, config->files, "PSASTRO.MODEL");
+    if (!astrom) return true;
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+        return false;
+    }
+
+    // acceptable limits
+    double pixelTol = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.TOLERANCE");
+    if (!status) psAbort ("missing recipe value");
+
+    double angleTol = psMetadataLookupF32 (&status, recipe, "PSASTRO.ANGLE.TOLERANCE");
+    if (!status) psAbort ("missing recipe value");
+
+    // make sure the astrometry model is loaded.  de-activate all files except PSASTRO.MODEL.
+    // supply the input concepts so the model is defined using the RA, DEC, POSANGLE of the input
+    // image.
+    psFree (astrom->fpa->concepts);
+    astrom->fpa->concepts = psMemIncrRefCounter (input->fpa->concepts);
+    pmFPAfileActivate (config->files, false, NULL);
+    pmFPAfileActivate (config->files, true, "PSASTRO.MODEL");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // files associated with the science image
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) {
+        psError (PS_ERR_IO, false, "Can't load the astrometry model file");
+        return false;
+    }
+
+    // We now have a set of chip solutions and a set of predictions.  Measure the overall offset and
+    // rotation of the two systems by comparing the chip corners, projected onto the focal plane
+    // (all 4 to prevent solutions tied to a single corner)
+
+    psVector *xObs = psVectorAllocEmpty (4*input->fpa->chips->n, PS_TYPE_F32);
+    psVector *yObs = psVectorAllocEmpty (4*input->fpa->chips->n, PS_TYPE_F32);
+    psVector *xRef = psVectorAllocEmpty (4*input->fpa->chips->n, PS_TYPE_F32);
+    psVector *yRef = psVectorAllocEmpty (4*input->fpa->chips->n, PS_TYPE_F32);
+
+    int nPts = 0;
+
+    if (DEBUG) {
+        f = fopen ("corners.raw.dat", "w");
+        chipName = NULL;
+    }
+
+    pmChip *obsChip = NULL;
+    while ((obsChip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+        if (!obsChip->process || !obsChip->file_exists || !obsChip->data_exists) { continue; }
+
+        // XXX we are currently inconsistent with marking the good vs the bad data
+        // psastroChipAstrom sets data_exists to false if the fit is bad.  this is
+        // probably wrong since it implies there is no data!
+
+        // skip chips for which the astrometry failed (NASTRO == 0)
+        if (!obsChip->cells->n) continue;
+        pmCell *cell = obsChip->cells->data[0];
+        if (!cell) continue;
+
+        if (!cell->readouts->n) continue;
+        pmReadout *readout = cell->readouts->data[0];
+        if (!readout) continue;
+
+        psMetadata *updates = psMetadataLookupMetadata (&status, readout->analysis, "PSASTRO.HEADER");
+        if (!updates) continue;
+
+        int nAstro = psMetadataLookupS32 (&status, updates, "NASTRO");
+        if (!nAstro) continue;
+
+        // set the chip astrometry using the astrom file
+        pmChip *refChip = pmFPAviewThisChip (view, astrom->fpa);
+
+        psRegion *region = pmChipPixels (obsChip);
+        psPlane ptCP, ptFP;
+
+        ptCP.x = region->x0; ptCP.y = region->y0;
+        psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+        xObs->data.F32[nPts] = ptFP.x;
+        yObs->data.F32[nPts] = ptFP.y;
+        psPlaneTransformApply (&ptFP, refChip->toFPA, &ptCP);
+        xRef->data.F32[nPts] = ptFP.x;
+        yRef->data.F32[nPts] = ptFP.y;
+
+        if (DEBUG) {
+            chipName = psMetadataLookupStr(NULL, obsChip->concepts, "CHIP.NAME");
+            fprintf (f, "%s  %f %f  %f %f\n", chipName, xObs->data.F32[nPts], yObs->data.F32[nPts], xRef->data.F32[nPts], yRef->data.F32[nPts]);
+        }
+        nPts ++;
+
+        ptCP.x = region->x0; ptCP.y = region->y1;
+        psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+        xObs->data.F32[nPts] = ptFP.x;
+        yObs->data.F32[nPts] = ptFP.y;
+        psPlaneTransformApply (&ptFP, refChip->toFPA, &ptCP);
+        xRef->data.F32[nPts] = ptFP.x;
+        yRef->data.F32[nPts] = ptFP.y;
+
+        if (DEBUG) {
+            chipName = psMetadataLookupStr(NULL, obsChip->concepts, "CHIP.NAME");
+            fprintf (f, "%s  %f %f  %f %f\n", chipName, xObs->data.F32[nPts], yObs->data.F32[nPts], xRef->data.F32[nPts], yRef->data.F32[nPts]);
+        }
+        nPts ++;
+
+        ptCP.x = region->x1; ptCP.y = region->y1;
+        psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+        xObs->data.F32[nPts] = ptFP.x;
+        yObs->data.F32[nPts] = ptFP.y;
+        psPlaneTransformApply (&ptFP, refChip->toFPA, &ptCP);
+        xRef->data.F32[nPts] = ptFP.x;
+        yRef->data.F32[nPts] = ptFP.y;
+
+        if (DEBUG) {
+            chipName = psMetadataLookupStr(NULL, obsChip->concepts, "CHIP.NAME");
+            fprintf (f, "%s  %f %f  %f %f\n", chipName, xObs->data.F32[nPts], yObs->data.F32[nPts], xRef->data.F32[nPts], yRef->data.F32[nPts]);
+        }
+        nPts ++;
+
+        ptCP.x = region->x1; ptCP.y = region->y0;
+        psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+        xObs->data.F32[nPts] = ptFP.x;
+        yObs->data.F32[nPts] = ptFP.y;
+        psPlaneTransformApply (&ptFP, refChip->toFPA, &ptCP);
+        xRef->data.F32[nPts] = ptFP.x;
+        yRef->data.F32[nPts] = ptFP.y;
+
+        if (DEBUG) {
+            chipName = psMetadataLookupStr(NULL, obsChip->concepts, "CHIP.NAME");
+            fprintf (f, "%s  %f %f  %f %f\n", chipName, xObs->data.F32[nPts], yObs->data.F32[nPts], xRef->data.F32[nPts], yRef->data.F32[nPts]);
+        }
+        nPts ++;
+
+        psFree (region);
+    }
+    xObs->n = yObs->n = xRef->n = yRef->n = nPts;
+    if (DEBUG) fclose (f);
+
+    psPlaneTransform *map = psPlaneTransformAlloc (1, 1);
+
+    psVector *mask = psVectorAlloc (nPts, PS_TYPE_VECTOR_MASK);
+    psVectorInit (mask, 0);
+
+    psStats *stats = psStatsAlloc (PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV);
+    stats->clipIter = 1;
+
+    for (int i = 0; i < 3; i++) {
+        psVectorClipFitPolynomial2D (map->x, stats, mask, 0xff, xObs, NULL, xRef, yRef);
+        psTrace ("psModules.astrom", 3, "x resid: %f +/- %f (%ld of %ld)\n", stats->clippedMean, stats->clippedStdev, stats->clippedNvalues, xObs->n);
+
+        psVectorClipFitPolynomial2D (map->y, stats, mask, 0xff, yObs, NULL, xRef, yRef);
+        psTrace ("psModules.astrom", 3, "y resid: %f +/- %f (%ld of %ld)\n", stats->clippedMean, stats->clippedStdev, stats->clippedNvalues, yObs->n);
+    }
+
+    // loop over all chips, select the outliers, and replace the measured astrometry with the model
+    // the measured transformation above must be applied to make the comparison, and also then applied to the
+    // model transformation
+
+    if (DEBUG) {
+        f = fopen ("corners.fit.dat", "w");
+        for (int i = 0; i < xObs->n; i++) {
+            psPlane obsCoord, refCoord;
+            refCoord.x = xRef->data.F32[i];
+            refCoord.y = yRef->data.F32[i];
+            psPlaneTransformApply (&obsCoord, map, &refCoord);
+            fprintf (f, "%f %f  %f %f  %f %f\n", xObs->data.F32[i], yObs->data.F32[i], xRef->data.F32[i], yRef->data.F32[i], obsCoord.x, obsCoord.y);
+        }
+        fclose (f);
+    }
+
+    psFree (xRef);
+    psFree (yRef);
+    psFree (mask);
+    psFree (stats);
+
+    psFree (view);
+    view = pmFPAviewAlloc (0);
+
+    while ((obsChip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, obsChip->file_exists, obsChip->process);
+        if (!obsChip->process || !obsChip->file_exists || !obsChip->data_exists) { continue; }
+
+        // set the chip astrometry using the astrom file
+        pmChip *refChip = pmFPAviewThisChip (view, astrom->fpa);
+
+        // bad Astrometry test:  ref pixel or angle outside nominal
+
+        psPlane refPixel = {0.0, 0.0, 0.0, 0.0};
+        psPlane obsCoord, refCoord, tmpCoord;
+
+        // find location of 0,0 pixel in focal plane coords for this chip
+        psPlaneTransformApply (&obsCoord, obsChip->toFPA, &refPixel);
+
+        // find location of 0,0 pixel in focal plane coords for ref chip
+        // apply the global field rotation and offset before comparing
+        psPlaneTransformApply (&tmpCoord, refChip->toFPA, &refPixel);
+        psPlaneTransformApply (&refCoord, map, &tmpCoord);
+
+        psPlane offPixel = {100.0, 0.0, 100.0, 0.0};
+        psPlane obsOffPt, refOffPt;
+
+        // find location of 0,0 pixel in focal plane coords for this chip
+        psPlaneTransformApply (&obsOffPt, obsChip->toFPA, &offPixel);
+
+        // find location of 0,0 pixel in focal plane coords for ref chip
+        psPlaneTransformApply (&tmpCoord, refChip->toFPA, &offPixel);
+        psPlaneTransformApply (&refOffPt, map, &tmpCoord);
+
+        double obsAngle = PM_DEG_RAD*atan2 (obsOffPt.y - obsCoord.y, obsOffPt.x - obsCoord.x);
+        double refAngle = PM_DEG_RAD*atan2 (refOffPt.y - refCoord.y, refOffPt.x - refCoord.x);
+
+        bool badAstrom = false;
+        badAstrom |= fabs(obsCoord.x - refCoord.x) > pixelTol;
+        badAstrom |= fabs(obsCoord.y - refCoord.y) > pixelTol;
+        badAstrom |= fabs(obsAngle   - refAngle)   > angleTol;
+
+        fprintf (stderr, "chip %d, angle: %f, pixel: %f,%f  : ", view->chip, obsAngle - refAngle, obsCoord.x - refCoord.x, obsCoord.y - refCoord.y);
+	if (badAstrom) {
+	    fprintf (stderr, "BAD ASTROM\n");
+	} else {
+	    fprintf (stderr, "GOOD ASTROM\n");
+	}
+
+        // XXX for now, just use first readout
+        pmCell *cell = obsChip->cells->data[0];
+        pmReadout *readout = cell->readouts->data[0];
+
+        // update the header (pull or create local view to entry on readout->analysis)
+        psMetadata *updates = psMetadataLookupMetadata (&status, readout->analysis, "PSASTRO.HEADER");
+        if (!updates) {
+            updates = psMetadataAlloc ();
+            psMetadataAddMetadata (readout->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", updates);
+            psFree (updates);
+        }
+
+        psMetadataAddF32 (updates, PS_LIST_TAIL, "AST_DX", PS_META_REPLACE, "chip x offset wrt model", obsCoord.x - refCoord.x);
+        psMetadataAddF32 (updates, PS_LIST_TAIL, "AST_DY", PS_META_REPLACE, "chip y offset wrt model", obsCoord.y - refCoord.y);
+        psMetadataAddF32 (updates, PS_LIST_TAIL, "AST_DT", PS_META_REPLACE, "chip rot offset wrt model", obsAngle - refAngle);
+
+	continue;
+
+        // for successful chips, save the measured offsets in the header
+        if (!badAstrom) continue;
+
+        // XXX for now, let's just fail on the bad chips.  In the future, let's try to recover, but we still need to
+        // catch the failures relative to the model
+        psMetadataAddS32 (updates, PS_LIST_TAIL, "NASTRO", PS_META_REPLACE, "number of astrometry stars", 0);
+        continue;
+
+        psLogMsg ("psastro", PS_LOG_INFO, "fixing chip %d, angle: %f, pixel: %f,%f\n",
+                  view->chip, obsAngle - refAngle, obsCoord.x - refCoord.x, obsCoord.y - refCoord.y);
+
+        psFree (obsChip->toFPA);
+        psFree (obsChip->fromFPA);
+
+        // apply the exiting fromTPA transformation to make the new toFPA consistent with the toTPA layter
+        // XXX this only works if toTPA is at most a linear transformation
+        psPlaneTransform *toFPA = psPlaneTransformAlloc(refChip->toFPA->x->nX, refChip->toFPA->x->nY);
+        for (int i = 0; i <= refChip->toFPA->x->nX; i++) {
+            for (int j = 0; j <= refChip->toFPA->x->nY; j++) {
+                double f1 = refChip->toFPA->x->coeffMask[i][j] ? 0.0 : map->x->coeff[1][0]*refChip->toFPA->x->coeff[i][j];
+                double f2 = refChip->toFPA->y->coeffMask[i][j] ? 0.0 : map->x->coeff[0][1]*refChip->toFPA->y->coeff[i][j];
+                toFPA->x->coeff[i][j] = f1 + f2;
+
+                double g1 = refChip->toFPA->x->coeffMask[i][j] ? 0.0 : map->y->coeff[1][0]*refChip->toFPA->x->coeff[i][j];
+                double g2 = refChip->toFPA->y->coeffMask[i][j] ? 0.0 : map->y->coeff[0][1]*refChip->toFPA->y->coeff[i][j];
+                toFPA->y->coeff[i][j] = g1 + g2;
+            }
+        }
+        toFPA->x->coeff[0][0] += map->x->coeff[0][0];
+        toFPA->y->coeff[0][0] += map->y->coeff[0][0];
+
+        psRegion *region = pmChipPixels (obsChip);
+        obsChip->toFPA   = toFPA;
+	// NOTE: when we call psPlaneTransformInvert here, we do not increase the order as in other places
+        obsChip->fromFPA = psPlaneTransformInvert(NULL, obsChip->toFPA, *region, 50);
+        psFree (region);
+
+        // use the new position to re-try the match fit
+        // select the raw objects for this readout
+        psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+        if (rawstars == NULL) { continue; }
+
+        // select the raw objects for this readout
+        psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+        if (refstars == NULL) { continue; }
+
+        // the absolute minimum number of stars is 4 (for order = 1)
+        if ((rawstars->n < 4) || (refstars->n < 4)) {
+            readout->data_exists = false;
+            psLogMsg ("psastro", 3, "insufficient rawstars (%ld) or refstars (%ld)",
+                      rawstars->n, refstars->n);
+            continue;
+        }
+
+        psastroUpdateChipToFPA (input->fpa, obsChip);
+
+        // XXX update the header with info to reflect the failure
+        if (!psastroOneChipFit (input->fpa, obsChip, readout, refstars, rawstars, recipe, updates)) {
+            readout->data_exists = false;
+            psLogMsg ("psastro", 3, "failed to find a solution\n");
+            continue;
+        }
+
+        pmAstromWriteWCS (updates, input->fpa, obsChip, NONLIN_TOL);
+    }
+
+    pmAstromVisualPlotFixChips (input, xObs, yObs);
+    psFree (xObs);
+    psFree (yObs);
+    psFree (map);
+    psFree (view);
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroFixChipsTest.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroFixChipsTest.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroFixChipsTest.c	(revision 41084)
@@ -0,0 +1,270 @@
+/** @file psastroFixChipsTest.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+# define NONLIN_TOL 0.001 /* tolerance in pixels */
+
+bool psPlaneTransformPrint (FILE *f, psPlaneTransform *in) {
+
+  fprintf (f, "x (%d,%d):\n", in->x->nX, in->x->nY);
+  for (int i = 0; i <= in->x->nX; i++) {
+    fprintf (f, " %d : ", i);
+    for (int j = 0; j <= in->x->nY; j++) {
+      fprintf (f, "%f ", in->x->coeff[i][j]);
+    }
+    fprintf (f, "\n");
+  }
+  fprintf (f, "y (%d,%d):\n", in->y->nX, in->y->nY);
+  for (int i = 0; i <= in->y->nX; i++) {
+    fprintf (f, " %d : ", i);
+    for (int j = 0; j <= in->y->nY; j++) {
+      fprintf (f, "%f ", in->y->coeff[i][j]);
+    }
+    fprintf (f, "\n");
+  }
+  return TRUE;
+}
+
+// XXX the designed version is not working correctly. Here we load a model which just has
+// the CRPIX1, CRPIX2 values from another image. Compare those positions to the current
+// postions, make an average (DC) correction, then fix the outliers.
+bool psastroFixChipsTest (pmConfig *config, psMetadata *recipe) {
+
+    bool status;
+
+    bool fixChips = psMetadataLookupBool (&status, config->arguments, "PSASTRO.FIX.CHIPS");
+    if (!status) {
+	fixChips = psMetadataLookupBool (&status, recipe, "PSASTRO.FIX.CHIPS");
+    }
+    if (!fixChips) return true;
+
+    // identify reference astrometry table
+    // if not defined, correction was not requested; skip step
+    char *modelFile = psMetadataLookupStr (&status, recipe, "PSASTRO.ROUGH.MODEL");
+    if (!modelFile) return true;
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+	return false;
+    }
+
+    // physical pixel scale in microns per pixel
+    double pixelScale = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.SCALE");
+    if (!status) {
+	psError(PS_ERR_IO, true, "Failed to lookup pixel scale"); 
+	return false; 
+    } 
+
+    // acceptable limits
+    double pixelTol = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.TOLERANCE");
+    if (!status) psAbort ("missing recipe value");
+
+    FILE *f = fopen (modelFile, "r");
+    if (f == NULL) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find model data %s", modelFile);
+	return false;
+    }
+
+    float Xo, Yo;
+
+    psArray *refPos = psArrayAllocEmpty (100);
+    while (fscanf (f, "%*s %*d %*d %*f %f %f", &Xo, &Yo) != EOF) {
+	psPlane *pos = psPlaneAlloc();
+	pos->x = Xo;
+	pos->y = Yo;
+	psArrayAdd (refPos, 100, pos);
+	psFree (pos);
+    }
+    assert (refPos->n == 60);
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // loop over all chips, get the current CRPIX1,CRPIX2 values
+    pmChip *obsChip = NULL;
+    psArray *obsPos = psArrayAllocEmpty (100);
+    while ((obsChip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+	psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, obsChip->file_exists, obsChip->process);
+	if (!obsChip->process || !obsChip->file_exists || !obsChip->data_exists) { continue; }
+
+	// extract CRPIX1, CRPIX2:
+	// XXX for now, pull from the header:
+	// pmCell *cell = obsChip->cells->data[0];
+	// pmReadout *readout = cell->readouts->data[0];
+	// psMetadata *header = psMetadataLookupMetadata (&status, readout->analysis, "PSASTRO.HEADER");
+	// Xo = psMetadataLookupF32 (&status, header, "CRPIX1");
+	// Yo = psMetadataLookupF32 (&status, header, "CRPIX2");
+
+	pmAstromWCS *wcs = pmAstromWCSfromFPA(input->fpa, obsChip, NONLIN_TOL);
+	Xo = wcs->crpix1;
+	Yo = wcs->crpix2;
+	psFree (wcs);
+
+	psPlane *pos = psPlaneAlloc();
+	pos->x = Xo;
+	pos->y = Yo;
+	psArrayAdd (obsPos, 100, pos);
+	psFree (pos);
+    }
+    psFree (view);
+    assert (obsPos->n == 60);
+
+    // get the offsets
+    psVector *dX = psVectorAlloc (obsPos->n, PS_TYPE_F32);
+    psVector *dY = psVectorAlloc (obsPos->n, PS_TYPE_F32);
+    for (int i = 0; i < obsPos->n; i++) {
+	psPlane *obs = obsPos->data[i];
+	psPlane *ref = refPos->data[i];
+    
+	if (i < 30) {
+	    dX->data.F32[i] = obs->x - ref->x;
+	    dY->data.F32[i] = obs->y - ref->y;
+	} else {
+	    dX->data.F32[i] = ref->x - obs->x;
+	    dY->data.F32[i] = ref->y - obs->y;
+	}
+	fprintf (stderr, "chip %d : %f - %f : %f, %f - %f: %f\n", i, obs->x, ref->x, dX->data.F32[i], obs->y, ref->y, dY->data.F32[i]);
+    }
+    
+    // get the average offset
+    psStats *stats;
+    stats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+    if (!psVectorStats (stats, dX, NULL, NULL, 0)) {
+	psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	return false;
+    }
+    Xo = stats->robustMedian;
+    fprintf (stderr, "offset x: %f +/- %f\n", stats->robustMedian, stats->robustStdev);
+    psFree (stats);
+
+    stats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+    if (!psVectorStats (stats, dY, NULL, NULL, 0)) {
+	psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	return false;
+    }
+    Yo = stats->robustMedian;
+    fprintf (stderr, "offset y: %f +/- %f\n", stats->robustMedian, stats->robustStdev);
+    psFree (stats);
+
+    // measure the rotation of the good chips
+    psVector *dT = psVectorAllocEmpty (obsPos->n, PS_TYPE_F32);
+    for (int i = 0; i < dX->n; i++) {
+	bool badAstrom = false;
+	badAstrom |= (fabs(dX->data.F32[i] - Xo) > pixelTol);
+	badAstrom |= (fabs(dY->data.F32[i] - Yo) > pixelTol);
+	if (badAstrom) continue;
+
+	pmChip *chip = input->fpa->chips->data[i];
+	float theta = atan2 (chip->toFPA->x->coeff[0][1], chip->toFPA->x->coeff[1][0]);
+	if (i >= 30) {
+	  theta -= M_PI;
+	}
+	dT->data.F32[dT->n] = theta;
+	dT->n ++;
+    }
+    stats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+    if (!psVectorStats (stats, dT, NULL, NULL, 0)) {
+	psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	return false;
+    }
+    float To = stats->robustMedian;
+    fprintf (stderr, "offset t: %f +/- %f\n", stats->robustMedian, stats->robustStdev);
+    psFree (stats);
+
+    // find the discrepant chips and reset
+    for (int i = 0; i < dX->n; i++) {
+	bool badAstrom = false;
+	badAstrom |= (fabs(dX->data.F32[i] - Xo) > pixelTol);
+	badAstrom |= (fabs(dY->data.F32[i] - Yo) > pixelTol);
+	if (!badAstrom) continue;
+
+	fprintf (stderr, "fixing chip %d, offset: %f %f\n",
+		 i, dX->data.F32[i] - Xo, dY->data.F32[i] - Yo);
+
+	// XXX this stuff below should be applied to each readout...
+	// XXX for now, just use first readout
+	pmFPA  *fpa  = input->fpa;
+	pmChip *chip = fpa->chips->data[i];
+	pmCell *cell = chip->cells->data[0];
+	pmReadout *readout = cell->readouts->data[0];
+
+	pmAstromWCS *wcs = pmAstromWCSfromFPA(fpa, chip, NONLIN_TOL);
+
+	psPlane *ref = refPos->data[i];
+	wcs->crpix1 = ref->x + Xo;
+	wcs->crpix2 = ref->y + Yo;
+
+	pmAstromWCStoFPA (fpa, chip, wcs, pixelScale);
+	psFree (wcs);
+
+	// apply average rotation
+	psPlaneTransform *toFPA = psPlaneTransformRotate (NULL, chip->toFPA, To);
+	psFree (chip->toFPA);
+	chip->toFPA = toFPA;
+
+	psPlaneTransform *fromFPA = psPlaneTransformRotate (NULL, chip->fromFPA, -To);
+	psFree (chip->fromFPA);
+	chip->fromFPA = fromFPA;
+
+	// XXX did we get the offset right?
+	wcs = pmAstromWCSfromFPA(fpa, chip, NONLIN_TOL);
+	psFree (wcs);
+
+	// save WCS and analysis metadata in update header
+	// (pull or create local view to entry on readout->analysis)
+	psMetadata *updates = psMetadataLookupMetadata (&status, readout->analysis, "PSASTRO.HEADER");
+	if (!updates) {
+	    updates = psMetadataAlloc ();
+	    psMetadataAddMetadata (readout->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", updates);
+	    psFree (updates);
+	}
+
+	// select the raw objects for this readout
+	psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+	if (rawstars == NULL) { continue; }
+
+	// select the raw objects for this readout
+	psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+	if (refstars == NULL) { continue; }
+
+	// the absolute minimum number of stars is 4 (for order = 1)
+	if ((rawstars->n < 4) || (refstars->n < 4)) {
+	    readout->data_exists = false;
+	    psLogMsg ("psastro", 3, "insufficient rawstars (%ld) or refstars (%ld)", 
+		      rawstars->n, refstars->n);
+	    continue;
+	} 
+
+	psastroUpdateChipToFPA (fpa, chip);
+
+	// XXX skip the re-fit step for a test
+	continue;
+
+	// XXX update the header with info to reflect the failure
+	if (!psastroOneChipFit (fpa, chip, readout, refstars, rawstars, recipe, updates)) {
+	    readout->data_exists = false;
+	    psLogMsg ("psastro", 3, "failed to find a solution\n");
+	    continue;
+	}
+
+	pmAstromWriteWCS (updates, fpa, chip, NONLIN_TOL);
+    }
+
+    psFree (dT);
+    psFree (dX);
+    psFree (dY);
+    psFree (refPos);
+    psFree (obsPos);
+  
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroGalaxyShapeErrors.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroGalaxyShapeErrors.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroGalaxyShapeErrors.c	(revision 41084)
@@ -0,0 +1,176 @@
+/** @file psastroGalaxyShapeError.c
+ *
+ *  @brief: monte carlo transformation of Sxx,Sxy,Syy -> Major,Minor,Theta
+ *
+ *  @ingroup psastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.15 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+double psastroNormalizeAngleToMidpoint (double angle, double midpoint);
+
+bool psastroGalaxyShapeErrors (psMetadata *recipe, pmReadout *readout) {
+
+    bool status = false;
+
+    bool TransformGalaxyShapeErrors = psMetadataLookupBool (&status, recipe, "TRANSFORM.GALAXY.SHAPE.ERRORS");
+    if (!TransformGalaxyShapeErrors) return true;
+
+    psTimerStart ("psastro.galaxy.shape.errors");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    if (!detections) return false;
+
+    psArray *sources = detections->allSources;
+    if (!detections) return false;
+
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS);
+
+    int nSample = 1000;
+    psVector *majorValues = psVectorAllocEmpty (nSample, PS_TYPE_F32);
+    psVector *minorValues = psVectorAllocEmpty (nSample, PS_TYPE_F32);
+    psVector *thetaValues = psVectorAllocEmpty (nSample, PS_TYPE_F32);
+
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+
+    int nObjects = 0;
+
+    // don't try and use bad models
+    pmModelStatus badModel = PM_MODEL_STATUS_NONE;
+    // badModel |= PM_MODEL_STATUS_NONCONVERGE;  this causes most objects to get unmeasured
+    badModel |= PM_MODEL_STATUS_BADARGS;
+    badModel |= PM_MODEL_STATUS_OFFIMAGE;
+    badModel |= PM_MODEL_STATUS_NAN_CHISQ;
+    badModel |= PM_MODEL_SERSIC_PCM_FAIL_GUESS;
+    badModel |= PM_MODEL_SERSIC_PCM_FAIL_GRID;
+    badModel |= PM_MODEL_PCM_FAIL_GUESS;
+    badModel |= PM_MODEL_STATUS_LIMITS;
+
+    // transform all sources, regardless of quality.  the source flags tell us the state
+    for (int i = 0; i < sources->n; i++) {
+
+        pmSource *source = sources->data[i];
+        if (source->modelFits == NULL) continue;
+
+        // We have multiple model fits to each source
+        for (int j = 0; j < source->modelFits->n; j++) {
+
+            // choose each model
+            pmModel *model = source->modelFits->data[j];
+            assert (model);
+
+	    // skip models which were not actually fitted
+	    if (model->flags & badModel) continue;
+
+            float *PAR = model->params->data.F32;
+            float *dPAR = model->dparams->data.F32;
+
+	    if (!(isfinite(PAR[PM_PAR_SXX]) && isfinite(PAR[PM_PAR_SXY]) && isfinite(PAR[PM_PAR_SYY]))) {
+	      // set a mask bit
+	      model->flags |= PM_MODEL_STATUS_NAN_SHAPE;
+	      continue;
+	    }
+	    if (!(isfinite(dPAR[PM_PAR_SXX]) && isfinite(dPAR[PM_PAR_SXY]) && isfinite(dPAR[PM_PAR_SYY]))) {
+	      // set a (different) mask bit
+	      model->flags |= PM_MODEL_STATUS_NAN_SHAPE_ERR;
+	      continue;
+	    }
+
+	    // psphot writes out dPAR, but PAR is writen as MAJOR,MINOR,THETA but transformed
+	    // to PAR[SXX,SXY,SYY] when the cmf is read.  here we use a monte carlo to generate
+	    // a set of PAR[] values which are converted to MAJOR,MINOR,THETA so sigma can be
+	    // measured.
+
+	    majorValues->n = 0;
+	    minorValues->n = 0;
+	    thetaValues->n = 0;
+
+	    bool useReff = model->class->useReff;
+	    psEllipseAxes axes;
+
+	    float SxxRef = PAR[PM_PAR_SXX];
+	    float SxyRef = PAR[PM_PAR_SXY];
+	    float SyyRef = PAR[PM_PAR_SYY];
+	    if (!(isfinite(SxxRef) && isfinite(SxyRef) && isfinite(SyyRef))) {
+	      // skip objects which are already poor ellipses
+	      model->flags |= PM_MODEL_STATUS_NAN_SHAPE;
+	      continue;
+	    }
+	    pmModelParamsToAxes (&axes, SxxRef, SxyRef, SyyRef, useReff);
+	    if (!(isfinite(axes.major) && isfinite(axes.minor) && isfinite(axes.theta))) {
+	      // skip objects which are already poor ellipses
+	      model->flags |= PM_MODEL_STATUS_NAN_SHAPE;
+	      continue;
+	    }
+	    double ThetaRef = axes.theta;
+
+	    for (int k = 0; k < nSample; k++) {
+
+		float Sxx = PAR[PM_PAR_SXX] + dPAR[PM_PAR_SXX]*psRandomGaussian(rng);
+		float Sxy = PAR[PM_PAR_SXY] + dPAR[PM_PAR_SXY]*psRandomGaussian(rng);
+		float Syy = PAR[PM_PAR_SYY] + dPAR[PM_PAR_SYY]*psRandomGaussian(rng);
+		if (!(isfinite(Sxx) && isfinite(Sxy) && isfinite(Syy))) {
+		  continue;
+		}
+
+		pmModelParamsToAxes (&axes, Sxx, Sxy, Syy, useReff);
+		if (!(isfinite(axes.major) && isfinite(axes.minor) && isfinite(axes.theta))) continue;
+
+		// theta should be in range ThetaRef - PI : ThetaRef + PI
+		double ThetaNorm = psastroNormalizeAngleToMidpoint (axes.theta, ThetaRef);
+
+		psVectorAppend (majorValues, axes.major);
+		psVectorAppend (minorValues, axes.minor);
+		psVectorAppend (thetaValues, ThetaNorm);
+	    }
+	    if (majorValues->n == 0) {
+		psWarning ("failed mc error search");
+		model->flags |= PM_MODEL_STATUS_FAIL_SHAPE_ERR;
+		continue;
+	    }
+
+	    psStatsInit (stats);
+	    psVectorStats (stats, majorValues, NULL, NULL, 0);
+	    float dMajor = stats->robustStdev;
+
+	    psStatsInit (stats);
+	    psVectorStats (stats, minorValues, NULL, NULL, 0);
+	    float dMinor = stats->robustStdev;
+
+	    // overload the results back on the PAR[] vectors
+	    dPAR[PM_PAR_SXX] = dMajor;
+	    dPAR[PM_PAR_SYY] = dMinor;
+	    nObjects ++;
+	}
+    }
+    psFree (rng);
+    psFree (stats);
+    psFree (majorValues);
+    psFree (minorValues);
+    psFree (thetaValues);
+
+    psMetadata *header = psMetadataLookupMetadata (&status, readout->analysis, "PSASTRO.HEADER");
+    psMetadataAddStr (header, PS_LIST_TAIL, "GALAXY.SHAPE.ERRORS", PS_META_REPLACE, "error analysis mode", "MONTE CARLO");
+
+    psLogMsg ("psastro.galaxy", PS_LOG_INFO, "monte carlo shape errors for %d of %d objects: %f sec\n", nObjects, (int) sources->n, psTimerMark ("psastro.galaxy.shape.errors"));
+    return true;
+}
+
+// these angles are in radians
+double psastroNormalizeAngleToMidpoint (double angle, double midpoint) {
+
+    double x = cos(angle);
+    double y = sin(angle);
+
+    double result = atan2 (y, x);
+    if (result < midpoint - M_PI) result += 2.0*M_PI;
+    if (result > midpoint + M_PI) result -= 2.0*M_PI;
+
+    return result;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroGhostUtils.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroGhostUtils.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroGhostUtils.c	(revision 41084)
@@ -0,0 +1,47 @@
+/** @file psastroGhostUtils.c
+ *
+ *  @brief Ghost support functions
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.19 $
+ *  @date $Date: 2009-02-09 21:25:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+static void psastroGhostFree (psastroGhost *ghost) {
+
+    if (ghost == NULL) return;
+
+    psFree (ghost->srcFP);
+    psFree (ghost->FP);
+    psFree (ghost->chip);
+
+    return;
+}
+
+psastroGhost *psastroGhostAlloc (void) {
+
+    psastroGhost *ghost = (psastroGhost *) psAlloc(sizeof(psastroGhost));
+    psMemSetDeallocator(ghost, (psFreeFunc) psastroGhostFree);
+
+    ghost->srcFP = psPlaneAlloc();
+    ghost->FP    = psPlaneAlloc();
+    ghost->chip  = psPlaneAlloc();
+    
+    ghost->Mag   = 0.0;
+
+    ghost->inner.major = 0.0;
+    ghost->inner.minor = 0.0;
+    ghost->inner.theta = 0.0;
+
+    ghost->outer.major = 0.0;
+    ghost->outer.minor = 0.0;
+    ghost->outer.theta = 0.0;
+
+    return ghost;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroInternal.h
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroInternal.h	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroInternal.h	(revision 41084)
@@ -0,0 +1,29 @@
+/** @file psastroInternal.h
+ *
+ *  @brief
+ *
+ *  @ingroup psastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# ifdef HAVE_CONFIG_H
+# include <config.h>
+# endif
+
+# ifndef PSASTRO_INTERNAL_H
+# define PSASTRO_INTERNAL_H
+
+# include <stdio.h>
+# include <string.h>
+# include <strings.h>  // for strcasecmp
+# include <unistd.h>   // for unlink
+# include <pslib.h>
+# include <psmodules.h>
+# include <ppStats.h>
+# include "psastro.h"
+
+#endif
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLoadCrosstalk.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLoadCrosstalk.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLoadCrosstalk.c	(revision 41084)
@@ -0,0 +1,425 @@
+/** @file psastroMaskUpdates.c
+ *
+ *  @brief
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "I/O failure in psastroMaskUpdate"); \
+  psFree (view); \
+  return false; \
+}
+
+pmChip * getChipByName (pmFPA *fpa, psString chipname) {
+  int i;
+
+  for (i = 0; i < fpa->chips->n; i++) {
+    pmChip *chip = fpa->chips->data[i];
+    if (strcmp(chipname,psMetadataLookupStr(NULL,chip->concepts,"CHIP.NAME")) == 0) {
+      return(chip);
+    }
+  }
+  return(NULL);
+}
+pmCell * getCellByName (pmChip *chip, psString cellname) {
+  int i;
+
+  for (i = 0; i < chip->cells->n; i++) {
+    pmCell *cell = chip->cells->data[i];
+    if (strcmp(cellname,(char *) psMetadataLookupStr(NULL,cell->concepts,"CELL.NAME")) == 0) {
+      return(cell);
+    }
+  }
+  return(NULL);
+}
+
+
+bool psastroLoadCrosstalk (pmConfig *config) {
+
+  bool status;
+  pmChip *chip = NULL;
+  pmCell *cell = NULL;
+  pmReadout *readout = NULL;
+
+  pmFPAview *view = pmFPAviewAlloc (0);
+  pmFPAview *viewMask = pmFPAviewAlloc (0);
+
+  float zeropt, exptime, MAX_MAG, SPIKE_MAX_MAG;
+
+  psLogMsg ("psastro", PS_LOG_INFO, "determine crosstalk positions");
+
+  // select the current recipe
+  psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+  if (!recipe) {
+    psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+    return false;
+  }
+
+  // Determine if we really want to do this masking:
+  bool CROSSTALK_MASK  = psMetadataLookupBool (&status, recipe, "CROSSTALK_MASK");
+  if (!CROSSTALK_MASK) {
+    return(true);
+  }
+
+  // select the input astrometry data (also carries the refstars)
+  pmFPAfile *astrom = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+  if (!astrom) {
+    psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+    goto escape;
+  }
+  pmFPA *fpa = astrom->fpa;
+
+  // select the reference mask fpa :: we use this to determine cell boundaries
+  pmFPAfile *refMask = psMetadataLookupPtr (NULL, config->files, "PSASTRO.REFMASK");
+  if (!refMask) {
+    psError(PSASTRO_ERR_CONFIG, true, "Can't find mask reference");
+    return false;
+  }
+  // Activate teh reference mask to generate an FPA structure we can use to map stars down to cells
+
+  pmFPAfileActivate (config->files, false, NULL);
+  pmFPAfileActivate (config->files, true, "PSASTRO.REFMASK");
+
+  //  pmFPA *refFPA = refMask->fpa;
+
+  if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+  // really error-out here?  or just skip?
+  if (!psastroZeroPointFromRecipe (&zeropt, &exptime, &MAX_MAG, fpa, recipe)) {
+    psLogMsg ("psastro", PS_LOG_INFO, "failed to load zeropt data from recipe");
+    goto escape;
+  }
+
+  MAX_MAG = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_CROSSTALK_MAG_MAX");
+  // recipe values are given in instrumental magnitudes
+  // use the zero point and exposure time to convert to apparent mags: M_ap = M_inst + C_0 + 2.5*log(exptime)
+  float MagOffset = zeropt + 2.5*log10(exptime);
+  MAX_MAG += MagOffset;
+
+  // Get the spike maximum, as well
+  SPIKE_MAX_MAG = psMetadataLookupF32(&status, recipe, "REFSTAR_MASK_BLEED_MAG_MAX");
+  SPIKE_MAX_MAG += MagOffset;
+  //  psTraceSetLevel("psastro.crosstalk",2);  
+  psTrace("psastro.crosstalk",2,"%f %f %f %f\n",zeropt,exptime,MAX_MAG,MagOffset);
+
+  // Load each chip.
+  while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+    psTrace ("psastro.crosstalk", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+    if (!chip->process || !chip->file_exists) { continue; }
+    if (!chip->fromFPA) { continue; }
+
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+    // Get the current chip's name, and parse out the grid location.
+    pmChip *refChip  = pmFPAviewThisChip (view, refMask->fpa);
+    const char *chipName = psMetadataLookupStr(NULL,refChip->concepts, "CHIP.NAME");
+    int X = chipName[2] - '0';
+    int Y = chipName[3] - '0';
+
+    // Read each cell (at this stage, we should only have one of them)
+      while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+        psTrace ("psastro.crosstalk", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+
+        if (!cell->process || !cell->file_exists) { continue; }
+
+        // Read each readout (similarly, we should only have one of these, too)
+        while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+          if (! readout->data_exists) { continue; }
+
+          // If this chip doesn't crosstalk, skip to the next one.
+          if (! ((X == 2)||(X == 3)||(X == 4)||(X == 5))) {
+            psTrace ("psastro.crosstalk",2,"Chip (%d%d) not a known crosstalk source.",X,Y);
+            continue;
+          }
+
+          // Get the array of reference stars we're concerned with.
+          psArray *refstars = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.REFSTARS");
+          if (refstars == NULL) { return(true); }
+
+          // Check each reference star
+          for (int i = 0; i < refstars->n; i++) {
+	    // This is the source of the ct.
+            pmAstromObj *ref = refstars->data[i];
+
+            if (ref->Mag > MAX_MAG) {
+              continue;
+            }
+
+            // Identify which cell holds the cell
+            pmChip *CTsourceChip = refChip;
+            pmCell *CTsourceCell = pmCellInChip(refChip,ref->chip->x,ref->chip->y);
+
+            if (!CTsourceCell) {
+              continue;
+            }
+
+            psTrace ("psastro.crosstalk",2,"REF: %d %f (%f %f) %f %f :: %f %f\n",
+                     i,ref->Mag, ref->sky->r,ref->sky->d, ref->FP->x,ref->FP->y, ref->chip->x,ref->chip->y);
+
+
+            const char *cellName = psMetadataLookupStr(NULL,CTsourceCell->concepts, "CELL.NAME");
+            int U = cellName[2] - '0';
+            int V = cellName[3] - '0';
+
+            int Xt = X,Yt = Y,Ut = U,Vt = V;
+            float x_cell,y_cell;
+            float x_t_cell,y_t_cell;
+
+            float x_t_chip,y_t_chip;
+	    // int faint_ct = 0; NOTE: not currently used
+            pmCellCoordsForChip(&x_cell,&y_cell,CTsourceCell,ref->chip->x,ref->chip->y);
+
+            x_t_cell = x_cell;
+            y_t_cell = y_cell;
+
+	    // 2014-04-15 CZW: Here are the list of known cross talks:
+	    // Inter-chip
+	    // OTA2yXY3v <=> OTA3yXY3v
+	    // OTA4yXY3v <=> OTA5yXY3v
+	    // Intra-chip
+	    // OTA2yXY5v <=> OTA2yXY6v
+	    // OTA5yXY5v <=> OTA5yXY6v
+	    // One way fainter
+	    // OTA2yXY7v => OTA3yXY2v
+	    // OTA5yXY7v => OTA4yXY2v
+            // Determine if this combination of chip and cell produces a crosstalk artifact
+            if ((X == 2)&&(! ((U == 3)||(U == 5)||(U == 6)||(U == 7)))) {
+              psTrace ("psastro.crosstalk",2,"Cell (%d%d) on chip (%d%d) not a known crosstalk source.",U,V,X,Y);
+	      psTrace ("psastro.crosstalk",2,"t1: %d t2: %d",
+		       (X == 2),
+		       (! ((U == 3)||(U == 5)||(U == 6)||(U == 7))));
+              continue;
+            }
+            if ((X == 3)&&(! ((U == 3)))) {
+              psTrace ("psastro.crosstalk",2,"Cell (%d%d) on chip (%d%d) not a known crosstalk source.",U,V,X,Y);
+              continue;
+            }
+            if ((X == 4)&&(! ((U == 3)))) {
+              psTrace ("psastro.crosstalk",2,"Cell (%d%d) on chip (%d%d) not a known crosstalk source.",U,V,X,Y);
+              continue;
+            }
+            if ((X == 5)&&(! ((U == 3)||(U == 5)||(U == 6)||(U == 7)))) {
+              psTrace ("psastro.crosstalk",2,"Cell (%d%d) on chip (%d%d) not a known crosstalk source.",U,V,X,Y);
+              continue;
+            }
+	    // czw: 2010-01-27 
+	    // Potential new crosstalk: OTA4yXY5v <=> OTA4yXY6v
+	    // Currently not enough info to be convinced of this, however, as the artifacts are very faint.
+
+            // Calculate destination positions
+            if (X == 2) {
+              if (U == 3) {
+                Xt = 3;
+              }
+              if (U == 5) {
+                Ut = 6;
+              }
+              if (U == 6) {
+                Ut = 5;
+              }
+	      if (U == 7) {
+		Xt = 3;
+		Ut = 2;
+		// faint_ct = 1; NOTE: not currently used
+	      }
+            }
+            else if (X == 3) {
+              Xt = 2;
+            }
+            else if (X == 4) {
+              Xt = 5;
+            }
+            if (X == 5) {
+              if (U == 3) {
+                Xt = 4;
+              }
+              if (U == 5) {
+                Ut = 6;
+              }
+              if (U == 6) {
+                Ut = 5;
+              }
+	      if (U == 7) {
+		Xt = 4;
+		Ut = 2;
+		// faint_ct = 1; NOTE: not currently used
+	      }
+            }
+
+            // Convert target cell coordinates to target chip coordinates
+            psString targetChipName = NULL;
+            psStringAppend(&targetChipName,"XY%d%d",Xt,Yt);
+            psString targetCellName = NULL;
+            psStringAppend(&targetCellName,"xy%d%d",Ut,Vt);
+
+            psTrace ("psastro.crosstalk",2,"CTsource @ OTA%d%dxy%d%d@%f,%f CTtarget OTA%d%dxy%d%d@%f,%f [%s %s]",
+                     X,Y,U,V,x_cell,y_cell,Xt,Yt,Ut,Vt,x_t_cell,y_t_cell,
+                     targetChipName,targetCellName);
+
+            pmCell *CTtargetCell = getCellByName (CTsourceChip,targetCellName);
+            if (!CTtargetCell) continue;
+
+            pmChipCoordsForCell(&x_t_chip,&y_t_chip,CTtargetCell,x_t_cell,y_t_cell);
+
+            psTrace ("psastro.crosstalk",2,"CTsource @ OTA%d%dxy%d%d@%f,%f CTtarget OTA%d%dxy%d%d@%f,%f ChipLoc: (%f,%f)",
+                     X,Y,U,V,x_cell,y_cell,Xt,Yt,Ut,Vt,x_t_cell,y_t_cell,x_t_chip,y_t_chip);
+
+            // Hunt down the readout for the target, and save the chip position and magnitude of source star.
+            pmChip *targetChip = getChipByName(fpa,targetChipName);
+            if (!targetChip) continue;
+            if (!targetChip->cells) continue;
+            if (!targetChip->cells->n) continue;
+
+            pmCell *targetCell = targetChip->cells->data[0];
+            if (!targetCell) continue;
+            if (!targetCell->readouts) continue;
+            if (!targetCell->readouts->n) continue;
+            pmReadout *targetReadout = targetCell->readouts->data[0];
+            if (!targetReadout) continue;
+
+            pmAstromObj *crosstalk = pmAstromObjAlloc();
+	    
+            crosstalk->Mag = ref->Mag - MagOffset;
+            crosstalk->chip->x = x_t_chip;
+            crosstalk->chip->y = y_t_chip;
+
+            psArray *crosstalks = psMetadataLookupPtr (&status, targetReadout->analysis, "PSASTRO.CROSSTALKS");
+            if (crosstalks == NULL) {
+              crosstalks = psArrayAllocEmpty(100);
+              if (!psMetadataAdd(targetReadout->analysis,PS_LIST_TAIL,"PSASTRO.CROSSTALKS", PS_DATA_ARRAY, "crosstalk locations", crosstalks)) {
+                psError(PSASTRO_ERR_CONFIG, false, "failure to add crosstalks to readout");
+                goto escape;
+              }
+              psFree(crosstalks);
+            }
+            psArrayAdd(crosstalks,100,crosstalk);
+	    
+            psFree(targetChipName);
+            psFree(targetCellName);
+            psFree(crosstalk);
+
+	    // Determine if we need to add a spike mask.
+	    // CZW 2014-04-15 I'm not adding the new faint CT to this operation.  There's no evidence they produce such things.
+/* 	    psWarning("BLAH: %g %g %g\n",ref->Mag,MagOffset,SPIKE_MAX_MAG); */
+	    if (ref->Mag < SPIKE_MAX_MAG) {
+	      if ((X == 2)&&(! ((U == 3)||(U == 5)||(U == 6)))) {
+		psTrace ("psastro.crosstalk",2,"Cell (%d%d) on chip (%d%d) not a known crosstalk source.",U,V,X,Y);
+		continue;
+	      }
+	      if ((X == 3)&&(! ((U == 3)))) {
+		psTrace ("psastro.crosstalk",2,"Cell (%d%d) on chip (%d%d) not a known crosstalk source.",U,V,X,Y);
+		continue;
+	      }
+	      if ((X == 4)&&(! ((U == 3)))) {
+		psTrace ("psastro.crosstalk",2,"Cell (%d%d) on chip (%d%d) not a known crosstalk source.",U,V,X,Y);
+		continue;
+	      }
+	      if ((X == 5)&&(! ((U == 3)||(U == 5)||(U == 6)))) {
+		psTrace ("psastro.crosstalk",2,"Cell (%d%d) on chip (%d%d) not a known crosstalk source.",U,V,X,Y);
+		continue;
+	      }
+	      
+	      Xt = X;
+	      Yt = Y;
+	      Vt = V;
+	      
+	      for (Ut = 0; Ut < 8; Ut++) {
+
+		psString targetChipName = NULL;
+		psStringAppend(&targetChipName,"XY%d%d",Xt,Yt);
+		psString targetCellName = NULL;
+		psStringAppend(&targetCellName,"xy%d%d",Ut,Vt);
+		
+		psTrace ("psastro.crosstalk",2,"CTsource @ OTA%d%dxy%d%d@%f,%f CTtarget OTA%d%dxy%d%d@%f,%f [%s %s]",
+			 X,Y,U,V,x_cell,y_cell,Xt,Yt,Ut,Vt,x_t_cell,y_t_cell,
+			 targetChipName,targetCellName);
+/* 		psWarning ("CT bleed ct @ OTA%d%dxy%d%d@%f,%f CTtarget OTA%d%dxy%d%d@%f,%f [%s %s]", */
+/* 			 X,Y,U,V,x_cell,y_cell,Xt,Yt,Ut,Vt,x_t_cell,y_t_cell, */
+/* 			 targetChipName,targetCellName); */
+		
+		pmCell *CTtargetCell = getCellByName (CTsourceChip,targetCellName);
+		if (!CTtargetCell) continue;
+		
+		pmChipCoordsForCell(&x_t_chip,&y_t_chip,CTtargetCell,x_t_cell,y_t_cell);
+
+		psTrace ("psastro.crosstalk",2,"CTsource @ OTA%d%dxy%d%d@%f,%f CTtarget OTA%d%dxy%d%d@%f,%f ChipLoc: (%f,%f)",
+			 X,Y,U,V,x_cell,y_cell,Xt,Yt,Ut,Vt,x_t_cell,y_t_cell,x_t_chip,y_t_chip);
+		
+		// Hunt down the readout for the target, and save the chip position and magnitude of source star.
+		pmChip *targetChip = getChipByName(fpa,targetChipName);
+		if (!targetChip) continue;
+		if (!targetChip->cells) continue;
+		if (!targetChip->cells->n) continue;
+		
+		pmCell *targetCell = targetChip->cells->data[0];
+		if (!targetCell) continue;
+		if (!targetCell->readouts) continue;
+		if (!targetCell->readouts->n) continue;
+		pmReadout *targetReadout = targetCell->readouts->data[0];
+		if (!targetReadout) continue;
+		
+		pmAstromObj *crosstalk = pmAstromObjAlloc();
+		
+		crosstalk->Mag = ref->Mag;
+		crosstalk->chip->x = x_t_chip;
+		crosstalk->chip->y = y_t_chip;
+		
+		psArray *crosstalks = psMetadataLookupPtr (&status, targetReadout->analysis, "PSASTRO.CROSSTALKS.SPIKES");
+		if (crosstalks == NULL) {
+		  crosstalks = psArrayAllocEmpty(100);
+		  if (!psMetadataAdd(targetReadout->analysis,PS_LIST_TAIL,"PSASTRO.CROSSTALKS.SPIKES", PS_DATA_ARRAY, "crosstalk locations", crosstalks)) {
+		    psError(PSASTRO_ERR_CONFIG, false, "failure to add crosstalks to readout");
+		    goto escape;
+		  }
+		  psFree(crosstalks);
+		}
+		psArrayAdd(crosstalks,100,crosstalk);
+		
+		psFree(targetChipName);
+		psFree(targetCellName);
+		psFree(crosstalk);
+	      }
+	    } // End satspike crosstalks
+
+	    
+          } // refstars
+        } // readout
+      } // cell
+      //    }
+
+      // File cleanup code.
+    *viewMask = *view;
+    while ((cell = pmFPAviewNextCell (viewMask, refMask->fpa, 1)) != NULL) {
+      psTrace ("psastro", 4, "Mask Cell %d: %x %x\n", viewMask->cell, cell->file_exists, cell->process);
+      if (!cell->process || !cell->file_exists) { continue; }
+
+      while ((readout = pmFPAviewNextReadout (viewMask, refMask->fpa, 1)) != NULL) {
+        if (! readout->data_exists) { continue; }
+        if (!pmFPAfileIOChecks (config, viewMask, PM_FPA_AFTER)) ESCAPE;
+      }
+      if (!pmFPAfileIOChecks (config, viewMask, PM_FPA_AFTER)) ESCAPE;
+    }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+  }
+  if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+
+  psTrace ("psastro.crosstalk", 2, "Leaving crosstalk code.\n");
+
+  psFree(view);
+  psFree(viewMask);
+  return(true);
+
+ escape:
+  psFree(view);
+  psFree(viewMask);
+  return(false);
+}
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLoadGhosts.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLoadGhosts.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLoadGhosts.c	(revision 41084)
@@ -0,0 +1,502 @@
+/** @file psastroLoadGhosts.c
+ *
+ *  @brief calculate ghost FPA and Chip positions for the stars loaded on the FPA based on the model
+ *
+ *  @ingroup psastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.7 $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+# define GET_2D_POLY(NAME,OUT) \
+    md = psMetadataLookupMetadata (&status, ghostModel, NAME); \
+    if (!md) { \
+	psError(PSASTRO_ERR_CONFIG, true, "Missing %s in model file %s", NAME, ghostFile); \
+	goto escape; \
+    } \
+    OUT = psPolynomial2DfromMetadata(md); \
+    if (!OUT) { \
+	psError(PSASTRO_ERR_CONFIG, true, "Trouble interpretting %s in model file %s", NAME, ghostFile); \
+	goto escape; \
+    }
+
+# define GET_1D_POLY(NAME,OUT) \
+    md = psMetadataLookupMetadata (&status, ghostModel, NAME); \
+    if (!md) { \
+	psError(PSASTRO_ERR_CONFIG, true, "Missing %s in model file %s", NAME, ghostFile); \
+	goto escape; \
+    } \
+    OUT = psPolynomial1DfromMetadata(md);	\
+    if (!OUT) { \
+	psError(PSASTRO_ERR_CONFIG, true, "Trouble interpretting %s in model file %s", NAME, ghostFile); \
+	goto escape; \
+    }
+
+/**
+ * calculate ghost FPA and Chip positions for the stars loaded on the FPA
+ */
+
+		    
+
+bool psastroLoadGhosts (pmConfig *config) {
+
+    bool status;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    float zeropt, exptime, MAX_MAG;
+    psMetadata *md = NULL;
+    psPolynomial2D *centerX = NULL;
+    psPolynomial2D *centerY = NULL;
+    psPolynomial1D *outerMajor = NULL;
+    psPolynomial1D *outerMinor = NULL;
+    psPolynomial1D *innerMajor = NULL;
+    psPolynomial1D *innerMinor = NULL;
+    psMetadata *ghostModel = NULL;
+
+    psLogMsg ("psastro", PS_LOG_INFO, "determine ghost positions");
+
+    // XXX this needs to be camera-dependent
+    if (0) {
+      psWarning("Doing the hard coded switch to HSC ghosts still.");
+      return (psastroLoadGhostsHSC(config));
+    } 
+
+    psWarning("hard coded switch to GPC ghosts.");
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+        return false;
+    }
+
+    bool REFSTAR_MASK_GHOST = psMetadataLookupBool (&status, recipe, "REFSTAR_MASK_GHOST");
+    if (!REFSTAR_MASK_GHOST) return true;
+
+    
+    char *ghostFile = psMetadataLookupStr (&status, recipe, "GHOST_MODEL");
+    if (!strcasecmp(ghostFile, "NONE")) return true;
+
+    if (!pmConfigFileRead (&ghostModel, ghostFile, "GHOST MODEL")) {
+	psError(PSASTRO_ERR_CONFIG, true, "Trouble loading ghost model");
+        return false;
+    }
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    GET_2D_POLY ("GHOST.CENTER.X", centerX);
+    GET_2D_POLY ("GHOST.CENTER.Y", centerY);
+
+    GET_1D_POLY ("GHOST.OUTER.MAJOR", outerMajor);
+    GET_1D_POLY ("GHOST.OUTER.MINOR", outerMinor);
+    GET_1D_POLY ("GHOST.INNER.MAJOR", innerMajor);
+    GET_1D_POLY ("GHOST.INNER.MINOR", innerMinor);
+
+    // select the input astrometry data (also carries the refstars)
+    pmFPAfile *astrom = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!astrom) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+	goto escape;
+    }
+    pmFPA *fpa = astrom->fpa;
+
+    // really error-out here?  or just skip?
+    if (!psastroZeroPointFromRecipe (&zeropt, &exptime, &MAX_MAG, fpa, recipe)) {
+        psLogMsg ("psastro", PS_LOG_INFO, "failed to load zeropt data from recipe");
+	goto escape;
+    }
+
+    // recipe values are given in instrumental magnitudes
+    // use the zero point and exposure time to convert to apparent mags: M_ap = M_inst + C_0 + 2.5*log(exptime)
+    float MagOffset = zeropt + 2.5*log10(exptime);
+    MAX_MAG += MagOffset;
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+        if (!chip->fromFPA) { continue; }
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                // select the raw objects for this readout (loaded in psastroChooseRefstars.c)
+		// XXX : note that we place limits on the refstar sample in psastroChooseRefstars.c:
+		// 1) on chip and 2) < PSASTRO.MAX.NREF. magnitude limits and clump exclusion are only 
+                psArray *refstars = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.REFSTARS");
+                if (refstars == NULL) { continue; }
+
+                // identify the bright stars of interest
+                for (int i = 0; i < refstars->n; i++) {
+                    pmAstromObj *ref = refstars->data[i];
+		    psTrace("psastro.ghost",5,"Begin ghost %d/%ld: MAX_MAG: %g; ref_mag: %g @ (%.10g,%.10g)",i,refstars->n,MAX_MAG,ref->Mag,ref->sky->r * 180 / M_PI,ref->sky->d * 180.0 / M_PI);
+                    if (ref->Mag > MAX_MAG) continue;
+
+		    // Ghost model:
+		    // X(ghost) = -X + dX(X,Y) --> GHOST.CENTER.X
+		    // Y(ghost) = -Y + dY(X,Y) --> GHOST.CENTER.Y
+		    // R1inner  = R1inner_0 + r dR1inner
+		    // R2inner  = R2inner_0 + r dR1inner
+		    // R1outer  = R1outer_0 + r dR1inner
+		    // R2outer  = R2outer_0 + r dR1inner
+
+		    psastroGhost *ghost = psastroGhostAlloc ();
+		    ghost->srcFP->x = ref->FP->x; 
+		    ghost->srcFP->y = ref->FP->y;
+
+		    // XXX it is stupid that this takes -X_fpa,-Y_fpa --> the analysis script is reporting the guess FPA position, and the fit is using that...
+		    ghost->FP->x = -ref->FP->x + psPolynomial2DEval(centerX, -ref->FP->x, -ref->FP->y);
+		    ghost->FP->y = -ref->FP->y + psPolynomial2DEval(centerY, -ref->FP->x, -ref->FP->y);
+
+		    float rSrc = hypot (ref->FP->x, ref->FP->y);
+
+		    ghost->inner.major = psPolynomial1DEval (innerMajor, rSrc);
+		    ghost->inner.minor = psPolynomial1DEval (innerMinor, rSrc);
+		    ghost->inner.theta = atan2(ref->FP->y, ref->FP->x);
+
+		    ghost->outer.major = psPolynomial1DEval (outerMajor, rSrc);
+		    ghost->outer.minor = psPolynomial1DEval (outerMinor, rSrc);
+		    ghost->outer.theta = atan2(ref->FP->y, ref->FP->x);
+
+		    // report instrumental ghost star mags
+		    ghost->Mag = ref->Mag - MagOffset;
+		    
+		    // XXX this code yields a single chip: we need to provide results for any chips
+		    // which encompass the full size of the ghost
+		    pmChip *ghostChip = psastroFindChip (&ghost->chip->x, &ghost->chip->y, fpa, -ghost->srcFP->x, -ghost->srcFP->y);
+		    // fprintf (stderr, "raw chip position: %f, %f ", ghost->chip->x, ghost->chip->y);
+/* 		    float ref_chip_x = ghost->chip->x; */
+/* 		    float ref_chip_y = ghost->chip->y; */
+		    ghostChip = psastroFindChip (&ghost->chip->x, &ghost->chip->y, fpa, ghost->FP->x, ghost->FP->y);
+		    // fprintf (stderr, "-> model chip position: %f, %f\n", ghost->chip->x, ghost->chip->y);
+
+		    if (ghostChip) {
+		      psTrace("psastro.ghost",5,"   in ghost: %d/%ld: ref: (%f,%f) or (%s) ghost: (%f,%f) or (%f,%f,%s) %f inner: (%f,%f,%f) outer: (%f,%f,%f)",
+			      i,refstars->n,
+			      ref->FP->x,ref->FP->y,
+			      psMetadataLookupStr(NULL,chip->concepts,"CHIP.NAME"),
+			      ghost->FP->x,ghost->FP->y,
+			      ghost->chip->x,ghost->chip->y,psMetadataLookupStr(NULL,ghostChip->concepts,"CHIP.NAME"),
+			      rSrc,
+			      ghost->inner.major,ghost->inner.minor,ghost->inner.theta,
+			      ghost->outer.major,ghost->outer.minor,ghost->outer.theta);
+/* 		      psWarning("   GHOST_DATA: %d/%ld: ref: (%f,%f) or (%f,%f,%s) ghost: (%f,%f) or (%f,%f,%s) %f inner: (%f,%f,%f) outer: (%f,%f,%f)", */
+/* 			      i,refstars->n, */
+/* 			      ref->FP->x,ref->FP->y, */
+/* 			      ref_chip_x,ref_chip_y,psMetadataLookupStr(NULL,chip->concepts,"CHIP.NAME"), */
+/* 			      ghost->FP->x,ghost->FP->y, */
+/* 			      ghost->chip->x,ghost->chip->y,psMetadataLookupStr(NULL,ghostChip->concepts,"CHIP.NAME"), */
+/* 			      rSrc, */
+/* 			      ghost->inner.major,ghost->inner.minor,ghost->inner.theta, */
+/* 			      ghost->outer.major,ghost->outer.minor,ghost->outer.theta); */
+		    }
+		    else {
+		      psTrace("psastro.ghost",5,"   in ghost: %d/%ld: ref: (%f,%f) ghost: (%f,%f) or (%f,%f,%s) inner: (%f,%f,%f) outer: (%f,%f,%f)",
+			      i,refstars->n,
+			      ref->FP->x,ref->FP->y,
+			      ghost->FP->x,ghost->FP->y,
+			      ghost->chip->x,ghost->chip->y,"NONE",
+			      ghost->inner.major,ghost->inner.minor,ghost->inner.theta,
+			      ghost->outer.major,ghost->outer.minor,ghost->outer.theta);
+		    }		      
+		      
+
+			    
+		    if (!ghostChip) goto skip;
+		    if (!ghostChip->cells) goto skip;
+		    if (!ghostChip->cells->n) goto skip;
+
+		    
+		    pmCell *ghostCell = ghostChip->cells->data[0];
+		    if (!ghostCell) goto skip;
+		    if (!ghostCell->readouts) goto skip;
+		    if (!ghostCell->readouts->n) goto skip;
+		    pmReadout *ghostReadout = ghostCell->readouts->data[0];
+		    if (!ghostReadout) goto skip;
+
+		    psArray *ghosts = psMetadataLookupPtr (&status, ghostReadout->analysis, "PSASTRO.GHOSTS");
+		    if (ghosts == NULL) { 
+			ghosts = psArrayAllocEmpty (100);
+			if (!psMetadataAdd (ghostReadout->analysis, PS_LIST_TAIL, "PSASTRO.GHOSTS", PS_DATA_ARRAY, "astrometry matches", ghosts)) {
+			  psError(PSASTRO_ERR_CONFIG, false, "failure to add ghosts to readout");
+			  goto escape;
+			}
+			psFree (ghosts);
+		    }
+
+		    psArrayAdd (ghosts, 100, ghost);
+
+		skip:
+		    
+		    psFree (ghost);
+                }
+            }
+        }
+    }
+
+    psastroExtractFreeChipBounds();
+
+    psFree (centerX);
+    psFree (centerY);
+    psFree (innerMajor);
+    psFree (innerMinor);
+    psFree (outerMajor);
+    psFree (outerMinor);
+    psFree (ghostModel);
+    psFree (view);
+    return true;
+
+escape:
+    psFree (centerX);
+    psFree (centerY);
+    psFree (innerMajor);
+    psFree (innerMinor);
+    psFree (outerMajor);
+    psFree (outerMinor);
+    psFree (ghostModel);
+    psFree (view);
+    return false;
+}
+
+
+ 
+bool psastroLoadGhostsHSC (pmConfig *config) {
+  bool status;
+  pmChip *chip = NULL;
+  pmCell *cell = NULL;
+  pmReadout *readout = NULL;
+  float zeropt, exptime, MAX_MAG;
+  psMetadata *ghostModel = NULL;
+  psVector *C_terms = NULL;
+  psVector *R_terms = NULL;
+  float glintPixelScale = 0.0;
+  
+  psLogMsg ("psastro", PS_LOG_INFO, "determine ghost positions");
+  
+  // select the current recipe
+  psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+  if (!recipe) {
+    psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+    return false;
+  }
+  
+  bool REFSTAR_MASK_GHOST = psMetadataLookupBool (&status, recipe, "REFSTAR_MASK_GHOST");
+  if (!REFSTAR_MASK_GHOST) return true;
+  
+  // Load model from file
+  char *ghostFile = psMetadataLookupStr (&status, recipe, "GHOST_MODEL");
+  if (!strcasecmp(ghostFile, "NONE")) return true;
+  psLogMsg("psastro", PS_LOG_INFO, "ghost file %s",ghostFile);
+  if (!pmConfigFileRead (&ghostModel, ghostFile, "GHOST MODEL")) {
+    psError(PSASTRO_ERR_CONFIG, true, "Trouble loading ghost model");
+    return false;
+  }
+  
+  // Allocate FPA and refstars
+  pmFPAview *view = pmFPAviewAlloc (0);
+  // select the input astrometry data (also carries the refstars)
+  pmFPAfile *astrom = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+  if (!astrom) {
+    psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+    goto escape;
+  }
+  pmFPA *fpa = astrom->fpa;
+  
+  char *filter = psMetadataLookupStr (&status, fpa->concepts, "FPA.FILTERID");
+  psMetadataItem *item = psMetadataLookup(ghostModel, "GHOST.MODEL.HSC");
+  if (!item) {
+    psLogMsg ("psastro", PS_LOG_INFO, "GHOST.MODEL.HSC data missing");
+    return false;
+  }
+  if (item->type != PS_DATA_METADATA_MULTI) {
+    psLogMsg ("psastro", PS_LOG_INFO, "GHOST.MODEL.HSC not multi");
+    return false;
+  }
+  
+  psListIterator *iter = psListIteratorAlloc(item->data.list,PS_LIST_HEAD, false);
+  psMetadataItem *refItem = NULL;
+  while ((refItem = psListGetAndIncrement(iter))) {
+    if (refItem->type != PS_DATA_METADATA) {
+      psLogMsg ("psastro", PS_LOG_INFO, "GHOST.MODEL.HSC entry not metadata");
+      return false;
+    }
+    char *refFilter = psMetadataLookupStr (&status, refItem->data.md, "FILTER");
+    if (!status) {
+      // psLogMsg ("psastro", PS_LOG_INFO, "a PHOTCODE.DATA recipe folder is missing FILTER");
+      continue;
+    }
+    if (strcmp(refFilter, filter)) continue;
+    
+    C_terms = psMetadataLookupPtr(&status, refItem->data.md, "C_TERMS");
+    R_terms = psMetadataLookupPtr(&status, refItem->data.md, "R_TERMS");
+    glintPixelScale = psMetadataLookupF32(&status, refItem->data.md, "SCALE");
+  }
+
+  // really error-out here?  or just skip?
+  if (!R_terms) {
+    psLogMsg ("psastro", PS_LOG_INFO, "failed to find HSC ghost model for filter %s", filter);
+    goto escape;
+  }
+  
+  // really error-out here?  or just skip?
+  if (!psastroZeroPointFromRecipe (&zeropt, &exptime, &MAX_MAG, fpa, recipe)) {
+    psLogMsg ("psastro", PS_LOG_INFO, "failed to load zeropt data from recipe");
+    goto escape;
+  }
+  
+  // recipe values are given in instrumental magnitudes
+  // use the zero point and exposure time to convert to apparent mags: M_ap = M_inst + C_0 + 2.5*log(exptime)
+  float MagOffset = zeropt + 2.5*log10(exptime);
+  MAX_MAG += MagOffset;
+  
+  // this loop selects the matched stars for all chips
+  while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+    psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+    if (!chip->process || !chip->file_exists) { continue; }
+    if (!chip->fromFPA) { continue; }
+    
+    while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+      psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+      if (!cell->process || !cell->file_exists) { continue; }
+      
+      // process each of the readouts
+      while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+	if (! readout->data_exists) { continue; }
+	
+	// select the raw objects for this readout (loaded in psastroChooseRefstars.c)
+	// XXX : note that we place limits on the refstar sample in psastroChooseRefstars.c:
+	// 1) on chip and 2) < PSASTRO.MAX.NREF. magnitude limits and clump exclusion are only 
+	psArray *refstars = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.REFSTARS");
+	if (refstars == NULL) { continue; }
+	
+	// identify the bright stars of interest
+	for (int i = 0; i < refstars->n; i++) {
+	  pmAstromObj *ref = refstars->data[i];
+	  if (ref->Mag > MAX_MAG) continue;
+	  
+	  psastroGhost *ghost = psastroGhostAlloc ();
+	  
+	  double R_TPA  = sqrt(pow(ref->TP->y,2) + pow(ref->TP->x,2));
+	  double theta0 = atan2(ref->TP->y,ref->TP->x);
+	  double star_radius_deg = R_TPA * glintPixelScale;
+	  
+	  //	  psTrace("psastro.ghost",5,
+	  psLogMsg("psastro",PS_LOG_INFO,
+		  "Begin ghost %d/%ld: MAX_MAG: %g; ref_mag: %g @ (%.10g,%.10g) TPA: (%f %f) R: %f %f %f",
+		  i,refstars->n,MAX_MAG,ref->Mag,ref->sky->r * 180 / M_PI,ref->sky->d * 180.0 / M_PI,ref->TP->x,ref->TP->y,
+		  R_TPA,star_radius_deg,theta0);
+	  
+	  if (star_radius_deg > 0.5) { continue; }
+	  
+	  // Calculate expected position.
+	  double C = C_terms->data.F32[0] + C_terms->data.F32[1] * star_radius_deg + C_terms->data.F32[2] * pow(star_radius_deg,2) +
+	    C_terms->data.F32[3] * pow(star_radius_deg,3) + C_terms->data.F32[4] * pow(star_radius_deg,4) +
+	    C_terms->data.F32[5] * pow(star_radius_deg,5) + C_terms->data.F32[6] * pow(star_radius_deg,6) +
+	    C_terms->data.F32[7] * pow(star_radius_deg,7);
+	  double R = R_terms->data.F32[0] + R_terms->data.F32[1] * star_radius_deg + R_terms->data.F32[2] * pow(star_radius_deg,2) +
+	    R_terms->data.F32[3] * pow(star_radius_deg,3) + R_terms->data.F32[4] * pow(star_radius_deg,4) +
+	    R_terms->data.F32[5] * pow(star_radius_deg,5) + R_terms->data.F32[6] * pow(star_radius_deg,6) +
+	    R_terms->data.F32[7] * pow(star_radius_deg,7);
+	  R = R / 0.015;
+	  
+	  psPlane *fp = psPlaneAlloc();
+	  psPlane *tp = psPlaneAlloc();
+	  
+	  tp->x = 13.5 * (C * cos(theta0) / 0.015 + 12.78);
+	  tp->y = 13.5 * (C * sin(theta0) / 0.015 + 57.74);
+	  psPlaneTransformApply(fp,fpa->fromTPA, tp);
+	  
+	  ghost->srcFP->x = ref->FP->x; 
+	  ghost->srcFP->y = ref->FP->y;
+	  ghost->FP->x    = fp->x;
+	  ghost->FP->y    = fp->y;
+	  
+	  ghost->inner.major = 0.0;
+	  ghost->inner.minor = 0.0;
+	  ghost->outer.major = R;
+	  ghost->outer.minor = R;
+	  
+	  pmChip *ghostChip = psastroFindChip (&ghost->chip->x, &ghost->chip->y, fpa, ghost->FP->x, ghost->FP->y);
+	  
+	  if (ghostChip) {
+	    psLogMsg("psastro",PS_LOG_INFO,
+		     //	    psTrace("psastro.ghost",5,
+		    "   in ghost: %d/%ld: ref: (%f,%f) or (%s) ghost: (%f,%f) or (%f,%f,%s) %f inner: (%f,%f,%f) outer: (%f,%f,%f)",
+		    i,refstars->n,
+		    ref->FP->x,ref->FP->y,
+		    psMetadataLookupStr(NULL,chip->concepts,"CHIP.NAME"),
+		    ghost->FP->x,ghost->FP->y,
+		    ghost->chip->x,ghost->chip->y,psMetadataLookupStr(NULL,ghostChip->concepts,"CHIP.NAME"),
+		    C,
+		    ghost->inner.major,ghost->inner.minor,ghost->inner.theta,
+		    ghost->outer.major,ghost->outer.minor,ghost->outer.theta);
+	  }
+	  else {
+	    psTrace("psastro.ghost",5,"   in ghost: %d/%ld: ref: (%f,%f) ghost: (%f,%f) or (%f,%f,%s) inner: (%f,%f,%f) outer: (%f,%f,%f)",
+		    i,refstars->n,
+		    ref->FP->x,ref->FP->y,
+		    ghost->FP->x,ghost->FP->y,
+		    ghost->chip->x,ghost->chip->y,"NONE",
+		    ghost->inner.major,ghost->inner.minor,ghost->inner.theta,
+		    ghost->outer.major,ghost->outer.minor,ghost->outer.theta);
+	  }		      
+	  
+	  
+	  
+	  if (!ghostChip) goto skip;
+	  if (!ghostChip->cells) goto skip;
+	  if (!ghostChip->cells->n) goto skip;
+	  
+	  
+	  pmCell *ghostCell = ghostChip->cells->data[0];
+	  if (!ghostCell) goto skip;
+	  if (!ghostCell->readouts) goto skip;
+	  if (!ghostCell->readouts->n) goto skip;
+	  pmReadout *ghostReadout = ghostCell->readouts->data[0];
+	  if (!ghostReadout) goto skip;
+	  
+	  psArray *ghosts = psMetadataLookupPtr (&status, ghostReadout->analysis, "PSASTRO.GHOSTS");
+	  if (ghosts == NULL) { 
+	    ghosts = psArrayAllocEmpty (100);
+	    if (!psMetadataAdd (ghostReadout->analysis, PS_LIST_TAIL, "PSASTRO.GHOSTS", PS_DATA_ARRAY, "astrometry matches", ghosts)) {
+	      psError(PSASTRO_ERR_CONFIG, false, "failure to add ghosts to readout");
+	      goto escape;
+	    }
+	    psFree (ghosts);
+	  }
+	  
+	  psArrayAdd (ghosts, 100, ghost);
+	  
+	skip:
+	  
+	  psFree (ghost);
+	}
+      }
+    }
+  }
+  
+  psastroExtractFreeChipBounds();
+  
+  psFree (C_terms);
+  psFree (R_terms);
+  //    psFree (ghostModel);
+  psFree (view);
+  return true;
+  
+ escape:
+  psFree (C_terms);
+  psFree (R_terms);
+  //    psFree (ghostModel);
+  psFree (view);
+  return false;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLoadGlints.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLoadGlints.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLoadGlints.c	(revision 41084)
@@ -0,0 +1,621 @@
+/** @file psastroLoadGlints.c
+ *
+ *  @brief calculate glint FPA and Chip positions for the stars loaded on the FPA
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.7 $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+# define ESCAPE(MSG) {							\
+	psError(PS_ERR_UNKNOWN, false, "I/O failure in psastroMaskUpdate: %s", MSG); \
+	psFree (view);							\
+	return false;							\
+    }
+
+/**
+ * calculate glint FPA and Chip positions for the stars loaded on the FPA
+ */
+bool psastroLoadGlints (pmConfig *config) {
+
+    bool status;
+    float zeropt, exptime;
+
+    psLogMsg ("psastro", PS_LOG_INFO, "determine glint positions");
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+        return false;
+    }
+
+    bool REFSTAR_MASK_GLINTS = psMetadataLookupBool (&status, recipe, "REFSTAR_MASK_GLINTS");
+    if (!REFSTAR_MASK_GLINTS) return true;
+
+    // select the limiting magnitude
+    double GLINT_MAX_MAG = psMetadataLookupF32 (&status, recipe, "GLINT_MAX_MAG");
+    double GLINT_LENGTH_MAG_SLOPE = psMetadataLookupF32 (&status, recipe, "GLINT_LENGTH_MAG_SLOPE");
+    double GLINT_LENGTH_MAG_ZERO = psMetadataLookupF32 (&status, recipe, "GLINT_LENGTH_MAG_ZERO");
+    double glintWidth = psMetadataLookupF32 (&status, recipe, "GLINT_WIDTH");
+    double pixelScale = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.SCALE");
+    
+
+    // select the set of glint regions (GLINT.REGION is a MULTI of METADATA items)
+    psMetadataItem *glintRegions = psMetadataLookup (recipe, "GLINT.REGION");
+    if (!status) {
+        psWarning ("glint masking requested but glint regions are missing (GLINT.REGION)\n");
+        return true;
+    }
+    if (glintRegions->type != PS_DATA_METADATA_MULTI) {
+        psWarning ("GLINT.REGION is not a MULTI\n");
+        return true;
+    }
+
+    // select the input astrometry data (also carries the glintStars)
+    pmFPAfile *astrom = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!astrom) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+        return false;
+    }
+    pmFPA *fpa = astrom->fpa;
+
+    // really error-out here?  or just skip?
+    if (!psastroZeroPointFromRecipe (&zeropt, &exptime, NULL, fpa, recipe)) {
+        psLogMsg ("psastro", PS_LOG_INFO, "failed to load zeropt data from recipe");
+        return false;
+    }
+
+    // recipe values are given in instrumental magnitudes
+    // use the zero point and exposure time to convert to apparent mags: M_ap = M_inst + C_0 + 2.5*log(exptime)
+    float MagOffset = zeropt + 2.5*log10(exptime);
+    GLINT_MAX_MAG += MagOffset;
+    GLINT_LENGTH_MAG_ZERO += MagOffset;
+
+    // select the raw objects for this readout (loaded in psastroExtract.c)
+    psArray *glintStars = psMetadataLookupPtr (&status, fpa->analysis, "PSASTRO.GLINT.STARS");
+    if (glintStars == NULL) { 
+        psLogMsg ("psastro", PS_LOG_INFO, "no glint stars found");
+        return false;
+    }
+
+    // set up the chip boundary vectors.
+    psastroChipBounds (fpa);
+
+    // find the possible glint star:s for each glint star, convert the position to FPA coordinates.
+    // search for stars within the glint regions
+    for (int i = 0; i < glintStars->n; i++) {
+	pmAstromObj *star = glintStars->data[i];
+	if (star->Mag > GLINT_MAX_MAG) continue; // XXX should not be needed...
+
+	// project glint star to the focal-plane
+	psProject (star->TP, star->sky, fpa->toSky);
+	psPlaneTransformApply (star->FP, fpa->fromTPA, star->TP);
+	fprintf (stderr, "glint: %7.2f @ %8.1f, %8.1f (%f %f) %8.1f %8.1f\n", star->Mag, star->FP->x, star->FP->y, star->sky->r * PS_DEG_RAD, star->sky->d * PS_DEG_RAD, star->TP->x,star->TP->y);
+
+	// find the GLINT.REGION this star lands in (if any)
+	psListIterator *glintIter = psListIteratorAlloc(glintRegions->data.list, PS_LIST_HEAD, false);
+	psMetadataItem *glintItem = NULL;
+	while ((glintItem = psListGetAndIncrement (glintIter))) {
+	    if (glintItem->type != PS_DATA_METADATA) {
+		psWarning ("GLINT.REGION entry is not a metadata folder");
+		continue;
+	    }
+	    
+	    char *glintRegionString = psMetadataLookupStr (&status, glintItem->data.md, "REGION");
+	    if (!glintRegionString) {
+		// psWarning ("GLINT.REGION entry is missing REGION entry");
+		continue;
+	    }
+	    psRegion glintRegion = psRegionFromString (glintRegionString);
+
+	    // select stars that land in this regio
+	    if (star->FP->x < glintRegion.x0) continue;
+	    if (star->FP->x > glintRegion.x1) continue;
+	    if (star->FP->y < glintRegion.y0) continue;
+	    if (star->FP->y > glintRegion.y1) continue;
+
+	    double glintLength = GLINT_LENGTH_MAG_SLOPE*(GLINT_LENGTH_MAG_ZERO - star->Mag);
+
+	    char *glintType = psMetadataLookupStr (&status, glintItem->data.md, "GLINT.TYPE");
+	    if (!status) {
+		psWarning ("GLINT.REGION entry is missing TYPE entry");
+		continue;
+	    }
+
+	    // depending on the glint type, we need to find either the chips in the row or in the column.
+	    if (!strcasecmp(glintType, "TOP") || !strcasecmp(glintType, "BOTTOM")) {
+		for (int nChip = 0; nChip < fpa->chips->n; nChip++) {
+
+		    pmChip *chip = fpa->chips->data[nChip];
+		    if (!chip) continue;
+
+		    if (!psastroFindChipInXrange (fpa, nChip, star->FP->x, star->FP->y)) {
+			continue;
+		    }
+
+		    // find the coordinate of the end point
+		    double xFPA  = star->FP->x;
+		    double yFPA0 = star->FP->y;
+		    double yFPA1;
+		    if (!strcasecmp(glintType, "TOP")) {
+			yFPA1 = yFPA0 - glintLength;
+		    } else {
+			yFPA1 = yFPA0 + glintLength;
+		    }
+
+		    // FPA coordinates of intersections with chip edges 
+		    double yFPAs, yFPAe;
+		    psastroFindChipYedges (&yFPAs, &yFPAe, fpa, nChip);
+
+		    if (yFPA0 > yFPA1) PS_SWAP (yFPA0, yFPA1);
+		    if (yFPAs > yFPAe) PS_SWAP (yFPAs, yFPAe);
+
+		    // does this glint cross this chip?
+		    if (yFPA0 > yFPAe) continue;
+		    if (yFPA1 < yFPAs) continue;
+
+		    yFPA0 = PS_MAX (yFPA0, yFPAs);
+		    yFPA1 = PS_MIN (yFPA1, yFPAe);
+
+		    double xChip0, yChip0, xChip1, yChip1;
+		    psastroFPAtoChip (&xChip0, &yChip0, fpa, nChip, xFPA, yFPA0);
+		    psastroFPAtoChip (&xChip1, &yChip1, fpa, nChip, xFPA, yFPA1);
+
+		    // we now have the location of the glint ends or edge-intersections in chip coordinates
+		    // double-check if this chip overlaps this glint
+		    if (xChip0 > xChip1) PS_SWAP (xChip0, xChip1);
+		    if (yChip0 > yChip1) PS_SWAP (yChip0, yChip1);
+
+		    // bounds of this chip
+		    psRegion *region = pmChipPixels (chip);
+		    if (yChip1 < region->y0) continue;
+		    if (yChip0 > region->y1) continue;
+
+		    // this glint touches this chip. calculate the start and end
+		    // coordinates on this chip
+		    double xChipS = PS_MAX(xChip0 - 0.5*glintWidth, region->x0);
+		    double xChipE = PS_MIN(xChip1 + 0.5*glintWidth, region->x1);
+		    double yChipS = PS_MAX (yChip0, region->y0);
+		    double yChipE = PS_MIN (yChip1, region->y1);
+
+		    // select the 0th readout of the 0th cell for this chip
+		    if (!chip->cells) continue;
+		    if (!chip->cells->n) continue;
+		    pmCell *glintCell = chip->cells->data[0];
+		    if (!glintCell) continue;
+		    if (!glintCell->readouts) continue;
+		    if (!glintCell->readouts->n) continue;
+		    pmReadout *glintReadout = glintCell->readouts->data[0];
+		    if (!glintReadout) continue;
+
+		    // save the glints on the readout->analysis metadata, creating if needed
+		    psArray *glints = psMetadataLookupPtr (&status, glintReadout->analysis, "PSASTRO.GLINTS");
+		    if (glints == NULL) { 
+			glints = psArrayAllocEmpty (100);
+			if (!psMetadataAdd (glintReadout->analysis, PS_LIST_TAIL, "PSASTRO.GLINTS", PS_DATA_ARRAY, "astrometry matches", glints)) {
+			    psWarning("failure to add glints to readout");
+			    psFree (glints);
+			    continue;
+			}
+			psFree (glints);
+		    }
+
+		    fprintf (stderr, "glint %s : %f,%f to %f,%f (%f,%f to %f,%f)\n", glintType, xChip0, yChip0, xChip1, yChip1, xChipS, yChipS, xChipE, yChipE);
+		    psRegion *glint = psRegionAlloc(xChipS, xChipE, yChipS, yChipE);
+		    psArrayAdd (glints, 100, glint);
+		    psFree (glint);
+		    psFree (region);
+		}
+	    }
+
+	    // depending on the glint type, we need to find either the chips in the row or in the column.
+	    if (!strcasecmp(glintType, "LEFT") || !strcasecmp(glintType, "RIGHT")) {
+		for (int nChip = 0; nChip < fpa->chips->n; nChip++) {
+
+		    pmChip *chip = fpa->chips->data[nChip];
+		    if (!chip) continue;
+
+		    if (!psastroFindChipInYrange (fpa, nChip, star->FP->x, star->FP->y)) {
+			continue;
+		    }
+
+		    // find the coordinate of the end point
+		    double yFPA  = star->FP->y;
+		    double xFPA0 = star->FP->x;
+		    double xFPA1;
+		    if (!strcasecmp(glintType, "RIGHT")) {
+			xFPA1 = xFPA0 - glintLength;
+		    } else {
+			xFPA1 = xFPA0 + glintLength;
+		    }
+
+		    // FPA coordinates of intersections with chip edges 
+		    double xFPAs, xFPAe;
+		    psastroFindChipXedges (&xFPAs, &xFPAe, fpa, nChip);
+
+		    if (xFPA0 > xFPA1) PS_SWAP (xFPA0, xFPA1);
+		    if (xFPAs > xFPAe) PS_SWAP (xFPAs, xFPAe);
+
+		    // does this glint cross this chip?
+		    if (xFPA0 > xFPAe) continue;
+		    if (xFPA1 < xFPAs) continue;
+
+		    xFPA0 = PS_MAX (xFPA0, xFPAs);
+		    xFPA1 = PS_MIN (xFPA1, xFPAe);
+
+		    double xChip0, yChip0, xChip1, yChip1;
+		    psastroFPAtoChip (&xChip0, &yChip0, fpa, nChip, xFPA0, yFPA);
+		    psastroFPAtoChip (&xChip1, &yChip1, fpa, nChip, xFPA1, yFPA);
+
+		    // we now have the location of the glint ends or edge-intersections in chip coordinates
+		    // double-check if this chip overlaps this glint
+		    if (xChip0 > xChip1) PS_SWAP (xChip0, xChip1);
+		    if (yChip0 > yChip1) PS_SWAP (yChip0, yChip1);
+
+		    // bounds of this chip
+		    psRegion *region = pmChipPixels (chip);
+		    if (xChip1 < region->x0) continue;
+		    if (xChip0 > region->x1) continue;
+
+		    // this glint touches this chip. calculate the start and end
+		    // coordinates on this chip
+		    double yChipS = PS_MAX (yChip0 - 0.5*glintWidth, region->y0);
+		    double yChipE = PS_MIN (yChip1 + 0.5*glintWidth, region->y1);
+		    double xChipS = PS_MAX (xChip0, region->x0);
+		    double xChipE = PS_MIN (xChip1, region->x1);
+
+		    // select the 0th readout of the 0th cell for this chip
+		    if (!chip->cells) continue;
+		    if (!chip->cells->n) continue;
+		    pmCell *glintCell = chip->cells->data[0];
+		    if (!glintCell) continue;
+		    if (!glintCell->readouts) continue;
+		    if (!glintCell->readouts->n) continue;
+		    pmReadout *glintReadout = glintCell->readouts->data[0];
+		    if (!glintReadout) continue;
+
+		    // save the glints on the readout->analysis metadata, creating if needed
+		    psArray *glints = psMetadataLookupPtr (&status, glintReadout->analysis, "PSASTRO.GLINTS");
+		    if (glints == NULL) { 
+			glints = psArrayAllocEmpty (100);
+			if (!psMetadataAdd (glintReadout->analysis, PS_LIST_TAIL, "PSASTRO.GLINTS", PS_DATA_ARRAY, "astrometry matches", glints)) {
+			    psWarning("failure to add glints to readout");
+			    psFree (glints);
+			    continue;
+			}
+			psFree (glints);
+		    }
+
+		    fprintf (stderr, "glint %s : %f,%f to %f,%f (%f,%f to %f,%f)\n", glintType, xChip0, yChip0, xChip1, yChip1, xChipS, yChipS, xChipE, yChipE);
+		    psRegion *glint = psRegionAlloc(xChipS, xChipE, yChipS, yChipE);
+		    psArrayAdd (glints, 100, glint);
+		    psFree (glint);
+		    psFree (region);
+		}
+	    }
+	    if (!strcasecmp(glintType, "HSC")) {
+	      // It's inefficient to keep looking these up.
+	      double GLINT_RADIUS_INNER = psMetadataLookupF32(&status, glintItem->data.md, "GLINT.RADIUS.INNER");
+	      double GLINT_RADIUS_OUTER = psMetadataLookupF32(&status, glintItem->data.md, "GLINT.RADIUS.OUTER");
+	      
+	      // double R_FPA = sqrt(pow(star->FP->y,2) + pow(star->FP->x,2));
+	      double R_FPA =  sqrt(pow(star->TP->y,2) + pow(star->TP->x,2));
+	      //	      R_FPA *= 1.30;
+	      if (R_FPA < GLINT_RADIUS_INNER) { continue; }
+	      if (R_FPA > GLINT_RADIUS_OUTER) { continue; }
+	      psTrace("psastro.masks",4,"HSC_GLINT_STARS: %f %f %f\n",
+		      //		      star->FP->x,star->FP->y,star->Mag);
+		      star->TP->x,star->TP->y,star->Mag);
+	      psVector *C_terms = NULL;
+	      psVector *R_terms = NULL;
+	      double scale_factor = 1.0;
+	      
+	      char *filter = psMetadataLookupStr (&status, fpa->concepts, "FPA.FILTERID");
+	      psMetadataItem *item = psMetadataLookup(glintItem->data.md, "GLINT.FILTER.TERM");
+	      if (!item) {
+		psLogMsg ("psastro", PS_LOG_INFO, "GLINT.FILTER.TERM data missing");
+		return false;
+	      }
+	      if (item->type != PS_DATA_METADATA_MULTI) {
+		psLogMsg ("psastro", PS_LOG_INFO, "GLINT.FILTER.TERM not multi");
+		return false;
+	      }
+	      psListIterator *iter = psListIteratorAlloc(item->data.list,PS_LIST_HEAD, false);
+	      psMetadataItem *refItem = NULL;
+	      while ((refItem = psListGetAndIncrement(iter))) {
+		if (refItem->type != PS_DATA_METADATA) {
+		  psLogMsg ("psastro", PS_LOG_INFO, "GLINT.FILTER.TERM entry not metadata");
+		  return false;
+		}
+		char *refFilter = psMetadataLookupStr (&status, refItem->data.md, "FILTER");
+		if (!status) {
+		  // psLogMsg ("psastro", PS_LOG_INFO, "a PHOTCODE.DATA recipe folder is missing FILTER");
+		  continue;
+		}
+		if (strcmp(refFilter, filter)) continue;
+
+		C_terms = psMetadataLookupPtr(&status, refItem->data.md, "C_TERMS");
+		R_terms = psMetadataLookupPtr(&status, refItem->data.md, "R_TERMS");
+		scale_factor = psMetadataLookupF32(&status, refItem->data.md, "FACTOR");
+		double limit = psMetadataLookupF32(&status, refItem->data.md, "LIMIT");
+		limit += MagOffset;
+		if (star->Mag > limit) { continue; }
+	      }
+	      // check 
+	      double glintPixelScale = psMetadataLookupF32(&status, glintItem->data.md, "GLINT.PIXEL.SCALE");
+	      //	      psMetadata *GLINT_PARAMETER_SET = psMetadataLookupPtr(&status, glintItem->data.md, "GLINT.FILTER.TERM");
+	      //	      psMetadata *GLINT_PARAMETERS    = psMetadataLookupPtr(&status, GLINT_PARAMETER_SET, filter);
+	      
+
+	      //	      double theta0 = atan2(star->FP->y,star->FP->x);
+	      double theta0 = atan2(star->TP->y,star->TP->x);
+	      double star_radius_deg = R_FPA * glintPixelScale;
+
+	      double C = C_terms->data.F32[0] + C_terms->data.F32[1] * star_radius_deg + C_terms->data.F32[2] * pow(star_radius_deg,2);
+	      double R = R_terms->data.F32[0] + R_terms->data.F32[1] * star_radius_deg + R_terms->data.F32[2] * pow(star_radius_deg,2);
+
+	      double inner_edge_angle, outer_edge_angle;
+	      if (star_radius_deg < 0.8831) {
+		outer_edge_angle = 0.0;
+	      }
+	      else if (star_radius_deg < 0.9368) {
+		outer_edge_angle = 46.2985 * sqrt(star_radius_deg - 0.8831);
+	      }
+	      else {
+		outer_edge_angle = -2.67 + 14.3 * star_radius_deg;
+	      }
+	      if (star_radius_deg < 0.964) {
+		inner_edge_angle = -243.866 * pow(star_radius_deg - 0.932,2) + 2.4636;
+	      }
+	      else {
+		inner_edge_angle = 66.2061 * sqrt(star_radius_deg - 0.9635);
+	      }
+
+	      inner_edge_angle = inner_edge_angle * PS_RAD_DEG;
+	      outer_edge_angle = outer_edge_angle * PS_RAD_DEG;
+	      
+	      // These define the ends of a single arc in arc-centric coordinates
+	      double x1 = C - R * cos(inner_edge_angle);
+	      double y1 = R * sin(inner_edge_angle);
+	      double x2 = C - R * cos(outer_edge_angle);
+	      double y2 = R * sin(outer_edge_angle);
+
+	      // Create the endpoints for the pair of arcs in device coordinates (in millimeters)
+	      double theta1 = theta0 + M_PI / 2.0;
+	      double x1s = (x1 * cos(theta1) - y1 * sin(theta1)) * scale_factor;
+	      double y1s = (y1 * cos(theta1) + x1 * sin(theta1)) * scale_factor;
+	      double x1e = (x2 * cos(theta1) - y2 * sin(theta1)) * scale_factor;
+	      double y1e = (y2 * cos(theta1) + x2 * sin(theta1)) * scale_factor;
+	      
+	      double x2s = (x1 * cos(theta1) + y1 * sin(theta1)) * scale_factor;
+	      double y2s = (-1.0 * y1 * cos(theta1) + x1 * sin(theta1)) * scale_factor;
+	      double x2e = (x2 * cos(theta1) + y2 * sin(theta1)) * scale_factor;
+	      double y2e = (-1.0 * y2 * cos(theta1) + x2 * sin(theta1)) * scale_factor;
+
+	      psVector *x_start = psVectorAlloc(4,PS_TYPE_F32);
+	      psVector *y_start = psVectorAlloc(4,PS_TYPE_F32);
+	      psVector *x_end   = psVectorAlloc(4,PS_TYPE_F32);
+	      psVector *y_end   = psVectorAlloc(4,PS_TYPE_F32);
+	      	      
+	      // CZW: I know this looks like a typo, but trust me, it's not a typo.
+	      psPlane *fp = psPlaneAlloc();
+	      psPlane *tp = psPlaneAlloc();
+
+	      fprintf(stderr,"HSC GLINT %f: x1x2(%f %f) (%f %f) x1sx1e (%f %f %f %f) x2sx2e (%f %f %f %f)\n",
+		      star->Mag,
+		      x1,y1,x2,y2,
+		      x1s,y1s,x1e,y1e,
+		      x2s,y2s,x2e,y2e);
+		      
+	      
+	      tp->x = 13.5 * (y1s / 0.015 + 12.78);
+	      tp->y = 13.5 * (x1s / -0.015 + 57.74);
+	      psPlaneTransformApply(fp,fpa->fromTPA, tp);
+	      x_start->data.F32[0] = fp->x;
+	      y_start->data.F32[0] = fp->y;
+	      //	      x_start->data.F32[0] = tp->x;
+	      //	      y_start->data.F32[0] = tp->y;
+
+	      tp->x = 13.5 * (y1e / 0.015 + 12.78);
+	      tp->y = 13.5 * (x1e / -0.015 + 57.74);
+	      psPlaneTransformApply(fp,fpa->fromTPA, tp);
+	      x_end->data.F32[0]   = fp->x;
+	      y_end->data.F32[0]   = fp->y;
+	      //	      x_end->data.F32[0]   = tp->x;
+	      //	      y_end->data.F32[0]   = tp->y;
+
+	      x_start->data.F32[1] = x_end->data.F32[0];  
+	      y_start->data.F32[1] = y_end->data.F32[0];  
+	      x_end->data.F32[1]   = x_start->data.F32[0];
+	      y_end->data.F32[1]   = y_start->data.F32[0];
+
+	      tp->x = 13.5 * (y2s / 0.015 + 12.78);
+	      tp->y = 13.5 * (x2s / -0.015 + 57.74);
+	      psPlaneTransformApply(fp,fpa->fromTPA, tp);
+	      x_start->data.F32[2] = fp->x;
+	      y_start->data.F32[2] = fp->y;
+	      //	      x_start->data.F32[2] = tp->x;
+	      //	      y_start->data.F32[2] = tp->y;
+
+	      tp->x = 13.5 * (y2e / 0.015 + 12.78);
+	      tp->y = 13.5 * (x2e / -0.015 + 57.74);
+	      psPlaneTransformApply(fp,fpa->fromTPA, tp);
+	      x_end->data.F32[2]   = fp->x;
+	      y_end->data.F32[2]   = fp->y;
+	      //	      x_end->data.F32[2]   = tp->x;
+	      //	      y_end->data.F32[2]   = tp->y;
+	      
+	      x_start->data.F32[3] = x_end->data.F32[2];  
+	      y_start->data.F32[3] = y_end->data.F32[2];  
+	      x_end->data.F32[3]   = x_start->data.F32[2];
+	      y_end->data.F32[3]   = y_start->data.F32[2];
+
+	      psFree(fp);
+	      psFree(tp);
+	      
+	      for (int glint_point = 0; glint_point < 4; glint_point++) {
+		for (int nChip = 0; nChip < fpa->chips->n; nChip++) {
+		  pmChip *chip = fpa->chips->data[nChip];
+		  if (!chip) continue;
+		  
+		  if (!psastroFindChipInYrange (fpa, nChip, x_start->data.F32[glint_point], y_start->data.F32[glint_point])) {
+		    continue;
+		  }
+		  if (!psastroFindChipInXrange (fpa, nChip, x_start->data.F32[glint_point], y_start->data.F32[glint_point])) {
+		    continue;
+		  }
+
+		  double xChip0, yChip0, xChip1, yChip1, chip_angle, glint_length;
+		  psastroFPAtoChip (&xChip0, &yChip0, fpa, nChip, x_start->data.F32[glint_point], y_start->data.F32[glint_point]);
+		  psastroFPAtoChip (&xChip1, &yChip1, fpa, nChip, x_end->data.F32[glint_point], y_end->data.F32[glint_point]);
+
+		  chip_angle = atan2(yChip1 - yChip0, xChip1 - xChip0);
+		  glint_length = sqrt(pow(yChip1 - yChip0,2) + pow(xChip1 - xChip0,2));
+
+		  // select the 0th readout of the 0th cell for this chip
+		  if (!chip->cells) continue;
+		  if (!chip->cells->n) continue;
+		  pmCell *glintCell = chip->cells->data[0];
+		  if (!glintCell) continue;
+		  if (!glintCell->readouts) continue;
+		  if (!glintCell->readouts->n) continue;
+		  pmReadout *glintReadout = glintCell->readouts->data[0];
+		  if (!glintReadout) continue;
+		  
+		  // save the glints on the readout->analysis metadata, creating if needed
+		  psArray *glints = psMetadataLookupPtr (&status, glintReadout->analysis, "PSASTRO.GLINTS.HSC");
+		  if (glints == NULL) { 
+		    glints = psArrayAllocEmpty (100);
+		    if (!psMetadataAdd (glintReadout->analysis, PS_LIST_TAIL, "PSASTRO.GLINTS.HSC", PS_DATA_ARRAY, "astrometry matches", glints)) {
+		      psWarning("failure to add glints to readout");
+		      psFree (glints);
+		      continue;
+		  }
+		    psFree (glints);
+		  }
+		  
+		  fprintf (stderr, "glint %s : %d %f,%f to %f,%f (%f %f %f)\n", glintType, nChip, xChip0, yChip0, xChip1, yChip1, glint_length, glintWidth, chip_angle);
+		  psTrace("psastro.masks",4,"HSC_GLINT: Star: %f %f Glint CR %f %f Parameters: %f %f %f Ends: %f %f -> %f %f Chip: %f %f -> %f %f @ %s %f\n",
+			  star->FP->x,star->FP->y,
+			  C,R,
+			  inner_edge_angle,outer_edge_angle,theta0,
+			  x_start->data.F32[glint_point],y_start->data.F32[glint_point],x_end->data.F32[glint_point],y_end->data.F32[glint_point],
+			  xChip0,yChip0,xChip1,yChip1,
+			  psMetadataLookupStr(&status,glintReadout->parent->parent->concepts,"CHIP.NAME"),chip_angle);
+		  psVector *glint = psVectorAlloc(5,PS_TYPE_F32);
+		  glint->data.F32[0] = xChip0;
+		  glint->data.F32[1] = yChip0;
+		  glint->data.F32[2] = glint_length * pixelScale / pixelScale;
+		  glint->data.F32[3] = glintWidth;
+		  glint->data.F32[4] = chip_angle;
+
+		  psArrayAdd (glints, 100, glint);
+
+		  psFree (glint);
+		} // End loop over chips
+	      } // End loop over glint endpoints
+
+	      psFree(x_start);
+	      psFree(x_end);
+	      psFree(y_start);
+	      psFree(y_end);
+	    } // End HSC glint block
+	    
+	}
+    }
+    psastroExtractFreeChipBounds();
+    return true;
+}
+
+// XXX need to place the glints on the right analysis...
+// psMetadataAdd (fpa->analysis, PS_LIST_TAIL, "PSASTRO.GLINT.STARS", PS_DATA_ARRAY, "possible glint stars", glintStars);
+// psFree (glintStars);
+
+
+
+# if (0)
+
+	    // depending on the glint type, we need to find either the chips in the row or in the column.
+	    if (!strcasecmp(glintType, "LEFT") || !strcasecmp(glintType, "RIGHT")) {
+		for (int nChip = 0; nChip < fpa->chips->n; nChip++) {
+
+		    double xChip0, yChip0;
+		    if (!psastroFindChipInYrange (&xChip0, &yChip0, fpa, nChip, star->FP->x, star->FP->y)) {
+			continue;
+		    }
+
+		    pmChip *chip = fpa->chips->data[nChip];
+
+		    // select the 0th readout of the 0th cell for this chip
+		    if (!chip) continue;
+		    if (!chip->cells) continue;
+		    if (!chip->cells->n) continue;
+		    pmCell *glintCell = chip->cells->data[0];
+		    if (!glintCell) continue;
+		    if (!glintCell->readouts) continue;
+		    if (!glintCell->readouts->n) continue;
+		    pmReadout *glintReadout = glintCell->readouts->data[0];
+		    if (!glintReadout) continue;
+
+		    // save the glints on the readout->analysis metadata, creating if needed
+		    psArray *glints = psMetadataLookupPtr (&status, glintReadout->analysis, "PSASTRO.GLINTS");
+		    if (glints == NULL) { 
+			glints = psArrayAllocEmpty (100);
+			if (!psMetadataAdd (glintReadout->analysis, PS_LIST_TAIL, "PSASTRO.GLINTS", PS_DATA_ARRAY, "astrometry matches", glints)) {
+			    psWarning("failure to add glints to readout");
+			    continue;
+			}
+			psFree (glints);
+		    }
+
+		    // bounds of this chip
+		    psRegion *region = pmChipPixels (chip);
+
+		    // find the coordinate of the end point
+		    double xChip1, yChip1;
+		    if (!strcasecmp(glintType, "RIGHT")) {
+			if (!psastroFindChipInYrange (&xChip1, &yChip1, fpa, nChip, star->FP->x - glintLength, star->FP->y)) {
+			    psAbort ("inconsistent chip position result"); 
+			}
+		    } else {
+			// find the coordinate of the end point
+			if (!psastroFindChipInYrange (&xChip1, &yChip1, fpa, nChip, star->FP->x + glintLength, star->FP->y)) {
+			    psAbort ("inconsistent chip position result"); 
+			}
+		    }
+
+		    // we have the location in chip coordinates of the two glint end-points.
+		    // check if this chip overlaps this glint
+		    if (xChip0 > xChip1) PS_SWAP (xChip0, xChip1);
+		    if (xChip1 < region->x0) continue;
+		    if (xChip0 > region->x1) continue;
+
+		    // this glint touches this chip. calculate the start and end
+		    // coordinates on this chip
+		    double yChip;
+		    double xChipS = PS_MAX (xChip0, region->x0);
+		    double xChipE = PS_MIN (xChip1, region->x1);
+
+		    // if the line has any tilt (in chip coordinates), interpolate for Y:
+		    if (fabs(region->y1 - region->y0) > 1.0) {
+			double yChipS = yChip0 + (yChip1 - yChip0) * (xChipS - xChip0) / (xChip1 - xChip0);
+			double yChipE = yChip0 + (yChip1 - yChip0) * (xChipE - xChip0) / (xChip1 - xChip0);
+			yChip = 0.5*(yChipS + yChipE);
+		    } else {
+			yChip = 0.5*(yChip0 + yChip1);
+		    }
+
+		    fprintf (stderr, "glint %s : %f,%f to %f,%f (%f - %f @ %f)\n", glintType, xChip0, yChip0, xChip1, yChip1, xChipS, xChipE, yChip);
+		    psRegion *glint = psRegionAlloc(xChipS, xChipE, yChip - 0.5*glintWidth, yChip + 0.5*glintWidth);
+		    psArrayAdd (glints, 100, glint);
+		    psFree (glint);
+		    psFree (region);
+		}
+	    }
+# endif
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLoadRefstars.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLoadRefstars.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLoadRefstars.c	(revision 41084)
@@ -0,0 +1,418 @@
+/** @file psastroLoadRefstars.c
+ *
+ *  @brief
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.36 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-09 21:25:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+# define ELIXIR_MODE 1
+
+psArray *psastroLoadRefstars (pmConfig *config, const char *source) {
+
+    int fd;
+    bool status;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+
+    // DVO APIs expect decimal degrees
+    float RAmin  = DEG_RAD*psMetadataLookupF32(NULL, recipe, "RA_MIN");
+    float RAmax  = DEG_RAD*psMetadataLookupF32(NULL, recipe, "RA_MAX");
+    float DECmin = DEG_RAD*psMetadataLookupF32(NULL, recipe, "DEC_MIN");
+    float DECmax = DEG_RAD*psMetadataLookupF32(NULL, recipe, "DEC_MAX");
+
+    // extra field fraction to add
+    double fieldPadding = psMetadataLookupF32 (&status, recipe, "PSASTRO.FIELD.PADDING");
+    PS_ASSERT (status, NULL);
+
+    float dRA = RAmax - RAmin;
+    if (dRA * (1. + 2 * fieldPadding) < 180.) {
+        RAmin -= dRA * fieldPadding;
+        RAmax += dRA * fieldPadding;
+    } else {
+        // if dRA > 180 getstar has problems. Just search the entire range
+        RAmin = 0;
+        RAmax = 360;
+    }
+
+    float dDEC = DECmax - DECmin;
+    DECmin -= dDEC * fieldPadding;
+    DECmax += dDEC * fieldPadding;
+
+    // grab the PSASTRO.CATDIR name from the PSASTRO recipe
+    char *catdir_recipe = psMetadataLookupStr(&status, recipe, "PSASTRO.CATDIR");
+    psAssert (catdir_recipe, "Need a recipe for the catdir!");
+
+    // substitute abstract name with concrete name, if present in PSASTRO.CATDIRS
+    psMetadata *catdirs = psMetadataLookupMetadata(&status, config->site, "PSASTRO.CATDIRS"); // List of cameras
+    if (!catdirs) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find PSASTRO.CATDIRS in the system configuration.\n");
+        return false;
+    }
+
+    // the name in the recipe may be one of:
+    // (A) the actual directory name
+    // (B) a reference to the name in the PSASTRO.CATDIRS folder in site.config
+    // (C) a reference to a folder in the PSASTRO.CATDIRS folder in site.config, containing multiple copy locations
+    char *catdir_virtual = psMetadataLookupStr(&status, catdirs, catdir_recipe);
+
+    psMetadata *catdir_folder = psMetadataLookupMetadata(&status, catdirs, catdir_recipe);
+    if (catdir_folder) {
+        // randomly choose one of the entries
+        psLogMsg ("psastro", 3, "choosing catdir_folder\n");
+        int nEntry = catdir_folder->list->n;
+
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS);
+        double frnd = psRandomUniform(rng);
+        int entry = PS_MIN(nEntry - 1, PS_MAX(0, nEntry * frnd));
+        psFree(rng);
+
+        psMetadataItem *item = psListGet(catdir_folder->list, entry);
+	assert (item);
+        if (item->type != PS_DATA_STRING) {
+            psError(PSASTRO_ERR_CONFIG, true, "Invalid entry in PSASTRO.CATDIR folder: %s\n", item->name);
+            return false;
+        }
+        catdir_virtual = item->data.str;
+    }
+
+    char *catdir = (catdir_virtual == NULL) ? catdir_recipe : catdir_virtual;
+
+    // convert the uri to a real filename (ie, path://foobar)
+    psString CATDIR = pmConfigConvertFilename(catdir, config, false, false); // Resolved filename
+    PS_ASSERT (CATDIR, NULL);
+
+    psLogMsg ("psastro", 3, "looking up reference objects in %s\n", CATDIR);
+
+    char *getstarCommand = psStringCopy(psMetadataLookupStr(NULL, recipe, "DVO.GETSTAR"));
+    PS_ASSERT (getstarCommand, NULL);
+
+    char *outformat = psMetadataLookupStr(NULL, recipe, "DVO.GETSTAR.OUTFORMAT");
+    PS_ASSERT (outformat, NULL);
+
+    // we have an exposure with a certain filter and exposure time: choose the maximum
+    // reference star density for stars fainter than an equivalent instrumental mag limit
+    float minMag;
+    float maxRho;
+    char *photcode = psastroSetMagLimit (&minMag, &maxRho, config, source);
+    PS_ASSERT (photcode, NULL);
+
+    // issue the following command:
+    // getstar -region RAmin RAmax DECmin DECmax
+    char tempFile[64];
+    sprintf (tempFile, "/tmp/psastro.XXXXXX");
+    if ((fd = mkstemp (tempFile)) == -1) {
+        psError(PSASTRO_ERR_REFSTARS, true, "error creating temp output file\n");
+        psFree(CATDIR);
+        return NULL;
+    }
+    close (fd);
+
+    psTimerStart ("psastro");
+
+    // supply a known output format (for CATALOG output) so the code below knows what to read
+    if (ELIXIR_MODE) {
+        psStringAppend (&getstarCommand, " -D CATMODE mef -D CATFORMAT elixir");
+    } else {
+        psStringAppend (&getstarCommand, " -D CATMODE mef -D CATFORMAT panstarrs");
+    }
+
+    // check for default name (use .ptolemyrc), or use specified CATDIR
+    if (strcasecmp(CATDIR, "NONE")) {
+        psStringAppend (&getstarCommand, " -D CATDIR %s", CATDIR);
+    }
+    psFree(CATDIR);
+
+    // supply the max magnitude, the output format, and the photcode
+    if (strcasecmp (photcode, "NONE")) {
+        psStringAppend (&getstarCommand, " -photcode %s -minmag %f -max-density %f", photcode, minMag, maxRho);
+    }
+    psStringAppend (&getstarCommand, " -format %s", outformat);
+
+    // add region and output filename
+    psStringAppend (&getstarCommand, " -region %f %f %f %f -o %s", RAmin, DECmin, RAmax, DECmax, tempFile);
+    psTrace ("psastro", 3, "%s\n", getstarCommand);
+
+    psLogMsg("psastro", PS_LOG_INFO, "getstar command: %s", getstarCommand);
+
+    // XXX use psPipe: catch stderr, stdout, allow for Nsec timeout...
+    // use fork to add timeout capability
+    status = system (getstarCommand);
+    if (status) {
+        psError(PSASTRO_ERR_REFSTARS, true, "error loading reference data\n");
+        return NULL;
+    }
+    psFree (getstarCommand);
+
+    psLogMsg ("psastro", 3, "ran getstar : %f sec\n", psTimerMark ("psastro"));
+
+    // the output from getstar is a file with the Average table
+    psFits *fits = psFitsOpen (tempFile, "r");
+
+    psTimerStart ("psastro");
+
+    psArray *refstars = NULL;
+    if (!strcmp (outformat, "CATALOG")) {
+      refstars = psastroReadGetstarCatalog (fits);
+    }
+    if (!strcmp (outformat, "PS1_DEV_0")) {
+      refstars = psastroReadGetstar_PS1_DEV_0 (fits);
+
+      // XXX test
+      // FILE *outfile = fopen ("refstars.dat", "w");
+      // assert (outfile);
+      // for (int nn = 0; nn < refstars->n; nn++) {
+      //          pmAstromObj *ref = refstars->data[nn];
+      //          fprintf (outfile, "%lf %lf\n", ref->sky->r*PS_DEG_RAD, ref->sky->d*PS_DEG_RAD);
+      // }
+      // fclose (outfile);
+    }
+    if (refstars == NULL) {
+        psError(PSASTRO_ERR_REFSTARS, true, "error reading reference data\n");
+        psFitsClose (fits);
+        return NULL;
+    }
+
+    // apply a color correction
+    {
+	// from Tonry 
+	// (w-r)_obs = 0.042 + 0.166 (r-i)_obs - 0.398 (r-i)_obs^2,  (r-i)_obs < 0.5
+	// (w-r)_obs = 0.268 - 0.435 (r-i)_obs - 0.078 (r-i)_obs^2,  (r-i)_obs > 0.5
+	// thus, for (r-i < 0.5):
+	// (r - i < 0.5) : w = r + 0.042 + 0.166*(r-i) - 0.398(r-i)^2
+	// (r - i > 0.5) : w = r + 0.268 - 0.435 (r-i) - 0.078(r-i)^2
+
+	// apply a color correction
+	// XXX this is very GPC1 specific and hard-wired -- be very afraid!
+	// select the filter; default to fixed photcode and mag limit otherwise
+	pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, source); // we've already done this before
+	char *filter = psMetadataLookupStr (&status, input->fpa->concepts, "FPA.FILTERID");
+	if (!strcmp(filter, "w")) {
+	    for (int i = 0; i < refstars->n; i++) {
+		pmAstromObj *ref = refstars->data[i];
+		float r = ref->Mag;
+		float ri = ref->Color;
+		// saturate at some valid range limits
+		ri = PS_MAX (PS_MIN(ri, 2.0), -0.2);
+		float w = NAN;
+		if (ri < 0.5) {
+		    w = r + 0.042 + 0.166*(ri) - 0.398*PS_SQR(ri);
+		} else {
+		    w = r + 0.268 - 0.435*(ri) - 0.078*PS_SQR(ri);
+		}
+		ref->Mag = w;
+	    }
+	}
+    }
+
+    psLogMsg ("psastro", 3, "loaded %ld reference stars : %f sec\n", refstars->n, psTimerMark ("psastro"));
+
+    psTrace ("psastro", 3, "loaded %ld reference stars from (%10.6f,%10.6f) - (%10.6f,%10.6f)\n",
+             refstars->n, RAmin, DECmin, RAmax, DECmax);
+
+    psFitsClose (fits);
+    unlink (tempFile);
+
+    // dump or plot the available refstars
+    if (psTraceGetLevel("psastro.dump") > 0) {
+        psastroDumpRefstars (refstars, "refstars.dat");
+    }
+
+    pmAstromVisualPlotRefStars (refstars, recipe);
+
+    if (psTraceGetLevel("psastro.plot") > 0) {
+        psastroPlotRefstars (refstars, recipe);
+    }
+
+    return refstars;
+}
+
+psArray *psastroReadGetstarCatalog (psFits *fits) {
+
+    bool status;
+
+    if (ELIXIR_MODE) {
+        psFitsMoveExtName (fits, "DVO_AVERAGE_ELIXIR");
+    } else {
+        psFitsMoveExtName (fits, "DVO_AVERAGE_PANSTARRS");
+    }
+
+    long numSources = psFitsTableSize(fits); // Number of sources in table
+
+    // convert the Average table to the pmAstromObj entries
+    psArray *refstars = psArrayAllocEmpty (numSources);
+    for (int i = 0; i < numSources; i++) {
+        pmAstromObj *ref = pmAstromObjAlloc ();
+
+        psMetadata *row = psFitsReadTableRow(fits, i); // Table row
+
+        // DVO tables are stored in degrees
+        if (ELIXIR_MODE) {
+            ref->sky->r   = RAD_DEG*psMetadataLookupF32 (&status, row, "RA");
+            ref->sky->d   = RAD_DEG*psMetadataLookupF32 (&status, row, "DEC");
+            ref->Mag      = 0.001*psMetadataLookupS32 (&status, row, "MAG");  // ELIXIR uses millimags
+            ref->Color    = 0.0;
+        } else {
+            ref->sky->r   = RAD_DEG*psMetadataLookupF64 (&status, row, "RA");
+            ref->sky->d   = RAD_DEG*psMetadataLookupF64 (&status, row, "DEC");
+            ref->Mag      = psMetadataLookupF32 (&status, row, "MAG"); // PANSTARRS uses mags
+            ref->Color    = 0.0;
+        }
+	ref->magCal   = ref->Mag;
+
+        // XXX VERY temporary hack to avoid M31 bulge
+        if ((fabs(ref->sky->r - 0.186438) < 0.002) && (fabs(ref->sky->d - 0.720270) < 0.002)) {
+          psFree (ref);
+          psFree (row);
+          continue;
+        }
+
+        psArrayAdd (refstars, 100, ref);
+        psFree (ref);
+        psFree (row);
+    }
+    return refstars;
+}
+
+psArray *psastroReadGetstar_PS1_DEV_0 (psFits *fits) {
+
+    bool status;
+
+    psFitsMoveExtName (fits, "GETSTAR_PS1_DEV_0");
+
+    long numSources = psFitsTableSize(fits); // Number of sources in table
+
+    // convert the Average table to the pmAstromObj entries
+    psArray *refstars = psArrayAllocEmpty (numSources);
+    for (int i = 0; i < numSources; i++) {
+        pmAstromObj *ref = pmAstromObjAlloc ();
+
+        psMetadata *row = psFitsReadTableRow(fits, i); // Table row
+
+        ref->sky->r   = RAD_DEG*psMetadataLookupF64 (&status, row, "RA");
+        ref->sky->d   = RAD_DEG*psMetadataLookupF64 (&status, row, "DEC");
+        ref->Mag      = psMetadataLookupF32 (&status, row, "MAG");
+        float MagC1   = psMetadataLookupF32 (&status, row, "MAG_C1");
+        float MagC2   = psMetadataLookupF32 (&status, row, "MAG_C2");
+        if (!isnan(MagC1) && !isnan(MagC2)) {
+            ref->Color = MagC1 - MagC2;
+        } else {
+            // XXX save the color and the slope in the table header?
+            ref->Color = 0.0;
+        }
+	ref->magCal   = ref->Mag;
+
+        // XXX VERY temporary hack to avoid M31 bulge
+        if ((fabs(ref->sky->r - 0.186438) < 0.002) && (fabs(ref->sky->d - 0.720270) < 0.002)) {
+          psFree (ref);
+          psFree (row);
+          continue;
+        }
+
+        psArrayAdd (refstars, 100, ref);
+        psFree (ref);
+        psFree (row);
+    }
+    return refstars;
+}
+
+# define ESCAPE(MSG) { \
+  psLogMsg ("psastro", PS_LOG_INFO, MSG); \
+  goto escape; }
+
+char *psastroSetMagLimit (float *minMag, float *maxRho, pmConfig *config, const char *source) {
+
+    bool status;
+    char *photcode;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, source);
+    if (!input) {
+        psLogMsg ("psastro", PS_LOG_DETAIL, "no supplied reference header data");
+        photcode = psStringCopy ("NONE");
+        return photcode;
+    }
+    assert (input->fpa);
+
+    *maxRho = psMetadataLookupF32(&status, recipe, "DVO.GETSTAR.MAX.RHO");
+    if (!status) {
+        psError(PSASTRO_ERR_CONFIG, false, "DVO.GETSTAR.MAX.RHO missing from recipe");
+        return NULL;
+    }
+
+    // select the filter; default to fixed photcode and mag limit otherwise
+    char *filter = psMetadataLookupStr (&status, input->fpa->concepts, "FPA.FILTERID");
+    if (!status) ESCAPE ("missing FPA.FILTER in concepts");
+
+    float exptime = psMetadataLookupF32 (&status, input->fpa->concepts, "FPA.EXPOSURE");
+    if (!status) ESCAPE ("missing FPA.EXPOSURE in concepts");
+
+    // we need to select the PHOTCODE.DATA folder that matches our filter
+    psMetadataItem *item = psMetadataLookup (recipe, "PHOTCODE.DATA");
+    if (!item) ESCAPE ("PHOTCODE.DATA folders missing");
+    if (item->type != PS_DATA_METADATA_MULTI) ESCAPE ("PHOTCODE.DATA not a multi");
+
+    float minInst = psMetadataLookupF32(&status, recipe, "DVO.GETSTAR.MIN.MAG.INST");
+    if (!status) ESCAPE ("missing DVO.GETSTAR.MIN.MAG.INST");
+
+    // if non zero override the zero point in the PHOTCODE.DATA with this value
+    float fixedzeropt = psMetadataLookupF32(&status, recipe, "DVO.GETSTAR.FIXED.ZEROPT");
+
+    // PHOTCODE.DATA is a multi of metadata items
+    psListIterator *iter = psListIteratorAlloc(item->data.list, PS_LIST_HEAD, false);
+
+    psMetadataItem *refItem = NULL;
+    while ((refItem = psListGetAndIncrement (iter))) {
+        if (refItem->type != PS_DATA_METADATA) ESCAPE ("PHOTCODE.DATA entry is not a metadata folder");
+
+        char *refFilter = psMetadataLookupStr (&status, refItem->data.md, "FILTER");
+        if (!status) {
+            psLogMsg ("psastro", PS_LOG_INFO, "a PHOTCODE.DATA recipe folder is missing FILTER");
+            continue;
+        }
+
+        // does this entry match the current filter?
+        if (strcmp (refFilter, filter)) continue;
+
+        psLogMsg ("psastro", PS_LOG_DETAIL, "PHOTCODE.DATA found for filter %s", filter);
+
+        float zeropt = psMetadataLookupF32 (&status, refItem->data.md, "ZEROPT");
+        if (!status) {
+            psLogMsg ("psastro", PS_LOG_INFO, "a PHOTCODE.DATA recipe folder is missing FILTER");
+            continue;
+        }
+        photcode = psMetadataLookupStr (&status, refItem->data.md, "PHOTCODE");
+        if (!status) {
+            psLogMsg ("psastro", PS_LOG_INFO, "a PHOTCODE.DATA recipe folder is missing FILTER");
+            continue;
+        }
+        if (fixedzeropt != 0.0) {
+            // override the recipe's zero point with the fixed value (used for stacks)
+            zeropt = fixedzeropt;
+        }
+
+        // convert the minInst to a calibrated minimum magnitude
+        *minMag = minInst + 2.5*log10(exptime) + zeropt;
+
+        psFree (iter);
+        return photcode;
+    }
+    psFree (iter);
+
+  escape:
+    photcode = psMetadataLookupStr(NULL, recipe, "DVO.GETSTAR.PHOTCODE");
+    PS_ASSERT (photcode, NULL);
+
+    // give up and use fixed value
+    *minMag = psMetadataLookupF32(NULL, recipe, "DVO.GETSTAR.MIN.MAG");
+    return photcode;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLuminosityFunction.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLuminosityFunction.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLuminosityFunction.c	(revision 41084)
@@ -0,0 +1,155 @@
+/** @file psastroLuminosityFunction.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.19 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-09 21:25:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+# define dMag 0.1
+// XXX put this in config?
+
+static void pmLumFuncFree (pmLumFunc *func) {
+
+    if (func == NULL) return;
+
+    return;
+}
+
+pmLumFunc *pmLumFuncAlloc () {
+
+    pmLumFunc *func = (pmLumFunc *) psAlloc(sizeof(pmLumFunc));
+    psMemSetDeallocator(func, (psFreeFunc) pmLumFuncFree);
+
+    func->mMin = 0.0;
+    func->mMax = 0.0;
+    func->slope = 0.0;
+    func->offset = 0.0;
+
+    func->mPeak = 0.0;
+    func->nPeak = 0;
+    func->sPeak = 0;
+
+    return func;
+}
+
+pmLumFunc *psastroLuminosityFunction (psArray *stars, pmLumFunc *rawFunc) {
+
+    if (stars->n == 0) return NULL;
+
+    // determine the max and min magnitude for the array of stars
+    pmAstromObj *star = stars->data[0];
+    double mMin = star->Mag;
+    double mMax = star->Mag;
+    for (int i = 0; i < stars->n; i++) {
+        star = stars->data[i];
+        mMin = PS_MIN (star->Mag, mMin);
+        mMax = PS_MAX (star->Mag, mMax);
+    }
+
+    int nBin = 1 + (mMax - mMin) / dMag;
+    if (nBin <= 1) {
+        psLogMsg ("psastro", 4, "insufficient valid stars for this readout\n");
+        return NULL;
+    }
+
+    // create a histogram of the magnitudes
+    // bin[0] = mMin : mMin + dMag
+    // bin[i] = mMin + i*dMag : mMin + (i+1)*dMag
+    psVector *nMags = psVectorAlloc (nBin, PS_TYPE_F32);
+    psVectorInit (nMags, 0);
+
+    for (int i = 0; i < stars->n; i++) {
+        star = stars->data[i];
+        if (!isfinite(star->Mag)) continue;
+        int bin = (star->Mag - mMin) / dMag;
+        if (bin < 0) psAbort("bin cannot be negative!");
+        if (bin >= nBin) psAbort("bin cannot be > %d!", nBin);
+        nMags->data.F32[bin] += 1.0;
+    }
+
+    // find the peak and position
+    int iPeak = 0;
+    int nPeak = 0;
+    int sPeak = 0;
+    for (int i = 0; i < nMags->n; i++) {
+        if (nMags->data.F32[i] < nPeak) continue;
+        iPeak = i;
+        nPeak = nMags->data.F32[i];
+        sPeak += nPeak;
+    }
+
+    psVector *lnMag = psVectorAllocEmpty (nBin, PS_TYPE_F32);
+    psVector *Mag = psVectorAllocEmpty (nBin, PS_TYPE_F32);
+
+    // create 2 vectors represnting the dlogN/dlogS line
+    // exclude bins with no stars
+    // exclude points after the peak with N < 0.8*peak
+    int n = 0;
+    for (int i = 0; i < nMags->n; i++) {
+        if (nMags->data.F32[i] < 1) continue;
+        if ((i > iPeak) && (nMags->data.F32[i] < 0.8*nPeak)) continue;
+        lnMag->data.F32[n] = log10 (nMags->data.F32[i]);
+        Mag->data.F32[n] = mMin + (i + 0.5)*dMag;
+        n++;
+    }
+    lnMag->n = n;
+    Mag->n = n;
+    psLogMsg ("psastro", 4, "fitting %d points to luminosity function\n", n);
+
+    psVector *mask = psVectorAlloc (Mag->n, PS_TYPE_VECTOR_MASK);
+    psVectorInit (mask, 0);
+
+    psPolynomial1D *line = psPolynomial1DAlloc (PS_POLYNOMIAL_ORD, 1);
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    stats->clipSigma = 3.0;
+    stats->clipIter = 3;
+
+    if (!psVectorClipFitPolynomial1D(line, stats, mask, 0xff, lnMag, NULL, Mag)) {
+        psLogMsg ("psastro", 4, "Failed to fit the luminosity function\n");
+        return(NULL);
+    }
+
+    // find min and max unmasked magnitudes
+    double mMinValid = NAN;
+    double mMaxValid = NAN;
+    for (int i = 0; i < Mag->n; i++) {
+        if (mask->data.PS_TYPE_VECTOR_MASK_DATA[i]) continue;
+        if (isnan(mMinValid) || (Mag->data.F32[i] < mMinValid)) {
+            mMinValid = Mag->data.F32[i];
+        }
+        if (isnan(mMaxValid) || (Mag->data.F32[i] > mMaxValid)) {
+            mMaxValid = Mag->data.F32[i];
+        }
+    }
+
+    psLogMsg ("psastro", 4, "logN = %f + %f mag\n", line->coeff[0], line->coeff[1]);
+    psLogMsg ("psastro", 4, "logN vs mag residuals: %f +/- %f\n", stats->sampleMedian, stats->sampleStdev);
+
+    pmLumFunc *lumFunc = pmLumFuncAlloc ();
+    lumFunc->mMin = mMinValid;
+    lumFunc->mMax = mMaxValid;
+    lumFunc->offset = line->coeff[0];
+    lumFunc->slope = line->coeff[1];
+    lumFunc->mPeak = mMin + (iPeak + 0.5)*dMag;
+    lumFunc->nPeak = nPeak;
+    lumFunc->sPeak = sPeak;
+
+    pmAstromVisualPlotLuminosityFunction(lnMag, Mag, lumFunc, rawFunc);
+
+    psFree (lnMag);
+    psFree (nMags);
+    psFree (Mag);
+    psFree (mask);
+    psFree (line);
+    psFree (stats);
+
+    return (lumFunc);
+}
+
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLuminosityFunctionPlot.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLuminosityFunctionPlot.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroLuminosityFunctionPlot.c	(revision 41084)
@@ -0,0 +1,100 @@
+#include "psastroInternal.h"
+
+bool psastroLuminosityFunctionPlot (psVector *lnMag, psVector *Mag,
+                                    pmLumFunc *lumFunc, pmLumFunc *rawFunc) {
+    //Set up graph window
+    Graphdata graphdata;
+    int kapa = pmKapaOpen(true);
+    KapaSection section1 = {"s1", .05, .05, .9, .4};
+    KapaSection section2 = {"s2", .05, .55, .9, .4};
+    KapaSection section;
+    if(rawFunc == NULL)
+        section = section1;
+    else
+        section = section2;
+    int DX = 1000, DY = 800;
+    KapaResize(kapa, DX, DY);
+    if (rawFunc == NULL)
+        KapaClearPlots(kapa);
+    KapaInitGraph(&graphdata);
+
+    //Determine Plot Limits
+    graphdata.xmin = 50;
+    graphdata.ymin = 50;
+    graphdata.xmax = -50;
+    graphdata.ymax = -50;
+
+    for(int i = 0; i < lnMag->n; i++) {
+        graphdata.xmin = PS_MIN(graphdata.xmin,   Mag->data.F32[i]);
+        graphdata.ymin = PS_MIN(graphdata.ymin, lnMag->data.F32[i]);
+        graphdata.xmax = PS_MAX(graphdata.xmax,   Mag->data.F32[i]);
+        graphdata.ymax = PS_MAX(graphdata.ymax, lnMag->data.F32[i]);
+    }
+
+    float range = graphdata.xmax - graphdata.xmin;
+    graphdata.xmin -= .05 * range;
+    graphdata.xmax += .05 * range;
+
+    range = graphdata.ymax - graphdata.ymin;
+    graphdata.ymin -= .05 * range;
+    graphdata.ymax += .05 * range;
+
+    //Make a line for the fit
+    float x[2] = {graphdata.xmin, graphdata.xmax};
+    float y[2] = {lumFunc->offset + x[0] * lumFunc->slope,
+                 lumFunc->offset + x[1] * lumFunc->slope};
+
+    //Plot Data
+    KapaSetSection(kapa, &section);
+    KapaSetLimits(kapa, &graphdata);
+
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaBox (kapa, &graphdata);
+    KapaSendLabel (kapa, "Magnitude", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "Log(N)", KAPA_LABEL_YM);
+    if (rawFunc == NULL)
+        KapaSendLabel (kapa, "Raw Star Luminosity Function", KAPA_LABEL_XP);
+    else
+        KapaSendLabel (kapa,
+                       "Reference Star Luminosity Function, Shifted Raw Fit, and Cutoff",
+                       KAPA_LABEL_XP);
+    graphdata.color=KapaColorByName("black");
+    graphdata.style = 1;
+    KapaPrepPlot (kapa, lnMag->n, &graphdata);
+    KapaPlotVector(kapa, lnMag->n,   Mag->data.F32, "x");
+    KapaPlotVector(kapa, lnMag->n, lnMag->data.F32, "y");
+
+    //Overplot fit
+    graphdata.style=0;
+    KapaPrepPlot(kapa,2,&graphdata);
+    KapaPlotVector(kapa, 2, x, "x");
+    KapaPlotVector(kapa, 2, y, "y");
+
+    //If rawFunc was supplied, calculate the cutoff magnitude
+    if( rawFunc != NULL) {
+        double mRef = 0.5*(lumFunc->mMin + lumFunc->mMax);
+        double logRho = mRef * lumFunc->slope + lumFunc->offset;
+        double mRaw = (logRho - rawFunc->offset) / rawFunc->slope;
+        double deltaM = mRef - mRaw;
+        double mRefMax = rawFunc->mMax + deltaM;
+
+        float xraw[2] = {rawFunc->mMin + deltaM, rawFunc->mMax + deltaM};
+        float yraw[2] = {rawFunc->offset + (rawFunc->slope) * rawFunc->mMin,
+                        rawFunc->offset + (rawFunc->slope) * rawFunc->mMax};
+        float x[2] = {mRefMax, mRefMax};
+        float y[2] = {graphdata.ymin, graphdata.ymax};
+        graphdata.color= KapaColorByName("red");
+        KapaPrepPlot(kapa, 2, &graphdata);
+        KapaPlotVector(kapa, 2, x, "x");
+        KapaPlotVector(kapa, 2, y, "y");
+        KapaPrepPlot (kapa, 2, &graphdata);
+        KapaPlotVector (kapa, 2, xraw, "x");
+        KapaPlotVector (kapa, 2, yraw, "y");
+
+        KapaPNG(kapa,"LumPlot.png");
+        //char c;
+        //fscanf(stdin, "%c", &c);
+        //        pmKapaClose(kapa);
+    }
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMaskMisc.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMaskMisc.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMaskMisc.c	(revision 41084)
@@ -0,0 +1,51 @@
+/** storage : I don't really need these print functions any more ***/
+# if (0)
+    bool REFSTAR_MASK_REGIONS              = psMetadataLookupBool (&status, recipe, "REFSTAR_MASK_REGIONS");
+
+        // text region files for testing
+        FILE *f = NULL;
+        if (REFSTAR_MASK_REGIONS) {
+          char *filename = NULL;
+          char *chipname = psMetadataLookupStr (&status, chip->concepts, "CHIP.NAME");
+          psStringAppend (&filename, "refstars.mask.%s.dat", chipname);
+          FILE *f = fopen (filename, "w");
+          if (!f) {
+            psWarning ("cannot create refstar mask file %s\n", filename);
+            continue;
+          }
+          psFree (filename);
+        }
+
+                    if (REFSTAR_MASK_REGIONS) {
+                      fprintf (f, "CIRCLE %f %f  %f %f\n", ref->chip->x, ref->chip->y, radius, radius);
+                    }
+
+                        if (REFSTAR_MASK_REGIONS) {
+                          // lower side
+                          x0 = ref->chip->x + spikeWidth*sin(Theta);
+                          y0 = ref->chip->y - spikeWidth*cos(Theta);
+                          x1 = ref->chip->x + spikeLength*cos(Theta) + spikeWidth*sin(Theta);
+                          y1 = ref->chip->y + spikeLength*sin(Theta) - spikeWidth*cos(Theta);
+                          dx = x1 - x0;
+                          dy = y1 - y0;
+
+                          fprintf (f, "LINE %f %f  %f %f\n", x0, y0, dx, dy);
+
+                          // upper side
+                          x0 = ref->chip->x - spikeWidth*sin(Theta);
+                          y0 = ref->chip->y + spikeWidth*cos(Theta);
+                          x1 = ref->chip->x + spikeLength*cos(Theta) - spikeWidth*sin(Theta);
+                          y1 = ref->chip->y + spikeLength*sin(Theta) + spikeWidth*cos(Theta);
+                          dx = x1 - x0;
+                          dy = y1 - y0;
+                          fprintf (f, "LINE %f %f  %f %f\n", x0, y0, dx, dy);
+                        }
+
+			if (REFSTAR_MASK_REGIONS) {
+			    fprintf (f, "LINE %f %f  %f %f\n", ref->chip->x, ref->chip->y, 0.0, -100.0);
+			}
+
+        if (REFSTAR_MASK_REGIONS) {
+          fclose (f);
+        }
+# endif
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMaskStats.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMaskStats.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMaskStats.c	(revision 41084)
@@ -0,0 +1,11 @@
+
+# include "psastroInternal.h"
+
+# define ESCAPE { \
+    psError(PS_ERR_UNKNOWN, false, "I/O failure in pmMaskStats");	\
+    psFree (view);							\
+    return false;							\
+  }
+
+
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMaskUpdates.Mosaic.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMaskUpdates.Mosaic.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMaskUpdates.Mosaic.c	(revision 41084)
@@ -0,0 +1,459 @@
+# include "psastroInternal.h"
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "I/O failure in psastroMaskUpdate"); \
+  psFree (view); \
+  return false; \
+}
+  
+pmCell *pmCellInChip (pmChip *chip, float x, float y);
+bool pmCellCoordsForChip (float *xCell, float *yCell, pmCell *cell, float xChip, float yChip);
+bool psastroMaskCircle (psImage *mask, psImageMaskType value, float x0, float y0, float dX, float dY);
+bool psastroMaskBox (psImage *mask, psImageMaskType value, float x0, float y0, float dL, float dW, float theta);
+void psastroMaskLine (psImage *mask, psImageMaskType value, double x1, double y1, double x2, double y2, int dW);
+void psastroMaskLineBresen (psImage *mask, psImageMaskType value, int X1, int Y1, int X2, int Y2, int dW, int swapcoords);
+void psastroMaskRectangle (psImage *mask, psImageMaskType value, int x0, int y0, int x1, int y1);
+
+// create a mask or mask regions based on the collection of reference stars that are 
+// in the vicinity of each chip
+bool psastroMaskUpdates (pmConfig *config) {
+
+    bool status;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    float zeropt, exptime;
+
+    psImageMaskType maskValue  = pmConfigMaskGet("GHOST", config); // Mask value for ghost pixels
+    psImageMaskType maskBlank  = pmConfigMaskGet("BLANK", config); // Mask value for blank pixels
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+        return false;
+    }
+
+    // this step is optional
+    bool REFSTAR_MASK                      = psMetadataLookupBool (&status, recipe, "REFSTAR_MASK");
+    if (!REFSTAR_MASK) return true;
+
+    double REFSTAR_MASK_MAX_MAG            = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_MAX_MAG");
+    double REFSTAR_MASK_SATSTAR_MAG_SLOPE  = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_SATSTAR_MAG_SLOPE");
+    double REFSTAR_MASK_SATSTAR_MAG_MAX    = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_SATSTAR_MAG_MAX");
+    double REFSTAR_MASK_SATSTAR_POS_ZERO   = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_SATSTAR_POS_ZERO");
+    double REFSTAR_MASK_SATSPIKE_MAG_SLOPE = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_SATSPIKE_MAG_SLOPE");
+    double REFSTAR_MASK_SATSPIKE_MAG_MAX   = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_SATSPIKE_MAG_MAX");
+    double REFSTAR_MASK_SATSPIKE_WIDTH     = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_SATSPIKE_WIDTH");
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+	return false;
+    }
+    pmFPA *fpa = input->fpa;
+
+    // really error-out here?  or just skip?
+    if (!psastroZeroPointFromRecipe (&zeropt, &exptime, fpa, recipe)) {
+	psLogMsg ("psastro", PS_LOG_INFO, "failed to load zeropt data from recipe");
+	return false;
+    }
+
+    // recipe values are given in instrumental magnitudes
+    // use the zero point and exposure time to convert to apparent mags: M_ap = M_inst + C_0 + 2.5*log(exptime)
+    float MagOffset = zeropt + 2.5*log10(exptime);
+    REFSTAR_MASK_MAX_MAG += MagOffset;
+    REFSTAR_MASK_SATSTAR_MAG_MAX += MagOffset;
+    REFSTAR_MASK_SATSPIKE_MAG_MAX += MagOffset;
+
+    // select the input mask image :: we modify those pixels, the mosaic to chip mosaic format
+    pmFPAfile *inMask = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT.MASK");
+    if (!inMask) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find input mask");
+	return false;
+    }
+    pmFPA *fpaMask = inMask->fpa;
+
+    // select the output mask image :: we mosaic to chip mosaic format
+    pmFPAfile *outMask = psMetadataLookupPtr (NULL, config->files, "PSASTRO.OUTPUT.MASK");
+    if (!outMask) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find output mask");
+	return false;
+    }
+
+    // select the reference mask fpa :: we use this to determine cell boundaries
+    pmFPAfile *refMask = psMetadataLookupPtr (NULL, config->files, "PSASTRO.REFMASK");
+    if (!refMask) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find mask reference");
+	return false;
+    }
+
+    double POSANGLE = PM_RAD_DEG * psMetadataLookupF64 (&status, fpa->concepts, "FPA.POSANGLE"); 
+    psAssert (status, "POSANGLE missing");
+
+    // de-activate all files except PSASTRO.INPUT.MASK and PSASTRO.OUTPUT.MASK
+    pmFPAfileActivate (config->files, false, NULL);
+    pmFPAfileActivate (config->files, true, "PSASTRO.INPUT.MASK");
+    pmFPAfileActivate (config->files, true, "PSASTRO.OUTPUT.MASK");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmFPAview *viewMask = pmFPAviewAlloc (0);
+
+    // open/load files as needed
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!chip->fromFPA) { continue; }
+
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+	char *filename = NULL;
+	char *chipname = psMetadataLookupStr (&status, chip->concepts, "CHIP.NAME");
+	psStringAppend (&filename, "refstars.mask.%s.dat", chipname);
+	FILE *f = fopen (filename, "w");
+	if (!f) {
+	    psWarning ("cannot create refstar mask file %s\n", filename);
+	    continue;
+	}
+	psFree (filename);
+
+	pmChip *chipMask = pmFPAviewThisChip (view, fpaMask);
+
+	// load sequence for mask corresponding to this chip (XXX this is needed if the input mask is not the same format as the astrometry file
+	*viewMask = *view;
+        while ((cell = pmFPAviewNextCell (viewMask, fpaMask, 1)) != NULL) {
+            psTrace ("psastro", 4, "Mask Cell %d: %x %x\n", viewMask->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+	    if (!pmFPAfileIOChecks (config, viewMask, PM_FPA_BEFORE)) ESCAPE;
+	    
+            while ((readout = pmFPAviewNextReadout (viewMask, fpaMask, 1)) != NULL) {
+		if (!pmFPAfileIOChecks (config, viewMask, PM_FPA_BEFORE)) ESCAPE;
+                if (! readout->data_exists) { continue; }
+	    }
+	}
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            // XXX there can only be one readout per chip in astrometry, right?
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                // select the raw objects for this readout
+                psArray *refstars = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.REFSTARS");
+                if (refstars == NULL) { continue; }
+
+		// we need to generate the following masks regions:
+		// 1) circle around the saturated stars (scaled by magnitude)
+		// 2) diffraction spikes in direction ROT - ROTo
+		// 3) bleed trail in the direction of the readout
+
+		// XXX for the moment, let's just generate mana region-file objects in chip pixel space
+		for (int i = 0; i < refstars->n; i++) {
+		    pmAstromObj *ref = refstars->data[i];
+		    if (ref->Mag > REFSTAR_MASK_MAX_MAG) continue;
+		    
+		    // convert x,y chip coordinates to cells in maskChip
+		    pmCell *maskCell = pmCellInChip (chipMask, ref->chip->x, ref->chip->y);
+
+		    // XXX convert ref->Mag to instrumental mags
+
+		    // CIRCLE around the stars (scaled by magnitude)
+		    float radius = REFSTAR_MASK_SATSTAR_MAG_SLOPE * (REFSTAR_MASK_SATSTAR_MAG_MAX - ref->Mag);
+		    fprintf (f, "CIRCLE %f %f  %f %f\n", ref->chip->x, ref->chip->y, radius, radius);
+
+		    if (maskCell) {
+			float xCell = 0.0;
+			float yCell = 0.0;
+			pmCellCoordsForChip (&xCell, &yCell, maskCell, ref->chip->x, ref->chip->y);
+			// XXX for now, assume cell binning is 1x1 relative to chip
+			if (maskCell->readouts->n) {
+			    pmReadout *readout = maskCell->readouts->data[0];
+			    psastroMaskCircle (readout->mask, maskValue, xCell, yCell, radius, radius);
+			}
+		    }
+
+		    // LINE for boundaries of the saturation spikes (scaled by magnitude)
+		    float spikeLength = REFSTAR_MASK_SATSPIKE_MAG_SLOPE * (REFSTAR_MASK_SATSPIKE_MAG_MAX - ref->Mag);
+		    float spikeWidth = REFSTAR_MASK_SATSPIKE_WIDTH;
+
+		    for (float theta = 0.0; theta < 2*M_PI; theta += M_PI / 2.0) {
+			float x0, y0, x1, y1, dx, dy;
+
+			float Theta = POSANGLE - REFSTAR_MASK_SATSTAR_POS_ZERO + theta;
+
+			// lower side 
+			x0 = ref->chip->x + spikeWidth*sin(Theta);
+			y0 = ref->chip->y - spikeWidth*cos(Theta);
+			x1 = ref->chip->x + spikeLength*cos(Theta) + spikeWidth*sin(Theta);
+			y1 = ref->chip->y + spikeLength*sin(Theta) - spikeWidth*cos(Theta);
+			dx = x1 - x0;
+			dy = y1 - y0;
+			fprintf (f, "LINE %f %f  %f %f\n", x0, y0, dx, dy);
+
+			// upper side 
+			x0 = ref->chip->x - spikeWidth*sin(Theta);
+			y0 = ref->chip->y + spikeWidth*cos(Theta);
+			x1 = ref->chip->x + spikeLength*cos(Theta) - spikeWidth*sin(Theta);
+			y1 = ref->chip->y + spikeLength*sin(Theta) + spikeWidth*cos(Theta);
+			dx = x1 - x0;
+			dy = y1 - y0;
+			fprintf (f, "LINE %f %f  %f %f\n", x0, y0, dx, dy);
+
+			if (maskCell) {
+			    float xCell = 0.0;
+			    float yCell = 0.0;
+			    pmCellCoordsForChip (&xCell, &yCell, maskCell, ref->chip->x, ref->chip->y);
+			    int xParityCell = psMetadataLookupS32(NULL, maskCell->concepts, "CELL.XPARITY");
+			    int yParityCell = psMetadataLookupS32(NULL, maskCell->concepts, "CELL.YPARITY");
+			    // XXX for now, assume cell binning is 1x1 relative to chip
+			    // XXX for now, assume only a single readout
+			    if (maskCell->readouts->n) {
+				pmReadout *readout = maskCell->readouts->data[0];
+				psastroMaskBox (readout->mask, maskValue, xCell, yCell, spikeLength, spikeWidth, Theta*xParityCell*yParityCell);
+			    }
+			}
+		    }
+
+		    // LINE for boundaries of the bleed lines
+		    fprintf (f, "LINE %f %f  %f %f\n", ref->chip->x, ref->chip->y, 0.0, -100.0);
+		    if (maskCell) {
+			float xCell = 0.0;
+			float yCell = 0.0;
+			pmCellCoordsForChip (&xCell, &yCell, maskCell, ref->chip->x, ref->chip->y);
+			// XXX for now, assume cell binning is 1x1 relative to chip
+			if (maskCell->readouts->n) {
+			    pmReadout *readout = maskCell->readouts->data[0];
+			    psastroMaskRectangle (readout->mask, maskValue, (int) xCell-3, (int) yCell, (int) xCell+4, readout->mask->numRows);
+			    // XXX this can be done more easily knowing the we mask to the end in y only
+			}
+		    }
+		}
+            }
+        }
+
+	pmChip *outChip = pmFPAviewThisChip(view, outMask->fpa);
+	if (!outChip->hdu && !outChip->parent->hdu) {
+	    const char *name = psMetadataLookupStr(&status, inMask->fpa->concepts, "FPA.OBS"); // Name of FPA
+	    pmFPAAddSourceFromView(outMask->fpa, name, view, outMask->format);
+	}
+
+	psTrace("psastro", 5, "mosaic chip %s to %s (xbin,ybin: %d,%d to %d,%d)\n",
+		inMask->name, outMask->name, inMask->xBin, inMask->yBin, outMask->xBin, outMask->yBin);
+
+	// Mosaic the chip, making a deep copy.  This has the side effect of making the output
+	// image products pure trimmed images, but also increases the memory footprint.
+	status = pmChipMosaic(outChip, chipMask, true, maskBlank);
+
+	// output sequence for mask corresponding to this chip
+	*viewMask = *view;
+        while ((cell = pmFPAviewNextCell (viewMask, outMask->fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Mask Cell %d: %x %x\n", viewMask->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+	    
+            while ((readout = pmFPAviewNextReadout (viewMask, outMask->fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+		if (!pmFPAfileIOChecks (config, viewMask, PM_FPA_AFTER)) ESCAPE;
+	    }
+	    if (!pmFPAfileIOChecks (config, viewMask, PM_FPA_AFTER)) ESCAPE;
+	}
+
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+	fclose (f);
+    }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+
+    // deactivate all files
+    pmFPAfileActivate (config->files, false, NULL);
+
+    psFree (view);
+    psFree (viewMask);
+    return true;
+}
+
+// XXX this is going to be very slow...
+pmCell *pmCellInChip (pmChip *chip, float x, float y) {
+
+# if (0)
+    int x0Cell = psMetadataLookupS32(NULL, cell->concepts, "CELL.X0");
+    int y0Cell = psMetadataLookupS32(NULL, cell->concepts, "CELL.Y0");
+    int xParityCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.XPARITY");
+    int yParityCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.YPARITY");
+
+    // XXX fix the binning : currently not selected from concepts
+    // int xBin = psMetadataLookupS32(NULL, cell->concepts, "CELL.XBIN"); // Binning in x and y
+    // int yBin = psMetadataLookupS32(NULL, cell->concepts, "CELL.YBIN"); // Binning in x and y
+    int xBin = 1;
+    int yBin = 1;
+
+    // Position on the cell 
+    float xCell = PM_CHIP_TO_CELL(xChip, x0Cell, xParityCell, xBin);
+    float yCell = PM_CHIP_TO_CELL(yChip, y0Cell, yParityCell, yBin);
+# endif
+
+    for (int i = 0; i < chip->cells->n; i++) {
+
+	pmCell *cell = chip->cells->data[i];
+	psRegion *region = pmCellExtent (cell);
+
+	if (x < region->x0) goto skip;
+	if (x > region->x1) goto skip;
+	if (y < region->y0) goto skip;
+	if (y > region->y1) goto skip;
+
+	psFree (region);
+	return cell;
+
+    skip:
+	psFree (region);
+    }
+    return NULL;
+}
+
+// convert chip coords to cell coords, given known cell
+bool pmCellCoordsForChip (float *xCell, float *yCell, pmCell *cell, float xChip, float yChip) {
+
+    int x0Cell = psMetadataLookupS32(NULL, cell->concepts, "CELL.X0");
+    int y0Cell = psMetadataLookupS32(NULL, cell->concepts, "CELL.Y0");
+    int xParityCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.XPARITY");
+    int yParityCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.YPARITY");
+
+    // XXX fix the binning : currently not selected from concepts
+    // int xBin = psMetadataLookupS32(NULL, cell->concepts, "CELL.XBIN"); // Binning in x and y
+    // int yBin = psMetadataLookupS32(NULL, cell->concepts, "CELL.YBIN"); // Binning in x and y
+    int xBin = 1;
+    int yBin = 1;
+
+    // Position on the cell 
+    // ((pos)*(binning)*(cellParity) + (cell0))
+    // XXX this is probably totally wrong now....
+    // ((pos) - (cell0))*(cellParity)/(binning))
+    *xCell = (xChip - x0Cell)*xParityCell/xBin;
+    *yCell = (yChip - y0Cell)*yParityCell/yBin;
+
+    return true;
+}
+
+// XXX should be doing an OR
+bool psastroMaskCircle (psImage *mask, psImageMaskType value, float x0, float y0, float dX, float dY) {
+
+    // XXX need to worry about row0, col0
+    for (int ix = -dX; ix <= +dX; ix++) {
+	int jx = ix + x0;
+	if (jx < 0) continue;
+	if (jx >= mask->numCols) continue;
+	for (int iy = -dY; iy <= +dY; iy++) {
+	    int jy = iy + y0;
+	    if (jy < 0) continue;
+	    if (jy >= mask->numRows) continue;
+
+	    double r2 = PS_SQR(ix/dX) + PS_SQR(iy/dY);
+	    if (r2 > 1.0) continue;
+	    
+	    mask->data.PS_TYPE_IMAGE_MASK_DATA[jy][jx] |= value;
+	}
+    }
+    return true;
+}
+
+// XXX should be doing an OR
+bool psastroMaskBox (psImage *mask, psImageMaskType value, float x0, float y0, float dL, float dW, float theta) {
+
+    // draw a series of lines (from -0.5*dW to +0.5*dW) of length dL, starting at x0, y0, angle theta
+
+    float xs = x0;
+    float ys = y0;
+
+    float xe = xs + dL*cos(theta);
+    float ye = ys + dL*sin(theta);
+
+    psastroMaskLine (mask, value, xs, ys, xe, ye, (int) dW);
+    return true;
+}
+
+// identify the quadrant and draw the correct line
+void psastroMaskLine (psImage *mask, psImageMaskType value, double x1, double y1, double x2, double y2, int dW) {
+
+  int FlipDirect, FlipCoords;
+  int X1, Y1, X2, Y2, dX, dY;
+
+  /* rather than draw the line from float positions, we find the closest
+     integer end-points and draw the line between those pixels */ 
+
+  X1 = ROUND(x1);
+  Y1 = ROUND(y1);
+  X2 = ROUND(x2);
+  Y2 = ROUND(y2);
+
+  dX = X2 - X1;
+  dY = Y2 - Y1;
+
+  FlipCoords = (abs(dX) < abs(dY));
+  FlipDirect = FlipCoords ? (y1 > y2) : (x1 > x2);
+
+  if (!FlipDirect && !FlipCoords) psastroMaskLineBresen (mask, value, X1, Y1, X2, Y2, dW, FALSE);
+  if ( FlipDirect && !FlipCoords) psastroMaskLineBresen (mask, value, X2, Y2, X1, Y1, dW, FALSE);
+  if (!FlipDirect &&  FlipCoords) psastroMaskLineBresen (mask, value, Y1, X1, Y2, X2, dW, TRUE);
+  if ( FlipDirect &&  FlipCoords) psastroMaskLineBresen (mask, value, Y2, X2, Y1, X1, dW, TRUE);
+
+  return;
+}
+
+// use the Bresenham line drawing technique
+// integer-only Bresenham line-draw version which is fast
+void psastroMaskLineBresen (psImage *mask, psImageMaskType value, int X1, int Y1, int X2, int Y2, int dW, int swapcoords) {
+
+    int X, Y, dX, dY;
+    int e, e2;
+
+    dX = X2 - X1;
+    dY = Y2 - Y1;
+
+    Y = Y1;
+    e = 0;
+    for (X = X1; X <= X2; X++) {
+	if (X > 0) {
+	    if (swapcoords) {
+		if (X >= mask->numRows) continue;
+		for (int y = Y - dW; y <= Y + dW; y++) {
+		    if (y < 0) continue;
+		    if (y >= mask->numCols) continue;
+		    mask->data.PS_TYPE_IMAGE_MASK_DATA[X][y] |= value;
+		}
+	    } else {
+		if (X >= mask->numCols) continue;
+		for (int y = Y - dW; y <= Y + dW; y++) {
+		    if (y < 0) continue;
+		    if (y >= mask->numRows) continue;
+		    mask->data.PS_TYPE_IMAGE_MASK_DATA[y][X] |= value;
+		}
+	    }
+	}
+	e += dY;
+	e2 = 2 * e;
+	if (e2 > dX) {
+	    Y++;
+	    e -= dX;
+	} 
+	if (e2 < -dX) {
+	    Y--;
+	    e += dX;
+	}
+    }
+    return;
+}
+
+void psastroMaskRectangle (psImage *mask, psImageMaskType value, int x0, int y0, int x1, int y1) {
+    for (int iy = PS_MAX(0,y0); iy < PS_MIN(y1,mask->numRows); iy++) {
+	for (int ix = PS_MAX(0,x0); ix < PS_MIN(x1,mask->numCols); ix++) {
+	    mask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] |= value;
+	}
+    }
+}
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMaskUpdates.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMaskUpdates.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMaskUpdates.c	(revision 41084)
@@ -0,0 +1,744 @@
+/** @file psastroMaskUpdates.c
+ *
+ *  @brief
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "I/O failure in psastroMaskUpdate"); \
+  psFree (view); \
+  return false; \
+}
+
+
+int colStart(psU16 corner_list,int i,int x_0,int y_0,int R,int max) {
+  if ((corner_list == 0x04)||(corner_list == 0x05)) {
+    return(0);
+  }
+  else {
+    return((int) fabs(x_0 - sqrt(pow(R,2) - pow(y_0 - i,2))));
+  }    
+}
+int colEnd(psU16 corner_list,int i,int x_0,int y_0,int R,int max) {
+  if ((corner_list == 0x04)||(corner_list == 0x05)) {
+    int v = (int) fabs(x_0 - sqrt(pow(R,2) - pow(y_0 - i,2)));
+    if (v > max) {
+      return(max);
+    }
+    else {
+      return(v);
+    }
+  }
+  else {
+    return(max);
+  }    
+  
+}
+int rowStart(psU16 corner_list,int j,int x_0,int y_0,int R,int max) {
+  if ((corner_list == 0x01)||(corner_list == 0x02)||
+      (corner_list == 0x03)||(corner_list == 0x0b)||
+      (corner_list == 0x07)||(corner_list == 0x0f)) {
+    return(0);
+  }
+  else {
+    return((int) fabs(y_0 - sqrt(pow(R,2) - pow(x_0 - j,2))));
+  }
+}
+int rowEnd(psU16 corner_list,int j,int x_0,int y_0,int R, int max) {
+  if (corner_list == 0x0f) {
+    return(max);
+  }
+  else if ((corner_list == 0x01)||(corner_list == 0x02)||
+	   (corner_list == 0x03)||(corner_list == 0x0b)||
+	   (corner_list == 0x07)) {
+    int v = (int) fabs(y_0 - sqrt(pow(R,2) - pow(x_0 - j,2)));
+    if (v > max) {
+      return(max);
+    }
+    else {
+      return(v);
+    }
+  }
+  else {
+    return(max);
+  }
+  
+}
+
+/* #define MASK_DEBUG 1 */
+
+/*
+ * create a mask or mask regions based on the collection of reference stars that * are in the vicinity of each chip
+ */
+bool psastroMaskUpdates (pmConfig *config, psMetadata *stats) {
+
+    bool status;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    float zeropt, exptime, GHOST_MAX_MAG;
+
+    psImageMaskType ghostMaskValue = pmConfigMaskGet("GHOST", config); // Mask value for ghost pixels
+    psImageMaskType glintMaskValue = pmConfigMaskGet("GHOST", config); // Mask value for glint pixels (overload ghost)
+    psImageMaskType spikeMaskValue = pmConfigMaskGet("SPIKE", config); // Mask value for ghost pixels
+    psImageMaskType starMaskValue  = pmConfigMaskGet("STARCORE", config); // Mask value for ghost pixels
+    psImageMaskType crosstalkMaskValue = pmConfigMaskGet("GHOST", config); // Mask value for crosstalk ghosts
+
+    // psImageMaskType maskBlank  = pmConfigMaskGet("BLANK", config); // Mask value for blank pixels
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+        return false;
+    }
+
+    // this step is optional
+    bool REFSTAR_MASK                      = psMetadataLookupBool (&status, recipe, "REFSTAR_MASK");
+    if (!REFSTAR_MASK) return true;
+
+    // convert star positions to crosstalk artifact positions and add to the readout->analysis data
+    bool CROSSTALK_MASK                    = psMetadataLookupBool (&status, recipe, "CROSSTALK_MASK");
+    if (CROSSTALK_MASK && !psastroLoadCrosstalk (config)) {
+      psError(PSASTRO_ERR_CONFIG, false, "Error loading crosstalk data");
+      return(false);
+    }
+
+    // convert star positions to ghost positions and add to the readout->analysis data
+    if (!psastroLoadGhosts (config)) {
+        psError(PSASTRO_ERR_CONFIG, false, "Error loading ghosts");
+        return false;
+    }
+    bool COUNT_GHOSTS = psMetadataLookupF32 (&status, recipe, "REFSTAR_COUNT_GHOSTS");
+    int nGhosts = 0;
+
+    // convert star positions to glint positions and add to the fpa->analysis data
+    if (!psastroLoadGlints (config)) {
+        psError(PSASTRO_ERR_CONFIG, false, "Error loading glints");
+        return false;
+    }
+    psLogMsg ("psastro", PS_LOG_INFO, "generating a bright-star mask");
+    
+    bool REFSTAR_MASK_BLEED                = psMetadataLookupBool (&status, recipe, "REFSTAR_MASK_BLEED");
+    bool REFSTAR_MASK_BLEED_ORIENTATION_X  = psMetadataLookupBool (&status, recipe, "REFSTAR_MASK_BLEED_ORIENTATION_X");
+    bool REFSTAR_MASK_BLEED_ORIENTATION_Y  = psMetadataLookupBool (&status, recipe, "REFSTAR_MASK_BLEED_ORIENTATION_Y");
+
+    
+    double REFSTAR_MASK_MAX_MAG            = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_MAX_MAG");
+    double REFSTAR_MASK_SATSTAR_MAG_SLOPE  = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_SATSTAR_MAG_SLOPE");
+    double REFSTAR_MASK_SATSTAR_MAG_MAX    = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_SATSTAR_MAG_MAX");
+    double REFSTAR_MASK_SATSTAR_POS_ZERO   = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_SATSTAR_POS_ZERO");
+    double REFSTAR_MASK_SATSPIKE_MAG_SLOPE = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_SATSPIKE_MAG_SLOPE");
+    double REFSTAR_MASK_SATSPIKE_MAG_MAX   = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_SATSPIKE_MAG_MAX");
+    double REFSTAR_MASK_SATSPIKE_WIDTH     = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_SATSPIKE_WIDTH");
+    double REFSTAR_MASK_SATSPIKE_L0        = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_SATSPIKE_L0");
+    double REFSTAR_MASK_SATSPIKE_WIDTH_SLOPE = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_SATSPIKE_WIDTH_SLOPE");
+    double REFSTAR_MASK_SATSPIKE_OFFSET    = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_SATSPIKE_OFFSET");
+    double REFSTAR_MASK_BLEED_MAG_MAX      = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_BLEED_MAG_MAX");
+    double REFSTAR_MASK_BLEED_MAG_SLOPE    = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_BLEED_MAG_SLOPE");
+
+    double REFSTAR_MASK_BLEED_MAG_MAX_X    = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_BLEED_MAG_MAX_X");
+    double REFSTAR_MASK_BLEED_MAG_SLOPE_X  = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_BLEED_MAG_SLOPE_X");
+    double REFSTAR_MASK_BLEED_MAG_MAX_Y    = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_BLEED_MAG_MAX_Y");
+    double REFSTAR_MASK_BLEED_MAG_SLOPE_Y  = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_BLEED_MAG_SLOPE_Y");
+
+    double REFSTAR_MASK_CROSSTALK_MAG_MAX  = psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_CROSSTALK_MAG_MAX");
+    double REFSTAR_MASK_CROSSTALK_MAG_SLOPE= psMetadataLookupF32 (&status, recipe, "REFSTAR_MASK_CROSSTALK_MAG_SLOPE");
+
+    // Mask stats variables
+    psU16 staticMaskVal = psMetadataLookupU32(&status, recipe, "MASKSTAT.STATIC");
+    psU16 magicMaskVal = psMetadataLookupU32(&status, recipe, "MASKSTAT.MAGIC");
+    psU16 dynamicMaskVal = psMetadataLookupU32(&status, recipe, "MASKSTAT.DYNAMIC");
+    psU16 advisoryMaskVal = psMetadataLookupU32(&status, recipe, "MASKSTAT.ADVISORY");
+
+    psS32 Npix_ref_valid = 0;
+    psS32 Npix_ref_static = 0;
+    psS32 Npix_ref_magic = 0;
+    psS32 Npix_ref_dynamic = 0;
+    psS32 Npix_ref_advisory = 0;
+
+    psS32 Npix_max_valid = 0;
+    psS32 Npix_max_static = 0;
+    psS32 Npix_max_magic = 0;
+    psS32 Npix_max_dynamic = 0;
+    psS32 Npix_max_advisory = 0;
+    
+    psU16 corner_list = 0x00;
+    
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+        return false;
+    }
+    pmFPA *fpa = input->fpa;
+
+    // really error-out here?  or just skip?
+    if (!psastroZeroPointFromRecipe (&zeropt, &exptime, &GHOST_MAX_MAG, fpa, recipe)) {
+        psLogMsg ("psastro", PS_LOG_INFO, "failed to load zeropt data from recipe");
+        return false;
+    }
+
+    psTrace("psastro.masks",2,"Configuration loaded for masking.");
+    
+    // recipe values are given in instrumental magnitudes
+    // use the zero point and exposure time to convert to apparent mags: M_ap = M_inst + C_0 + 2.5*log(exptime)
+    float MagOffset = zeropt + 2.5*log10(exptime);
+    REFSTAR_MASK_MAX_MAG += MagOffset;
+    REFSTAR_MASK_SATSTAR_MAG_MAX += MagOffset;
+    REFSTAR_MASK_SATSPIKE_MAG_MAX += MagOffset;
+    REFSTAR_MASK_BLEED_MAG_MAX += MagOffset;
+    REFSTAR_MASK_BLEED_MAG_MAX_X += MagOffset;
+    REFSTAR_MASK_BLEED_MAG_MAX_Y += MagOffset;
+    
+    GHOST_MAX_MAG += MagOffset;
+
+    psTrace("psastro.masks",2,"Magnitudes: max_mag: %f satstar: %f satspike: %f bleed: %f ghost %f\n",
+	    REFSTAR_MASK_MAX_MAG,REFSTAR_MASK_SATSTAR_MAG_MAX,REFSTAR_MASK_SATSPIKE_MAG_MAX,REFSTAR_MASK_BLEED_MAG_MAX,GHOST_MAX_MAG);
+    
+    // select the output mask image :: we mosaic to chip mosaic format
+    pmFPAfile *outMask = psMetadataLookupPtr (NULL, config->files, "PSASTRO.OUTPUT.MASK");
+    if (!outMask) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find output mask");
+        return false;
+    }
+    pmFPA *fpaMask = outMask->fpa;
+
+    // Get camera specific mask stat options
+    psF32 FOV_REF = psMetadataLookupF32(&status, fpaMask->camera, "FOV_REF");
+    psF32 FOV_MAX = psMetadataLookupF32(&status, fpaMask->camera, "FOV_MAX");
+    psS32 NPIX_REF = psMetadataLookupS32(&status, fpaMask->camera, "NPIX_REF");
+    psS32 NPIX_MAX = psMetadataLookupS32(&status, fpaMask->camera, "NPIX_MAX");
+    
+    // select the reference mask fpa :: we use this to determine cell boundaries
+    pmFPAfile *refMask = psMetadataLookupPtr (NULL, config->files, "PSASTRO.REFMASK");
+    if (!refMask) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find mask reference");
+        return false;
+    }
+
+    psLogMsg ("psastro", PS_LOG_INFO, "loading output and reference mask files");
+
+    // double POSANGLE = PM_RAD_DEG * psMetadataLookupF64 (&status, fpa->concepts, "FPA.POSANGLE");
+    // psAssert (status, "POSANGLE missing");
+    double ROTANGLE = PM_RAD_DEG * psMetadataLookupF64 (&status, fpa->concepts, "FPA.ROTANGLE");
+    psAssert (status, "ROTANGLE missing");
+
+    // de-activate all files except PSASTRO.INPUT.MASK and PSASTRO.OUTPUT.MASK
+    pmFPAfileActivate (config->files, false, NULL);
+    pmFPAfileActivate (config->files, true, "PSASTRO.INPUT.MASK");
+    pmFPAfileActivate (config->files, true, "PSASTRO.OUTPUT.MASK");
+    pmFPAfileActivate (config->files, true, "PSASTRO.REFMASK");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmFPAview *viewMask = pmFPAviewAlloc (0);
+
+    // open/load files as needed
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+#ifdef MASK_DEBUG
+    psImage *masktest = psImageAlloc(10500,10500,PS_TYPE_U16);
+    psImageMaskType **maskIData = masktest->data.PS_TYPE_IMAGE_MASK_DATA;
+#endif
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+        if (!chip->fromFPA) { continue; }
+
+        if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+        pmChip *refChip  = pmFPAviewThisChip (view, refMask->fpa);
+
+        // load sequence for mask corresponding to this chip (XXX this is needed if the input mask is not the same format as the astrometry file
+        *viewMask = *view;
+        while ((cell = pmFPAviewNextCell (viewMask, fpaMask, 1)) != NULL) {
+            psTrace ("psastro", 4, "Mask Cell %d: %x %x\n", viewMask->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+            if (!pmFPAfileIOChecks (config, viewMask, PM_FPA_BEFORE)) ESCAPE;
+
+            while ((readout = pmFPAviewNextReadout (viewMask, fpaMask, 1)) != NULL) {
+                if (!pmFPAfileIOChecks (config, viewMask, PM_FPA_BEFORE)) ESCAPE;
+                if (! readout->data_exists) { continue; }
+            }
+        }
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // the input mask is a chip-mosaic image
+            // the output mask is a chip-mosaic image
+            // we mark the masked pixels in the chip space, BUT
+            // we need to find the ends of the cells for the bleeds
+
+            // we mask pixels on the input mask image (chip-mosaic)
+            // pmCell *cellMask = pmFPAviewThisCell(view, outMask->fpa);
+            // pmReadout *readoutMask = NULL;
+            // if (cellMask->readouts->n) {
+            //     readoutMask = cellMask->readouts->data[0];
+            // }
+
+            // process each of the readouts
+            // XXX there can only be one readout per chip in astrometry, right?
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                // XXX why not do this?
+                pmReadout *readoutMask = pmFPAviewThisReadout (view, outMask->fpa);
+                if (!readoutMask) continue;
+
+                // select the raw objects for this readout
+                // XXX : note that we place limits on the refstar sample in psastroChooseRefstars.c:
+                // 1) on chip and 2) < PSASTRO.MAX.NREF. magnitude limits and clump exclusion are only
+                // applied to the SUBSETs
+                psArray *refstars = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.REFSTARS");
+                if (!refstars) continue;
+
+		psTrace("psastro.masks",2,"In Readout loop.");
+                // we need to generate the following masks regions:
+                // 1) circle around the saturated stars (scaled by magnitude)
+                // 2) diffraction spikes in direction ROT - ROTo
+                // 3) bleed trail in the direction of the readout
+                //    update: with 'burntool' applied to the data, the bleed trail mask is not needed.
+
+                for (int i = 0; i < refstars->n; i++) {
+                    pmAstromObj *ref = refstars->data[i];
+
+                    if (COUNT_GHOSTS) {
+                        if (ref->Mag > GHOST_MAX_MAG) {
+                            nGhosts ++;
+                        }
+                    }
+		    psTrace("psastro.masks",4,"In refstar loop: %d/%ld %f %f\n",
+			    i,refstars->n,ref->Mag,REFSTAR_MASK_MAX_MAG);
+                    if (ref->Mag > REFSTAR_MASK_MAX_MAG) continue;
+
+
+                    // the reference magnitudes have been converted from the instrumental
+                    // values supplied in the recipe to apparent mags (above)
+
+                    // CIRCLE around the stars (scaled by magnitude)
+                    float radius = REFSTAR_MASK_SATSTAR_MAG_SLOPE * (REFSTAR_MASK_SATSTAR_MAG_MAX - ref->Mag);
+
+                    // XXX for now, assume cell binning is 1x1 relative to chip
+                    psastroMaskCircle (readoutMask->mask, starMaskValue, ref->chip->x, ref->chip->y, radius, radius);
+
+                    for (float theta = 0.0; theta < 2*M_PI; theta += M_PI / 2.0) {
+
+                        float Theta = theta - ROTANGLE - REFSTAR_MASK_SATSTAR_POS_ZERO;
+
+                        // LINE for boundaries of the saturation spikes (scaled by magnitude)
+                        // float MAG_MAX = (theta == 0.0) || (theta == M_PI) ? REFSTAR_MASK_SATSPIKE_MAG_MAX1 : REFSTAR_MASK_SATSPIKE_MAG_MAX2;
+
+                        float MAG_MAX = REFSTAR_MASK_SATSPIKE_MAG_MAX;
+			float spikeLength = pow(10,REFSTAR_MASK_SATSPIKE_MAG_SLOPE * (MAG_MAX - ref->Mag)) - REFSTAR_MASK_SATSPIKE_OFFSET;
+                        float spikeWidth = 0.5*REFSTAR_MASK_SATSPIKE_WIDTH;
+			if (spikeLength < 0.0) {
+			  spikeLength = 0.0;
+			}
+			// XXX we can make the width depend on the spike as well...
+			// CZW: 2011-02-18 it does so now. It's a weak function (slope = 0.01), but it should help
+			if (spikeLength > REFSTAR_MASK_SATSPIKE_L0) {
+			  spikeWidth += 0.5 * (spikeLength - REFSTAR_MASK_SATSPIKE_L0) * REFSTAR_MASK_SATSPIKE_WIDTH_SLOPE;
+			}
+
+                        // The length should also be a function of the image background level
+			psTrace("psastro.masks",4,"Masking: Radius: %f Theta: %f Length: %f Width: %f\n",
+				radius,Theta,spikeLength,spikeWidth);
+			psTrace("psastro.masks",4,"Also: %f %f %f %f\n",ref->chip->x,ref->chip->y,ROTANGLE,REFSTAR_MASK_SATSTAR_POS_ZERO);
+                        psastroMaskBox (readoutMask->mask, spikeMaskValue, ref->chip->x, ref->chip->y, spikeLength, spikeWidth, Theta);
+                    }
+
+                    // This masking option was needed for persistent charge trails in GPC1; it
+                    // has since been replaced with 'burntool', which is applied upon readout
+                    // by the camera software, and therefore is aware of the image sequence.
+                    if (REFSTAR_MASK_BLEED) {
+                        // convert x,y chip coordinates to cells in maskChip
+                        pmCell *refCell = pmCellInChip (refChip, ref->chip->x, ref->chip->y);
+
+                        // LINE for boundaries of the bleed lines
+                        if (refCell) {
+                            float xCell = 0.0;
+                            float yCell = 0.0;
+			    float xStart = 0.0;
+			    float xEnd   = 0.0;
+			    float yStart = 0.0;
+			    float yEnd   = 0.0;
+			    //                            float xEnd = 0.0;
+			    //                            float yEnd = 0.0;
+			    float width = 0.0;
+			    float length = 0.0;
+                            // find coordinate of star on cell
+                            pmCellCoordsForChip (&xCell, &yCell, refCell, ref->chip->x, ref->chip->y);
+                            // find coordinate of end-point on chip
+
+			    if (REFSTAR_MASK_BLEED_ORIENTATION_X) {
+			      length = pow(10,REFSTAR_MASK_BLEED_MAG_SLOPE_X*(REFSTAR_MASK_BLEED_MAG_MAX_X - ref->Mag));
+			      width  = REFSTAR_MASK_BLEED_MAG_SLOPE  *(REFSTAR_MASK_BLEED_MAG_MAX - ref->Mag);
+
+			      pmChipCoordsForCell (&xStart, &yStart, refCell, xCell - length, yCell - 0.5 * width);
+			      pmChipCoordsForCell (&xEnd, &yEnd,     refCell, xCell + length, yCell + 0.5 * width);
+			      psastroMaskRectangle (readoutMask->mask, spikeMaskValue, (int) xStart, (int) yStart, (int) xEnd, (int) yEnd + 1);
+			    }
+			    if (REFSTAR_MASK_BLEED_ORIENTATION_Y) {
+			      length = pow(10,REFSTAR_MASK_BLEED_MAG_SLOPE_Y*(REFSTAR_MASK_BLEED_MAG_MAX_Y - ref->Mag));
+			      width  = REFSTAR_MASK_BLEED_MAG_SLOPE  *(REFSTAR_MASK_BLEED_MAG_MAX - ref->Mag);
+
+			      pmChipCoordsForCell (&xStart, &yStart, refCell, xCell - 0.5 * width, yCell - length);
+			      pmChipCoordsForCell (&xEnd, &yEnd,     refCell, xCell + 0.5 * width, yCell + length);
+			      psastroMaskRectangle (readoutMask->mask, spikeMaskValue, (int) xStart, (int) yStart, (int) xEnd + 1, (int) yEnd);
+			    }
+			    
+                        }
+                    }
+                }
+
+                // select the ghost object for this readout (loaded in psastroExtractGhosts.c).
+                // These differ from the reference stars since the star position is not
+                // contained by the readout; instead, the ghost position is predicted based on
+                // the ghost model, and the ghost positions associated with a given readout are
+                // supplied here.
+                psArray *ghosts = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.GHOSTS");
+                if (ghosts) {
+                    // mask the ghosts on this readout
+                    for (int i = 0; i < ghosts->n; i++) {
+                        psastroGhost *ghost = ghosts->data[i];
+                        // XXX bright vs faint ghost bits? (OR with SUSPECT)
+                        psastroMaskEllipticalAnnulus (readoutMask->mask, ghostMaskValue, ghost->chip->x, ghost->chip->y, ghost->inner, ghost->outer);
+                    }
+                }
+
+                // Select the glint mask regions for this readout (loaded in
+                // psastroExtractGlints.c).  These glint regions are defined as rectangular
+                // boxes and are generated for each chip based on the position of the bright
+                // stars beyond the edge of the focal plane.  This masking is currently very
+                // GPC1-specific
+                psArray *glints = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.GLINTS");
+                if (glints) {
+                    // mask the glints on this readout
+                    for (int i = 0; i < glints->n; i++) {
+                        psRegion *glint = glints->data[i];
+                        psastroMaskRectangle (readoutMask->mask, glintMaskValue, glint->x0, glint->y0, glint->x1, glint->y1);
+                    }
+                }
+		// Because it's very GPC1-specific, handle alternate glint types here
+		psArray *hsc_glints = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.GLINTS.HSC");
+		if (hsc_glints) {
+		  for (int i = 0; i < hsc_glints->n; i++) {
+		    // Contains (start_x, start_y, length, width, theta)
+		    psVector *glint = hsc_glints->data[i];
+
+		    psTrace("psastro.masks", 4, "Masking glint %d on chip %s with params (%f,%f,%f,%f,%f)\n",
+			    i, psMetadataLookupStr(&status,readout->parent->parent->concepts,"CHIP.NAME"),
+			    glint->data.F32[0], glint->data.F32[1],
+			    glint->data.F32[2], glint->data.F32[3], glint->data.F32[4]);
+
+		    psastroMaskBox (readoutMask->mask, glintMaskValue, glint->data.F32[0], glint->data.F32[1],
+				    glint->data.F32[2], glint->data.F32[3], glint->data.F32[4]);
+		  }
+		}
+		
+                // Select the crosstalk artifact regions for this readout, and mask a circular region
+                // corresponding to the source star
+
+                psArray *crosstalks = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.CROSSTALKS");
+                if (crosstalks) {
+                  for (int i = 0; i < crosstalks->n; i++) {
+                    pmAstromObj *ref = crosstalks->data[i];
+                    float radius = REFSTAR_MASK_CROSSTALK_MAG_SLOPE * (REFSTAR_MASK_CROSSTALK_MAG_MAX - ref->Mag);
+                    psTrace("psastro.crosstalk",2,"Masking star on Chip %s @ (%f,%f) Magnitude: %f Radius %f\n",
+                            psMetadataLookupStr(&status,readout->parent->parent->concepts,"CHIP.NAME"),
+                            ref->chip->x,ref->chip->y,ref->Mag,radius);
+                    // XXX for now, assume cell binning is 1x1 relative to chip
+                    psastroMaskCircle (readoutMask->mask, crosstalkMaskValue, ref->chip->x, ref->chip->y, radius, radius);
+                  }
+                }
+		// Crosstalk Bleeds
+                psArray *bleedcrosstalks = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.CROSSTALKS.SPIKES");
+                if (bleedcrosstalks) {
+                  for (int i = 0; i < bleedcrosstalks->n; i++) {
+                    pmAstromObj *ref = bleedcrosstalks->data[i];
+                    float width = REFSTAR_MASK_BLEED_MAG_SLOPE * (REFSTAR_MASK_BLEED_MAG_MAX - ref->Mag);
+                    psTrace("psastro.crosstalk",2,"Masking spike on Chip %s @ (%f,%f) Magnitude: %f Radius %f\n",
+                            psMetadataLookupStr(&status,readout->parent->parent->concepts,"CHIP.NAME"),
+                            ref->chip->x,ref->chip->y,ref->Mag,width);
+                    // XXX for now, assume cell binning is 1x1 relative to chip
+		    pmCell *refCell = pmCellInChip(refChip,ref->chip->x,ref->chip->y);
+		    if (refCell) {
+		      float xCell = 0.0;
+		      float yCell = 0.0;
+		      pmCellCoordsForChip (&xCell, &yCell, refCell, ref->chip->x, ref->chip->y);
+		      int ySize = psMetadataLookupS32(NULL,refCell->concepts,"CELL.YSIZE");
+/* 		      psWarning("Masking CTspike on Chip %s @ (%f,%f) Magnitude: %f (%f %f) Radius %f Z: %d %d %d %d\n", */
+/* 				psMetadataLookupStr(&status,readout->parent->parent->concepts,"CHIP.NAME"), */
+/* 				ref->chip->x,ref->chip->y,ref->Mag,REFSTAR_MASK_BLEED_MAG_SLOPE,REFSTAR_MASK_BLEED_MAG_MAX,width, */
+/* 				(int) (ref->chip->x - 0.5 * width),   (int) (ref->chip->y - yCell), */
+/* 				(int) (ref->chip->x+0.5 * width + 1), (int) (ref->chip->y + (ySize - yCell))); */
+		      psastroMaskRectangle (readoutMask->mask, crosstalkMaskValue,
+					    (int) (ref->chip->x - 0.5 * width),   (int) (ref->chip->y - yCell),
+					    (int) (ref->chip->x+0.5 * width + 1), (int) (ref->chip->y + (ySize - yCell)));
+		    }
+                  }
+                }
+
+                // this probably should move into a function of its own:
+                {
+                    // select the raw objects for this readout, flag is they fall in a mask
+                    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+                    if (!detections) continue;
+
+                    psArray *inSources = detections->allSources;
+                    psAssert (inSources, "missing sources?");
+
+                    // create a replacement output array:
+                    // psArray *outSources = psAllocArrayEmpty(100);
+
+                    // XXX finish this: raise a bit for stars that land on certain types of masks;
+                    // others (eg, bright star core) should be ignored.
+                    for (int i = 0; i < inSources->n; i++) {
+                        pmSource *source = inSources->data[i];
+
+                        int xChip = source->peak->x;
+                        int yChip = source->peak->y;
+
+                        bool onChip = true;
+                        onChip &= (xChip >= 0);
+                        onChip &= (xChip < readoutMask->mask->numCols);
+                        onChip &= (yChip >= 0);
+                        onChip &= (yChip < readoutMask->mask->numRows);
+                        if (!onChip) {
+                            // if the source is off the edge of the chip, raise a different bit?
+                            source->mode |= PM_SOURCE_MODE_OFF_CHIP;
+                            continue;
+                        }
+
+                        psImageMaskType value = readoutMask->mask->data.PS_TYPE_IMAGE_MASK_DATA[yChip][xChip];
+                        if (value & ghostMaskValue) {
+                            source->mode |= PM_SOURCE_MODE_ON_GHOST;
+                        }
+                        // XXX note that for now, glint and ghost are identical
+                        pmSourceMode PM_SOURCE_MODE_ON_GLINT = PM_SOURCE_MODE_ON_GHOST;
+                        if (value & glintMaskValue) {
+                            source->mode |= PM_SOURCE_MODE_ON_GLINT;
+                        }
+                        if (value & spikeMaskValue) {
+                            source->mode |= PM_SOURCE_MODE_ON_SPIKE;
+                        }
+                    }
+                }
+            }
+	}
+
+	// Do the mask stats bit from cell down.
+	if (psMetadataLookupBool(&status,recipe,"MASK.STATS")) {
+	  if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+	  while ((cell = pmFPAviewNextCell(view, fpa, 1)) != NULL) {
+	    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+	    if (!cell->process || !cell->file_exists) {continue; }
+	    while ((readout = pmFPAviewNextReadout(view, fpa, 1)) != NULL) {
+	      pmReadout *readoutMask = pmFPAviewThisReadout (view, outMask->fpa);
+	      if (!pmFPAfileIOChecks (config, viewMask, PM_FPA_BEFORE)) ESCAPE;
+	      if (!readoutMask->data_exists) {continue; }
+
+	      psPlane coordFPA;
+	      psPlane coordCell;
+	      
+	      psImage *mask = readoutMask->mask;
+	      if (!mask) {continue;}
+	      psImageMaskType **maskData = mask->data.PS_TYPE_IMAGE_MASK_DATA;
+	      // Dance coordinates around
+	      // Calculate which corners fall within the field of view.  If this chip is fully contained, we can
+	      // do a simple scan instead of checking it falls within the FOV.
+	      // 0x04   0x08
+	      // 0x01   0x02
+	      corner_list = 0;
+	      
+	      coordCell.x = 0.0;
+	      coordCell.y = 0.0;
+	      psPlaneTransformApply(&coordFPA,chip->toFPA,&coordCell);
+	      if (pow(coordFPA.x,2) + pow(coordFPA.y,2) <= pow(FOV_REF,2)) {
+		corner_list = corner_list | 0x01;
+	      }
+	      coordCell.x = (1.0 * mask->numCols - 1.0);
+	      psPlaneTransformApply(&coordFPA,chip->toFPA,&coordCell);
+	      if (pow(coordFPA.x,2) + pow(coordFPA.y,2) <= pow(FOV_REF,2)) {
+		corner_list = corner_list | 0x02;
+	      }
+	      coordCell.y = 1.0 * (mask->numRows - 1);
+	      psPlaneTransformApply(&coordFPA,chip->toFPA,&coordCell);
+	      if (pow(coordFPA.x,2) + pow(coordFPA.y,2) <= pow(FOV_REF,2)) {
+		corner_list = corner_list | 0x08;
+	      }
+	      coordCell.x = 0.0;
+	      psPlaneTransformApply(&coordFPA,chip->toFPA,&coordCell);
+	      if (pow(coordFPA.x,2) + pow(coordFPA.y,2) <= pow(FOV_REF,2)) {
+		corner_list = corner_list | 0x04;
+	      }
+
+	      // Scan over the valid regions of the image and count masked pixels
+	      for (int i = 0; i < mask->numRows - 1; i++) {
+		for (int j = 0; j < mask->numCols - 1; j++) {
+		  coordCell.x = j;
+		  coordCell.y = i;
+		  coordFPA.x = 0.0;
+		  coordFPA.y = 0.0;
+		  int region = 0;
+
+		  if (corner_list == 0x0f) {
+		    Npix_ref_valid++;
+		    Npix_max_valid++;
+		    region = 1;
+/* #ifdef MASK_DEBUG */
+/* 		    psPlaneTransformApply(&coordFPA,chip->toFPA,&coordCell); */
+/* 		    maskIData[(int) (coordFPA.y/4 + 5250)][(int) (coordFPA.x/4 + 5250)] = */
+/* 		      maskIData[(int) (coordFPA.y/4 + 5250)][(int) (coordFPA.x/4 + 5250)] + 0x01; */
+/* #endif */
+		  }
+		  if (!region) {
+		    psPlaneTransformApply(&coordFPA,chip->toFPA,&coordCell);
+		    if (pow(coordFPA.x,2) + pow(coordFPA.y,2) <= pow(FOV_REF,2)) {
+		      Npix_ref_valid++;
+		      Npix_max_valid++;
+		      region = 1;
+/* #ifdef MASK_DEBUG */
+/* 		      maskIData[(int) (coordFPA.y/4 + 5250)][(int) (coordFPA.x/4 + 5250)] = */
+/* 			maskIData[(int) (coordFPA.y/4 + 5250)][(int) (coordFPA.x/4 + 5250)] +  0x01; */
+/* #endif */
+		    }
+		    else if (pow(coordFPA.x,2) + pow(coordFPA.y,2) <= pow(FOV_MAX,2)) {
+		      Npix_max_valid++;
+		      region = 2;
+/* #ifdef MASK_DEBUG  */
+/* 		      maskIData[(int) (coordFPA.y/4 + 5250)][(int) (coordFPA.x/4 + 5250)] = */
+/* 			maskIData[(int) (coordFPA.y/4 + 5250)][(int) (coordFPA.x/4 + 5250)] + 0x01; */
+/* #endif */
+		    }
+		  }
+		  if (!region) {
+		    continue;
+		  }
+		  
+		  if (maskData[i][j] & staticMaskVal) {
+#ifdef MASK_DEBUG
+		    psPlaneTransformApply(&coordFPA,chip->toFPA,&coordCell);
+		    maskIData[(int) (coordFPA.y/4 + 5250)][(int) (coordFPA.x/4 + 5250)] =
+		      maskIData[(int) (coordFPA.y/4 + 5250)][(int) (coordFPA.x/4 + 5250)] + 1;
+#endif
+		    if (region == 1) {
+		      Npix_ref_static++;
+		      Npix_max_static++;
+		    }
+		    if (region == 2) {
+		      Npix_max_static++;
+		    }
+		    continue;
+		  }
+		  if (maskData[i][j] & dynamicMaskVal) {
+		    if (region == 1) {
+		      Npix_ref_dynamic++;
+		      Npix_max_dynamic++;
+		    }
+		    if (region == 2) {
+		      Npix_max_dynamic++;
+		    }
+		    continue;
+		  }
+		  if (maskData[i][j] & magicMaskVal) {
+		    if (region == 1) {
+		      Npix_ref_magic++;
+		      Npix_max_magic++;
+		    }
+		    if (region == 2) {
+		      Npix_max_magic++;
+		    }
+		    continue;
+		  }
+		  if (maskData[i][j] & advisoryMaskVal) {
+		    if (region == 1) {
+		      Npix_ref_advisory++;
+		      Npix_max_advisory++;
+		    }
+		    if (region == 2) {
+		      Npix_max_advisory++;
+		    }
+		    continue;
+		  }
+		}
+	      }
+	    }
+	  }
+	}
+        // output sequence for mask corresponding to this chip (XXX this may not be needed...)
+        *viewMask = *view;
+        while ((cell = pmFPAviewNextCell (viewMask, outMask->fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Mask Cell %d: %x %x\n", viewMask->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            while ((readout = pmFPAviewNextReadout (viewMask, outMask->fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+                if (!pmFPAfileIOChecks (config, viewMask, PM_FPA_AFTER)) ESCAPE;
+            }
+            if (!pmFPAfileIOChecks (config, viewMask, PM_FPA_AFTER)) ESCAPE;
+        }
+        if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+    }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+
+#ifdef MASK_DEBUG
+    psFits *maskFits = psFitsOpen("/data/ipp007.0/watersc1/mask.test.fits","w");
+    psFitsWriteImage(maskFits,NULL,masktest,1,"mask");
+
+    psFree(maskFits);
+    psFree(masktest);
+#endif
+    if (COUNT_GHOSTS) {
+        // save nGhosts to update header.
+        psMetadata *updates = psMetadataLookupMetadata (&status, fpa->analysis, "PSASTRO.HEADER");
+        if (!updates) {
+            updates = psMetadataAlloc ();
+            psMetadataAddMetadata (fpa->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", updates);
+            psFree (updates);
+        }
+        psMetadataAddS32 (updates, PS_LIST_TAIL, "NGHOSTS", PS_META_REPLACE, "total expected ghosts", nGhosts);
+    }
+
+    Npix_ref_static += (NPIX_REF - Npix_ref_valid);
+    Npix_max_static += (NPIX_MAX - Npix_max_valid);
+    psMetadataAddS32(stats,PS_LIST_TAIL, "MASKFRAC_REF_NPIX", 0,
+		     "Number of valid pixels", Npix_ref_valid);
+    psMetadataAddF32(stats,PS_LIST_TAIL, "MASKFRAC_REF_STATIC", 0,
+		     "Fraction of pixels statically masked", (float) Npix_ref_static / NPIX_REF);
+    psMetadataAddF32(stats,PS_LIST_TAIL, "MASKFRAC_REF_DYNAMIC", 0,
+		     "Fraction of pixels dynamically masked", (float) Npix_ref_dynamic / NPIX_REF);
+    psMetadataAddF32(stats,PS_LIST_TAIL, "MASKFRAC_REF_MAGIC", 0,
+		     "Fraction of pixels magically masked", (float) Npix_ref_magic / NPIX_REF);
+    psMetadataAddF32(stats,PS_LIST_TAIL, "MASKFRAC_REF_ADVISORY", 0,
+		     "Fraction of pixels masked as an advisory", (float) Npix_ref_advisory / NPIX_REF);
+
+    psMetadataAddS32(stats,PS_LIST_TAIL, "MASKFRAC_MAX_NPIX", 0,
+		     "Number of valid pixels", Npix_max_valid);
+    psMetadataAddF32(stats,PS_LIST_TAIL, "MASKFRAC_MAX_STATIC", 0,
+		     "Fraction of pixels statically masked", (float) Npix_max_static / NPIX_MAX);
+    psMetadataAddF32(stats,PS_LIST_TAIL, "MASKFRAC_MAX_DYNAMIC", 0,
+		     "Fraction of pixels dynamically masked", (float) Npix_max_dynamic / NPIX_MAX);
+    psMetadataAddF32(stats,PS_LIST_TAIL, "MASKFRAC_MAX_MAGIC", 0,
+		     "Fraction of pixels magically masked", (float) Npix_max_magic / NPIX_MAX);
+    psMetadataAddF32(stats,PS_LIST_TAIL, "MASKFRAC_MAX_ADVISORY", 0,
+		     "Fraction of pixels masked as an advisory", (float) Npix_max_advisory / NPIX_MAX);
+    
+    // deactivate all files
+    pmFPAfileActivate (config->files, false, NULL);
+
+    psFree (view);
+    psFree (viewMask);
+    return true;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMaskUtils.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMaskUtils.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMaskUtils.c	(revision 41084)
@@ -0,0 +1,350 @@
+/** @file psastroMaskUpdates.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "I/O failure in psastroMaskUpdate"); \
+  psFree (view); \
+  return false; \
+}
+
+// XXX this is going to be very slow...
+pmCell *pmCellInChip (pmChip *chip, float x, float y) {
+
+# if (0)
+    int x0Cell = psMetadataLookupS32(NULL, cell->concepts, "CELL.X0");
+    int y0Cell = psMetadataLookupS32(NULL, cell->concepts, "CELL.Y0");
+    int xParityCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.XPARITY");
+    int yParityCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.YPARITY");
+
+    // XXX fix the binning : currently not selected from concepts
+    // int xBin = psMetadataLookupS32(NULL, cell->concepts, "CELL.XBIN"); // Binning in x and y
+    // int yBin = psMetadataLookupS32(NULL, cell->concepts, "CELL.YBIN"); // Binning in x and y
+    int xBin = 1;
+    int yBin = 1;
+
+    // Position on the cell
+    float xCell = PM_CHIP_TO_CELL(xChip, x0Cell, xParityCell, xBin);
+    float yCell = PM_CHIP_TO_CELL(yChip, y0Cell, yParityCell, yBin);
+# endif
+
+    for (int i = 0; i < chip->cells->n; i++) {
+
+        pmCell *cell = chip->cells->data[i];
+        psRegion *region = pmCellExtent (cell);
+
+        if (x < region->x0) goto skip;
+        if (x > region->x1) goto skip;
+        if (y < region->y0) goto skip;
+        if (y > region->y1) goto skip;
+
+        psFree (region);
+        return cell;
+
+    skip:
+        psFree (region);
+    }
+    return NULL;
+}
+
+/**
+ * convert chip coords to cell coords, given known cell
+ */
+bool pmCellCoordsForChip (float *xCell, float *yCell, pmCell *cell, float xChip, float yChip) {
+
+    int x0Cell = psMetadataLookupS32(NULL, cell->concepts, "CELL.X0");
+    int y0Cell = psMetadataLookupS32(NULL, cell->concepts, "CELL.Y0");
+    int xParityCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.XPARITY");
+    int yParityCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.YPARITY");
+
+    // XXX fix the binning : currently not selected from concepts
+    // int xBin = psMetadataLookupS32(NULL, cell->concepts, "CELL.XBIN"); // Binning in x and y
+    // int yBin = psMetadataLookupS32(NULL, cell->concepts, "CELL.YBIN"); // Binning in x and y
+    int xBin = 1;
+    int yBin = 1;
+
+    // Position on the cell
+    // ((pos)*(binning)*(cellParity) + (cell0))
+    // XXX this is probably totally wrong now....
+    // ((pos) - (cell0))*(cellParity)/(binning))
+    *xCell = (xChip - x0Cell)*xParityCell/xBin;
+    *yCell = (yChip - y0Cell)*yParityCell/yBin;
+
+    return true;
+}
+
+/**
+ * convert chip coords to cell coords, given known cell
+ */
+bool pmChipCoordsForCell (float *xChip, float *yChip, pmCell *cell, float xCell, float yCell) {
+
+    int x0Cell = psMetadataLookupS32(NULL, cell->concepts, "CELL.X0");
+    int y0Cell = psMetadataLookupS32(NULL, cell->concepts, "CELL.Y0");
+    int xParityCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.XPARITY");
+    int yParityCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.YPARITY");
+
+    // XXX fix the binning : currently not selected from concepts
+    // int xBin = psMetadataLookupS32(NULL, cell->concepts, "CELL.XBIN"); // Binning in x and y
+    // int yBin = psMetadataLookupS32(NULL, cell->concepts, "CELL.YBIN"); // Binning in x and y
+    int xBin = 1;
+    int yBin = 1;
+
+    // Position on the cell
+    // ((pos)*(binning)*(cellParity) + (cell0))
+    // XXX this is probably totally wrong now....
+    // ((pos) - (cell0))*(cellParity)/(binning))
+    *xChip = xCell*xBin*xParityCell + x0Cell;
+    *yChip = yCell*yBin*yParityCell + y0Cell;
+
+    return true;
+}
+
+bool psastroMaskCircle (psImage *mask, psImageMaskType value, float x0, float y0, float dX, float dY) {
+
+    for (int ix = -dX; ix <= +dX; ix++) {
+        int jx = ix + x0;
+        if (jx < 0) continue;
+        if (jx >= mask->numCols) continue;
+        for (int iy = -dY; iy <= +dY; iy++) {
+            int jy = iy + y0;
+            if (jy < 0) continue;
+            if (jy >= mask->numRows) continue;
+
+            double r2 = PS_SQR(ix/dX) + PS_SQR(iy/dY);
+            if (r2 > 1.0) continue;
+
+            mask->data.PS_TYPE_IMAGE_MASK_DATA[jy][jx] |= value;
+        }
+    }
+    return true;
+}
+
+
+void psastroMinMaxForShape (float *min, float *max, float y, psEllipseShape shape) {
+
+	// XXX optimize this
+	float A = 1.0;
+	float B = 2.0 * shape.sxy*y*PS_SQR(shape.sx);
+	float C = PS_SQR(y*shape.sx/shape.sy) - PS_SQR(shape.sx);
+
+	float T1 = PS_SQR(B);
+	float T2 = 4.0*A*C;
+	float R = T1 - T2;
+	if (R < 0) R = 0;
+
+	*min =  (-B - sqrt (R)) / (2.0*A);
+	*max =  (-B + sqrt (R)) / (2.0*A);
+}
+
+bool psastroMaskEllipse (psImage *mask, psImageMaskType value, float x0, float y0, psEllipseAxes axes) {
+
+    psEllipseShape shape = psEllipseAxesToShape (axes);
+
+    // phi is the coordinate along the elliptical path
+    // phiMin, phiMax are the points on the path for Ymin and Ymax
+    float phiMin = atan2 (-axes.minor*cos(axes.theta), axes.major*sin(axes.theta));
+    float phiMax = phiMin + M_PI;
+
+    float Ymin = -axes.major*cos(phiMin)*sin(axes.theta) + axes.minor*sin(phiMin)*cos(axes.theta);
+    float Ymax = -axes.major*cos(phiMax)*sin(axes.theta) + axes.minor*sin(phiMax)*cos(axes.theta);
+    if (Ymin > Ymax) PS_SWAP (Ymin, Ymax);
+  
+    for (int iy = Ymin; iy <= Ymax; iy++) {
+	int jy = iy + y0;
+	if (jy < 0) continue;
+	if (jy >= mask->numRows) continue;
+
+	float Xmin, Xmax;
+	psastroMinMaxForShape (&Xmin, &Xmax, iy, shape);
+	fprintf (stderr, "mask %d : %f -> %f == %f -> %f (%x)\n", jy, Xmin, Xmax, Xmin + x0, Xmax + x0, value);
+
+	for (int ix = Xmin; ix <= Xmax; ix++) {
+	    int jx = ix + x0;
+	    if (jx < 0) continue;
+	    if (jx >= mask->numCols) continue;
+	    mask->data.PS_TYPE_IMAGE_MASK_DATA[jy][jx] |= value;
+	}
+    }
+    return true;
+}
+
+bool psastroMaskEllipticalAnnulus (psImage *mask, psImageMaskType value, float x0, float y0, psEllipseAxes eInner, psEllipseAxes eOuter) {
+
+    // skip the masking if the outer ellipse is nonsensical
+    psEllipseShape sOuter = psEllipseAxesToShape (eOuter);
+    if (isnan(sOuter.sx) || isnan(sOuter.sy) || isnan(sOuter.sxy)) return false;
+
+    psEllipseShape sInner = psEllipseAxesToShape (eInner);
+    if (isnan(sInner.sx) || isnan(sInner.sy) || isnan(sInner.sxy)) {
+	// use a solid ellipse if the inner ellipse is nonsensical
+	sInner.sx = 0.1;
+	sInner.sy = 0.1;
+	sInner.sxy = 0.0;
+    }
+
+    // phi is the coordinate along the elliptical path
+    // phiMin, phiMax are the points on the path for Ymin and Ymax
+    float phiMinInner = atan2 (-eInner.minor*cos(eInner.theta), eInner.major*sin(eInner.theta));
+    float phiMaxInner = phiMinInner + M_PI;
+    float phiMinOuter = atan2 (-eOuter.minor*cos(eOuter.theta), eOuter.major*sin(eOuter.theta));
+    float phiMaxOuter = phiMinOuter + M_PI;
+
+    float YminInner = -eInner.major*cos(phiMinInner)*sin(eInner.theta) + eInner.minor*sin(phiMinInner)*cos(eInner.theta);
+    float YmaxInner = -eInner.major*cos(phiMaxInner)*sin(eInner.theta) + eInner.minor*sin(phiMaxInner)*cos(eInner.theta);
+    if (YminInner > YmaxInner) PS_SWAP (YminInner, YmaxInner);
+
+    float YminOuter = -eOuter.major*cos(phiMinOuter)*sin(eOuter.theta) + eOuter.minor*sin(phiMinOuter)*cos(eOuter.theta);
+    float YmaxOuter = -eOuter.major*cos(phiMaxOuter)*sin(eOuter.theta) + eOuter.minor*sin(phiMaxOuter)*cos(eOuter.theta);
+    if (YminOuter > YmaxOuter) PS_SWAP (YminOuter, YmaxOuter);
+  
+    for (int iy = YminOuter; iy <= YmaxOuter; iy++) {
+	int jy = iy + y0;
+	if (jy < 0) continue;
+	if (jy >= mask->numRows) continue;
+
+	float XminOuter, XmaxOuter;
+	psastroMinMaxForShape (&XminOuter, &XmaxOuter, iy, sOuter);
+
+	if ((iy > YmaxInner) || (iy < YminInner)) {
+	    for (int ix = XminOuter; ix <= XmaxOuter; ix++) {
+		int jx = ix + x0;
+		if (jx < 0) continue;
+		if (jx >= mask->numCols) continue;
+		mask->data.PS_TYPE_IMAGE_MASK_DATA[jy][jx] |= value;
+	    }
+	} else {
+	    float XminInner, XmaxInner;
+	    psastroMinMaxForShape (&XminInner, &XmaxInner, iy, sInner);
+
+	    for (int ix = XminOuter; ix <= XminInner; ix++) {
+		int jx = ix + x0;
+		if (jx < 0) continue;
+		if (jx >= mask->numCols) continue;
+		mask->data.PS_TYPE_IMAGE_MASK_DATA[jy][jx] |= value;
+	    }
+	    for (int ix = XmaxInner; ix <= XmaxOuter; ix++) {
+		int jx = ix + x0;
+		if (jx < 0) continue;
+		if (jx >= mask->numCols) continue;
+		mask->data.PS_TYPE_IMAGE_MASK_DATA[jy][jx] |= value;
+	    }
+	}
+    }
+    return true;
+}
+
+bool psastroMaskBox (psImage *mask, psImageMaskType value, float x0, float y0, float dL, float dW, float theta) {
+
+    // draw a series of lines (from -0.5*dW to +0.5*dW) of length dL, starting at x0, y0, angle theta
+
+    float xs = x0;
+    float ys = y0;
+
+    float xe = xs + dL*cos(theta);
+    float ye = ys + dL*sin(theta);
+
+    psastroMaskLine (mask, value, xs, ys, xe, ye, (int) dW);
+    return true;
+}
+
+/**
+ * identify the quadrant and draw the correct line
+ */
+void psastroMaskLine (psImage *mask, psImageMaskType value, double x1, double y1, double x2, double y2, int dW) {
+
+  int FlipDirect, FlipCoords;
+  int X1, Y1, X2, Y2, dX, dY;
+
+  /* rather than draw the line from float positions, we find the closest
+     integer end-points and draw the line between those pixels */
+
+  X1 = ROUND(x1);
+  Y1 = ROUND(y1);
+  X2 = ROUND(x2);
+  Y2 = ROUND(y2);
+
+  dX = X2 - X1;
+  dY = Y2 - Y1;
+
+  FlipCoords = (abs(dX) < abs(dY));
+  FlipDirect = FlipCoords ? (y1 > y2) : (x1 > x2);
+
+  if (!FlipDirect && !FlipCoords) psastroMaskLineBresen (mask, value, X1, Y1, X2, Y2, dW, FALSE);
+  if ( FlipDirect && !FlipCoords) psastroMaskLineBresen (mask, value, X2, Y2, X1, Y1, dW, FALSE);
+  if (!FlipDirect &&  FlipCoords) psastroMaskLineBresen (mask, value, Y1, X1, Y2, X2, dW, TRUE);
+  if ( FlipDirect &&  FlipCoords) psastroMaskLineBresen (mask, value, Y2, X2, Y1, X1, dW, TRUE);
+
+  return;
+}
+
+/**
+ * use the Bresenham line drawing technique
+ * integer-only Bresenham line-draw version which is fast
+ */
+void psastroMaskLineBresen (psImage *mask, psImageMaskType value, int X1, int Y1, int X2, int Y2, int dW, int swapcoords) {
+
+    int X, Y, dX, dY;
+    int e, e2;
+
+    dX = X2 - X1;
+    dY = Y2 - Y1;
+
+    Y = Y1;
+    e = 0;
+    for (X = X1; X <= X2; X++) {
+        if (X > 0) {
+            if (swapcoords) {
+                if (X >= mask->numRows) continue;
+                for (int y = Y - dW; y <= Y + dW; y++) {
+                    if (y < 0) continue;
+                    if (y >= mask->numCols) continue;
+                    mask->data.PS_TYPE_IMAGE_MASK_DATA[X][y] |= value;
+                }
+            } else {
+                if (X >= mask->numCols) continue;
+                for (int y = Y - dW; y <= Y + dW; y++) {
+                    if (y < 0) continue;
+                    if (y >= mask->numRows) continue;
+                    mask->data.PS_TYPE_IMAGE_MASK_DATA[y][X] |= value;
+                }
+            }
+        }
+        e += dY;
+        e2 = 2 * e;
+        if (e2 > dX) {
+            Y++;
+            e -= dX;
+        }
+        if (e2 < -dX) {
+            Y--;
+            e += dX;
+        }
+    }
+    return;
+}
+
+void psastroMaskRectangle (psImage *mask, psImageMaskType value, int x0, int y0, int x1, int y1) {
+
+    int xs = PS_MAX (0, PS_MIN (mask->numCols, PS_MIN (x0, x1)));
+    int xe = PS_MAX (0, PS_MIN (mask->numCols, PS_MAX (x0, x1)));
+    int ys = PS_MAX (0, PS_MIN (mask->numRows, PS_MIN (y0, y1)));
+    int ye = PS_MAX (0, PS_MIN (mask->numRows, PS_MAX (y0, y1)));
+
+    for (int iy = ys; iy < ye; iy++) {
+        for (int ix = xs; ix < xe; ix++) {
+            mask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] |= value;
+        }
+    }
+}
+
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMetadataStats.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMetadataStats.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMetadataStats.c	(revision 41084)
@@ -0,0 +1,103 @@
+/** @file psastroMetaDataStats.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+bool psastroMetadataStats (pmConfig *config, psMetadata *stats) {
+
+    bool status;
+
+    char *filename = psMetadataLookupStr (&status, config->arguments, "STATS");
+    if (!filename) return true; // stats not requested
+
+    // Extract statistics from the last output fpa
+    pmFPAfile *output = psMetadataLookupPtr(&status, config->files, "PSASTRO.OUTPUT");
+
+    if (!output) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find output file (PSASTRO.OUTPUT).");
+        return false;
+    }
+
+    // extract stats for the complete fpa
+    pmFPAview *view = pmFPAviewAlloc(0);
+
+    // Entries in the PSASTRO.HEADER in the FPA don't get seen by ppStats
+    psMetadata *header = psMetadataLookupMetadata(&status, output->fpa->analysis, "PSASTRO.HEADER");
+    if (status && header) {
+        psMetadataItemSupplement(&status, stats, header, "ZPT_OBS");
+        psMetadataItemSupplement(&status, stats, header, "ZPT_ERR");
+        psMetadataItemSupplement(&status, stats, header, "CERROR");
+        psMetadataItemSupplement(&status, stats, header, "NASTRO");
+        psMetadataItemSupplement(&status, stats, header, "AST_R0");
+        psMetadataItemSupplement(&status, stats, header, "AST_D0");
+        psMetadataItemSupplement(&status, stats, header, "AST_T0");
+        psMetadataItemSupplement(&status, stats, header, "AST_S0");
+        psMetadataItemSupplement(&status, stats, header, "AST_RS");
+        psMetadataItemSupplement(&status, stats, header, "AST_DS");
+    }
+
+    if (!ppStatsMetadata(stats, output->fpa, view, 0, config)) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to generate stats for image.");
+        psFree(view);
+        return false;
+    }
+
+    psFree(view);
+
+    // if we did not request any specific stats, the structure is empty
+    if (stats && stats->list->n == 0) {
+        psWarning ("stats output specified, but no requested stats entries in headers");
+        return true;
+    }
+
+    // convert the stats MDC to a string
+    char *statsMDC = psMetadataConfigFormat(stats);
+    if (!statsMDC || strlen(statsMDC) == 0) {
+        psError(PS_ERR_IO, false, "Unable to serialize stats metadata.\n");
+        return false;
+    }
+
+    // convert to a real UNIX filename
+    psString resolved = pmConfigConvertFilename(filename, config, true, false); // Resolved filename
+    if (!resolved) {
+        psError(psErrorCodeLast(), false, "Unable to resolve statistics filename: %s", filename);
+        psFree(statsMDC);
+        return false;
+    }
+    FILE *statsFile = fopen (resolved, "w");
+    if (!statsFile) {
+        psError(PS_ERR_IO, true, "Unable to open statistics file %s for writing.\n", resolved);
+        psFree(statsMDC);
+        psFree(resolved);
+        return false;
+    }
+
+    // write the stats MDC to a file
+    if (fprintf(statsFile, "%s", statsMDC) != strlen(statsMDC)) {
+        psError(PS_ERR_IO, false, "Unable to write statistics file %s", resolved);
+        psFree(statsMDC);
+        psFree(resolved);
+        return false;
+    }
+    psFree(statsMDC);
+    if (fclose(statsFile) == EOF) {
+        psError(PS_ERR_IO, false, "Unable to write statistics file %s", resolved);
+        psFree(resolved);
+        return false;
+    }
+
+    psFree(resolved);
+
+    pmConfigRunFilenameAddWrite(config, "STATS", filename);
+
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModel.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModel.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModel.c	(revision 41084)
@@ -0,0 +1,65 @@
+/** @file psastroModel.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastroModel
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+
+int main (int argc, char **argv) {
+
+    pmConfig *config = NULL;
+
+    psTimerStart ("complete");
+
+    psastroErrorRegister();              // register our error codes/messages
+
+    // model inits are needed in pmSourceIO
+    // models defined in psphot/src/models are not available in psastro
+    pmModelClassInit ();
+
+    // load configuration information
+    config = psastroModelArguments (argc, argv);
+
+    // load identify the data sources
+    if (!psastroModelParseCamera (config)) {
+	psErrorStackPrint(stderr, "error setting up the camera\n");
+	exit (1);
+    }
+
+    // load the raw pixel data (from PSPHOT.SOURCES)
+    // select subset of stars for astrometry
+    if (!psastroModelDataLoad (config)) {
+	psErrorStackPrint(stderr, "error loading input data\n");
+	exit (1);
+    }
+
+    // run the full astrometry analysis (chip and/or mosaic)
+    if (!psastroModelAnalysis (config)) {
+	psErrorStackPrint(stderr, "failure in psastro model analysis\n");
+	exit (1);
+    }
+    
+    // run the full astrometry analysis (chip and/or mosaic)
+    if (!psastroModelAdjust (config)) {
+	psErrorStackPrint(stderr, "failure in psastro model adjust\n");
+	exit (1);
+    }
+    
+    // save the model
+    if (!psastroModelDataSave (config)) {
+	psErrorStackPrint(stderr, "error saving output data\n");
+	exit (1);
+    }
+
+    psLogMsg ("psastro", 3, "complete psastro run: %f sec\n", psTimerMark ("complete"));
+
+    // psastroCleanup (config);
+    exit (EXIT_SUCCESS);
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelAdjust.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelAdjust.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelAdjust.c	(revision 41084)
@@ -0,0 +1,280 @@
+/** @file psastroModelAdjust.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastroModel
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+# define NONLIN_TOL 0.001 ///< tolerance in pixels 
+# define DEBUG 0
+
+bool psastroModelAdjustBoresite (pmFPAfile *output, pmChip *refChip);
+
+bool psastroModelAdjust (pmConfig *config) {
+
+    bool status;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+	return false;
+    }
+
+
+    
+    // if we have not measured the boresite position, no adjustment is needed
+    bool fitBoresite = psMetadataLookupBool (&status, recipe, "PSASTRO.MODEL.FIT.BORESITE");
+    if (!status) psAbort ("Can't find recipe option PSASTRO.MODEL.FIT.BORESITE");
+
+    // as an alternative to fit the boresite from a rotation sequence, we can set the boresite
+    // relative to the reference chip coordinates
+    bool setBoresite = psMetadataLookupBool (&status, recipe, "PSASTRO.MODEL.SET.BORESITE");
+    if (!status) psAbort ("Can't find recipe option PSASTRO.MODEL.SET.BORESITE");
+
+    if (fitBoresite && setBoresite) {
+	psError(PS_ERR_IO, true, "invalid to choose both FIT.BORESITE and SET.BORESITE"); 
+	return false; 
+    } 
+	
+    pmFPAfile *output = psMetadataLookupPtr (&status, config->files, "PSASTRO.OUT.MODEL");
+    if (!status) psAbort ("Can't find output pmFPAfile PSASTRO.OUT.MODEL");
+    if (!output->fpa) psAbort ("no existing input fpa contains the reference chip");
+
+    // physical pixel scale in microns per pixel
+    char *refChipName = psMetadataLookupStr (&status, recipe, "PSASTRO.MODEL.REF.CHIP");
+    if (!refChipName) {
+	psError(PS_ERR_IO, true, "reference chip is missing from recipe"); 
+	return false; 
+    } 
+    float refChipAngleNominal = PS_RAD_DEG*psMetadataLookupF32 (&status, recipe, "PSASTRO.MODEL.REF.CHIP.ANGLE");
+    if (!refChipName) {
+	psError(PS_ERR_IO, true, "reference chip is missing from recipe"); 
+	return false; 
+    } 
+
+    int rotatorParity = psMetadataLookupS32(&status, recipe, "PSASTRO.MODEL.ROT.PARITY");
+    if (!status) psAbort ("Can't find recipe option PSASTRO.MODEL.ROT.PARITY");
+    psMetadataAddS32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.ROT_PARITY", PS_META_REPLACE, "rotator parity parameter", rotatorParity);
+    
+    // get reference chip from name
+    pmChip *refChip = pmConceptsChipFromName (output->fpa, refChipName);
+    if (!refChip) psAbort ("invalid chip name for reference");
+    if (!refChip->toFPA) psAbort ("invalid astrometry for reference chip");
+
+    // save the TPA region for tranformation inversions below
+    // psRegion *tpaRegion = pmAstromFPInTP (output->fpa);
+    psRegion *fpaRegion = pmAstromFPAExtent (output->fpa);
+
+    if (DEBUG) psastroDumpCorners ("corners.up.raw.dat", "corners.dn.raw.dat", output->fpa);
+
+    if (setBoresite) {
+	float boreXchip = psMetadataLookupF32 (&status, recipe, "PSASTRO.MODEL.BORESITE.X");
+	float boreYchip = psMetadataLookupF32 (&status, recipe, "PSASTRO.MODEL.BORESITE.Y");
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.X0", PS_META_REPLACE, "boresite parameter", boreXchip); 
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.Y0", PS_META_REPLACE, "boresite parameter", boreYchip); 
+    }
+
+    if (fitBoresite || setBoresite) {
+	psastroModelAdjustBoresite (output, refChip);
+    } else {
+	// FPA.BORE.X0,Y0 should be 0,0 in the focal plane, not the chip.  Ask for the
+	// coordinates which make refChip->toFPA(x,y) = (0,0)
+	psPlane *PT = psPlaneTransformGetCenter (refChip->toFPA, NONLIN_TOL);
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.X0", PS_META_REPLACE, "boresite parameter", PT->x); 
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.Y0", PS_META_REPLACE, "boresite parameter", PT->y); 
+	psFree (PT);
+    }
+    
+    // rotate the chip-to-FPA transforms to have 0.0 posangle for refChip; 
+    // compensate by rotating fpa to TPA transform
+
+    // get the current posangle of the ref chip
+    // this should have a negative sign : chipAngle = -atan2 (dM/dy, dM/dx)
+    float chipAngle = -atan2 (refChip->toFPA->y->coeff[1][0], refChip->toFPA->x->coeff[1][0]);
+
+    // chipAngle should be refChipAngleNominal @ POSANGLE = 0.0
+    float posAngleOffset = rotatorParity * (chipAngle - refChipAngleNominal);
+
+    fprintf (stderr, "chipAngle is: %f, at PA = 0.0, it should be %f, rotating model by %f (parity %d)\n", 
+	     chipAngle*PS_DEG_RAD, refChipAngleNominal*PS_DEG_RAD, posAngleOffset*PS_DEG_RAD, rotatorParity);
+
+    // psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.POSANGLE", PS_META_REPLACE, "boresite parameter", posangle);
+
+    // rotate the chip transforms
+    for (int i = 0; i < output->fpa->chips->n; i++) {
+	pmChip *chip = output->fpa->chips->data[i];
+	if (!chip->toFPA) continue;
+	// skip chips without astrometry
+
+	// save the region of this chip for the inversion below
+	psRegion *region = pmChipPixels (chip);
+
+	// this should ALSO have a negative sign: this rotates by +posAngleOffset
+	psPlaneTransform *toFPA = psPlaneTransformRotate (NULL, chip->toFPA, posAngleOffset);
+	psFree (chip->toFPA);
+	chip->toFPA = toFPA;
+
+	// invert the new fromFPA transform to get the new toFPA transform
+	// NOTE: when we call psPlaneTransformInvert here, we do not increase the order as in other places
+	psPlaneTransform *fromFPA = psPlaneTransformInvert(NULL, chip->toFPA, *region, 50);
+	psFree (chip->fromFPA);
+	chip->fromFPA = fromFPA;
+
+	psFree (region);
+
+	// save the transformation in the header
+	pmAstromWriteBilevelChip (chip->hdu->header, chip, NONLIN_TOL);
+    }
+
+    // get the current posangle of the fpa
+    float fpaAngle = atan2 (output->fpa->toTPA->y->coeff[1][0], output->fpa->toTPA->x->coeff[1][0]);
+    fprintf (stderr, "fpaAngle: %f\n", fpaAngle*PS_DEG_RAD);
+    // psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.POSANGLE", PS_META_REPLACE, "boresite parameter", posangle);
+
+    // remove the fpa rotation to generate a rotation-free model
+    psPlaneTransform *toTPA = psPlaneTransformRotate (NULL, output->fpa->toTPA, fpaAngle);
+    psFree (output->fpa->toTPA);
+    output->fpa->toTPA = toTPA;
+
+    psFree (output->fpa->fromTPA);
+    // NOTE: when we call psPlaneTransformInvert here, we do not increase the order as in other places
+    output->fpa->fromTPA = psPlaneTransformInvert(NULL, output->fpa->toTPA, *fpaRegion, 50);
+
+    // the model now describes the unrotated focal-plane
+    if (DEBUG) psastroDumpCorners ("corners.up.rot.dat", "corners.dn.rot.dat", output->fpa);
+
+    psMetadata *header = output->fpa->hdu->header;
+    
+    pmAstromWriteBilevelMosaic (header, output->fpa, NONLIN_TOL);
+
+    psFree (fpaRegion);
+
+    return true;
+}
+
+bool psastroModelAdjustBoresite (pmFPAfile *output, pmChip *refChip) {
+
+    bool status;
+
+    psPlane  *boreCH  = psPlaneAlloc();
+    psPlane  *boreFP  = psPlaneAlloc();    
+    psPlane  *boreTP  = psPlaneAlloc();    
+    psSphere *boreSky = psSphereAlloc();    
+
+    // correct Xo,Yo to Lo,Mo using the ref chip toFPA
+    // ref chip position of the true boresite center
+    boreCH->x = psMetadataLookupF32 (&status, output->fpa->concepts, "FPA.BORE.X0"); 
+    boreCH->y = psMetadataLookupF32 (&status, output->fpa->concepts, "FPA.BORE.Y0"); 
+    psPlaneTransformApply (boreFP, refChip->toFPA, boreCH);
+
+    // adjust the reference pixel for all chips
+    for (int i = 0; i < output->fpa->chips->n; i++) {
+	pmChip *chip = output->fpa->chips->data[i];
+	if (!chip->fromFPA) continue;
+	// skip the chips without astrometry
+
+	// save the FPA region of this chip for the inversion below
+	psRegion *region = pmChipPixels (chip);
+
+	// the current toFPA returns boreFP->x,y for the boresite; subtract this from the transformations
+	chip->toFPA->x->coeff[0][0] -= boreFP->x;
+	chip->toFPA->y->coeff[0][0] -= boreFP->y;
+
+	// psPlaneTransform *toFPA = psPlaneTransformSetCenter (NULL, chip->toFPA, -boreFP->x, -boreFP->y);
+	// psFree (chip->toFPA);
+	// chip->toFPA = toFPA;
+
+	// invert the new fromFPA transform to get the new toFPA transform
+	// the region used here is the region covered by the chip in the FPA
+	// NOTE: when we call psPlaneTransformInvert here, we do not increase the order as in other places
+	psPlaneTransform *fromFPA = psPlaneTransformInvert(NULL, chip->toFPA, *region, 50); 
+	psFree (chip->fromFPA);
+	chip->fromFPA = fromFPA;
+
+	psFree (region);
+    }
+
+    if (DEBUG) psastroDumpCorners ("corners.up.shf.dat", "corners.dn.shf.dat", output->fpa);
+
+    // we have now adjusted the chips to use the correct boresite position as the center of the focal-plane system.
+    // we now need to reconstruct the TP to FP transformation, starting from stars projected about this new boresite position.
+
+    // find the R,D of the new boresite (boreFP -> 0,0; 0,0 -> -boreFP)
+    boreFP->x = -boreFP->x;
+    boreFP->y = -boreFP->y;
+    psPlaneTransformApply (boreTP, output->fpa->toTPA, boreFP);
+    psDeproject (boreSky, boreTP, output->fpa->toSky); // find the RA,DEC coord of the focal-plane coordinate
+
+    psProjection *newSky = psProjectionAlloc (boreSky->r, boreSky->d, output->fpa->toSky->Xs, output->fpa->toSky->Ys, output->fpa->toSky->type);
+
+    // generate a collection of points on the sky using the old toTPA transformation and toSky projection, projected with the newSky projection
+    // this is the FPA coordinate range covered by the FP: 
+    psRegion *fpaRegion = pmAstromFPAExtent (output->fpa);
+    float dx = (fpaRegion->x1 - fpaRegion->x0) / 50.0;
+    float dy = (fpaRegion->y1 - fpaRegion->y0) / 50.0;
+
+    psPlane fp, tp;
+    psSphere sky;
+
+    psVector *FPx = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *FPy = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *TPx = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *TPy = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+    // XXX a test: boreFP->x,y, should transform to tp.x,y = 0,0
+    fp.x = boreFP->x;
+    fp.y = boreFP->y;
+    psPlaneTransformApply (&tp, output->fpa->toTPA, &fp);
+    psDeproject (&sky, &tp, output->fpa->toSky); // find the RA,DEC coord of the focal-plane coordinate
+    psProject (&tp, &sky, newSky); // find the RA,DEC coord of the focal-plane coordinate
+
+    int Npts = 0;
+    for (fp.x = fpaRegion->x0; fp.x <= fpaRegion->x1; fp.x += dx) {
+	for (fp.y = fpaRegion->y0; fp.y <= fpaRegion->y1; fp.y += dy) {
+	    psPlaneTransformApply (&tp, output->fpa->toTPA, &fp);
+	    psDeproject (&sky, &tp, output->fpa->toSky); // find the RA,DEC coord of the focal-plane coordinate
+	    psProject (&tp, &sky, newSky); // find the RA,DEC coord of the focal-plane coordinate
+
+	    // we are fitting points in the NEW FP system to points in the NEW TP system
+	    FPx->data.F32[Npts] = fp.x - boreFP->x;
+	    FPy->data.F32[Npts] = fp.y - boreFP->y;
+	    TPx->data.F32[Npts] = tp.x;
+	    TPy->data.F32[Npts] = tp.y;
+	    psVectorExtend (FPx, 100, 1);
+	    psVectorExtend (FPy, 100, 1);
+	    psVectorExtend (TPx, 100, 1);
+	    psVectorExtend (TPy, 100, 1);
+	    Npts ++;
+	}
+    }
+    psFree (fpaRegion);
+
+    // fit both up and down transformations to the same points
+    psVectorFitPolynomial2D (output->fpa->toTPA->x, NULL, 0, TPx, NULL, FPx, FPy);
+    psVectorFitPolynomial2D (output->fpa->toTPA->y, NULL, 0, TPy, NULL, FPx, FPy);
+    psVectorFitPolynomial2D (output->fpa->fromTPA->x, NULL, 0, FPx, NULL, TPx, TPy);
+    psVectorFitPolynomial2D (output->fpa->fromTPA->y, NULL, 0, FPy, NULL, TPx, TPy);
+
+    psFree (output->fpa->toSky);
+    output->fpa->toSky = newSky;
+
+    if (DEBUG) psastroDumpCorners ("corners.up.bore.dat", "corners.dn.bore.dat", output->fpa);
+
+    psFree (FPx);
+    psFree (FPy);
+    psFree (TPx);
+    psFree (TPy);
+
+    psFree (boreCH);
+    psFree (boreFP);
+    psFree (boreTP);
+    psFree (boreSky);
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelAnalysis.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelAnalysis.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelAnalysis.c	(revision 41084)
@@ -0,0 +1,177 @@
+/** @file psastroModelAnalysis.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastroModel
+ *
+ *  @author IfA
+ *  @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+# define NONLIN_TOL 0.001
+
+bool psastroModelAnalysis (pmConfig *config) {
+
+    bool status;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+	return false;
+    }
+
+    // reference chip for boresite parameters
+    char *refChip = psMetadataLookupStr (&status, recipe, "PSASTRO.MODEL.REF.CHIP");
+    if (!refChip) {
+	psError(PS_ERR_IO, true, "reference chip is missing from recipe"); 
+	return false; 
+    } 
+
+    pmFPAfile *output = psMetadataLookupPtr (&status, config->files, "PSASTRO.OUT.MODEL");
+    if (!status) psAbort ("Can't find output pmFPAfile PSASTRO.OUT.MODEL");
+
+    // measure the boresite position from a rotation sequence?
+    bool fitBoresite = psMetadataLookupBool (&status, recipe, "PSASTRO.MODEL.FIT.BORESITE");
+    if (!fitBoresite) {
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.X0", PS_META_REPLACE, "boresite parameter", 0.0); 
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.Y0", PS_META_REPLACE, "boresite parameter", 0.0); 
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.RX", PS_META_REPLACE, "boresite parameter", 0.0); 
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.RY", PS_META_REPLACE, "boresite parameter", 0.0); 
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.T0", PS_META_REPLACE, "boresite parameter", 0.0); 
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.P0", PS_META_REPLACE, "boresite parameter", 0.0); 
+	psMetadataAddStr (output->fpa->concepts, PS_LIST_TAIL, "FPA.REF.CHIP", PS_META_REPLACE, "boresite parameter", refChip);
+	return true; 
+    } 
+
+    char *outroot = psMetadataLookupStr (&status, config->arguments, "OUTPUT");
+    if (!status || !outroot) psAbort ("Can't find outroot on config->arguments");
+
+    /* model analysis:
+     *
+     * determine POS_ZERO via comparison of measured and reported posangles
+     * POS_ZERO = FPA.POSANGLE - posangle
+     *
+     * determine boresite model:
+     * X = Xo + R_X cos(FPA.POSANGLE - T_0) cos(P_0) + R_Y sin(FPA.POSANGLE - T_0) sin(P_0) 
+     * Y = Yo + R_Y sin(FPA.POSANGLE - T_0) cos(P_0) - R_X cos(FPA.POSANGLE - T_0) sin(P_0) 
+     * position of reported boresite in reference chip pixels
+     * Xo, Yo : true coordinate of boresite (rotator center) in reference chip pixels
+     * R_X, R_Y : amplitude of boresite offset
+     * T_0 : reference angle for rotator
+     * P_0 : orientation of boresite ellipse
+     *
+     */
+
+    // select the input pmFPAfile pointers
+    psMetadataItem *item = psMetadataLookup (config->files, "PSASTRO.WCS");
+    if (item == NULL) psAbort("missing PSASTRO.WCS entries in config->files");
+    if (item->type != PS_DATA_METADATA_MULTI) psAbort("unexpected type for PSASTRO.WCS");
+    psArray *files = psListToArray (item->data.list);
+
+    // data storage vectors for measurements
+    psVector *posZero  = psVectorAlloc (files->n, PS_TYPE_F32);
+    psVector *Po       = psVectorAlloc (files->n, PS_TYPE_F32);
+    psVector *Xo       = psVectorAlloc (files->n, PS_TYPE_F32);
+    psVector *Yo       = psVectorAlloc (files->n, PS_TYPE_F32);
+
+    // counter for accepted measured values
+    int n = 0;
+
+    // Re-select the output fpa.  The output fpa needs to have valid astrometry for the
+    // refChip.  output->fpa is a copy of the pointer to one of the input->fpa, but the choice
+    // is arbitrary.  select a new one that has an existing ref chip
+    psFree (output->fpa);
+    output->fpa = NULL;
+
+    char filename[256];
+    snprintf (filename, 256, "%s.bore", outroot);
+    FILE *outfile = fopen (filename, "w");
+    if (!outfile) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "cannot open %s for output", filename);
+	return false;
+    }
+
+    float posBoundary = 0.0;
+
+    // extract the relevant measured and reported values from the reference chip
+    for (int i = 0; i < files->n; i++) {
+	psMetadataItem *file = files->data[i];
+	pmFPAfile *input = file->data.V;
+
+	// reported rotator position angle
+	double POSANGLE = psMetadataLookupF64 (&status, input->fpa->concepts, "FPA.POSANGLE"); 
+	if (!status) psAbort ("missing FPA.POSANGLE");
+
+    	// get reference chip from name
+	pmChip *chip = pmConceptsChipFromName (input->fpa, refChip);
+	if (!chip) psAbort ("invalid chip name for reference");
+
+	if (!chip->toFPA) continue;
+
+	if (!output->fpa) {
+	    // this one matches
+	    output->fpa = psMemIncrRefCounter(input->fpa);
+	}
+
+	// we have two measurements of the posangle (may be parity flipped to different quadrants)
+	// atan2 returns values in the range 0-2pi
+	// all posZero values should be clustered in some region, but we need to flip over the 0,360 boundary correctly.
+	// push all to one side or the other
+	float chipAngle = PM_DEG_RAD * atan2 (chip->toFPA->y->coeff[1][0], chip->toFPA->x->coeff[1][0]);
+	float fpaAngle = PM_DEG_RAD * atan2 (input->fpa->toTPA->y->coeff[1][0], input->fpa->toTPA->x->coeff[1][0]);
+
+	posZero->data.F32[n] = POSANGLE - chipAngle - fpaAngle;
+	if (n == 0) {
+	    posBoundary = posZero->data.F32[n] + 180.0;
+	} else {
+	    while (posZero->data.F32[n] > posBoundary) posZero->data.F32[n] -= 360.0;
+	    while (posZero->data.F32[n] < posBoundary - 360.0) posZero->data.F32[n] += 360.0;
+	}
+
+	Po->data.F32[n] = POSANGLE * PM_RAD_DEG; // reported position angle
+	float xc = chip->fromFPA->x->coeff[0][0]; // reported boresite x position in ref chip coordinates
+	float yc = chip->fromFPA->y->coeff[0][0]; // reported boresite y position in ref chip coordinates
+	// XXX this can also be derived from toFPA via GetCenter....
+	
+	psPlane *PT = psPlaneTransformGetCenter (chip->toFPA, NONLIN_TOL);
+	Xo->data.F32[n] = PT->x; // reported boresite x position in ref chip coordinates
+	Yo->data.F32[n] = PT->y; // reported boresite y position in ref chip coordinates
+	psFree (PT);
+
+	fprintf (outfile, "%d : %f %f : %f = %f - %f - %f | %f %f\n", i, Xo->data.F32[n], Yo->data.F32[n], posZero->data.F32[n], POSANGLE, chipAngle, fpaAngle, xc, yc);
+	n ++;
+    }
+      
+    Xo->n = n;
+    Yo->n = n;
+    Po->n = n;
+    posZero->n = n;
+
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    if (!psVectorStats (stats, posZero, NULL, NULL, 0)) {
+	psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	return false;
+    }
+
+    fprintf (outfile, "# pos zero %f +/- %f\n", stats->sampleMedian, stats->sampleStdev);
+    psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.POS_ZERO", PS_META_REPLACE, "offset between obs and meas posangle", stats->sampleMedian); 
+    fclose (outfile);
+
+    psVector *params = psastroModelFitBoresite (Xo, Yo, Po, outroot);
+    if (params->n != 6) psAbort ("error");
+
+
+    psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.X0", PS_META_REPLACE, "boresite parameter", params->data.F32[PAR_X0]); 
+    psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.Y0", PS_META_REPLACE, "boresite parameter", params->data.F32[PAR_Y0]); 
+    psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.RX", PS_META_REPLACE, "boresite parameter", params->data.F32[PAR_RX]); 
+    psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.RY", PS_META_REPLACE, "boresite parameter", params->data.F32[PAR_RY]); 
+    psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.T0", PS_META_REPLACE, "boresite parameter", params->data.F32[PAR_T0]); 
+    psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.P0", PS_META_REPLACE, "boresite parameter", params->data.F32[PAR_P0]); 
+    psMetadataAddStr (output->fpa->concepts, PS_LIST_TAIL, "FPA.REF.CHIP", PS_META_REPLACE, "boresite parameter", refChip);
+
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelArguments.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelArguments.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelArguments.c	(revision 41084)
@@ -0,0 +1,75 @@
+/** @file psastroModelArguments.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastroModel
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+
+static char *usage = "USAGE: psastroModel [-output root] (cmffiles)";
+
+pmConfig *psastroModelArguments (int argc, char **argv) {
+
+    int N;
+
+    if (argc == 1) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "No arguments supplied");
+        psErrorStackPrint(stderr, "%s", usage);
+	exit (1);
+    }
+
+    // load config data from default locations
+    pmConfig *config = pmConfigRead(&argc, argv, PSASTRO_RECIPE);
+    if (config == NULL) {
+        psError(PSASTRO_ERR_CONFIG, false, "Can't read site configuration");
+        psErrorStackPrint(stderr, "%s", usage);
+	exit (1);
+    }
+
+    // save the following additional recipe values based on command-line options
+    // these options override the PSASTRO recipe values loaded from recipe files
+
+    // XXX no options yet defined
+    // psMetadata *options = pmConfigRecipeOptions (config, PSASTRO_RECIPE);
+
+    // define the output filename
+    if ((N = psArgumentGet (argc, argv, "-output"))) {
+        psArgumentRemove (N, &argc, argv);
+	psMetadataAddStr (config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    } else {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "Missing -output (root)");
+        psErrorStackPrint(stderr, "%s", usage);
+	exit (1);
+    }
+
+    // chip selection is used to limit chips to be processed
+    if ((N = psArgumentGet (argc, argv, "-chip"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (config->arguments, PS_LIST_TAIL, "CHIP_SELECTIONS", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+    
+    if (argc < 1) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "Incorrect arguments supplied");
+        psErrorStackPrint(stderr, "exit");
+	exit (1);
+    }
+
+    // each additional word is a file; create names INPUT.%d for them
+    for (int i = 0; i < argc - 1; i++) {
+	char name[PS_SMALLWORD];
+	ps_snprintf_nowarn (name, PS_SMALLWORD, "INPUT.%d", i);
+	psArray *array = psArrayAlloc(1);
+	array->data[0] = psStringCopy (argv[i+1]);
+	psMetadataAddPtr(config->arguments, PS_LIST_TAIL, name,  PS_DATA_ARRAY, "", array);
+    }	
+    psMetadataAddS32(config->arguments, PS_LIST_TAIL, "INPUT.N", 0, "", argc - 1);
+    return (config);
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelBoresite.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelBoresite.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelBoresite.c	(revision 41084)
@@ -0,0 +1,66 @@
+/** @file psastroModelBoresite.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastroModel
+ *
+ *  @author IfA
+ *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+
+/**
+ * the full chisq is built of two associated sums over coordinates:
+ * chisq = sum ((X_obs - X_fit(t))^2 + (Y_obs - Y_fit(t))^2)
+ * we use split this into a 2x long vector and use coord[1] to distinguish the X and Y terms:
+ * coord[0] = measured X or measured Y
+ * coord[1] =          0 or          1
+ */
+psF32 psastroModelBoresite (psVector *deriv, const psVector *params, const psVector *coord) {
+
+    psF32 *PAR = params->data.F32;
+
+    float csphi = cos(PAR[PAR_P0]);
+    float snphi = sin(PAR[PAR_P0]);
+
+    float dtheta = coord->data.F32[0] - PAR[PAR_T0];
+    float cstht = cos(dtheta);
+    float sntht = sin(dtheta);
+
+    // value is X
+    if (coord->data.F32[1] == 0) {
+
+	float value = PAR[PAR_X0] + PAR[PAR_RX]*cstht*csphi + PAR[PAR_RY]*sntht*snphi;
+
+	if (deriv) {
+	    psF32 *dPAR = deriv->data.F32;
+	    dPAR[PAR_X0] = 1.0;
+	    dPAR[PAR_Y0] = 0.0;
+	    dPAR[PAR_RX] = +cstht*csphi;
+	    dPAR[PAR_RY] = +sntht*snphi;
+	    dPAR[PAR_P0] = -PAR[PAR_RX]*cstht*snphi + PAR[PAR_RY]*sntht*csphi;
+	    dPAR[PAR_T0] =  PAR[PAR_RX]*sntht*csphi - PAR[PAR_RY]*cstht*snphi;
+	}
+	return (value);
+    }  
+
+    // value is Y
+    if (coord->data.F32[1] == 1) {
+	float value = PAR[PAR_Y0] + PAR[PAR_RY]*sntht*csphi - PAR[PAR_RX]*cstht*snphi;
+
+	if (deriv) {
+	    psF32 *dPAR = deriv->data.F32;
+	    dPAR[PAR_X0]  = 0.0;
+	    dPAR[PAR_Y0]  = 1.0;
+	    dPAR[PAR_RX]  = -cstht*snphi;
+	    dPAR[PAR_RY]  = +sntht*csphi;
+	    dPAR[PAR_P0]  = -PAR[PAR_RY]*sntht*snphi - PAR[PAR_RX]*cstht*csphi;
+	    dPAR[PAR_T0]  = -PAR[PAR_RY]*cstht*csphi - PAR[PAR_RX]*sntht*snphi;
+	}
+	return (value);
+    }  
+    psAbort ("programming error: invalid coordinate");
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelDataLoad.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelDataLoad.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelDataLoad.c	(revision 41084)
@@ -0,0 +1,113 @@
+/** @file psastroModelDataLoad.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastroModel
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "Failure in psastroModelDataLoad"); \
+  psFree (view); \
+  return false; \
+}
+  
+/**
+ * this loop loads the header data from the input files, using the output 
+ * pmFPAfile to guide the chip selection and related issues
+ * all of the different astrometry analysis modes use the same data load loop
+ */
+bool psastroModelDataLoad (pmConfig *config) {
+
+    bool status;
+    pmChip *chip;
+
+    psTimerStart ("psastro");
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+    // physical pixel scale in microns per pixel
+    double pixelScale = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.SCALE");
+    if (!status) {
+	psError(PS_ERR_IO, true, "Failed to lookup pixel scale"); 
+	return false; 
+    } 
+
+    // select the input data sources
+    pmFPAfile *output = psMetadataLookupPtr (NULL, config->files, "PSASTRO.OUT.MODEL");
+    if (!output) psAbort ("PSASTRO.OUT.MODEL not listed in config->files");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // files associated with the science image
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+    while ((chip = pmFPAviewNextChip (view, output->fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process) { continue; }
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+    }
+    psLogMsg ("psastro", 3, "load headers : %f sec\n", psTimerMark ("psastro"));
+
+    // we should have a number of different files stored in config->files as PSASTRO.WCS
+    psMetadataItem *item = psMetadataLookup (config->files, "PSASTRO.WCS");
+    if (item == NULL) psAbort("missing PSASTRO.WCS entries in config->files");
+    if (item->type != PS_DATA_METADATA_MULTI) psAbort("unexpected type for PSASTRO.WCS");
+    psArray *files = psListToArray (item->data.list);
+
+    // convert the headers for the input file into fpa astrometry terms
+    for (int i = 0; i < files->n; i++) {
+	psMetadataItem *file = files->data[i];
+	pmFPAfile *input = file->data.V;
+
+	pmFPAviewReset (view);
+
+	// check PHU header to see if we are using mosaic-level or per-chip astrometry
+	bool bilevelAstrometry = false;
+	pmHDU *phu = pmFPAviewThisPHU (view, input->fpa);
+	if (phu) {
+	    char *ctype = psMetadataLookupStr (NULL, phu->header, "CTYPE1");
+	    if (ctype) bilevelAstrometry = !strcmp (&ctype[4], "-DIS");
+	}
+	if (bilevelAstrometry) {
+	    pmAstromReadBilevelMosaic (input->fpa, phu->header);
+	} 
+
+	while ((chip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+	    psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+	    if (!chip->process || !chip->file_exists || !chip->data_exists) { continue; }
+
+	    // read WCS data from the corresponding header
+	    pmHDU *hdu = pmFPAviewThisHDU (view, input->fpa);
+	    int nAstro = psMetadataLookupS32 (&status, hdu->header, "NASTRO");
+	    if (!nAstro) continue;
+
+	    if (bilevelAstrometry) {
+		if (!pmAstromReadBilevelChip (chip, hdu->header)) {
+		    psWarning("Could not get WCS information from header for chip %d, skipping", view->chip); 
+		    continue;
+		} 
+	    } else {
+		if (!pmAstromReadWCS (input->fpa, chip, hdu->header, pixelScale)) {
+		    psWarning("Could not get WCS information from header for chip %d, skipping", view->chip); 
+		    continue;
+		} 
+	    }
+	}
+    }
+    psLogMsg ("psastro", 3, "convert wcs terms to internal format : %f sec\n", psTimerMark ("psastro"));
+
+    psFree (view);
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelDataSave.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelDataSave.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelDataSave.c	(revision 41084)
@@ -0,0 +1,52 @@
+/** @file psastroModelDataSave.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastroModel
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+# define NONLIN_TOL 0.001 ///< tolerance in pixels 
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "Failure in psastroModelDataSave"); \
+  psFree (view); \
+  return false; \
+}
+  
+bool psastroModelDataSave (pmConfig *config) {
+
+    bool status;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+	return false;
+    }
+
+    pmFPAfile *output = psMetadataLookupPtr (&status, config->files, "PSASTRO.OUT.MODEL");
+    if (!status) psAbort ("Can't find output pmFPAfile PSASTRO.OUT.MODEL");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmChip *chip = NULL;
+
+    while ((chip = pmFPAviewNextChip (view, output->fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process) { continue; }
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+    }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+
+    psLogMsg ("psastro", 3, "save headers : %f sec\n", psTimerMark ("psastro"));
+
+    return true;
+}
+
+
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelFit.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelFit.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelFit.c	(revision 41084)
@@ -0,0 +1,49 @@
+/** @file psastroModelFit.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastroModel
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+
+int main (int argc, char **argv) {
+
+    psTimerStart ("complete");
+
+    if (argc != 3) {
+	fprintf (stderr, "USAGE: psastroModelFit (input) (output)\n");
+	exit (1);
+    }
+
+    FILE *f = fopen (argv[1], "r");
+    if (f == NULL) {
+	fprintf (stderr, "problem opening data file %s\n", argv[1]);
+	exit (2);
+    }
+
+    char name[1024];
+    psVector *Xo = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *Yo = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *Po = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+    float x, y, p;
+
+    while (fscanf (f, "%s %f %f %f", name, &x, &y, &p) != EOF) {
+	psVectorAppend (Xo, x);
+	psVectorAppend (Yo, y);
+	psVectorAppend (Po, p*PS_RAD_DEG);
+    }	
+
+    // psTraceSetLevel("psLib.math", 5);
+    psastroModelFitBoresite (Xo, Yo, Po, argv[2]);
+
+    psLogMsg ("psastro", 3, "complete psastroModelFit run: %f sec\n", psTimerMark ("complete"));
+
+    exit (EXIT_SUCCESS);
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelFitBoresite.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelFitBoresite.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelFitBoresite.c	(revision 41084)
@@ -0,0 +1,113 @@
+/** @file psastroModelFitBoresite.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastroModel
+ *
+ *  @author IfA
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroStandAlone.h"
+
+/** 
+ * we now have a set of observed L,M values.  fit these to the boresite model
+ */
+psVector *psastroModelFitBoresite (psVector *Xo, psVector *Yo, psVector *Po, char *outroot) {
+
+    assert (Xo->n > 2);
+    assert (Xo->n == Yo->n);
+    assert (Xo->n == Po->n);
+
+    // arrays to hold the data to be fitted
+    psArray *x = psArrayAlloc(2*Xo->n);
+    psVector *y = psVectorAlloc(2*Xo->n, PS_TYPE_F32);
+
+    int n = 0;
+    for (int i = 0; i < Xo->n; i++) {
+
+	psVector *coord = NULL;
+
+	// X coordinate value
+	coord = psVectorAlloc (2, PS_TYPE_F32);
+	coord->data.F32[1] = 0.0;
+	coord->data.F32[0] = Po->data.F32[i];
+	x->data[n] = coord;
+	y->data.F32[n] = Xo->data.F32[i];
+	n++;
+	
+	// Y coordinate value
+	coord = psVectorAlloc (2, PS_TYPE_F32);
+	coord->data.F32[1] = 1.0;
+	coord->data.F32[0] = Po->data.F32[i];
+	x->data[n] = coord;
+	y->data.F32[n] = Yo->data.F32[i];
+	n++;
+    }	
+    assert (x->n == n);
+    assert (y->n == n);
+
+    psVector *params = psVectorAlloc (6, PS_TYPE_F32);
+    
+    // create the minimization constraints
+    psMinConstraint *constraint = psMinConstraintAlloc();
+
+    // XXX for now, no parameter masks, skip checkLimits
+    // constraint->checkLimits = psastroModelBoresiteLimits;
+
+    // make an initial guess:
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEAN | PS_STAT_MAX | PS_STAT_MIN);
+
+    // center (Xo) = mean(Xo), RX = range / 2
+    if (!psVectorStats (stats, Xo, NULL, NULL, 0)) {
+	psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	return NULL;
+    }
+    params->data.F32[PAR_X0] = stats->sampleMean;
+    params->data.F32[PAR_RX] = (stats->max - stats->min) / 2.0;
+
+    // center (Yo) = mean(Yo), RY = range / 2
+    if (!psVectorStats (stats, Yo, NULL, NULL, 0)) {
+	psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
+	return NULL;
+    }
+    params->data.F32[PAR_Y0] = stats->sampleMean;
+    params->data.F32[PAR_RY] = (stats->max - stats->min) / 2.0;
+
+    params->data.F32[PAR_P0] = 0.0;
+    params->data.F32[PAR_T0] = 0.0;
+
+    psMinimization *myMin = psMinimizationAlloc (25, 0.001, 1.0);
+    psImage *covar = psImageAlloc (params->n, params->n, PS_TYPE_F32);
+    
+    // fprintf (stderr, "guess values:\n");
+    // fprintf (stderr, "Xo:  %f\n", params->data.F32[PAR_X0]);
+    // fprintf (stderr, "Yo:  %f\n", params->data.F32[PAR_Y0]);
+    // fprintf (stderr, "RX:  %f\n", params->data.F32[PAR_RX]);
+    // fprintf (stderr, "RY:  %f\n", params->data.F32[PAR_RY]);
+    // fprintf (stderr, "P0:  %f\n", params->data.F32[PAR_P0]);
+    // fprintf (stderr, "T0:  %f\n", params->data.F32[PAR_T0]);
+
+    // XXX skip the weights for now
+    psMinimizeLMChi2(myMin, covar, params, constraint, x, y, NULL, psastroModelBoresite);
+
+    char filename[256];
+    snprintf (filename, 256, "%s.pars", outroot);
+    FILE *outfile = fopen (filename, "w");
+    if (!outfile) {
+        psAbort("cannot open %s for output", filename);
+    }
+
+    fprintf (outfile, "# fitted values:\n");
+    fprintf (outfile, "Xo:  %f\n", params->data.F32[PAR_X0]);
+    fprintf (outfile, "Yo:  %f\n", params->data.F32[PAR_Y0]);
+    fprintf (outfile, "RX:  %f\n", params->data.F32[PAR_RX]);
+    fprintf (outfile, "RY:  %f\n", params->data.F32[PAR_RY]);
+    fprintf (outfile, "P0:  %f\n", params->data.F32[PAR_P0]);
+    fprintf (outfile, "T0:  %f\n", params->data.F32[PAR_T0]);
+    fclose (outfile);
+
+    return params;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelParseCamera.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelParseCamera.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroModelParseCamera.c	(revision 41084)
@@ -0,0 +1,56 @@
+/** @file psastroModelParseCamera.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastroModel
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+bool psastroModelParseCamera (pmConfig *config) {
+
+    bool status = false;
+    pmFPAfile *input = NULL;
+
+    int nInput = psMetadataLookupS32 (&status, config->arguments, "INPUT.N");
+    if (!status) psAbort ("missing INPUT.N in config->arguments");
+
+    // find and load the input files (this determines the camera and builds the fpa structures,
+    // but does not load the data)
+    for (int i = 0; i < nInput; i++) {
+	char name[PS_SMALLWORD];
+	ps_snprintf_nowarn (name, PS_SMALLWORD, "INPUT.%d", i);
+	input = pmFPAfileDefineFromArgs (&status, config, "PSASTRO.WCS", name);
+    }
+	
+    // set up an output fpa structure & file based on the last input file
+    pmFPAfile *output = pmFPAfileDefineOutput (config, input->fpa, "PSASTRO.OUT.MODEL");
+    if (!output) {
+	psError(PSASTRO_ERR_CONFIG, false, "Failed to build FPA for PSASTRO.OUT.MODEL from input");
+	return false;
+    }
+    output->save = true;
+
+    // Chip selection: turn on only the chips specified (option is not required)
+    char *chipLine = psMetadataLookupStr(&status, config->arguments, "CHIP_SELECTIONS"); 
+    psArray *chips = psStringSplitArray (chipLine, ",", false);
+    if (chips->n > 0) {
+	pmFPASelectChip (output->fpa, -1, true); // deselect all chips
+	for (int i = 0; i < chips->n; i++) {
+	    int chipNum = atoi(chips->data[i]);
+	    if (! pmFPASelectChip(output->fpa, chipNum, false)) {
+		psError(PSASTRO_ERR_CONFIG, true, "Chip number %d doesn't exist in camera.\n", chipNum);
+		return false;
+	    }
+        }
+    }
+    psFree (chips);
+
+    psTrace("psastro", 1, "Done with psastroModelParseCamera...\n");
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicAstrom.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicAstrom.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicAstrom.c	(revision 41084)
@@ -0,0 +1,304 @@
+/** @file psastroMosaicAstrom.c
+ *
+ *  @brief
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.30 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+# define NONLIN_TOL 0.001 /* tolerance in pixels */
+
+bool psastroMosaicFit (pmFPA *fpa, psMetadata *recipe, const char *rootname, int pass);
+bool psastroProjectionRefit (pmFPA *fpa, psMetadata *recipe);
+
+// XXX require this fpa to have multiple chip extensions and a PHU?
+bool psastroMosaicAstrom (pmConfig *config) {
+
+    bool status;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, false, "Can't find PSASTRO recipe!\n");
+        return false;
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+        psError(PSASTRO_ERR_CONFIG, false, "Can't find input data!\n");
+        return false;
+    }
+
+    pmFPA *fpa = input->fpa;
+
+    char *outroot = psMetadataLookupStr (&status, config->arguments, "OUTPUT");
+    if (!status || !outroot) psAbort ("Can't find outroot on config->arguments");
+
+    int nIter = psMetadataLookupS32 (&status, recipe, "PSASTRO.MOSAIC.CHIP.NITER");
+    if (!status) psAbort ("missing config value");
+
+    // if projection is not TAN, fit the measured projection here and combine toTPA/fromTPA functions
+    bool fitMosaicDistortion = true;
+    if ((fpa->toSky->type != PS_PROJ_TAN) && (fpa->toSky->type != PS_PROJ_DIS)) {
+      if (!psastroProjectionRefit (fpa, recipe)) psAbort ("failed to refit distortion");
+      fitMosaicDistortion = false;
+    }
+
+    // this should be in a loop with nIter =
+    for (int iter = 0; fitMosaicDistortion && (iter < nIter); iter++) {
+        if (!psastroMosaicFit (fpa, recipe, outroot, iter)) return false;
+    }
+
+    // now fit the chips under the common distortion with higher-order terms
+    // first, re-perform the match with a slightly tighter circle
+    if (!psastroMosaicSetMatch (fpa, recipe, nIter)) {
+      psError(PSASTRO_ERR_UNKNOWN, false, "failed to match raw and ref stars for mosaic (pass %d)", nIter);
+      return false;
+    }
+    if (!psastroMosaicChipAstrom (fpa, recipe, nIter)) {
+      psError(PSASTRO_ERR_UNKNOWN, false, "failed to measure chip astrometry in mosaic mode (pass %d)", nIter);
+      return false;
+    }
+    
+    if (psTraceGetLevel("psastro.dump") > 0) {
+      // the last filename (see filenames in psastroMosaicFit)
+      char filename[256];
+      snprintf (filename, 256, "%s.%d.dat", outroot, 2*nIter + 2);
+      psastroDumpMatches (fpa, filename);
+    }
+    
+    // save WCS and analysis metadata in update header.
+    // (pull or create local view to entry on readout->analysis)
+    psMetadata *updates = psMetadataLookupMetadata (&status, fpa->analysis, "PSASTRO.HEADER");
+    if (!updates) {
+        updates = psMetadataAlloc ();
+        psMetadataAddMetadata (fpa->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", updates);
+        psFree (updates);
+    }
+    if (!pmAstromWriteBilevelMosaic (updates, fpa, NONLIN_TOL)) {
+        psError(psErrorCodeLast(), false, "Failed to save header terms");
+        return false;
+    }
+
+    // write the elapsed time here; this will be updated in psastroMosaicAstrometry, if called
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "DT_ASTR", PS_META_REPLACE, "elapsed psastro time", psTimerMark ("psastroAnalysis"));
+
+    // update the headers based on the results
+    // XXX need to add global summary statistics
+    // psastroMosaicHeaders (config);
+
+    return true;
+}
+
+/* coordinate frame hierachy
+ * pixels (on a given readout)
+ * cell
+ * chip
+ * FP (focal plane)
+ * TP (tangent plane)
+ * sky (ra, dec)
+ */
+
+// 1: match 4,5
+// 2: match 6,7
+// 3: match 8,9
+bool psastroMosaicFit (pmFPA *fpa, psMetadata *recipe, const char *rootname, int pass) {
+
+    char filename[256];
+
+    // given the existing per-chip astrometry, determine matches between raw and ref stars
+    // is this needed? yes, if we didn't do SingleChip astrometry first
+    if (!psastroMosaicSetMatch (fpa, recipe, pass)) {
+        psError(PSASTRO_ERR_UNKNOWN, false, "failed to match raw and ref stars for mosaic (pass %d)", pass);
+        return false;
+    }
+
+    if ((pass == 0) && (psTraceGetLevel("psastro.dump.psastroMosaicAstrom") > 1)) {
+        snprintf (filename, 256, "%s.0.dat", rootname);
+        psastroDumpMatches (fpa, filename);
+    }
+
+    // fitted chips will follow the local plate-scale, hiding the distortion
+    // modify the chip->toFPA scaling to match knowledge about pixel scale,
+    // then recalculate raw and ref positions
+    if (!psastroMosaicCommonScale (fpa, recipe)) {
+        psError(PSASTRO_ERR_UNKNOWN, false, "failed to set a common scale for the chips (pass %d)", pass);
+        return false;
+    }
+
+    if ((pass == 0) && (psTraceGetLevel("psastro.dump.psastroMosaicAstrom") > 1)) {
+        snprintf (filename, 256, "%s.1.dat", rootname);
+        psastroDumpMatches (fpa, filename);
+    }
+
+    // fit the distortion by fitting its gradient
+    // apply the new distortion terms up and down
+    // refit the per-chip terms with linear fits only
+    if (!psastroMosaicDistortion (fpa, recipe, pass)) {
+        psError(PSASTRO_ERR_UNKNOWN, false, "failed to measure mosaic gradients (pass %d)", pass);
+        return false;
+    }
+
+    snprintf (filename, 256, "%s.%d.dat", rootname, 2*pass + 2);
+    if (psTraceGetLevel("psastro.dump.psastroMosaicAstrom") > 1) { psastroDumpMatches (fpa, filename); }
+
+    // measure the astrometry for the chips under the distortion term
+    if (!psastroMosaicChipAstrom (fpa, recipe, pass)) {
+        psError(PSASTRO_ERR_UNKNOWN, false, "failed to measure chip astrometry in mosaic mode (pass %d)", pass);
+        return false;
+    }
+
+    if (psTraceGetLevel("psastro.dump.psastroMosaicAstrom") > 1) {
+        snprintf (filename, 256, "%s.%d.dat", rootname, 2*pass + 3);
+        psastroDumpMatches (fpa, filename);
+    }
+
+    return true;
+}
+
+// we have a fpa->toSky projection which is NOT of type TAN along with a toTPA which is
+// the Identity transform.  we want to convert this to TAN projection plus a non-identity
+// transformation to take the non-TAN components.
+
+// we are going to generate a set of FP->(x,y) using the current projection + fromTPA
+// transformations along with a set of TP->(x,y) values using the desired TAN projection,
+// then we will fit TP vs FP
+
+bool psastroProjectionRefit (pmFPA *fpa, psMetadata *recipe) {
+
+  bool status;
+
+    // allocate mosaic-level polynomial transformation and set masks needed by DVO
+    int order = psMetadataLookupF32 (&status, recipe, "PSASTRO.MOSAIC.ORDER");
+    if (!status) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to find mosaic distortion fit order\n");
+        return false;
+    }
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    psVector *L = psVectorAllocEmpty (1000, PS_TYPE_F32);
+    psVector *M = psVectorAllocEmpty (1000, PS_TYPE_F32);
+
+    psVector *P = psVectorAllocEmpty (1000, PS_TYPE_F32);
+    psVector *Q = psVectorAllocEmpty (1000, PS_TYPE_F32);
+
+    psProjection *toSkyTan = psProjectionAlloc (fpa->toSky->R, fpa->toSky->D, fpa->toSky->Xs, fpa->toSky->Ys, PS_PROJ_TAN);
+
+    float xMin = NAN;
+    float xMax = NAN;
+    float yMin = NAN;
+    float yMax = NAN;
+
+    bool firstObject = true;
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) continue;
+	
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) continue;
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) continue;
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS.SUBSET");
+		if (refstars == NULL) continue;
+		psTrace ("psastro", 4, "Trying %ld refstars\n", refstars->n);
+
+		psArray *matches = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+		if (matches == NULL) continue;
+
+		// we are looking over the matched refstars only to be sure we are
+		// covering the valid space of the projection + transformation, and not
+		// beyond
+
+		for (int i = 0; i < matches->n; i++) {
+		    pmAstromMatch *match = matches->data[i];
+		    pmAstromObj *ref = refstars->data[match->ref];
+
+		    psPlane fpOld, tpNew, tpOld;
+
+		    psProject (&tpOld, ref->sky, fpa->toSky); // find the focal-plane coord of this RA,DEC coord using the ref chip projection
+		    psPlaneTransformApply (&fpOld, fpa->fromTPA, &tpOld);
+
+		    psProject (&tpNew, ref->sky, toSkyTan); // find the focal-plane coord of this RA,DEC coord using the ref chip projection
+
+		    psVectorAppend (L, fpOld.x);
+		    psVectorAppend (M, fpOld.y);
+
+		    psVectorAppend (P, tpNew.x);
+		    psVectorAppend (Q, tpNew.y);
+
+		    if (firstObject) {
+		      xMin = xMax = fpOld.x;
+		      yMin = yMax = fpOld.y;
+		      firstObject = false;
+		    }
+
+		    xMin = PS_MIN (xMin, fpOld.x);
+		    xMax = PS_MAX (xMax, fpOld.x);
+		    yMin = PS_MIN (yMin, fpOld.y);
+		    yMax = PS_MAX (yMax, fpOld.y);
+		}
+	    }
+	}
+    }
+
+    // the original transforms are (1, 1), but we will need higher order to fit the distortions
+    psFree (fpa->toTPA);
+
+    // the original transforms are (1, 1), but we will need higher order to fit the distortions
+    fpa->toTPA = psPlaneTransformAlloc (order, order);
+    for (int i = 0; i <= fpa->toTPA->x->nX; i++) {
+        for (int j = 0; j <= fpa->toTPA->x->nY; j++) {
+            if (i + j > order) {
+		fpa->toTPA->x->coeffMask[i][j] = PS_POLY_MASK_SET;
+		fpa->toTPA->y->coeffMask[i][j] = PS_POLY_MASK_SET;
+            }
+        }
+    }
+
+    psVectorFitPolynomial2D (fpa->toTPA->x, NULL, 0, P, NULL, L, M);
+    psVectorFitPolynomial2D (fpa->toTPA->y, NULL, 0, Q, NULL, L, M);
+    
+    psRegion fitRegion = psRegionSet (xMin, xMax, yMin, yMax);
+
+    // psPlaneTransformInvert will generate a new fromTPA with order to match toTPA
+    psFree (fpa->fromTPA);
+    psPlaneTransform *myPT = psPlaneTransformAlloc(fpa->toTPA->x->nX+4, fpa->toTPA->x->nY+4);
+    fpa->fromTPA = psPlaneTransformInvert (myPT, fpa->toTPA, fitRegion, 100);
+    psFree (myPT);
+
+    psFree (fpa->toSky);
+    fpa->toSky = toSkyTan;
+
+    psFree (L);
+    psFree (M);
+    psFree (P);
+    psFree (Q);
+
+    psFree (view);
+
+    if (!psastroMosaicSetAstrom (fpa)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to apply mosaic distortion terms\n");
+        return false;
+    }
+
+    return true;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicChipAstrom.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicChipAstrom.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicChipAstrom.c	(revision 41084)
@@ -0,0 +1,69 @@
+/** @file psastroMosaiciChipAstrom.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.15 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+# define NONLIN_TOL 0.001 ///< tolerance in pixels
+
+bool psastroMosaicChipAstrom (pmFPA *fpa, psMetadata *recipe, int iteration) {
+
+    bool status;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+        if (!chip->toFPA) { continue; }
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            // XXX there can only be one readout per chip, right?
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                // save WCS and analysis metadata in update header
+		// (pull or create local view to entry on readout->analysis)
+		psMetadata *updates = psMetadataLookupMetadata (&status, readout->analysis, "PSASTRO.HEADER");
+		if (!updates) {
+		    updates = psMetadataAlloc ();
+		    psMetadataAddMetadata (readout->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", updates);
+		    psFree (updates);
+		}
+
+                if (!psastroMosaicOneChip (chip, readout, recipe, updates, iteration)) {
+                    readout->data_exists = false;
+                    psError(PS_ERR_UNKNOWN, false, "failed to find a solution for %d,%d,%d\n", view->chip, view->cell, view->readout);
+		    psErrorStackPrint(stderr, "failure for one chip\n");
+		    psErrorClear();
+		    continue;
+                }
+
+                // create the header keywords to descripe the results
+                if (!pmAstromWriteBilevelChip (updates, chip, NONLIN_TOL)) {
+                    readout->data_exists = false;
+                    psError(PS_ERR_UNKNOWN, false, "invalid solution for %d,%d,%d\n", view->chip, view->cell, view->readout);
+		    psErrorStackPrint(stderr, "failure for one chip\n");
+		    psErrorClear();
+		    continue;
+                }
+            }
+        }
+    }
+    psFree (view);
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicCorrectDistortion.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicCorrectDistortion.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicCorrectDistortion.c	(revision 41084)
@@ -0,0 +1,95 @@
+/** @file psastroMosaicCorrectDistortion.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+bool psastroMosaicCorrectDistortion (pmFPA *fpa, psPlaneTransform *TPtoFP) {
+
+    // invert the linear TPtoFP transform
+    psPlaneTransform *FPtoTP = p_psPlaneTransformLinearInvert (TPtoFP);
+    if (FPtoTP == NULL) {
+        psError (PS_ERR_UNKNOWN, false, "failed to invert TPtoFP\n");
+        return false;
+    }
+
+    // store the new coeffs in a new structure
+    psPlaneTransform *toTPAnew = psPlaneTransformAlloc(fpa->toTPA->x->nX, fpa->toTPA->x->nY);
+
+    // set the new coeffs, or set the mask
+    for (int i = 0; i <= toTPAnew->y->nX; i++) {
+        for (int j = 0; j <= toTPAnew->y->nY; j++) {
+
+	    // init the coeffs
+	    toTPAnew->x->coeff[i][j] = 0.0;
+	    toTPAnew->y->coeff[i][j] = 0.0;
+
+	    // if both are masked, mask the outpu
+            if ((fpa->toTPA->x->coeffMask[i][j] & PS_POLY_MASK_SET) && (fpa->toTPA->x->coeffMask[i][j] & PS_POLY_MASK_SET)) {
+		toTPAnew->x->coeffMask[i][j] = PS_POLY_MASK_SET;
+		toTPAnew->y->coeffMask[i][j] = PS_POLY_MASK_SET;
+		continue;
+	    } 
+
+	    // set the X terms
+	    toTPAnew->x->coeff[i][j] += (fpa->toTPA->x->coeffMask[i][j] & PS_POLY_MASK_SET) ? 0.0 : FPtoTP->x->coeff[1][0] * fpa->toTPA->x->coeff[i][j];
+	    toTPAnew->x->coeff[i][j] += (fpa->toTPA->y->coeffMask[i][j] & PS_POLY_MASK_SET) ? 0.0 : FPtoTP->x->coeff[0][1] * fpa->toTPA->y->coeff[i][j];
+
+	    // set the Y terms
+	    toTPAnew->y->coeff[i][j] += (fpa->toTPA->x->coeffMask[i][j] & PS_POLY_MASK_SET) ? 0.0 : FPtoTP->y->coeff[1][0] * fpa->toTPA->x->coeff[i][j];
+	    toTPAnew->y->coeff[i][j] += (fpa->toTPA->y->coeffMask[i][j] & PS_POLY_MASK_SET) ? 0.0 : FPtoTP->y->coeff[0][1] * fpa->toTPA->y->coeff[i][j];
+        }
+    }
+
+    // adjust the 0,0 terms:
+    toTPAnew->x->coeff[0][0] += FPtoTP->x->coeff[0][0];
+    toTPAnew->y->coeff[0][0] += FPtoTP->y->coeff[0][0];
+
+    psFree (fpa->toTPA);
+    fpa->toTPA = toTPAnew;
+
+    // invert toTPA to determine fromTPA. choose an appropriate region based on the dimensions
+    // of the complete FPA
+    psRegion *region = pmAstromFPAExtent (fpa);
+
+    psFree (fpa->fromTPA);
+    psPlaneTransform *myPT = psPlaneTransformAlloc(fpa->toTPA->x->nX+4, fpa->toTPA->x->nY+4);
+    fpa->fromTPA = psPlaneTransformInvert(myPT, fpa->toTPA, *region, 50);
+    psFree (myPT);
+    psFree (region);
+
+    if (fpa->fromTPA == NULL) {
+        psError (PS_ERR_UNKNOWN, false, "failed to invert fpa->toTPA\n");
+	psFree (FPtoTP);
+        return false;
+    }
+
+    psFree (FPtoTP);
+    return true;
+}
+
+// we have three coordinate systems and two polynomial transformations:
+// TP is the raw tangent plane coordinate system (aligned with RA,DEC)
+// FP is the raw focal plane coordinate system (aligned with camera X,Y)
+// dFP is the distorted focal plane coordinate system
+
+// fpa->toTPA is a polynomial transformation from FP to dFP
+//   L(x,y) = \sum_i \sum_j A_{i,j} x^i y^j and 
+//   M(x,y) = \sum_i \sum_j B_{i,j} x^i y^j 
+//   where (x,y) are the FP coords and L,M  are the dFP coords
+
+// FPtoTP is a linear transformation from dFP to TP (we recover this from TPtoFP by inversion)
+// P(L,M) = r_xo + r_xx L + r_xy M
+// Q(L,M) = r_yo + r_yx L + r_yy M
+
+// we need to merge them into a single transformation:
+// P(x,y) = r_xo + r_xx * \sum_i \sum_j A_{i,j} x^i y^j + r_xy * \sum_i \sum_j B_{i,j} x^i y^j 
+// Q(x,y) = r_yo + r_yx * \sum_i \sum_j A_{i,j} x^i y^j + r_yy * \sum_i \sum_j B_{i,j} x^i y^j 
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicDistortion.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicDistortion.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicDistortion.c	(revision 41084)
@@ -0,0 +1,67 @@
+/** @file psastroMosaicDistortion.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+# define DEBUG 0
+
+bool psastroMosaicDistortion (pmFPA *fpa, psMetadata *recipe, int pass) {
+
+    // fit a linear transformation from reference TP to observed FP coords
+    psPlaneTransform *TPtoFP = psastroMosaicFitRotAndScale (fpa);
+    if (!TPtoFP) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to fit a linear TP correction\n");
+        return false;
+    }
+
+    if (DEBUG && (pass == 0)) psastroDumpCorners ("corners.up.v1.dat", "corners.dn.v1.dat", fpa);
+
+    // Correct the current reference star TP coordinates to the nearly-FP
+    // system.  We note two points here: 1) the corrected TP coordinates are
+    // NOT consistent with the sky coordinates, 2) the remaining differnce
+    // between the new TP reference coords and the observerd FP coordinates is
+    // only distortion
+    if (!psastroMosaicApplyRotAndScale (fpa, TPtoFP)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to apply the linear TP correction\n");
+	psFree (TPtoFP);
+        return false;
+    }
+
+    if (DEBUG && (pass == 0)) psastroDumpCorners ("corners.up.v2.dat", "corners.dn.v2.dat", fpa);
+
+    if (!psastroMosaicDistortionFromGradients (fpa, recipe)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to fit the distortion field\n");
+	psFree (TPtoFP);
+        return false;
+    }
+
+    if (DEBUG && (pass == 0)) psastroDumpCorners ("corners.up.v3.dat", "corners.dn.v3.dat", fpa);
+
+    if (!psastroMosaicCorrectDistortion (fpa, TPtoFP)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to correct the distortion for the linear fit\n");
+	psFree (TPtoFP);
+        return false;
+    }
+	
+    if (DEBUG && (pass == 0)) psastroDumpCorners ("corners.up.v4.dat", "corners.dn.v4.dat", fpa);
+
+    if (!psastroMosaicSetAstrom (fpa)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to apply mosaic distortion terms\n");
+	psFree (TPtoFP);
+        return false;
+    }
+
+    if (DEBUG && (pass == 0)) psastroDumpCorners ("corners.up.v5.dat", "corners.dn.v5.dat", fpa);
+
+    psFree (TPtoFP);
+    return true;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicFPtoTP.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicFPtoTP.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicFPtoTP.c	(revision 41084)
@@ -0,0 +1,176 @@
+/** @file psastroMosaicFPtoTP.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+psPlaneTransform *psastroMosaicFitRotAndScale (pmFPA *fpa) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    // XXX first pass: fit and remove any linear fp->tp transformation
+    // accumulate FP(x,y) & TP(x,y) in a single set of vectors
+
+    psVector *X  = psVectorAllocEmpty(100, PS_TYPE_F32);
+    psVector *Y  = psVectorAllocEmpty(100, PS_TYPE_F32);
+    psVector *x  = psVectorAllocEmpty(100, PS_TYPE_F32);
+    psVector *y  = psVectorAllocEmpty(100, PS_TYPE_F32);
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // int nPts = 0;
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!chip->toFPA) { continue; }
+	
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS.SUBSET");
+		if (rawstars == NULL) { continue; }
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS.SUBSET");
+		if (refstars == NULL) { continue; }
+
+		psArray *match = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+		if (match == NULL) { continue; }
+
+		// take the matched stars, first fit
+		for (int i = 0; i < match->n; i++) {
+		    pmAstromMatch *pair = match->data[i];
+		    pmAstromObj *rawStar = rawstars->data[pair->raw];
+		    pmAstromObj *refStar = refstars->data[pair->ref];
+
+		    // independent variables
+		    X->data.F32[X->n] = refStar->TP->x;
+		    Y->data.F32[Y->n] = refStar->TP->y;
+
+		    // fitted values
+		    x->data.F32[x->n] = rawStar->FP->x;
+		    y->data.F32[y->n] = rawStar->FP->y;
+
+		    psVectorExtend (X, 100, 1);
+		    psVectorExtend (Y, 100, 1);
+		    psVectorExtend (x, 100, 1);
+		    psVectorExtend (y, 100, 1);
+		}
+	    }
+	}
+    }
+    // x->n = y->n = X->n = Y->n = nPts;
+
+    // linear fit without xy cross term
+    psPlaneTransform *map = psPlaneTransformAlloc (1, 1);
+    map->x->coeffMask[1][1] = PS_POLY_MASK_SET;
+    map->y->coeffMask[1][1] = PS_POLY_MASK_SET;
+
+    // constant errors
+    psVector *mask = psVectorAlloc (X->n, PS_TYPE_VECTOR_MASK);
+    psVectorInit (mask, 0);
+
+    // the stats options supplied are used to perform the clip fitting
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    stats->clipIter = 1;
+
+    // fit TP-to-FP transformation
+
+    // we run 3 cycles clipping in each of x and y, with only one iteration each.
+    // XXX use the stats lookups functions to get the width and center
+    for (int i = 0; i < 3; i++) {
+	if (!psVectorClipFitPolynomial2D (map->x, stats, mask, 0xff, x, NULL, X, Y)) {
+            psError(PS_ERR_UNKNOWN, false, "failure in clip-fitting for x\n");
+	    psFree (map);
+	    map = NULL;
+	    goto escape;
+	}
+        psTrace ("psastro", 3, "x resid: %f +/- %f (%ld of %ld)\n", stats->clippedMean, stats->clippedStdev, stats->clippedNvalues, x->n);
+
+        if (!psVectorClipFitPolynomial2D (map->y, stats, mask, 0xff, y, NULL, X, Y)) {
+            psError(PS_ERR_UNKNOWN, false, "failure in clip-fitting for y\n");
+	    psFree (map);
+	    map = NULL;
+	    goto escape;
+	}
+        psTrace ("psastro", 3, "y resid: %f +/- %f (%ld of %ld)\n", stats->clippedMean, stats->clippedStdev, stats->clippedNvalues, y->n);
+    }
+
+escape:
+    psFree (x);
+    psFree (y);
+    psFree (X);
+    psFree (Y);
+    psFree (mask);
+    psFree (view);
+    psFree (stats);
+
+    return (map);
+}
+
+// apply the rotation and scale to all stars in PSASTRO.REFSTARS (also adjusts
+// PSASTRO.REFSTARS.SUBSET since they are the same pointers)
+bool psastroMosaicApplyRotAndScale (pmFPA *fpa, psPlaneTransform *TPtoFP) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    psPlane newTP;
+
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!chip->toFPA) { continue; }
+	
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) { continue; }
+
+		// take the matched stars, first fit
+		for (int i = 0; i < refstars->n; i++) {
+		    pmAstromObj *refStar = refstars->data[i];
+
+		    // Correct the current reference star TP coordinates to the nearly-FP
+		    // system.  We note two points here: 1) the corrected TP coordinates are
+		    // NOT consistent with the sky coordinates, 2) the remaining differnce
+		    // between the new TP reference coords and the observerd FP coordinates is
+		    // only distortion
+
+		    psPlaneTransformApply (&newTP, TPtoFP, refStar->TP);
+		    refStar->TP->x = newTP.x;
+		    refStar->TP->y = newTP.y;
+		}
+	    }
+	}
+    }
+    psFree (view);
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicGetGrads.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicGetGrads.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicGetGrads.c	(revision 41084)
@@ -0,0 +1,48 @@
+# include "psastroInternal.h"
+
+psArray *psastroMosaicGetGrads (pmFPA *fpa, psMetadata *recipe) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    psArray *grads = NULL;
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	
+	psRegion *region = pmChipExtent (chip);
+
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS.SUBSET");
+		if (rawstars == NULL) { continue; }
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS.SUBSET");
+		if (refstars == NULL) { continue; }
+
+		psArray *match = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+		if (match == NULL) { continue; }
+
+		// measure the local gradients for this set of stars
+		// the new elements are added to the incoming gradient structure 
+		grads = pmAstromMeasureGradients (grads, rawstars, refstars, match, region, 2, 2);
+	    }
+	}
+	psFree (region);
+    }
+    psFree (view);
+    return (grads);
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicGradients.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicGradients.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicGradients.c	(revision 41084)
@@ -0,0 +1,118 @@
+/** @file psastroMosaicGradients.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.11 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+static int nPass = 0;
+
+bool psastroMosaicDistortionFromGradients (pmFPA *fpa, psMetadata *recipe) {
+
+    bool status;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    psArray *gradients = NULL;
+
+    // Measure the gradient as a function of position.  This must be performed between the
+    // corrected ref->TP and the observed raw->FP, for which the distortion is a perturbation.
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    int nXcell = psMetadataLookupS32 (&status, recipe, "PSASTRO.MOSAIC.GRADIENT.NX");
+    int nYcell = psMetadataLookupS32 (&status, recipe, "PSASTRO.MOSAIC.GRADIENT.NY");
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!chip->toFPA) { continue; }
+	
+	psRegion *region = pmChipExtent (chip);
+
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS.SUBSET");
+		if (rawstars == NULL) { continue; }
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS.SUBSET");
+		if (refstars == NULL) { continue; }
+
+		psArray *match = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+		if (match == NULL) { continue; }
+
+		// measure the local gradients for this set of stars
+		// XXX 2,2 are the number of test boxes on the chip.  put this in the recipe
+		gradients = pmAstromMeasureGradients (gradients, rawstars, refstars, match, region, nXcell, nYcell);
+	    }
+	}
+	psFree (region);
+    }
+
+    // if desired, dump the gradients to a file
+    if (psTraceGetLevel("psastro.dump") > 0) { 
+	char name[80];
+	sprintf (name, "gradients.%d.dat", nPass); 
+	psastroDumpGradients (gradients, name); 
+	nPass ++;
+    }
+
+    // Fit the gradient field and convert to the distortion terms.
+
+    // allocate mosaic-level polynomial transformation and set masks needed by DVO
+    int order = psMetadataLookupF32 (&status, recipe, "PSASTRO.MOSAIC.ORDER");
+    if (!status) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to find mosaic distortion fit order\n");
+	psFree (gradients);
+	psFree (view);
+        return false;
+    }
+    psFree (fpa->toTPA);
+    fpa->toTPA = psPlaneTransformAlloc (order, order);
+    for (int i = 0; i <= fpa->toTPA->x->nX; i++) {
+        for (int j = 0; j <= fpa->toTPA->x->nY; j++) {
+            if (i + j > order) {
+		fpa->toTPA->x->coeffMask[i][j] = PS_POLY_MASK_SET;
+		fpa->toTPA->y->coeffMask[i][j] = PS_POLY_MASK_SET;
+            }
+        }
+    }
+
+    // physical pixel scale in microns per pixel (FP is in physical units, chip is in pixels)
+    double pixelScale = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.SCALE");
+    if (!status) {
+	psError(PS_ERR_IO, false, "Failed to lookup pixel scale"); 
+	psFree (gradients);
+	psFree (view);
+	return false; 
+    } 
+
+    // fit the measured gradients with the telescope distortion model (polynomial order based on toTPA)
+    if (!pmAstromFitDistortion (fpa, gradients, pixelScale)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to fit the distortion terms\n");
+	psFree (gradients);
+	psFree (view);
+        return false;
+    }
+	
+    psFree (gradients);
+    psFree (view);
+    return true;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicHeaders.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicHeaders.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicHeaders.c	(revision 41084)
@@ -0,0 +1,59 @@
+# include "psastroInternal.h"
+
+bool psastroMosaicHeaders (pmConfig *config) {
+
+    bool status = false;
+    pmChip *chip = NULL;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+ 	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find input data!\n");
+	return false;
+    }
+
+    char *mosastro = psMetadataLookupStr (NULL, config->arguments, "MOSASTRO");
+
+    double plateScale = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLATE.SCALE");
+    if (!status) plateScale = 1.0;
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmFPA *fpa = input->fpa;
+
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+
+        // read WCS data from the corresponding header
+        pmHDU *hdu = pmFPAviewThisHDU (view, fpa);
+
+        pmAstromWriteBilevelChip (chip->toFPA, hdu->header, plateScale);
+    }
+
+    psMetadata *mosaic = pmAstromWriteBilevelMosaic (fpa->toSky, fpa->toTPA, plateScale);
+
+    // XXX what is the EXTNAME??
+    psFits *fits = psFitsOpen (mosastro, "w");
+    psFitsWriteBlank(fits, mosaic, "");
+    psFitsClose (fits);
+
+    psFree (view);
+    return true;
+}
+
+/* coordinate frame hierachy
+   pixels (on a given readout)
+   cell
+   chip
+   FP (focal plane)
+   TP (tangent plane)
+   sky (ra, dec)
+*/
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicOneChip.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicOneChip.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicOneChip.c	(revision 41084)
@@ -0,0 +1,184 @@
+/** @file psastroMosaicOneChip.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.10 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-09 21:25:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+# define REQUIRED_RECIPE_VALUE(VALUE, NAME, TYPE, MESSAGE)\
+  VALUE = psMetadataLookup##TYPE (&status, recipe, NAME); \
+  if (!status) { \
+   psError(PSASTRO_ERR_CONFIG, false, MESSAGE); \
+   return false; }
+
+bool psastroMosaicOneChip (pmChip *chip, pmReadout *readout, psMetadata *recipe, psMetadata *updates, int iteration) {
+
+    bool status;
+    char errorWord[64];
+    char orderWord[64];
+
+    PS_ASSERT_PTR_NON_NULL(chip,    false);
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(recipe,  false);
+    PS_ASSERT_PTR_NON_NULL(updates, false);
+
+    // select the raw objects for this readout
+    psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS.SUBSET");
+    if (rawstars == NULL) return false;
+
+    psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS.SUBSET");
+    if (refstars == NULL) return false;
+
+    psArray *match = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+    if (match == NULL) return false;
+
+    // correct radius to FP units (physical pixel scale in microns per pixel)
+    REQUIRED_RECIPE_VALUE (double pixelScale, "PSASTRO.PIXEL.SCALE", F32, "Failed to lookup pixel scale");
+
+    // allowed limits for valid solutions
+    snprintf (errorWord, 64, "PSASTRO.MOSAIC.MAX.ERROR.N%d", iteration);
+    REQUIRED_RECIPE_VALUE (float maxError, errorWord, F32, "failed to find single-chip max allowed error\n");
+    REQUIRED_RECIPE_VALUE (int minNstar, "PSASTRO.MOSAIC.MIN.NSTAR", S32, "failed to find single-chip min allowed stars\n");
+
+    // set the order of the per-chip fit (higher order only if iteration > 0)
+    REQUIRED_RECIPE_VALUE (int defaultOrder, "PSASTRO.MOSAIC.CHIP.ORDER", S32, "failed to find mosaic chip-level fit default order\n");
+
+    snprintf (orderWord, 64, "PSASTRO.MOSAIC.CHIP.ORDER.N%d", iteration);
+    int order = psMetadataLookupS32 (&status, recipe, orderWord);
+    if (!status || (order == -1)) {
+        order = defaultOrder;
+    }
+
+    // modify the order to correspond to the actual number of matched stars:
+    int Ndof_min = 3;
+    int order_max = 0.5*(sqrt(4*match->n - 4*Ndof_min + 1) - 3);
+    order = PS_MIN (order, order_max);
+
+    // if ((match->n < 17) && (order >= 3)) order = 2;
+    // if ((match->n < 13) && (order >= 2)) order = 1;
+    // if ((match->n <  9) && (order >= 1)) order = 0;
+
+    if (order < 0) {
+        psLogMsg ("psastro", 3, "insufficient stars (%ld) or invalid order (%d)", match->n, order);
+        return false;
+    }
+
+    psLogMsg ("psastro", PS_LOG_DETAIL, "mosaic fit chip order %d", order);
+
+    // create output toFPA; set masks appropriate to the Elixir DVO astrometry format if we are
+    // fitting 0th order, use the current polynomial of whatever order, with higher order
+    // coefficients frozen to the current values
+    if (order == 0) {
+        // set FIT mask for all higher order terms of the existing solution
+        // any existing SET masks will be retained.
+        for (int i = 0; i <= chip->toFPA->x->nX; i++) {
+            for (int j = 0; j <= chip->toFPA->x->nY; j++) {
+                if (i + j > 0) {
+                    chip->toFPA->x->coeffMask[i][j] |= PS_POLY_MASK_FIT;
+                    chip->toFPA->y->coeffMask[i][j] |= PS_POLY_MASK_FIT;
+                }
+            }
+        }
+    } else {
+        psFree (chip->toFPA);
+        chip->toFPA = psPlaneTransformAlloc (order, order);
+        for (int i = 0; i <= chip->toFPA->x->nX; i++) {
+            for (int j = 0; j <= chip->toFPA->x->nY; j++) {
+                if (i + j > order) {
+                    chip->toFPA->x->coeffMask[i][j] = PS_POLY_MASK_SET;
+                    chip->toFPA->y->coeffMask[i][j] = PS_POLY_MASK_SET;
+                }
+            }
+        }
+    }
+
+    // XXX allow statistic to be set by the user
+    // only clip if we are fitting the chip parameters.
+    psStats *fitStats = NULL;
+    if (FALSE && (order == 0)) {
+      fitStats = psStatsAlloc (PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV);
+      fitStats->clipSigma = psMetadataLookupF32 (&status, recipe, "PSASTRO.MOSAIC.CHIP.NSIGMA");
+      fitStats->clipIter = psMetadataLookupS32 (&status, recipe, "PSASTRO.MOSAIC.CHIP.NITER");
+    } else {
+      fitStats = psStatsAlloc (PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV);
+      fitStats->clipSigma = psMetadataLookupF32 (&status, recipe, "PSASTRO.MOSAIC.CHIP.NSIGMA");
+      fitStats->clipIter = psMetadataLookupS32 (&status, recipe, "PSASTRO.MOSAIC.CHIP.NITER");
+    }
+
+    // need to pass in an update header, sent in from above
+    pmAstromFitResults *results = pmAstromMatchFit (chip->toFPA, rawstars, refstars, match, fitStats, recipe);
+    if (!results) {
+        psError(PSASTRO_ERR_DATA, false, "failed to perform the matched fit\n");
+        return false;
+    }
+
+    // toSky converts from FPA & TPA units (microns) to sky units (radians)
+    pmFPA *fpa = chip->parent;
+    float plateScale = 0.5*(fpa->toSky->Xs + fpa->toSky->Ys)*3600.0*PM_DEG_RAD;
+
+    float rawXstdev = psStatsGetValue (results->xStats, psStatsStdevOption(results->xStats->options));
+    float rawYstdev = psStatsGetValue (results->yStats, psStatsStdevOption(results->yStats->options));
+
+    // pixError is the average 1D scatter in pixels ('results' are in FPA units = microns)
+    float pixError = 0.5*(rawXstdev + rawYstdev) / pixelScale;
+
+    // astError is the average 1D scatter in arcsec ('results' are in FPA units = microns)
+    float astError = 0.5*(rawXstdev + rawYstdev) * plateScale;
+    int astNstar = results->yStats->clippedNvalues;
+
+    // if we clip away too many stars, the order may be invalid
+    if (order == 3) { minNstar = PS_MAX (15, minNstar); }
+    if (order == 2) { minNstar = PS_MAX (11, minNstar); }
+    if (order == 1) { minNstar = PS_MAX ( 8, minNstar); }
+
+    bool validSolution = true;
+
+    // XXX should these result in errors or be handled another way?
+    psLogMsg ("psastro", PS_LOG_INFO, "astrometry solution: error: %f arcsec, Nstars: %d", astError, astNstar);
+    if ((maxError > 0) && (astError > maxError)) {
+        psLogMsg("psastro", PS_LOG_INFO, "residual error is too large, failed to find a solution: %f > %f", astError, maxError);
+        validSolution = false;
+    }
+    if (astNstar < minNstar) {
+        psLogMsg("psastro", PS_LOG_INFO, "solution uses too few stars: %d < %d", astNstar, minNstar);
+        validSolution = false;
+    }
+
+    // DVO expects NASTRO = 0 if we fail to find a solution
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "PERROR",   PS_META_REPLACE, "astrometry error (pixels)", pixError);
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "CERROR",   PS_META_REPLACE, "astrometry error (arcsec)", astError);
+    if (validSolution) {
+        psMetadataAddF32 (updates, PS_LIST_TAIL, "CPRECISE", PS_META_REPLACE, "astrometry precision (arcsec)", astError/sqrt(astNstar));
+        psMetadataAddS32 (updates, PS_LIST_TAIL, "NASTRO",   PS_META_REPLACE, "number of astrometry stars", astNstar);
+    } else {
+        psMetadataAddF32 (updates, PS_LIST_TAIL, "CPRECISE", PS_META_REPLACE, "astrometry precision (arcsec)", 0.0);
+        psMetadataAddS32 (updates, PS_LIST_TAIL, "NASTRO",   PS_META_REPLACE, "number of astrometry stars", 0);
+    }
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "EQUINOX",  PS_META_REPLACE, "", 2000.0); // XXX this is bogus: should be defined based on equinox of refstars
+
+    // additional error measurements:
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "AST_MDX",   PS_META_REPLACE, "mosaic astrometry X stdev (arcsec)", rawXstdev * plateScale);
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "AST_MSX",   PS_META_REPLACE, "mosaic astrometry X systematic err (arcsec)", results->dXsys * plateScale);
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "AST_MRX",   PS_META_REPLACE, "mosaic astrometry X 10-90 percentile (arcsec)", results->dXrange * plateScale);
+
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "AST_MDY",   PS_META_REPLACE, "mosaic astrometry Y stdev (arcsec)", rawYstdev * plateScale);
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "AST_MSY",   PS_META_REPLACE, "mosaic astrometry Y systematic err (arcsec)", results->dYsys * plateScale);
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "AST_MRY",   PS_META_REPLACE, "mosaic astrometry Y 10-90 percentile (arcsec)", results->dYrange * plateScale);
+
+    // determine fromFPA transformation and apply new transformation to raw & ref stars
+    psastroUpdateChipToFPA (fpa, chip);
+
+    //plot results
+    pmAstromVisualPlotMosaicOneChip(rawstars, refstars, match, recipe);
+
+    psFree (fitStats);
+    psFree (results);
+    return validSolution;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicSetAstrom.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicSetAstrom.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicSetAstrom.c	(revision 41084)
@@ -0,0 +1,64 @@
+/** @file psastroMosaicSetAstrom.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.12 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+bool psastroMosaicSetAstrom (pmFPA *fpa) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!chip->toFPA) { continue; }
+	
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+		if (rawstars == NULL) { continue; }
+
+		for (int i = 0; i < rawstars->n; i++) {
+		    pmAstromObj *raw = rawstars->data[i];
+	
+		    psPlaneTransformApply (raw->FP, chip->toFPA, raw->chip);
+		    psPlaneTransformApply (raw->TP, fpa->toTPA, raw->FP);
+		    psDeproject (raw->sky, raw->TP, fpa->toSky);
+		}
+
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) { continue; }
+
+		for (int i = 0; i < refstars->n; i++) {
+		    pmAstromObj *ref = refstars->data[i];
+	
+		    psProject (ref->TP, ref->sky, fpa->toSky);
+		    psPlaneTransformApply (ref->FP, fpa->fromTPA, ref->TP);
+		    psPlaneTransformApply (ref->chip, chip->fromFPA, ref->FP);
+		}
+	    }
+	}
+    }
+    psFree (view);
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicSetMatch.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicSetMatch.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroMosaicSetMatch.c	(revision 41084)
@@ -0,0 +1,94 @@
+/** @file psastroMosaicSetMatch.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.15 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-09 21:25:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+bool psastroMosaicSetMatch (pmFPA *fpa, psMetadata *recipe, int iteration) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    pmFPAview *view = pmFPAviewAlloc (0);
+    char radiusWord[64];
+
+    // use small radius to match stars (assume starting astrometry is good)
+    bool status = false;
+    sprintf (radiusWord, "PSASTRO.MOSAIC.RADIUS.N%d", iteration);
+    double RADIUS = psMetadataLookupF32 (&status, recipe, radiusWord);
+    if (!status) {
+        psAbort("Failed to lookup matching radius: %s", radiusWord);
+    }
+
+    int uniqIter = psMetadataLookupS32 (&status, recipe, "PSASTRO.MOSAIC.UNIQ.ITER");
+    if (!status) {
+        psAbort("Failed to lookup matching PSASTRO.MOSAIC.UNIQ.ITER");
+    }
+
+    if (RADIUS <= 0.0) {
+        if (iteration == 0) {
+            psError(PS_ERR_IO, false, "Invalid match radius for first iteration: %s", radiusWord);
+            psFree (view);
+            return false;
+        }
+        psWarning ("skipping match for iteration %d\n", iteration);
+        psFree (view);
+        return true;
+    }
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+        if (!chip->fromFPA) { continue; }
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            // XXX there can only be one readout per chip, right?
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                // select the raw objects for this readout
+                psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS.SUBSET");
+                if (rawstars == NULL) { continue; }
+
+                // select the raw objects for this readout
+                psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS.SUBSET");
+                if (refstars == NULL) { continue; }
+                psTrace ("psastro", 4, "Trying %ld refstars\n", refstars->n);
+
+                psArray *matches = pmAstromRadiusMatchChip (rawstars, refstars, RADIUS);
+                psTrace ("psastro", 4, "Matched %ld refstars\n", matches->n);
+
+		if (iteration >= uniqIter) {
+		    psArray *unique = pmAstromRadiusMatchUniq (rawstars, refstars, matches);
+		    if (!unique) {
+			psLogMsg ("psastro", 3, "failed to generate a uniq set of matched sources\n");
+			return false;
+		    }
+		    psFree (matches);
+		    matches = unique;
+		}
+
+                pmAstromVisualPlotMosaicMatches(rawstars, refstars, matches, iteration, recipe);
+
+                // XXX drop the old one
+                psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.MATCH", PS_DATA_ARRAY | PS_META_REPLACE, "astrometry matches", matches);
+                psFree (matches);
+            }
+        }
+    }
+    psFree (view);
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroOneChipFit.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroOneChipFit.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroOneChipFit.c	(revision 41084)
@@ -0,0 +1,239 @@
+/** @file psastroOneChipFit.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-09 21:25:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+# define REQUIRED_RECIPE_VALUE(VALUE, NAME, TYPE)\
+  VALUE = psMetadataLookup##TYPE (&status, recipe, NAME); \
+  if (!status) { \
+   psAbort ("Failed to find %s in recipe", NAME); }
+
+bool psastroOneChipFit (pmFPA *fpa, pmChip *chip, pmReadout *readout, psArray *refstars, psArray *rawstars, psMetadata *recipe, psMetadata *updates) {
+
+    bool status;
+
+    // default value for match/fit : radius is in pixels
+    REQUIRED_RECIPE_VALUE (double RADIUS, "PSASTRO.MATCH.RADIUS", F32);
+
+    // run the match/fit sequence NITER times
+    REQUIRED_RECIPE_VALUE (int nIter, "PSASTRO.MATCH.FIT.NITER", S32);
+
+    // for iterations >= uniqIter, require a single match per reference
+    REQUIRED_RECIPE_VALUE (int uniqIter, "PSASTRO.MATCH.UNIQ.ITER", S32);
+
+    // correct radius to FP units (physical pixel scale in microns per pixel)
+    REQUIRED_RECIPE_VALUE (double pixelScale, "PSASTRO.PIXEL.SCALE", F32);
+    RADIUS *= pixelScale;
+
+    // select the desired chip order
+    REQUIRED_RECIPE_VALUE (int order, "PSASTRO.CHIP.ORDER", S32);
+
+    // allowed limits for valid solutions
+    REQUIRED_RECIPE_VALUE (float maxError, "PSASTRO.MAX.ERROR", F32);
+    REQUIRED_RECIPE_VALUE (int minNstar, "PSASTRO.MIN.NSTAR", S32);
+
+    psArray *match = NULL;
+    psStats *fitStats = NULL;
+    pmAstromFitResults *results = NULL;
+
+    for (int iter = 0; iter < nIter; iter++) {
+
+        char name[128];
+
+        sprintf (name, "PSASTRO.MATCH.RADIUS.N%d", iter);
+        float radius = psMetadataLookupF32 (&status, recipe, name);
+        radius *= pixelScale;
+        if (!status || (radius == 0.0)) {
+            radius = RADIUS;
+        }
+
+
+        // use small radius to match stars
+        match = pmAstromRadiusMatchFP (rawstars, refstars, radius);
+        if (match == NULL) {
+            psLogMsg ("psastro", 3, "failed to find radius-matched sources\n");
+	    psastroChipFailureHeader (updates);
+            return false;
+        }
+
+	if (iter >= uniqIter) {
+	    psArray *unique = pmAstromRadiusMatchUniq (rawstars, refstars, match);
+	    if (!unique) {
+		psLogMsg ("psastro", 3, "failed to generate a uniq set of matched sources\n");
+		psastroChipFailureHeader (updates);
+		return false;
+	    }
+	    psFree (match);
+	    match = unique;
+	}
+
+        // modify the order to correspond to the actual number of matched stars:
+        int Ndof_min = 3;
+        int order_max = 0.5*(sqrt(4*match->n - 4*Ndof_min + 1) - 3);
+        order = PS_MIN (order, order_max);
+
+	// order 0 : Ro -> nterms = 1 * 2;
+	// order 1 : Ro, Rx, Ry -> nterms = 3 * 2;
+	// order 2 : Ro, Rx, Ry, Rxx, Rxy, Ryy -> nterms = 6 * 2;
+	// order 3 : Ro, Rx, Ry, Rxx, Rxy, Ryy, Rxxx, Rxxy, Rxyy, Ryyy -> nterms = 10 * 2
+	// 2*(N+1)*(N+2)/2 = (N+1)*(N+2) = nterms;
+	// (order+1)(order+2) + ndof = nvalues
+	// order^2 + 3*order + 2 + ndof = nvalue;
+	// order^2 + 3*order + 2 + ndof - nvalue = 0;
+	// 2*order = -3 +/- sqrt (9 - 4*(2 - nvalue + ndof));
+	// 2*order = -3 +/- sqrt (9 - 8 + 4*nvalue - 4*ndof);
+	// 2*order = (sqrt (1 + 4*nvalue - 4*ndof) - 3);
+
+        // if ((match->n < 11) && (order >= 3)) order = 2;
+        // if ((match->n <  7) && (order >= 2)) order = 1;
+        // if ((match->n <  4) && (order >= 1)) order = 0;
+
+        if (order < 1) {
+            psLogMsg ("psastro", 3, "insufficient stars or invalid order: %ld stars", match->n);
+            psFree (match);
+	    psastroChipFailureHeader (updates);
+            return false;
+        }
+
+        // create output toFPA; set masks appropriate to the Elixir DVO astrometry format
+        psFree (chip->toFPA);
+        chip->toFPA = psPlaneTransformAlloc (order, order);
+        for (int i = 0; i <= chip->toFPA->x->nX; i++) {
+            for (int j = 0; j <= chip->toFPA->x->nY; j++) {
+                if (i + j > order) {
+                    chip->toFPA->x->coeffMask[i][j] = PS_POLY_MASK_SET;
+                    chip->toFPA->y->coeffMask[i][j] = PS_POLY_MASK_SET;
+                }
+            }
+        }
+
+        // XXX allow statistic to be set by the user
+        // fitStats = psStatsAlloc (PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV);
+        fitStats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+        fitStats->clipSigma = psMetadataLookupF32 (&status, recipe, "PSASTRO.CHIP.NSIGMA");
+        fitStats->clipIter = psMetadataLookupS32 (&status, recipe, "PSASTRO.CHIP.NITER");
+
+        // improved fit for astrometric terms
+        results = pmAstromMatchFit (chip->toFPA, rawstars, refstars, match, fitStats, recipe);
+        if (!results) {
+            psLogMsg ("psastro", 3, "failed to perform the matched fit\n");
+            psFree (match);
+            psFree (fitStats);
+	    psastroChipFailureHeader (updates);
+            return false;
+        }
+
+        // determine fromFPA transformation and apply new transformation to raw & ref stars
+	// this applies the transformation to all stars (including subset)
+        psastroUpdateChipToFPA (fpa, chip); // updates PSASTRO.RAWSTARS and PSASTRO.REFSTARS
+
+        // toSky converts from FPA & TPA units (microns) to sky units (radians)
+        float plateScale = 0.5*(fpa->toSky->Xs + fpa->toSky->Ys)*3600.0*PM_DEG_RAD;
+
+	float rawXstdev = psStatsGetValue (results->xStats, psStatsStdevOption(results->xStats->options));
+	float rawYstdev = psStatsGetValue (results->yStats, psStatsStdevOption(results->yStats->options));
+
+        float astError = 0.5*(rawXstdev + rawYstdev) * plateScale;
+        int astNstar = results->yStats->clippedNvalues;
+        psLogMsg ("psastro", PS_LOG_INFO, "pass %d, error: %f arcsec, Nstars: %d", iter, astError, astNstar);
+
+        if (iter < nIter - 1) {
+            psFree (fitStats);
+            psFree (results);
+            psFree (match);
+        }
+    }
+
+    // toSky converts from FPA & TPA units (microns) to sky units (radians)
+    float plateScale = 0.5*(fpa->toSky->Xs + fpa->toSky->Ys)*3600.0*PM_DEG_RAD;
+
+    float rawXstdev = psStatsGetValue (results->xStats, psStatsStdevOption(results->xStats->options));
+    float rawYstdev = psStatsGetValue (results->yStats, psStatsStdevOption(results->yStats->options));
+
+    // pixError is the average 1D scatter in pixels ('results' are in FPA units = microns)
+    float pixError = 0.5*(rawXstdev + rawYstdev) / pixelScale;
+
+    // astError is the average 1D scatter in arcsec ('results' are in FPA units = microns)
+    float astError = 0.5*(rawXstdev + rawYstdev) * plateScale;
+
+    // x and y are forced to use the same subset of values:
+    int astNstar = results->yStats->clippedNvalues;
+
+    bool validSolution = true;
+
+    // XXX should these result in errors or be handled another way?
+    psLogMsg ("psastro", PS_LOG_INFO, "astrometry solution: error: %f arcsec, Nstars: %d", astError, astNstar);
+    if (astError > maxError) {
+        psLogMsg("psastro", PS_LOG_INFO, "residual error is too large, failed to find a solution: %f > %f", astError, maxError);
+        validSolution = false;
+    }
+    if (astNstar < minNstar) {
+        psLogMsg("psastro", PS_LOG_INFO, "solution uses too few stars: %d < %d", astNstar, minNstar);
+        validSolution = false;
+    }
+
+    // DVO expects NASTRO = 0 if we fail to find a solution
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "PERROR",   PS_META_REPLACE, "astrometry error (pixels)", pixError);
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "CERROR",   PS_META_REPLACE, "astrometry error (arcsec)", astError);
+    if (validSolution) {
+        psMetadataAddF32 (updates, PS_LIST_TAIL, "CPRECISE", PS_META_REPLACE, "astrometry precision (arcsec)", astError/sqrt(astNstar));
+        psMetadataAddS32 (updates, PS_LIST_TAIL, "NASTRO",   PS_META_REPLACE, "number of astrometry stars", astNstar);
+    } else {
+	psastroChipFailureHeader (updates);
+    }
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "EQUINOX",  PS_META_REPLACE, "equinox of ref catalog", 2000.0); // XXX this is bogus: should be defined based on equinox of refstars
+
+    // additional error measurements:
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "AST_CDX",   PS_META_REPLACE, "chip astrometry X stdev (arcsec)", rawXstdev * plateScale);
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "AST_CSX",   PS_META_REPLACE, "chip astrometry X systematic err (arcsec)", results->dXsys * plateScale);
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "AST_CRX",   PS_META_REPLACE, "chip astrometry X 10-90 percentile (arcsec)", results->dXrange * plateScale);
+
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "AST_CDY",   PS_META_REPLACE, "chip astrometry Y stdev (arcsec)", rawYstdev * plateScale);
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "AST_CSY",   PS_META_REPLACE, "chip astrometry Y systematic err (arcsec)", results->dYsys * plateScale);
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "AST_CRY",   PS_META_REPLACE, "chip astrometry Y 10-90 percentile (arcsec)", results->dYrange * plateScale);
+
+    // XXX check if we correctly applied the new transformation:
+    if (psTraceGetLevel("psastro.dump") > 0) {
+        psastroDumpRawstars (rawstars, fpa, chip);
+        psastroDumpMatchedStars ("match.dat", rawstars, refstars, match);
+        psastroDumpStars (refstars, "refstars.cal.dat");
+    }
+
+    pmAstromVisualPlotOneChipFit (rawstars, refstars, match, recipe);
+
+    if (psTraceGetLevel("psastro.plot") > 0) {
+        psastroPlotOneChipFit (rawstars, refstars, match, recipe);
+    }
+
+    // save the match table for zero points and other tests
+    psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.MATCH", PS_DATA_ARRAY | PS_META_REPLACE, "astrometry matches", match);
+
+    psFree (match);
+    psFree (results);
+    psFree (fitStats);
+
+    return validSolution;
+}
+
+bool psastroChipFailureHeader (psMetadata *updates) {
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "CPRECISE", PS_META_REPLACE, "astrometry precision (arcsec)", 0.0);
+    psMetadataAddS32 (updates, PS_LIST_TAIL, "NASTRO",   PS_META_REPLACE, "number of astrometry stars", 0);
+    psMetadataAddU64 (updates, PS_LIST_TAIL, "ASTROM_CHIPS", PS_META_REPLACE, "chips that passed astrometry", 0);
+    return true;
+}
+
+
+
+// psastroWriteStars ("raw.1.dat", rawstars);
+// psastroWriteStars ("ref.1.dat", refstars);
+// psastroWriteTransform (chip->toFPA);
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroOneChipGrid.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroOneChipGrid.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroOneChipGrid.c	(revision 41084)
@@ -0,0 +1,91 @@
+/** @file psastroOneChipGrid.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+# define REQUIRED_RECIPE_VALUE(VALUE, NAME, TYPE)\
+  VALUE = psMetadataLookup##TYPE (&status, recipe, NAME); \
+  if (!status) { \
+   psAbort ("Failed to find %s in recipe", NAME); }
+
+bool psastroOneChipGrid (pmFPA *fpa, pmChip *chip, psArray *refstars, psArray *rawstars, psMetadata *recipe, psMetadata *updates) {
+
+    bool status;
+    pmAstromStats *stats = NULL;
+
+    // do we need to get a rough initial match?
+    REQUIRED_RECIPE_VALUE (bool gridSearch, "PSASTRO.GRID.SEARCH", Bool);
+    if (!gridSearch) return true;
+
+    // do we need to get a rough initial match?
+    int nMaxRaw = psMetadataLookupS32(&status, recipe, "PSASTRO.GRID.NRAW.MAX");
+    if (!status) {
+	nMaxRaw = psMetadataLookupS32(&status, recipe, "PSASTRO.GRID.NSTAR.MAX");
+    }
+    int nMaxRef = psMetadataLookupS32(&status, recipe, "PSASTRO.GRID.NREF.MAX");
+    if (!status) {
+	nMaxRef = refstars->n;
+    }
+
+    // generate the bright subset of nMaxRaw entries of rawstars (already sorted by S/N)
+    psArray *rawGridStars = psArrayAlloc (PS_MIN (nMaxRaw, rawstars->n));
+    for (int i = 0; (i < nMaxRaw) && (i < rawstars->n); i++) {
+	rawGridStars->data[i] = psMemIncrRefCounter (rawstars->data[i]);
+    }
+
+    // generate the bright subset of nMaxRef entries of refstars (already sorted by S/N)
+    psArray *refGridStars = psArrayAlloc (PS_MIN (nMaxRef, refstars->n));
+    for (int i = 0; (i < nMaxRef) && (i < refstars->n); i++) {
+	refGridStars->data[i] = psMemIncrRefCounter (refstars->data[i]);
+    }
+
+    psLogMsg ("psastro", 3, "grid search using %ld raw vs %ld ref stars\n", rawGridStars->n, refGridStars->n);
+
+    // find initial offset / rotation / scale
+    pmAstromStats *gridStats = pmAstromGridMatch (rawGridStars, refGridStars, recipe);
+    if (gridStats == NULL) {
+	psLogMsg ("psastro", 3, "failed to find a grid match solution\n");
+	psFree (rawGridStars);
+	psFree (refGridStars);
+	psastroChipFailureHeader (updates);
+	return false;
+    }
+    psLogMsg ("psastro", 3, "basic grid search result - offset: %f,%f pixels, rotation: %f deg\n", gridStats->offset.x, gridStats->offset.y, DEG_RAD*gridStats->angle);
+
+# if (1) 
+    // tweak the position by finding peak of matches stars
+    stats = pmAstromGridTweak (rawGridStars, refGridStars, recipe, gridStats);
+    if (stats == NULL) {
+	psLogMsg ("psastro", 3, "failed to measure tweaked grid solution\n");
+	psFree (gridStats);
+	psFree (rawGridStars);
+	psFree (refGridStars);
+	psastroChipFailureHeader (updates);
+	return false;
+    }
+    psLogMsg ("psastro", 3, "tweak grid search result - offset: %f,%f pixels, rotation: %f deg\n", stats->offset.x, stats->offset.y, DEG_RAD*stats->angle);
+# else
+    // EAM TEST: skip tweak
+    stats = pmAstromStatsAlloc();
+   *stats = *gridStats;
+# endif
+
+    // adjust the chip.toFPA terms only
+    pmAstromGridApply (chip->toFPA, stats);
+    psastroUpdateChipToFPA (fpa, chip); // updates PSASTRO.RAWSTARS and PSASTRO.REFSTARS
+    psFree (gridStats);
+    psFree (rawGridStars);
+    psFree (refGridStars);
+    psFree (stats);
+
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroParseCamera.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroParseCamera.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroParseCamera.c	(revision 41084)
@@ -0,0 +1,49 @@
+/** @file psastroParseCamera.c
+ *
+ *  @brief
+ *
+ *  @ingroup psastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.18 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+bool psastroParseCamera (pmConfig *config) {
+
+    bool status = false;
+
+    // the input image(s) are required arguments; they define the camera
+    pmFPAfile *input = pmFPAfileDefineFromArgs (&status, config, "PSASTRO.INPUT", "INPUT");
+    if (!status) {
+	psError(PSASTRO_ERR_CONFIG, false, "Failed to build FPA from PSASTRO.INPUT");
+	return false;
+    }
+
+    // define the additional input/output files associated with psphot 
+    if (!psastroDefineFiles (config, input)) {
+	psError(PSASTRO_ERR_CONFIG, false, "Trouble defining the additional input/output files");
+	return false;
+    }
+
+    // Chip selection: turn on only the chips specified (option is not required)
+    char *chipLine = psMetadataLookupStr(&status, config->arguments, "CHIP_SELECTIONS"); 
+    psArray *chips = psStringSplitArray (chipLine, ",", false);
+    if (chips->n > 0) {
+	pmFPASelectChip (input->fpa, -1, true); // deselect all chips
+	for (int i = 0; i < chips->n; i++) {
+	    int chipNum = atoi(chips->data[i]);
+	    if (! pmFPASelectChip(input->fpa, chipNum, false)) {
+		psError(PSASTRO_ERR_CONFIG, true, "Chip number %d doesn't exist in camera.\n", chipNum);
+		return false;
+	    }
+        }
+    }
+    psFree (chips);
+
+    psTrace("psastro", 1, "Done with psastroParseCamera...\n");
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroRefstarSubset.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroRefstarSubset.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroRefstarSubset.c	(revision 41084)
@@ -0,0 +1,102 @@
+/** @file psastroRefstarSubset.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+bool psastroRefstarSubset (pmReadout *readout) {
+
+  // select the raw objects for this readout
+  psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS.SUBSET");
+  if (rawstars == NULL)  {
+    psError(PSASTRO_ERR_DATA, false, "missing rawstars in psastroRefstarSubset\n");
+    return false;
+  }
+
+  // select the raw objects for this readout
+  psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS.SUBSET");
+  if (refstars == NULL)  {
+    psError(PSASTRO_ERR_DATA, false, "missing refstars in psastroRefstarSubset\n");
+    return false;
+  }
+
+  // calculate luminosity functions for rawstars and refstars
+  // the samples cover the same area (the chip), so no area correction
+  // is needed...
+  psLogMsg ("psastro", 4, "measuring luminosity function for rawstars\n");
+  pmLumFunc *rawfunc = psastroLuminosityFunction (rawstars, NULL);
+  if (rawfunc == NULL) {
+    psLogMsg ("psastro", 4, "giving up on rawstars for this readout\n");
+    return true;
+  }
+  psLogMsg ("psastro", 4, "measuring luminosity function for refstars\n");
+  pmLumFunc *reffunc = psastroLuminosityFunction (refstars, rawfunc);
+  if (reffunc == NULL) {
+    psLogMsg ("psastro", 4, "giving up on refstars for this readout\n");
+    return true;
+  }
+
+// XXX code to better deal with mismatches in the fitted lum function
+# if (0)
+  // if the fitted slopes differ by too much, give up and just try to match the peak bin
+  fSlope = (reffunc->slope / rawfunc->slope);
+  if ((fSlope > 1.3) || (fSlope < 0.77)) {
+      // XXX do something here (choose the peak of the smaller set, generate a histogram for
+      // the other set, then choose the bin from the larger set which has nBin = nPeak
+  }
+# endif
+
+  // what is the offset between the two lines at the average magnitude?
+  double mRef = 0.5*(reffunc->mMin + reffunc->mMax);
+  double logRho = mRef * reffunc->slope + reffunc->offset;
+  double mRaw = (logRho - rawfunc->offset) / rawfunc->slope;
+
+  psLogMsg ("psastro", 4, "mRef: %f, logRho: %f, mRaw: %f\n", mRef, logRho, mRaw);
+
+  double mRefMax = rawfunc->mMax - mRaw + mRef;
+  psLogMsg ("psastro", 4, "clipping stars fainter than %f\n", mRefMax);
+
+  psArray *subset = psArrayAllocEmpty (100);
+  for (int i = 0; i < refstars->n; i++) {
+    pmAstromObj *ref = refstars->data[i];
+    if (ref->Mag > mRefMax) continue;
+    psArrayAdd (subset, 100, ref);
+  }
+
+  psLogMsg ("psastro", 4, "keeping %ld of %ld reference stars\n", subset->n, refstars->n);
+
+  psMetadataRemoveKey (readout->analysis, "PSASTRO.REFSTARS.SUBSET");
+  psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.REFSTARS.SUBSET", PS_DATA_ARRAY, "astrometry matches", subset);
+
+  if (psTraceGetLevel("psastro.dump") > 0) {
+      pmChip *chip = readout->parent->parent;
+
+      char *filename = NULL;
+      char *chipname = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+      psStringAppend (&filename, "refstars.%s.dat", chipname);
+      psastroDumpRefstars (subset, filename);
+      psFree (filename);
+  }
+
+  psFree (rawfunc);
+  psFree (reffunc);
+  psFree (subset);
+
+  return true;
+}
+
+/* this test is a bit sensitive to the total number of refstars or rawstars available
+   watch out if:
+   - the fitted slopes are extremely different
+   - the average number of stars per bin is ~1
+
+   skip the cut in these cases?
+*/
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroRemoveClumps.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroRemoveClumps.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroRemoveClumps.c	(revision 41084)
@@ -0,0 +1,195 @@
+/** @file psastroRemoveClumps.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-09 21:25:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+bool psastroRemoveClumpsRawstars (pmConfig *config) {
+
+    bool status;
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+        return false;
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (&status, config->files, "PSASTRO.INPUT");
+    if (!input) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+        return false;
+    }
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmFPA *fpa = input->fpa;
+
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+            if (!chip->fromFPA) { continue; }
+
+            // process each of the readouts
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                // select the raw objects for this readout
+                psArray *rawstars = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.RAWSTARS");
+                if (rawstars == NULL) { continue; }
+
+		// generate a reduced subset excluding the clumps
+		// XXX do we need both RAWSTARS and SUBSET? 
+		// XXX put these parameters in the recipe, please
+		psArray *subset = psastroRemoveClumpsIterate(rawstars, 150, 3);
+		psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.RAWSTARS.SUBSET", PS_DATA_ARRAY, "astrometry objects", subset);
+		psFree (subset);
+
+                psArray *gridstars = psMetadataLookupPtr(&status, readout->analysis, "PSASTRO.GRID.RAWSTARS");
+                if ((gridstars == rawstars) || (gridstars == NULL)) { continue; }
+
+		psArray *gridstars_subset = psastroRemoveClumpsIterate(gridstars, 150, 3);
+		psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.GRID.RAWSTARS.SUBSET", PS_DATA_ARRAY, "astrometry objects", gridstars_subset);
+		psFree (gridstars_subset);
+	    }
+	}
+    }
+    psFree (view);
+    return true;
+}
+
+psArray *psastroRemoveClumpsIterate (psArray *input, int scale, int nIter) {
+
+    psArray *newset = psMemIncrRefCounter (input);
+    for (int i = 0; i < nIter; i++) {
+	psArray *subset = psastroRemoveClumps (newset, 150);
+	psFree (newset);
+	newset = subset;
+    }
+    return newset;
+}    
+
+/**
+ * look for and exclude objects in clumps in the input list
+ */
+psArray *psastroRemoveClumps (psArray *input, int scale) {
+
+    if (!input) return NULL;
+
+    if (!input->n) {
+	psArray *output = psArrayAllocEmpty (16);
+	return output;
+    }
+
+    // determine data range
+    pmAstromObj *obj = input->data[0];
+    float Xmin = obj->FP->x;
+    float Xmax = obj->FP->x;
+    float Ymin = obj->FP->y;
+    float Ymax = obj->FP->y;
+    for (int i = 0; i < input->n; i++) {
+        obj = (pmAstromObj *)input->data[i];
+        if (!isfinite(obj->FP->x)) continue;
+        if (!isfinite(obj->FP->y)) continue;
+        Xmin = PS_MIN (Xmin, obj->FP->x);
+        Xmax = PS_MAX (Xmax, obj->FP->x);
+        Ymin = PS_MIN (Ymin, obj->FP->y);
+        Ymax = PS_MAX (Ymax, obj->FP->y);
+    }
+
+    int nX = (Xmax - Xmin) / scale + 10;
+    int nY = (Ymax - Ymin) / scale + 10;
+    psImage *count = psImageAlloc (nX, nY, PS_TYPE_U32);
+    psImageInit (count, 0);
+
+    // accumulate 2D histogram in image
+    for (int i = 0; i < input->n; i++) {
+        obj = (pmAstromObj *)input->data[i];
+        if (!isfinite(obj->FP->x)) continue;
+        if (!isfinite(obj->FP->y)) continue;
+        int Xi = PS_MIN (PS_MAX((obj->FP->x - Xmin) / scale + 5, 0), count->numCols);
+        int Yi = PS_MIN (PS_MAX((obj->FP->y - Ymin) / scale + 5, 0), count->numRows);
+        count->data.U32[Yi][Xi] ++;
+    }
+
+    // determine image statistics
+    psStats *stats = psStatsAlloc (PS_STAT_MAX | PS_STAT_MAX | PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    if (!psImageStats(stats, count, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to get image statistics.\n");
+        psFree(stats);
+        psFree(count);
+        return NULL;
+    }
+
+    if (stats->max < 1) {
+        psError(PS_ERR_UNKNOWN, false, "no valid sources in image\n");
+        psFree(stats);
+        psFree(count);
+        return NULL;
+    }
+
+    // XXX make this a user option
+    float limit = PS_MAX (5.0*stats->sampleStdev, 5.0);
+    psTrace ("psastro", 4, "skipping stars in cells with more than %f stars\n", limit);
+
+    pmAstromVisualPlotRemoveClumps (input, count, scale, limit);
+
+    // find and exclude objects in bad pixels
+    psArray *output = psArrayAllocEmpty (input->n);
+    for (int i = 0; i < input->n; i++) {
+        obj = (pmAstromObj *)input->data[i];
+        if (!isfinite(obj->FP->x)) continue;
+        if (!isfinite(obj->FP->y)) continue;
+        int Xi = PS_MIN (PS_MAX((obj->FP->x - Xmin) / scale + 5, 0), count->numCols);
+        int Yi = PS_MIN (PS_MAX((obj->FP->y - Ymin) / scale + 5, 0), count->numRows);
+        if (count->data.U32[Yi][Xi] > limit) continue;
+        psArrayAdd (output, 16, obj);
+    }
+
+    psFree(stats);
+    psFree(count);
+    return output;
+}
+
+# if (0)
+/**
+ * make a list of the outlier pixels
+ */
+psArray *badpix = psArrayAllocEmpty (16);
+for (int iy = 0; iy < count->numRows; iy++) {
+    for (int ix = 0; ix < count->numCols; ix++) {
+        if (count->data.U32[iy][ix] > limit) {
+            psPlane *pixel = psPlaneAlloc();
+            pixel->x = ix;
+            pixel->y = iy;
+            psArrayAdd (badpix, 16, pixel);
+            psFree (pixel);
+        }
+    }
+}
+
+if (badpix->n == 0) {
+    psArray *output = psMemIncrRefCounter (input);
+    psFree (stats);
+    psFree (count);
+    psFree (badpix);
+    return output;
+}
+# endif
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroStandAlone.h
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroStandAlone.h	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroStandAlone.h	(revision 41084)
@@ -0,0 +1,50 @@
+/** @file psastroStandAlone.h
+ *
+ *  @brief
+ *
+ *  @ingroup psastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# ifdef HAVE_CONFIG_H
+# include <config.h>
+# endif
+
+#ifndef PSASTRO_STAND_ALONE_H
+#define PSASTRO_STAND_ALONE_H
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "psastro.h"
+
+// Top level functions
+pmConfig         *psastroArguments (int argc, char **argv);
+void              psastroCleanup (pmConfig *config);
+bool              psastroParseCamera (pmConfig *config);
+bool              psastroDataLoad (pmConfig *config);
+
+pmConfig         *psastroModelArguments (int argc, char **argv);
+bool 		  psastroModelParseCamera (pmConfig *config);
+bool 		  psastroModelDataLoad (pmConfig *config);
+bool 		  psastroModelAnalysis (pmConfig *config);
+bool 		  psastroModelAdjust (pmConfig *config);
+bool 		  psastroModelDataSave (pmConfig *config);
+
+psVector         *psastroModelFitBoresite (psVector *Xo, psVector *Yo, psVector *Po, char *outroot);
+psF32 		  psastroModelBoresite (psVector *deriv, const psVector *params, const psVector *coord);
+
+// these are used to define the boresite model parameters
+# define PAR_X0  0  // Xo = params[0] 
+# define PAR_Y0  1  // Yo = params[1] 
+# define PAR_RX  2  // RX = params[2] 
+# define PAR_RY  3  // RY = params[3] 
+# define PAR_P0  4  // P0 = params[4]
+# define PAR_T0  5  // phi = params[4]
+
+#endif
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroTestFuncs.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroTestFuncs.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroTestFuncs.c	(revision 41084)
@@ -0,0 +1,59 @@
+/** @file psastroTestFuncs.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+/**
+ * write out objects
+ */
+bool psastroWriteStars (char *filename, psArray *sources) {
+
+    // re-open, add data to end of file
+    FILE *f = fopen (filename, "w");
+    if (f == NULL) {
+	psLogMsg ("psastroWriteStars", 3, "can't open output file for output %s\n", filename);
+	return false;
+    }
+
+    for (int i = 0; i < sources->n; i++) {
+	
+	pmAstromObj *star = sources->data[i];
+
+	fprintf (f, "%8.2f %8.2f   %8.2f %8.2f   %8.2f %8.2f   %10.6f %10.6f   %8.2f %8.2f\n", 
+		 star->chip->x, star->chip->y, 
+		 star->FP->x, star->FP->y, 
+		 star->TP->x, star->TP->y, 
+		 star->sky->r*DEG_RAD, star->sky->d*DEG_RAD, 
+		 star->Mag, star->dMag);
+    }
+    fclose (f);
+    return true;
+}
+
+bool psastroWriteTransform (psPlaneTransform *map) {
+
+    // dump initial values:
+    for (int i = 0; i < map->x->nX + 1; i++) {
+	for (int j = 0; j < map->x->nY + 1; j++) {
+	    if (map->x->coeffMask[i][j] & PS_POLY_MASK_SET) continue;
+	    psLogMsg ("psastro", 4, "x term %d,%d: %f +/- %f\n", i, j, map->x->coeff[i][j], map->x->coeffErr[i][j]);
+	}
+    }
+
+    for (int i = 0; i < map->y->nX + 1; i++) {
+	for (int j = 0; j < map->y->nY + 1; j++) {
+	    if (map->y->coeffMask[i][j] & PS_POLY_MASK_SET) continue;
+	    psLogMsg ("psastro", 4, "y term %d,%d: %f +/- %f\n", i, j, map->y->coeff[i][j], map->y->coeffErr[i][j]);
+	}
+    }
+    return true;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroUseModel.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroUseModel.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroUseModel.c	(revision 41084)
@@ -0,0 +1,158 @@
+/** @file psastroMosiacAstrom.c
+ *
+ *  @brief
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+# define NONLIN_TOL 0.001 ///< tolerance in pixels
+# define DEBUG 0
+
+/**
+ * Apply the generic astrometry model to this image.  This assumes the WCS i
+ * terms either do not exist or are invalid
+ */
+bool psastroUseModel (pmConfig *config, psMetadata *recipe) {
+
+  bool status;
+
+  bool useModel = psMetadataLookupBool (&status, config->arguments, "PSASTRO.USE.MODEL");
+  if (!status) {
+      useModel = psMetadataLookupBool (&status, recipe, "PSASTRO.USE.MODEL");
+  }
+  if (!useModel) return true;
+
+  // identify reference astrometry table.
+  // if not defined, correction was not requested; skip step
+  pmFPAfile *astrom = psMetadataLookupPtr (NULL, config->files, "PSASTRO.MODEL");
+  if (!astrom) psAbort ("programming error: model was not supplied, though requested");
+
+  // select the input data sources
+  pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+  if (!input) psAbort ("programming error: no input data");
+
+  // make sure the astrometry model is loaded.  de-activate all files except PSASTRO.MODEL.
+  pmFPAfileActivate (config->files, false, NULL);
+  pmFPAfileActivate (config->files, true, "PSASTRO.MODEL");
+
+  pmFPAview *view = pmFPAviewAlloc (0);
+
+  // files associated with the science image
+  if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) {
+      psError (PS_ERR_IO, false, "Can't load the astrometry model file");
+      return false;
+  }
+
+  // XXX TEST: apply the input image RA & DEC to the astrometry model, save corners
+  if (0) {
+      // these externally supplied values are used to set the final transformation terms
+      double RA  = psMetadataLookupF64 (&status, input->fpa->concepts, "FPA.RA");
+      double DEC = psMetadataLookupF64 (&status, input->fpa->concepts, "FPA.DEC");
+      // double POS = PM_RAD_DEG * psMetadataLookupF64 (&status, input->fpa->concepts, "FPA.POSANGLE");
+
+      // get projection scale; center is supplied
+      // XXX should this be astrom or input??
+      float Xs = psMetadataLookupF32(&status, astrom->fpa->concepts, "XSCALE") * PM_RAD_DEG;
+      float Ys = psMetadataLookupF32(&status, astrom->fpa->concepts, "YSCALE") * PM_RAD_DEG;
+
+      // allocate a new toSky projection using the reported position
+      psFree (astrom->fpa->toSky);
+      astrom->fpa->toSky = psProjectionAlloc (RA, DEC, Xs, Ys, PS_PROJ_DIS);
+
+      // local view
+      pmFPAview *myView = pmFPAviewAlloc (0);
+
+      // loop over all chips, replace input astrometry elements with those from astrom
+      pmChip *obsChip = NULL;
+      while ((obsChip = pmFPAviewNextChip (myView, input->fpa, 1)) != NULL) {
+          psTrace ("psastro", 4, "Chip %d: %x %x\n", myView->chip, obsChip->file_exists, obsChip->process);
+          if (!obsChip->process || !obsChip->file_exists || !obsChip->data_exists) { continue; }
+
+          // set the chip astrometry using the astrom file
+          pmChip *refChip = pmFPAviewThisChip (myView, astrom->fpa);
+
+          psFree (obsChip->toFPA);
+          psFree (obsChip->fromFPA);
+
+          // supply astrometry from model
+          obsChip->toFPA   = psMemIncrRefCounter (refChip->toFPA);
+          obsChip->fromFPA = psMemIncrRefCounter (refChip->fromFPA);
+
+          // XXX if we want to write out the result, update the header here.  this needs to be
+          // updated with the correct HDU selection.  obsChip->hdu may not exist.
+          // pmAstromWriteBilevelChip (obsChip->hdu->header, obsChip, NONLIN_TOL);
+      }
+
+      psFree (input->fpa->toSky);
+      psFree (input->fpa->toTPA);
+      psFree (input->fpa->fromTPA);
+      input->fpa->toSky   = psMemIncrRefCounter (astrom->fpa->toSky);
+      input->fpa->toTPA   = psMemIncrRefCounter (astrom->fpa->toTPA);
+      input->fpa->fromTPA = psMemIncrRefCounter (astrom->fpa->fromTPA);
+
+      // XXX this is temporarily hardwired because of model error
+      input->fpa->toSky->type = PS_PROJ_TAN;
+
+      if (DEBUG) psastroDumpCorners ("corners.up.ast1.dat", "corners.dn.ast1.dat", input->fpa);
+  }
+
+  // set the model using the RA, DEC, POSANGLE of the input image.
+  pmAstromModelSetTP (astrom, input->fpa->concepts);
+
+  if (DEBUG) psastroDumpCorners ("corners.up.ast2.dat", "corners.dn.ast2.dat", astrom->fpa);
+
+  // loop over all chips, replace input astrometry elements with those from astrom
+  pmChip *obsChip = NULL;
+  while ((obsChip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+    psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, obsChip->file_exists, obsChip->process);
+    if (!obsChip->process || !obsChip->file_exists || !obsChip->data_exists) { continue; }
+
+    // set the chip astrometry using the astrom file
+    pmChip *refChip = pmFPAviewThisChip (view, astrom->fpa);
+
+    psFree (obsChip->toFPA);
+    psFree (obsChip->fromFPA);
+
+    // supply astrometry from model
+    obsChip->toFPA   = psMemIncrRefCounter (refChip->toFPA);
+    obsChip->fromFPA = psMemIncrRefCounter (refChip->fromFPA);
+
+    // XXX if we want to write out the result, update the header here.  this needs to be
+    // updated with the correct HDU selection.  obsChip->hdu may not exist.
+    // pmAstromWriteBilevelChip (obsChip->hdu->header, obsChip, NONLIN_TOL);
+  }
+
+  psFree (input->fpa->toSky);
+  psFree (input->fpa->toTPA);
+  psFree (input->fpa->fromTPA);
+  input->fpa->toSky   = psMemIncrRefCounter (astrom->fpa->toSky);
+  input->fpa->toTPA   = psMemIncrRefCounter (astrom->fpa->toTPA);
+  input->fpa->fromTPA = psMemIncrRefCounter (astrom->fpa->fromTPA);
+
+  // XXX this is temporarily hardwired because of model error
+  input->fpa->toSky->type = PS_PROJ_TAN;
+
+  if (DEBUG) psastroDumpCorners ("corners.up.inp.dat", "corners.dn.inp.dat", input->fpa);
+
+  // updated with the correct HDU selection.  obsChip->hdu may not exist.
+  // psMetadata *updates = psMetadataAlloc();
+  // pmAstromWriteBilevelMosaic (updates, input->fpa, NONLIN_TOL);
+  // psMetadataAddMetadata (input->fpa->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", updates);
+  // psFree (updates);
+
+  // files associated with the science image
+  if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) {
+      psError (PS_ERR_IO, false, "Can't close the astrometry model file");
+      return false;
+  }
+
+  psFree (view);
+  return true;
+}
+
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroUtils.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroUtils.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroUtils.c	(revision 41084)
@@ -0,0 +1,262 @@
+/** @file psastroUtils.c
+ *
+ *  @brief 
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.25 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-09 21:25:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+# define RENORM 0
+
+// fix this to look up the value in the chip concepts
+static double getChipPixelScale (pmChip *chip) {
+    return 10.0;
+}
+
+// I have an FPA structure with multiple chips.  we have loaded or measured astrometry for each
+// chip with independent pixel/degree scaling values.  These will naturally compensate locally
+// somewhat for the telescope distortion.  to measure the telescope distortion, we need to
+// force the chips to have the same pixel scale and measure the difference from that solution.
+// Convert an FPA with disparate pixel scales to a common pixel scale (perhaps depending on the
+// chip -- eg, TC3)
+
+bool psastroMosaicCommonScale (pmFPA *fpa, psMetadata *recipe) {
+
+    // options : use the MIN or MAX chip as global reference or supplied pixel scales
+    // (microns/pixel), which may depend on the chip
+
+    float pixelScaleUse = 1.0, pixelScale1 = 1.0,  pixelScale2 = 1.0,  pixelScale = 1.0;
+    psVector *oldScale = psVectorAllocEmpty (fpa->chips->n, PS_TYPE_F32);
+
+    char *option = psMetadataLookupStr (NULL, recipe, "PSASTRO.COMMON.SCALE.OPTION");
+    if (option == NULL) {
+        psError(PSASTRO_ERR_DATA, false, "no choice set for common scale option\n");
+        return false;
+    }
+
+    bool useExternal = true;
+    int nobj = 0;
+
+    // find the min or max scale chip
+    if (!strcasecmp (option, "MIN") || !strcasecmp (option, "MAX")) {
+
+        bool useMax = !strcasecmp (option, "MAX");
+        pixelScaleUse = (useMax) ? FLT_MIN : FLT_MAX;
+
+        for (int i = 0; i < fpa->chips->n; i++) {
+            pmChip *chip = fpa->chips->data[i];
+            if (!chip->process || !chip->file_exists) { continue; }
+            if (!chip->toFPA) { continue; }
+
+            if (chip->cells->n == 0) { continue; }
+            pmCell *cell = chip->cells->data[0];
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            if (cell->readouts->n == 0) { continue; }
+            pmReadout *readout = cell->readouts->data[0];
+            if (! readout->data_exists) { continue; }
+
+            pixelScale1 = hypot (chip->toFPA->x->coeff[1][0], chip->toFPA->x->coeff[0][1]);
+            pixelScale2 = hypot (chip->toFPA->y->coeff[1][0], chip->toFPA->y->coeff[0][1]);
+            pixelScale = 0.5*(pixelScale1 + pixelScale2);
+            oldScale->data.F32[nobj++] = pixelScale;
+            pixelScaleUse = (useMax) ? PS_MAX (pixelScale, pixelScaleUse) : PS_MIN (pixelScale, pixelScaleUse);
+        }
+        useExternal = false;
+        oldScale->n = nobj;
+    }
+
+
+    // rescale each chip by the reference scale
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!chip->process || !chip->file_exists) { continue; }
+        if (!chip->toFPA) { continue; }
+
+        psPlaneTransform *toFPA = chip->toFPA;
+        psPlaneTransform *fromFPA = chip->fromFPA;
+
+        pixelScale1 = hypot (toFPA->x->coeff[1][0], toFPA->x->coeff[0][1]);
+        pixelScale2 = hypot (toFPA->y->coeff[1][0], toFPA->y->coeff[0][1]);
+
+        if (useExternal) {
+            pixelScaleUse = getChipPixelScale (chip);
+        }
+
+        for (int i = 0; i <= toFPA->x->nX; i++) {
+            for (int j = 0; j <= toFPA->x->nX; j++) {
+                toFPA->x->coeff[i][j] *= pixelScaleUse/pixelScale1;
+                toFPA->y->coeff[i][j] *= pixelScaleUse/pixelScale2;
+                fromFPA->x->coeff[i][j] *= pixelScale1/pixelScaleUse;
+                fromFPA->y->coeff[i][j] *= pixelScale2/pixelScaleUse;
+            }
+        }
+    }
+    psastroMosaicSetAstrom (fpa);
+    if (!useExternal) {
+        pmAstromVisualPlotCommonScale (fpa, oldScale);
+    }
+    psFree (oldScale);
+    return true;
+}
+
+bool psastroUpdateChipToFPA (pmFPA *fpa, pmChip *chip) {
+
+    psRegion *region = pmChipPixels (chip);
+
+    psFree (chip->fromFPA);
+    chip->fromFPA = psPlaneTransformInvert (NULL, chip->toFPA, *region, 50);
+    psFree (region);
+
+    // loop over cells in this chip
+    for (int nCell = 0; nCell < chip->cells->n; nCell++) {
+	pmCell *cell = chip->cells->data[nCell];
+	if (!cell->process || !cell->file_exists) { continue; }
+
+	// loop over readouts in this cell
+	for (int nRead = 0; nRead < cell->readouts->n; nRead++) {
+	    pmReadout *readout = cell->readouts->data[nRead];
+	    if (! readout->data_exists) { continue; }
+
+	    // select the raw objects for this readout
+	    psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+	    if (rawstars) { 
+		for (int i = 0; i < rawstars->n; i++) {
+		    pmAstromObj *raw = rawstars->data[i];
+		    psPlaneTransformApply (raw->FP, chip->toFPA, raw->chip);
+		    psPlaneTransformApply (raw->TP, fpa->toTPA, raw->FP);
+		    psDeproject (raw->sky, raw->TP, fpa->toSky);
+		}
+	    }
+
+	    psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+	    if (refstars) { 
+		for (int i = 0; i < refstars->n; i++) {
+		    pmAstromObj *ref = refstars->data[i];
+		    psPlaneTransformApply (ref->chip, chip->fromFPA, ref->FP);
+		}
+	    }
+	}
+    }
+
+    return true;
+}
+
+# if 0
+
+bool psastroSelectBrightStars (pmFPA *fpa, psMetadata *config) {
+
+    bool status;
+
+    // add exclusions for objects on some basis?
+    int MAX_NSTARS = psMetadataLookupS32 (&status, config, "PSASTRO.STARS.MAX");
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        for (int j = 0; j < chip->cells->n; j++) {
+            pmCell *cell = chip->cells->data[j];
+            for (int k = 0; k < cell->readouts->n; k++) {
+                pmReadout *readout = cell->readouts->data[k];
+
+                psArray *stars = psMetadataLookupPtr (&status, readout->analysis, "STARS.FULLSET");
+                stars = psArraySort (stars, pmAstromObjSortByMag);
+
+                int nSubset = PS_MIN (MAX_NSTARS, stars->n);
+                psArray *subset = psArrayAlloc (nSubset);
+
+                for (int i = 0; i < nSubset; i++) {
+                    subset->data[i] = stars->data[i];
+                }
+                psMetadataAdd (readout->analysis, PS_LIST_TAIL, "STARS.SUBSET", PS_DATA_ARRAY, "stars from analysis", subset);
+            }
+        }
+    }
+    return true;
+}
+
+psPlaneDistort *psPlaneDistortCopy (psPlaneDistort *input) {
+
+    psPlaneDistort *output = psPlaneDistortAlloc (input->x->nX, input->x->nY, input->x->nZ, input->x->nT);
+
+    for (int i = 0; i < input->x->nX; i++) {
+        for (int j = 0; j < input->x->nY; j++) {
+            for (int k = 0; k < input->x->nZ; k++) {
+                for (int m = 0; m < input->x->nT; m++) {
+                    // x-terms
+                    output->x->mask[i][j][k][m]     = input->x->mask[i][j][k][m];
+                    output->x->coeff[i][j][k][m]    = input->x->coeff[i][j][k][m];
+                    output->x->coeffErr[i][j][k][m] = input->x->coeffErr[i][j][k][m];
+                    // y-terms
+                    output->y->mask[i][j][k][m]     = input->y->mask[i][j][k][m];
+                    output->y->coeff[i][j][k][m]    = input->y->coeff[i][j][k][m];
+                    output->y->coeffErr[i][j][k][m] = input->y->coeffErr[i][j][k][m];
+                }
+            }
+        }
+    }
+    return (output);
+}
+
+psPlaneTransform *psPlaneTransformCopy (psPlaneTransform *input) {
+
+    psPlaneTransform *output = psPlaneTransformAlloc (input->x->nX, input->x->nY);
+
+    for (int i = 0; i < input->x->nX; i++) {
+        for (int j = 0; j < input->x->nY; j++) {
+            // x-terms
+            output->x->mask[i][j]     = input->x->mask[i][j];
+            output->x->coeff[i][j]    = input->x->coeff[i][j];
+            output->x->coeffErr[i][j] = input->x->coeffErr[i][j];
+            // y-terms
+            output->y->mask[i][j]     = input->y->mask[i][j];
+            output->y->coeff[i][j]    = input->y->coeff[i][j];
+            output->y->coeffErr[i][j] = input->y->coeffErr[i][j];
+        }
+    }
+    return (output);
+}
+
+psProjection *psProjectionCopy (psProjection *input) {
+
+    psProjection *output = psProjectionAlloc (input->R, input->D, input->Xs, input->Ys, input->type);
+    return (output);
+}
+
+// returns the rotation term, forcing positive parity
+double psPlaneTransformGetRotation (psPlaneTransform *map) {
+
+    if (map->x->nX < 1) return 0;
+    if (map->x->nY < 1) return 0;
+
+    if (map->y->nX < 1) return 0;
+    if (map->y->nY < 1) return 0;
+
+    double pc1_1 = map->x->coeff[1][0];
+    double pc1_2 = map->x->coeff[0][1];
+    double pc2_1 = map->y->coeff[1][0];
+    double pc2_2 = map->y->coeff[0][1];
+
+    double px = SIGN (pc1_1);
+    double py = SIGN (pc2_2);
+
+    // both x and y terms imply an angle. take the average
+    double t1 = -atan2 (px*pc1_2, px*pc1_1);
+    double t2 = +atan2 (py*pc2_1, py*pc2_2);
+
+    // careful near -pi,+pi boundary...
+    if (t1 - t2 > M_PI/2) t2 += 2*M_PI;
+    if (t2 - t1 > M_PI/2) t1 += 2*M_PI;
+
+    double theta = 0.5*(t1 + t2);
+    while (theta < M_PI) theta += 2*M_PI;
+    while (theta > M_PI) theta -= 2*M_PI;
+
+    return (theta);
+}
+
+# endif
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroVersion.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroVersion.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroVersion.c	(revision 41084)
@@ -0,0 +1,119 @@
+/** @file psastroVersion.c
+ *
+ *  @brief
+ *
+ *  @ingroup libpsastro
+ *
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#include "psastroInternal.h"
+#include "psastroVersionDefinitions.h"
+
+#ifndef PSASTRO_VERSION
+#error "PSASTRO_VERSION is not set"
+#endif
+#ifndef PSASTRO_BRANCH
+#error "PSASTRO_BRANCH is not set"
+#endif
+#ifndef PSASTRO_SOURCE
+#error "PSASTRO_SOURCE is not set"
+#endif
+
+psString psastroVersion(void)
+{
+    char *value = NULL;
+    psStringAppend(&value, "%s@%s", PSASTRO_BRANCH, PSASTRO_VERSION);
+    return value;
+}
+
+psString psastroSource(void)
+{
+  return psStringCopy(PSASTRO_SOURCE);
+}
+
+psString psastroVersionLong(void)
+{
+    psString version = psastroVersion();  // Version, to return
+    psString source = psastroSource();    // Source
+
+    psStringPrepend(&version, "psastro ");
+    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 psastroVersionHeader(psMetadata *header)
+{
+    PS_ASSERT_METADATA_NON_NULL(header, false);
+
+    psString version = psastroVersion(); // Software version
+    psString source = psastroSource();   // Software source
+
+    psMetadataAddStr(header, PS_LIST_TAIL, "ASTRO_V", PS_META_REPLACE, NULL, PSASTRO_VERSION);
+    
+    psStringPrepend(&version, "psastro version: ");
+    psStringPrepend(&source, "psastro 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;
+}
+
+bool psastroVersionHeaderFull(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, "psastro 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);
+    psastroVersionHeader(header);
+
+    return true;
+}
+
+void psastroVersionPrint(void)
+{
+    psTime *time = psTimeGetNow(PS_TIME_TAI); // The time now
+    psString timeString = psTimeToISO(time); // The time in an ISO string
+    psFree(time);
+    psLogMsg("psastro", PS_LOG_INFO, "psastro at %s", timeString);
+    psFree(timeString);
+
+    psString pslib = psLibVersionLong();// psLib version
+    psString psmodules = psModulesVersionLong(); // psModules version
+    psString ppStats = ppStatsVersionLong(); // ppStats version
+    psString psastro = psastroVersionLong(); // psastro version
+
+    psLogMsg("psastro", PS_LOG_INFO, "%s", pslib);
+    psLogMsg("psastro", PS_LOG_INFO, "%s", psmodules);
+    psLogMsg("psastro", PS_LOG_INFO, "%s", ppStats);
+    psLogMsg("psastro", PS_LOG_INFO, "%s", psastro);
+
+    psFree(pslib);
+    psFree(psmodules);
+    psFree(ppStats);
+    psFree(psastro);
+
+    return;
+}
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroVersionDefinitions.h.in
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroVersionDefinitions.h.in	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroVersionDefinitions.h.in	(revision 41084)
@@ -0,0 +1,8 @@
+#ifndef PSASTRO_VERSION_DEFINITIONS_H
+#define PSASTRO_VERSION_DEFINITIONS_H
+
+#define PSASTRO_VERSION @PSASTRO_VERSION@ // SVN version
+#define PSASTRO_BRANCH  @PSASTRO_BRANCH@  // SVN branch
+#define PSASTRO_SOURCE  @PSASTRO_SOURCE@  // SVN source
+
+#endif
Index: /branches/ccl_branches/ipponly-20191108/psastro/src/psastroZeroPoint.c
===================================================================
--- /branches/ccl_branches/ipponly-20191108/psastro/src/psastroZeroPoint.c	(revision 41084)
+++ /branches/ccl_branches/ipponly-20191108/psastro/src/psastroZeroPoint.c	(revision 41084)
@@ -0,0 +1,610 @@
+/** @file psastroMosaicZeroPoint.c
+ *
+ *  @brief
+ *
+ *  @ingroup libpsastro
+ *
+ *  @author IfA
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-07 02:03:34 $
+ *  Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+# include "psastroInternal.h"
+
+bool psastroZeroPoint (pmConfig *config) {
+
+    bool status;
+    float zeropt, exptime;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    float refminMag,refmaxMag;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, false, "Can't find PSASTRO recipe!\n");
+        return false;
+    }
+
+    // recipe options
+    bool byExposure = psMetadataLookupBool (&status, recipe, "ZERO.POINT.BY.EXPOSURE");
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+        psError(PSASTRO_ERR_CONFIG, false, "Can't find input data!\n");
+        return false;
+    }
+    pmFPA *fpa = input->fpa;
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // given the existing per-chip astrometry, determine matches between raw and ref stars
+    // is this needed? yes, if we didn't do SingleChip astrometry first
+    // if (!psastroMosaicSetMatch (fpa, recipe, 0)) {
+    //     psError(PSASTRO_ERR_UNKNOWN, false, "failed to match raw and ref stars for mosaic");
+    //     return false;
+    // }
+
+    // XXX eventually: look up the photcode info from the recipe, use the ZEROPT and A_0 terms
+    // to determine transparency = Mref - Mraw - 2.5*log10(exptime) - ZEROPT - A_0*Color_ref
+    // to get there, we need: a) getstar to write out the color-term magnitudes and b)  the
+    // pmAstromObj to carry a color value as well.
+
+    // really error-out here?  or just skip?
+    if (!psastroZeroPointFromRecipe (&zeropt, &exptime, NULL, fpa, recipe)) {
+        psLogMsg ("psastro", PS_LOG_INFO, "failed to load zeropt data from recipe");
+        return false;
+    }
+
+    // MEH -- RefMagLimit for ZP with photcode option (merr probably better but not an option) 
+    // -- defualt is to use general config value (no hardcode..)
+    if (!psastroZeroPointRefMagLimitFromRecipe (&refminMag, &refmaxMag, fpa, recipe)) {
+        psLogMsg ("psastro", PS_LOG_INFO, "failed to load ref min,maxMag data from recipe");
+	return false;
+    }
+
+    // if we measure the zero point by exposure, accumulate the dMag values here:
+    psVector *dMag = NULL;
+    // MEH -- also the number of refcat sources used
+    int zpt_nref = 0;
+
+    float fpaZP = 0.0;                  // Average zero point
+    int numZP = 0.0;                    // Number of measurements
+
+    // this loop selects the matched stars for all chips
+    // XXX optionally measure zero point for entire exposure in a single statistic
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            // XXX there can only be one readout per chip, right?
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                // calculate dMag for the matched stars
+                //dMag = psastroZeroPointReadoutAccum (dMag, readout, exptime);
+                // MEH -- additional limits should be available for ZP if just robust mean and w/ stack skycal 
+		dMag = psastroZeroPointReadoutAccum (dMag, readout, exptime, refminMag, refmaxMag);
+                if (!byExposure) {
+                    // calculate dMag for the matched stars just for this readout (well, chip)
+                    psMetadata *header = psMetadataLookupMetadata (&status, readout->analysis, "PSASTRO.HEADER");
+                    psastroZeroPointAnalysis (header, dMag, zeropt, recipe);
+		    // MEH -- per readout N refcat, min/max mag  -- added here, not part of median/edge process
+		    zpt_nref += dMag->n;
+		    psMetadataAddS32 (header, PS_LIST_TAIL, "ZPT_NREF", PS_META_REPLACE, "N refcat sources for zero point", dMag->n);
+		    psMetadataAddF32 (header, PS_LIST_TAIL, "ZPT_MIN", PS_META_REPLACE, "min refcat mag for zero point", refminMag);
+		    psMetadataAddF32 (header, PS_LIST_TAIL, "ZPT_MAX", PS_META_REPLACE, "max refcat mag for zero point", refmaxMag);
+                    psFree (dMag);
+                    dMag = NULL;
+
+                    float zp = psMetadataLookupF32(NULL, header, "ZPT_OBS");
+                    if (isfinite(zp)) {
+                        fpaZP += zp;
+                        numZP++;
+                    }
+
+
+
+                }
+            }
+        }
+    }
+
+    if (byExposure) {
+        psMetadata *header = psMetadataLookupMetadata (&status, fpa->analysis, "PSASTRO.HEADER");
+        if (!header) {
+            header = psMetadataAlloc ();
+            psMetadataAddMetadata (fpa->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", header);
+            psFree (header);
+        }
+        psastroZeroPointAnalysis (header, dMag, zeropt, recipe);
+	//MEH -- all N refcat sources here
+	zpt_nref = dMag->n;
+        psFree (dMag);
+        dMag = NULL;
+
+        float zptObs = psMetadataLookupF32 (&status, header, "ZPT_OBS");
+        float zptRef = psMetadataLookupF32 (&status, header, "ZPT_REF");
+        float zptErr = psMetadataLookupF32 (&status, header, "ZPT_ERR");
+        float zptOff = psMetadataLookupF32 (&status, header, "ZPT_OFF");
+
+        fpaZP = zptObs;
+        numZP = 1;
+
+        // copy the zero point metadata from the fpa header to the chip headers
+        for (int i = 0; i < fpa->chips->n; i++) {
+            pmChip *chip = fpa->chips->data[i];
+            if (!chip) continue;
+            if (!chip->process) continue;
+            if (!chip->file_exists) continue;
+
+            for (int j = 0; j < chip->cells->n; j++) {
+                pmCell *cell = chip->cells->data[j];
+                if (!cell) continue;
+                if (!cell->process) continue;
+                if (!cell->file_exists) continue;
+
+                for (int k = 0; k < cell->readouts->n; k++) {
+                    pmReadout *readout = cell->readouts->data[k];
+                    if (!readout) continue;
+                    if (!readout->data_exists) continue;
+
+                    // calculate dMag for the matched stars just for this readout (well, chip)
+                    psMetadata *header = psMetadataLookupMetadata (&status, readout->analysis, "PSASTRO.HEADER");
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "ZPT_OBS", PS_META_REPLACE, "measured zero point",  zptObs);
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "ZPT_REF", PS_META_REPLACE, "reference zero point", zptRef);
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "ZPT_ERR", PS_META_REPLACE, "error on zero point",  zptErr);
+                    psMetadataAddF32 (header, PS_LIST_TAIL, "ZPT_OFF", PS_META_REPLACE, "zero point offset",    zptOff);
+                    //MEH -- per readout  N refcat, MIN/MAX for consistency
+                    psMetadataAddS32 (header, PS_LIST_TAIL, "ZPT_NREF", PS_META_REPLACE, "N refcat sources for zero point", zpt_nref);
+		    psMetadataAddF32 (header, PS_LIST_TAIL, "ZPT_MIN", PS_META_REPLACE, "min refcat mag for zero point", refminMag);
+		    psMetadataAddF32 (header, PS_LIST_TAIL, "ZPT_MAX", PS_META_REPLACE, "max refcat mag for zero point", refmaxMag);
+                }
+            }
+        }
+    }
+
+    // MEH add N refcat, min/max mag to primary header 
+    psMetadata *header = psMetadataLookupMetadata (&status, fpa->analysis, "PSASTRO.HEADER");
+    if (!header) {
+	header = psMetadataAlloc ();
+	psMetadataAddMetadata (fpa->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", header);
+	psFree (header);
+    }
+    psMetadataAddS32 (header, PS_LIST_TAIL, "ZPT_NREF", PS_META_REPLACE, "Total N refcat sources for zero point", zpt_nref);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "ZPT_MIN", PS_META_REPLACE, "min refcat mag for zero point", refminMag);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "ZPT_MAX", PS_META_REPLACE, "max refcat mag for zero point", refmaxMag);
+
+    psMetadataItem *item = psMetadataLookup(fpa->concepts, "FPA.ZP");
+    item->data.F32 = fpaZP / (float)numZP;
+
+    psFree (view);
+    return true;
+}
+
+/**
+ * accumulate the dMag values from this readout
+ */
+psVector *psastroZeroPointReadoutAccum(psVector *dMag, pmReadout *readout, float exptime, float refminMag, float refmaxMag) {
+
+    bool status;
+
+    // select the raw objects for this readout
+    psArray *rawstars = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.RAWSTARS.SUBSET");
+    if (rawstars == NULL) return dMag;
+
+    // select the raw objects for this readout
+    psArray *refstars = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.REFSTARS.SUBSET");
+    if (refstars == NULL) return dMag;
+
+    psArray *matches = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.MATCH");
+    if (matches == NULL) return dMag;
+
+    if (!dMag) {
+        dMag  = psVectorAllocEmpty (100, PS_TYPE_F32);
+    }
+
+    for (int i = 0; i < matches->n; i++) {
+
+      pmAstromMatch *match = matches->data[i];
+
+      pmAstromObj *raw = rawstars->data[match->raw];
+
+      pmAstromObj *ref = refstars->data[match->ref];
+
+      // XXX I should be applying the airmass term here or in psastroLoadRefstars
+      // (ie, ref->Mag would be a prediction for a given star.
+      // MEH -- only append if in allowed ref mag range -- may want merr but not available..
+      if ( ref->Mag >= refminMag && ref->Mag <= refmaxMag ){
+        float value = ref->Mag - raw->Mag - 2.5*log10(exptime);
+        psVectorAppend (dMag, value);
+      }
+    }
+    return dMag;
+}
+
+bool psastroZeroPointAnalysis (psMetadata *header, psVector *dMag, float zeropt, psMetadata *recipe) {
+
+    // XXX make this depend on the mode?
+    if (!dMag || dMag->n < 3) {
+      fprintf (stderr, "zero point NaN +/- NaN\n");
+      return false;
+    }
+
+    bool status;
+    bool useMean = psMetadataLookupBool (&status, recipe, "ZERO.POINT.USE.MEAN");
+
+    // the zero point analysis depends on the type of desired statistic.  For comparisons
+    // against a high-quality reference catalog with a good match to the actual filter used, it
+    // is best to use a standard clipped mean or global mean statistic.  If the reference
+    // catalog has some unmodeled extra parameter, as is the case for the synthetic grizy
+    // database vs PS1, then it is best to use some consistent feature in the color
+    // distribution.
+
+    psStats *stats = NULL;
+    float zeroPtObs, zeroPtErr;
+    if (useMean) {
+        // stats structure and mask for use in measuring the clipping statistic
+        // this analysis has too few data points to use the robust median method
+        stats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+        if (!psVectorStats (stats, dMag, NULL, NULL, 0)) {
+            psError(PS_ERR_UNKNOWN, false, "failure to measure zero point by mean");
+            return false;
+        }
+	zeroPtObs = stats->robustMedian;
+	zeroPtErr = stats->robustStdev;
+    } else {
+        stats = psastroStatsPercentile (dMag, recipe);
+        if (!stats) {
+            psError(PS_ERR_UNKNOWN, false, "failure to measure zero point by edge");
+            return false;
+        }
+	zeroPtObs = stats->clippedMean;
+	zeroPtErr = stats->clippedStdev;
+    }
+    fprintf (stderr, "zero point %f +/- %f using %ld stars; transparency %f\n", zeroPtObs, zeroPtErr, dMag->n, zeropt - zeroPtObs);
+
+    psMetadataAddF32 (header, PS_LIST_TAIL, "ZPT_OBS", PS_META_REPLACE, "measured zero point", zeroPtObs);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "ZPT_REF", PS_META_REPLACE, "reference zero point", zeropt);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "ZPT_ERR", PS_META_REPLACE, "error on zero point",  zeroPtErr);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "ZPT_OFF", PS_META_REPLACE, "zero point offset",    zeropt - zeroPtObs);
+
+    psFree (stats);
+    return true;
+}
+
+#define MAG_RESOLUTION 0.0002
+
+// set the bin closest to the corresponding value.  if USE_END is +/- 1,
+// out-of-range saturates on lower/upper bin REGARDLESS of actual value
+#define PS_BIN_FOR_VALUE(RESULT, VECTOR, VALUE, USE_END) { \
+        psVectorBinaryDisectResult result; \
+        psScalar tmpScalar; \
+        tmpScalar.type.type = PS_TYPE_F32; \
+        tmpScalar.data.F32 = (VALUE); \
+        RESULT = psVectorBinaryDisect (&result, VECTOR, &tmpScalar); \
+        switch (result) { \
+          case PS_BINARY_DISECT_PASS: \
+            break; \
+          case PS_BINARY_DISECT_OUTSIDE_RANGE: \
+            psTrace("psastro", 6, "selected bin outside range"); \
+            if (USE_END == -1) { RESULT = 0; } \
+            if (USE_END == +1) { RESULT = VECTOR->n - 1; } \
+            break; \
+          case PS_BINARY_DISECT_INVALID_INPUT: \
+          case PS_BINARY_DISECT_INVALID_TYPE: \
+            psAbort ("programming error"); \
+            break; \
+        } }
+
+# define PS_BIN_INTERPOLATE(RESULT, VECTOR, BOUNDS, BIN, VALUE) { \
+        float dX, dY, Xo, Yo, Xt; \
+        if (BIN == BOUNDS->n - 1) { \
+            dX = 0.5*(BOUNDS->data.F32[BIN+1] - BOUNDS->data.F32[BIN-1]); \
+            dY = VECTOR->data.F32[BIN] - VECTOR->data.F32[BIN-1]; \
+            Xo = 0.5*(BOUNDS->data.F32[BIN+1] + BOUNDS->data.F32[BIN]); \
+            Yo = VECTOR->data.F32[BIN]; \
+        } else { \
+            dX = 0.5*(BOUNDS->data.F32[BIN+2] - BOUNDS->data.F32[BIN]); \
+            dY = VECTOR->data.F32[BIN+1] - VECTOR->data.F32[BIN]; \
+            Xo = 0.5*(BOUNDS->data.F32[BIN+1] + BOUNDS->data.F32[BIN]); \
+            Yo = VECTOR->data.F32[BIN]; \
+        } \
+        if (dY != 0) { \
+            Xt = (VALUE - Yo)*dX/dY + Xo; \
+        } else { \
+            Xt = Xo; \
+        } \
+        Xt = PS_MIN (BOUNDS->data.F32[BIN+1], PS_MAX(BOUNDS->data.F32[BIN], Xt)); \
+        psTrace("psastro", 6, "(Xo, Yo, dX, dY, Xt, Yt) is (%.2f %.2f %.2f %.2f %.2f %.2f)\n", \
+                Xo, Yo, dX, dY, Xt, VALUE); \
+        RESULT = Xt; }
+
+// measure the edge of the sample at flimit
+// return results in stats->sampleMean, sampleStdev
+// XXX this is a misuse of psStats -- make our own structure?
+psStats *psastroStatsPercentile (psVector *myVector, psMetadata *recipe) {
+
+    // search for the 'blue' edge of the dMag distribution:
+    // the distribution is not a normal population, but instead has a broad range with fairly hard edges.
+    // construct a histogram and look for the
+
+    bool status;
+    float edgeFraction = psMetadataLookupF32 (&status, recipe, "ZERO.POINT.EDGE.FRACTION");
+    int edgeSample = psMetadataLookupS32 (&status, recipe, "ZERO.POINT.EDGE.SAMPLE");
+    float edgeSampleFraction = psMetadataLookupF32 (&status, recipe, "ZERO.POINT.EDGE.SAMPLE.FRACTION");
+
+    // stats is first used to find the data range
+    psStats *stats = psStatsAlloc(PS_STAT_MIN | PS_STAT_MAX); // Statistics for min and max
+    psHistogram *histogram = NULL;      // Histogram of the data
+    psHistogram *cumulative = NULL;     // Cumulative histogram of the data
+    float min = NAN, max = NAN;         // Mimimum and maximum values
+
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS);
+
+    // Get the minimum and maximum values
+    if (!psVectorStats(stats, myVector, NULL, NULL, 0)) goto escape;
+    min = stats->min;
+    max = stats->max;
+    if (isnan(min) || isnan(max)) goto escape;
+    psTrace("psastro", 5, "Data min/max is (%.2f, %.2f)\n", min, max);
+
+    // If all data points have the same value, then we set the appropriate members of stats and return.
+    if (fabs(max - min) <= FLT_EPSILON) {
+        stats->clippedMean = min;
+        stats->clippedStdev = NAN;
+        stats->results |= PS_STAT_CLIPPED_MEAN;
+        stats->results |= PS_STAT_CLIPPED_STDEV;
+        return stats;
+    }
+
+    // Define the histogram bin size.
+    float binSize = MAG_RESOLUTION;
+    long numBins = (max - min) / binSize; // Number of bins
+    psTrace("psastro", 5, "Numbins is %ld\n", numBins);
+    psTrace("psastro", 5, "Creating a robust histogram from data range (%.2f - %.2f)\n", min, max);
+
+    // allocate the histogram containers
+    histogram = psHistogramAlloc(min, max, numBins);
+    cumulative = psHistogramAlloc(min, max, numBins);
+
+    // find the mean value:
+    stats->clippedMean = psastroStatsPercentileValue (histogram, cumulative, myVector, edgeFraction);
+
+    int nSubset = myVector->n * edgeSampleFraction;
+    psVector *subset = psVectorAlloc (nSubset, PS_TYPE_F32);
+
+    float Sum = 0.0;
+    float S2 = 0.0;
+    for (int i = 0; i < edgeSample; i++) {
+
+        // generate the subset vector
+        for (long i = 0; i < nSubset; i++) {
+            double frnd = psRandomUniform(rng);
+            int entry = PS_MIN(myVector->n - 1, PS_MAX(0, myVector->n * frnd));
+
+            subset->data.F32[i] = myVector->data.F32[entry];
+        }
+
+        float value = psastroStatsPercentileValue (histogram, cumulative, subset, edgeFraction);
+
+        Sum += value;
+        S2 += value*value;
+    }
+    psTrace("psastro", 6, "subset stats: Sum: %f, S2: %f, Npts: %d\n", Sum, S2, edgeSample);
+
+    stats->clippedStdev = PS_MAX (sqrt(S2 / edgeSample - PS_SQR(Sum/edgeSample)), MAG_RESOLUTION);
+    psTrace("psastro", 5, "percentile stats %f +/- %f\n", stats->clippedMean, stats->clippedStdev);
+
+    stats->results |= PS_STAT_CLIPPED_MEAN;
+    stats->results |= PS_STAT_CLIPPED_STDEV;
+    stats->options = PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV;
+
+    // Clean up
+    psFree(histogram);
+    psFree(cumulative);
+    psFree(subset);
+    psFree(rng);
+    return stats;
+
+escape:
+    stats->clippedMean = NAN;
+    stats->clippedStdev = NAN;
+    stats->results |= PS_STAT_CLIPPED_MEAN;
+    stats->results |= PS_STAT_CLIPPED_STDEV;
+    stats->options = PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV;
+
+    psFree(histogram);
+    psFree(cumulative);
+
+    return stats;
+}
+
+
+// measure the edge of the sample at flimit
+// return results in stats->sampleMean, sampleStdev
+// XXX this is a misuse of psStats -- make our own structure?
+float psastroStatsPercentileValue (psHistogram *histogram, psHistogram *cumulative, psVector *myVector, float flimit) {
+
+    // need to initialize the histogram on each pass
+    psVectorInit (histogram->nums, 0);
+    if (!psVectorHistogram(histogram, myVector, NULL, NULL, 0)) {
+        // if psVectorHistogram returns false, we have a programming error
+        psAbort ("Unable to generate histogram for psastroZeroPointAnalysis");
+    }
+    if (psTraceGetLevel("psastro") >= 8) {
+        PS_VECTOR_PRINT_F32(histogram->bounds);
+        PS_VECTOR_PRINT_F32(histogram->nums);
+    }
+
+    // Convert the specific histogram to a cumulative histogram
+    // The cumulative histogram data points correspond to the UPPER bound value (N < Bin[i+1])
+    cumulative->nums->data.F32[0] = histogram->nums->data.F32[0];
+    for (long i = 1; i < histogram->nums->n; i++) {
+        cumulative->nums->data.F32[i] = cumulative->nums->data.F32[i-1] + histogram->nums->data.F32[i];
+        cumulative->bounds->data.F32[i-1] = histogram->bounds->data.F32[i];
+    }
+    if (psTraceGetLevel("psastro") >= 8) {
+        PS_VECTOR_PRINT_F32(cumulative->bounds);
+        PS_VECTOR_PRINT_F32(cumulative->nums);
+    }
+
+    // Find the bin which contains the first data point above the limit
+    long numBins = cumulative->nums->n;
+    long totalDataPoints = cumulative->nums->data.F32[numBins - 1];
+    psTrace("psastro", 6, "Total data points is %ld\n", totalDataPoints);
+
+    // find bin which is the lower bound of the limit value (value[bin] < f < value[bin+1]
+    long bin;
+    PS_BIN_FOR_VALUE(bin, cumulative->nums, flimit * totalDataPoints, 0);
+    psTrace("psastro", 6, "The bin is %ld (%.4f to %.4f)\n", bin, cumulative->bounds->data.F32[bin], cumulative->bounds->data.F32[bin+1]);
+
+    // Linear interpolation to the limit value in bin units
+    float value;
+    PS_BIN_INTERPOLATE (value, cumulative->nums, cumulative->bounds, bin, totalDataPoints * flimit);
+    psTrace("psastro", 6, "limit value is %f\n", value);
+
+    return value;
+}
+
+# define ESCAPE(MSG) { \
+  psLogMsg ("psastro", PS_LOG_INFO, MSG); \
+  return false; }
+
+bool psastroZeroPointFromRecipe (float *zeropt, float *exptime, float *ghostMaxMag, pmFPA *fpa, psMetadata *recipe) {
+
+    bool status;
+
+    // select the filter; default to fixed photcode and mag limit otherwise
+    char *filter = psMetadataLookupStr (&status, fpa->concepts, "FPA.FILTERID");
+    if (!status) ESCAPE ("missing FPA.FILTER in concepts");
+
+    *exptime = psMetadataLookupF32 (&status, fpa->concepts, "FPA.EXPOSURE");
+    if (!status) ESCAPE ("missing FPA.EXPOSURE in concepts");
+
+    // we need to select the PHOTCODE.DATA folder that matches our filter
+    psMetadataItem *item = psMetadataLookup (recipe, "PHOTCODE.DATA");
+    if (!item) ESCAPE ("PHOTCODE.DATA folders missing");
+    if (item->type != PS_DATA_METADATA_MULTI) ESCAPE ("PHOTCODE.DATA not a multi");
+
+    // PHOTCODE.DATA is a multi of metadata items
+    psListIterator *iter = psListIteratorAlloc(item->data.list, PS_LIST_HEAD, false);
+
+    psMetadataItem *refItem = NULL;
+    while ((refItem = psListGetAndIncrement (iter))) {
+        if (refItem->type != PS_DATA_METADATA) ESCAPE ("PHOTCODE.DATA entry is not a metadata folder");
+
+        char *refFilter = psMetadataLookupStr (&status, refItem->data.md, "FILTER");
+        if (!status) {
+            // psLogMsg ("psastro", PS_LOG_INFO, "a PHOTCODE.DATA recipe folder is missing FILTER");
+            continue;
+        }
+
+        // does this entry match the current filter?
+        if (strcmp (refFilter, filter)) continue;
+
+        psLogMsg ("psastro", PS_LOG_DETAIL, "PHOTCODE.DATA found for filter %s", filter);
+
+        *zeropt = psMetadataLookupF32 (&status, refItem->data.md, "ZEROPT");
+        if (!status) {
+            psLogMsg ("psastro", PS_LOG_INFO, "a PHOTCODE.DATA recipe folder is missing ZEROPT");
+            continue;
+        }
+        if (ghostMaxMag) {
+            *ghostMaxMag = psMetadataLookupF32 (&status, refItem->data.md, "GHOST_MAX_MAG");
+            if (!status) {
+                psLogMsg ("psastro", PS_LOG_INFO, "a PHOTCODE.DATA recipe folder is missing GHOST_MAX_MAG");
+                continue;
+            }
+	    //MEH null is a pain.. so only log if set
+            psLogMsg ("psastro", PS_LOG_INFO, "found GHOST_MAX_MAG %f",*ghostMaxMag);
+        }
+	//MEH what zpt is set to 
+        psLogMsg ("psastro", PS_LOG_INFO, "found ZEROPT  %f",*zeropt);
+        psFree (iter);
+        return true;
+    }
+    psFree (iter);
+    return false;
+}
+
+
+
+bool psastroZeroPointRefMagLimitFromRecipe (float *refminMag, float *refmaxMag, pmFPA *fpa, psMetadata *recipe) {
+
+    bool status;
+    float tmpmin,tmpmax;
+
+    // MEH -- duplicate psastroZeroPointFromRecipe for ZeroPoint adding RefMag limits with photcode option for extra restrictions
+    // must have non-photcode dependent setting -- use as default rather than a hardcoded value 
+    *refminMag = psMetadataLookupF32 (&status, recipe, "REFSTAR.ZP.MIN.MAG");
+    //if (!status) *refminMag = -5.0;
+    if (!status) {
+        psLogMsg ("psastro", PS_LOG_INFO, "RefMagLimit is missing REFSTAR.ZP.MIN.MAG");
+        return false;
+    }
+    *refmaxMag = psMetadataLookupF32 (&status, recipe, "REFSTAR.ZP.MAX.MAG");
+    //if (!status) *refmaxMag = 30.0;
+    if (!status) {
+        psLogMsg ("psastro", PS_LOG_INFO, "RefMagLimit is missing REFSTAR.ZP.MAX.MAG");
+        return false;
+    }
+
+    // select the filter; default to any photcode and mag limit otherwise
+    char *filter = psMetadataLookupStr (&status, fpa->concepts, "FPA.FILTERID");
+    if (!status) ESCAPE ("RefMagLimit missing FPA.FILTER in concepts");
+
+    // need to select the PHOTCODE.DATA folder that matches our filter
+    psMetadataItem *item = psMetadataLookup (recipe, "PHOTCODE.DATA");
+    if (!item) ESCAPE ("RefMagLimit PHOTCODE.DATA folders missing");
+    if (item->type != PS_DATA_METADATA_MULTI) ESCAPE ("RefMagLimit PHOTCODE.DATA not a multi");
+
+    // PHOTCODE.DATA is a multi of metadata items
+    psListIterator *iter = psListIteratorAlloc(item->data.list, PS_LIST_HEAD, false);
+
+    psMetadataItem *refItem = NULL;
+    while ((refItem = psListGetAndIncrement (iter))) {
+        if (refItem->type != PS_DATA_METADATA) ESCAPE ("RefMagLimit PHOTCODE.DATA entry is not a metadata folder");
+
+        char *refFilter = psMetadataLookupStr (&status, refItem->data.md, "FILTER");
+        if (!status) {
+	    // always seems to happen once, commented out in ZeroPoint above as well
+            //psLogMsg ("psastro", PS_LOG_INFO, "RefMagLimit PHOTCODE.DATA recipe folder is missing FILTER");
+            continue;
+        }
+
+        // does this entry match the current filter?
+        if (strcmp (refFilter, filter)) continue;
+
+        psLogMsg ("psastro", PS_LOG_DETAIL, "RefMagLimit PHOTCODE.DATA found for filter %s", filter);
+
+        // -- may set one or other or both -- no reason to log if found or often not
+        tmpmin = psMetadataLookupF32 (&status, refItem->data.md, "REFSTAR.ZP.MIN.MAG");
+	if (status) {
+	    *refminMag = tmpmin;
+	} 
+        tmpmax = psMetadataLookupF32 (&status, refItem->data.md, "REFSTAR.ZP.MAX.MAG");
+	if (status) {
+	    *refmaxMag = tmpmax;
+	}
+        // once finds filter, should free and return true but multiple groups cause issue so just go through all groups now
+        //psFree (iter);
+        //return true;
+    }
+
+    // log out set values
+    psLogMsg ("psastro", PS_LOG_INFO, "using REFSTAR.ZP.MIN.MAG %f",*refminMag);
+    psLogMsg ("psastro", PS_LOG_INFO, "using REFSTAR.ZP.MAX.MAG %f",*refmaxMag);
+
+    psFree (iter);
+    return true;
+}
+
