Index: /trunk/ppSim/.cvsignore
===================================================================
--- /trunk/ppSim/.cvsignore	(revision 12833)
+++ /trunk/ppSim/.cvsignore	(revision 12833)
@@ -0,0 +1,14 @@
+aclocal.m4
+autom4te.cache
+config.guess
+config.log
+config.status
+config.sub
+configure
+depcomp
+install-sh
+libtool
+ltmain.sh
+Makefile
+Makefile.in
+missing
Index: /trunk/ppSim/Makefile.am
===================================================================
--- /trunk/ppSim/Makefile.am	(revision 12833)
+++ /trunk/ppSim/Makefile.am	(revision 12833)
@@ -0,0 +1,3 @@
+SUBDIRS = src
+
+CLEANFILES = *~ core core.*
Index: /trunk/ppSim/autogen.sh
===================================================================
--- /trunk/ppSim/autogen.sh	(revision 12833)
+++ /trunk/ppSim/autogen.sh	(revision 12833)
@@ -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=ppConfigDump
+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: /trunk/ppSim/configure.ac
===================================================================
--- /trunk/ppSim/configure.ac	(revision 12833)
+++ /trunk/ppSim/configure.ac	(revision 12833)
@@ -0,0 +1,29 @@
+AC_PREREQ(2.59)
+
+AC_INIT([ppConfigDump], [1.1.0], [ipp-support@ifa.hawaii.edu])
+AC_CONFIG_SRCDIR([src])
+
+AM_INIT_AUTOMAKE([1.6 foreign dist-bzip2])
+AM_CONFIG_HEADER([src/config.h])
+AM_MAINTAINER_MODE
+
+IPP_STDCFLAGS
+
+AC_LANG(C)
+AC_GNU_SOURCE
+AC_PROG_CC
+AC_PROG_INSTALL
+AC_PROG_LIBTOOL
+AC_SYS_LARGEFILE
+
+PKG_CHECK_MODULES([PSLIB], [pslib >= 1.0.0])
+PKG_CHECK_MODULES([PSMODULE], [psmodules >= 1.0.0])
+
+IPP_STDOPTS
+CFLAGS="${CFLAGS=} -Wall -Werror -std=c99"
+
+AC_CONFIG_FILES([
+  Makefile
+  src/Makefile
+])
+AC_OUTPUT
Index: /trunk/ppSim/src/.cvsignore
===================================================================
--- /trunk/ppSim/src/.cvsignore	(revision 12833)
+++ /trunk/ppSim/src/.cvsignore	(revision 12833)
@@ -0,0 +1,8 @@
+config.h
+config.h.in
+Makefile
+Makefile.in
+ppSim
+stamp-h1
+.deps
+.libs
Index: /trunk/ppSim/src/Makefile.am
===================================================================
--- /trunk/ppSim/src/Makefile.am	(revision 12833)
+++ /trunk/ppSim/src/Makefile.am	(revision 12833)
@@ -0,0 +1,21 @@
+bin_PROGRAMS = ppSim
+
+ppSim_CPPFLAGS = $(PSLIB_CFLAGS) $(PSMODULE_CFLAGS) $(ppSim_CFLAGS)
+ppSim_LDFLAGS = $(PSLIB_LIBS) $(PSMODULE_LIBS)
+ppSim_SOURCES = \
+	ppSim.c			\
+	ppSimArguments.c	\
+	ppSimCreate.c		\
+	ppSimLoop.c
+
+noinst_HEADERS = \
+	ppSim.h
+
+
+CLEANFILES = *~
+
+clean-local:
+	-rm -f TAGS
+# Tags for emacs
+tags:
+	etags `find . -name \*.[ch] -print`
Index: /trunk/ppSim/src/ppSim.c
===================================================================
--- /trunk/ppSim/src/ppSim.c	(revision 12833)
+++ /trunk/ppSim/src/ppSim.c	(revision 12833)
@@ -0,0 +1,40 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "ppSim.h"
+
+int main(int argc, char *argv[])
+{
+    pmConfig *config = pmConfigRead(&argc, argv, NULL); // Configuration
+    if (!config) {
+        psErrorStackPrint(stderr, "Unable to read configurations.");
+        exit(PS_EXIT_CONFIG_ERROR);
+    }
+
+    ppSimArguments(config);
+
+    if (!ppSimCreate(config)) {
+        psErrorStackPrint(stderr, "Unable to create output file.");
+        psFree(config);
+        exit(PS_EXIT_CONFIG_ERROR);
+    }
+
+    psExit loopError;
+    if ((loopError = ppSimLoop(config))) {
+        psErrorStackPrint(stderr, "Unable to generate data.");
+        psFree(config);
+        exit(loopError);
+    }
+
+    psFree(config);
+    pmConfigDone();
+    pmConceptsDone();
+    psLibFinalize();
+
+    return PS_EXIT_SUCCESS;
+}
Index: /trunk/ppSim/src/ppSim.h
===================================================================
--- /trunk/ppSim/src/ppSim.h	(revision 12833)
+++ /trunk/ppSim/src/ppSim.h	(revision 12833)
@@ -0,0 +1,35 @@
+#ifndef PP_SIM_H
+#define PP_SIM_H
+
+#define PPSIM_RECIPE "PPSIM"
+#define OUTPUT_FILE "PPSIM.OUTPUT"
+
+
+
+
+// Type of image to simulate
+typedef enum {
+    PPSIM_TYPE_BIAS,                    // Bias image
+    PPSIM_TYPE_DARK,                    // Dark image
+    PPSIM_TYPE_FLAT,                    // Flat-field image
+    PPSIM_TYPE_OBJECT                   // Object image
+} ppSimType;
+
+
+/// Parse command-line arguments
+void ppSimArguments(pmConfig *config ///< Configuration
+    );
+
+/// Create output file
+///
+/// Returns a borrowed pointer to the FPA file.
+pmFPAfile *ppSimCreate(const pmConfig *config ///< Configuration
+    );
+
+/// Loop over the output file, generating simulated data
+psExit ppSimLoop(pmConfig *config ///< Configuration
+    );
+
+
+
+#endif
Index: /trunk/ppSim/src/ppSimArguments.c
===================================================================
--- /trunk/ppSim/src/ppSimArguments.c	(revision 12833)
+++ /trunk/ppSim/src/ppSimArguments.c	(revision 12833)
@@ -0,0 +1,131 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <strings.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "ppSim.h"
+
+// Print usage information and die
+static void usage(psMetadata *arguments, // Command-line arguments
+                  pmConfig *config      // Configuration
+    )
+{
+    fprintf(stderr, "\nPan-STARRS data simulator\n\n");
+    fprintf(stderr, "Usage: %s -camera CAMERA_NAME -format FORMAT_NAME\n", config->argv[0]);
+    fprintf(stderr, "\n");
+    psArgumentHelp(arguments);
+    psFree(arguments);
+    psFree(config);
+    exit(PS_EXIT_CONFIG_ERROR);
+}
+
+// Get a value from the command-line arguments or the recipe, and add it to the target
+bool valueArgRecipe(pmConfig *config,   // Configuration
+                    psMetadata *arguments, // Command-line arguments
+                    const char *argName, // Argument name in the command-line arguments
+                    const psMetadata *recipe, // Recipe
+                    const char *recipeName, // Name for value in the recipe
+                    psMetadata *target // Target to which to add value
+    )
+{
+    float value = psMetadataLookupF32(NULL, arguments, argName); // Bias level
+    if (isnan(value)) {
+        value = psMetadataLookupF32(NULL, recipe, recipeName);
+        if (isnan(value)) {
+            psErrorStackPrint(stderr, "Unable to find %s in recipe %s", recipeName, PPSIM_RECIPE);
+            psFree((psPtr)arguments);
+            psFree(config);
+            exit(PS_EXIT_CONFIG_ERROR);
+        }
+    }
+
+    return psMetadataAddF32(target, PS_LIST_TAIL, recipeName, 0, NULL, value);
+}
+
+
+void ppSimArguments(pmConfig *config)
+{
+    assert(config);
+
+    psMetadata *arguments = psMetadataAlloc(); // Command-line arguments
+    psMetadataAddStr(arguments, PS_LIST_TAIL, "-format", 0, "Camera format name", NULL);
+    psMetadataAddStr(arguments, PS_LIST_TAIL, "-type", 0, "Exposure type (BIAS|DARK|FLAT|OBJECT)", NULL);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-exptime", 0, "Exposure time (s)", NAN);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-biaslevel", 0, "Bias level (e)", NAN);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-biasrange", 0, "Bias range (e)", NAN);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-darkrate", 0, "Dark rate (e/s)", NAN);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-flatsigma", 0, "Flat sigma", NAN);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-flatrate", 0, "Flat rate (e/s)", NAN);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-shuttertime", 0, "Shutter time (s)", NAN);
+    psMetadataAddF32(arguments, PS_LIST_TAIL, "-skyrate", 0, "Sky rate (e/s)", NAN);
+
+    if (!config->camera) {
+        psErrorStackPrint(stderr, "A camera name must be specified using the -camera option.");
+        usage(arguments, config);
+    }
+
+    if (*config->argc == 1 || !psArgumentParse(arguments, config->argc, config->argv)) {
+        usage(arguments, config);
+    }
+
+    const char *formatName = psMetadataLookupStr(NULL, arguments, "-format"); // Name of format
+    if (!formatName) {
+        psErrorStackPrint(stderr, "A camera format must be specified using -format");
+        usage(arguments, config);
+    }
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "FORMAT", 0, "Camera format name", formatName);
+
+    const char *typeStr = psMetadataLookupStr(NULL, arguments, "-type"); // Exposure type
+    if (!typeStr) {
+        psErrorStackPrint(stderr, "An exposure type must be specified using -type");
+        usage(arguments, config);
+    }
+    ppSimType type;                     // Type to simulate
+    if (strcasecmp(typeStr, "BIAS") == 0) {
+        type = PPSIM_TYPE_BIAS;
+    } else if (strcasecmp(typeStr, "DARK") == 0) {
+        type = PPSIM_TYPE_DARK;
+    } else if (strcasecmp(typeStr, "FLAT") == 0) {
+        type = PPSIM_TYPE_FLAT;
+    } else if (strcasecmp(typeStr, "OBJECT") == 0) {
+        type = PPSIM_TYPE_OBJECT;
+    } else {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unrecognised exposure type: %s", typeStr);
+        usage(arguments, config);
+    }
+    psMetadataAdd(config->arguments, PS_LIST_TAIL, "TYPE", 0, "Exposure type", type);
+
+    float expTime;
+    if (type == PPSIM_TYPE_BIAS) {
+        expTime = 0.0;
+    } else {
+        expTime = psMetadataLookupF32(NULL, arguments, "-exptime"); // Exposure time
+        if (isnan(expTime)) {
+            psErrorStackPrint(stderr, "The exposure time must be specified using -exptime");
+            usage(arguments, config);
+        }
+    }
+    psMetadataAdd(config->arguments, PS_LIST_TAIL, "EXPTIME", 0, "Exposure time (s)", expTime);
+
+    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, PPSIM_RECIPE); // Recipe for ppSim
+    if (!recipe) {
+        psErrorStackPrint(stderr, "Unable to find recipe %s", PPSIM_RECIPE);
+        psFree(arguments);
+        psFree(config);
+        exit(PS_EXIT_CONFIG_ERROR);
+    }
+
+    valueArgRecipe(config, arguments, "-biaslevel",   recipe, "BIAS.LEVEL",   config->arguments);
+    valueArgRecipe(config, arguments, "-biasrange",   recipe, "BIAS.RANGE",   config->arguments);
+    valueArgRecipe(config, arguments, "-darkrate",    recipe, "DARK.RATE",    config->arguments);
+    valueArgRecipe(config, arguments, "-flatsigma",   recipe, "FLAT.SIGMA",   config->arguments);
+    valueArgRecipe(config, arguments, "-flatrate",    recipe, "FLAT.RATE",    config->arguments);
+    valueArgRecipe(config, arguments, "-shuttertime", recipe, "SHUTTER.TIME", config->arguments);
+    valueArgRecipe(config, arguments, "-skyrate",     recipe, "SKY.RATE",     config->arguments);
+
+    psFree(arguments);
+}
Index: /trunk/ppSim/src/ppSimCreate.c
===================================================================
--- /trunk/ppSim/src/ppSimCreate.c	(revision 12833)
+++ /trunk/ppSim/src/ppSimCreate.c	(revision 12833)
@@ -0,0 +1,84 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "ppSim.h"
+
+pmFPAfile *ppSimCreate(const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    const char *formatName = psMetadataLookupStr(NULL, config->arguments, "FORMAT"); // Input format name
+
+    psMetadata *formats = psMetadataLookupMetadata(NULL, config->camera, "FORMATS"); // The camera formats
+    if (!formats) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find FORMATS in camera configuration.");
+        return NULL;
+    }
+    psMetadata *format = psMetadataLookupMetadata(NULL, formats, config->formatName); // The format of interest
+    if (!format) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find format %s in camera FORMATS.", formatName);
+        return NULL;
+    }
+    pmFPALevel phuLevel = pmFPAPHULevel(format); // Level at which PHU goes
+
+    pmFPA *fpa = pmFPAConstruct(config->camera); // FPA to contain the observation
+    if (!fpa) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to construct an FPA from camera configuration.");
+        return NULL;
+    }
+
+    pmFPAview *view = pmFPAviewAlloc(0);// View for current level
+
+    if (phuLevel == PM_FPA_LEVEL_FPA) {
+        if (!pmFPAAddSourceFromView(fpa, "Simulation", view, format)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to add PHU to FPA.");
+            psFree(fpa);
+            psFree(view);
+            return NULL;
+        }
+    }
+
+    pmChip *chip;                       // Chip from FPA
+    while ((chip = pmFPAviewNextChip(view, fpa, 1))) {
+        if (phuLevel == PM_FPA_LEVEL_CHIP) {
+            if (!pmFPAAddSourceFromView(fpa, "Simulation", view, format)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to add PHU to FPA.");
+                psFree(fpa);
+                psFree(view);
+                return NULL;
+            }
+        }
+
+        pmCell *cell;                   // Cell from chip
+        while ((cell = pmFPAviewNextCell(view, fpa, 1))) {
+            if (phuLevel == PM_FPA_LEVEL_CELL) {
+                if (!pmFPAAddSourceFromView(fpa, "Simulation", view, format)) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to add PHU to FPA.");
+                    psFree(fpa);
+                    psFree(view);
+                    return NULL;
+                }
+            }
+        }
+    }
+    psFree(view);
+
+    pmFPAfile *file = pmFPAfileDefineOutput(config, fpa, OUTPUT_FILE);
+    psFree(fpa);
+    if (!file) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to create output file from %s", OUTPUT_FILE);
+        return NULL;
+    }
+    if (file->type != PM_FPA_FILE_IMAGE) {
+        psError(PS_ERR_BAD_PARAMETER_TYPE, true, "%s type is not IMAGE", OUTPUT_FILE);
+        psFree(file);
+        return NULL;
+    }
+
+    return file;
+}
Index: /trunk/ppSim/src/ppSimLoop.c
===================================================================
--- /trunk/ppSim/src/ppSimLoop.c	(revision 12833)
+++ /trunk/ppSim/src/ppSimLoop.c	(revision 12833)
@@ -0,0 +1,268 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "ppSim.h"
+
+#define COMPARE(VALUE,MIN,MAX) \
+    if (VALUE < MIN) { \
+        MIN = VALUE; \
+    } \
+    if (VALUE > MAX) { \
+        MAX = VALUE; \
+    }
+
+// Return bounds of an FPA, based on the concepts
+static psRegion *fpaBounds(const pmFPA *fpa       // FPA for which to determine size
+    )
+{
+    assert(fpa);
+
+    pmFPAview *view = pmFPAviewAlloc(0);// View for iterating over FPA
+
+    // Bounds of focal plane
+    int xMin = INT_MAX;
+    int xMax = - INT_MAX;
+    int yMin = INT_MAX;
+    int yMax = - INT_MAX;
+
+    pmChip *chip;                       // Chip from FPA
+    while ((chip = pmFPAviewNextChip(view, fpa, 1))) {
+        int x0Chip = psMetadataLookupS32(NULL, chip->concepts, "CHIP.X0");
+        int y0Chip = psMetadataLookupS32(NULL, chip->concepts, "CHIP.Y0");
+        int xParityChip = psMetadataLookupS32(NULL, chip->concepts, "CHIP.XPARITY");
+        int yParityChip = psMetadataLookupS32(NULL, chip->concepts, "CHIP.YPARITY");
+
+        if ((xParityChip != 1 && xParityChip != -1) || (yParityChip != 1 && yParityChip != -1)) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Chip parities are not set.");
+            psFree(view);
+            return NULL;
+        }
+
+        pmCell *cell;                   // Cell from chip
+        while ((cell = pmFPAviewNextCell(view, fpa, 1))) {
+            int xSize = psMetadataLookupS32(NULL, cell->concepts, "CELL.XSIZE");
+            int ySize = psMetadataLookupS32(NULL, cell->concepts, "CELL.YSIZE");
+
+            if (xSize == 0 || ySize == 0) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Cell sizes are not set.");
+                psFree(view);
+                return NULL;
+            }
+
+            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");
+
+            if ((xParityCell != 1 && xParityCell != -1) || (yParityCell != 1 && yParityCell != -1)) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Cell parities are not set.");
+                psFree(view);
+                return NULL;
+            }
+
+            // "Left", "Right", "Bottom" and "Top" don't take into account the parity
+            int cellLeft = x0Chip + xParityChip * x0Cell;
+            int cellRight = x0Chip + xParityChip * (x0Cell + xParityCell * xSize);
+            int cellBottom = y0Chip + yParityChip * y0Cell;
+            int cellTop = y0Chip + yParityChip * (y0Cell + yParityCell * ySize);
+
+            COMPARE(cellLeft, xMin, xMax);
+            COMPARE(cellRight, xMin, xMax);
+            COMPARE(cellBottom, yMin, yMax);
+            COMPARE(cellTop, yMin, yMax);
+        }
+    }
+
+    return psRegionAlloc(xMin, xMax, yMin, yMax);
+}
+
+
+psExit ppSimLoop(pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, PS_EXIT_PROG_ERROR);
+
+    pmFPAfile *file = psMetadataLookupPtr(NULL, config->files, OUTPUT_FILE); // Output file
+    assert(file);
+    pmFPA *fpa = file->fpa;             // FPA for file
+    assert(fpa);
+
+    psRegion *bounds = fpaBounds(fpa);  // Bounds of FPA
+    if (!bounds) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to determine bounds of FPA");
+        return PS_EXIT_CONFIG_ERROR;
+    }
+
+    float biasLevel = psMetadataLookupF32(NULL, config->arguments, "BIAS.LEVEL"); // Bias level
+    float biasRange = psMetadataLookupF32(NULL, config->arguments, "BIAS.RANGE"); // Bias range
+    float darkRate = psMetadataLookupF32(NULL, config->arguments, "DARK.RATE"); // Dark rate
+    float flatSigma = psMetadataLookupF32(NULL, config->arguments, "FLAT.SIGMA"); // Flat-field coefficient
+    float flatRate = psMetadataLookupF32(NULL, config->arguments, "FLAT.RATE"); // Flat-field rate
+    float shutterTime = psMetadataLookupF32(NULL, config->arguments, "SHUTTER.TIME"); // Shutter time
+    float skyRate = psMetadataLookupF32(NULL, config->arguments, "SKY.RATE"); // Sky rate
+    float expTime = psMetadataLookupF32(NULL, config->arguments, "EXPTIME"); // Exposure time
+    ppSimType type = psMetadataLookupS32(NULL, config->arguments, "TYPE"); // Type of image to simulate
+
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 0); // Random number generator
+    pmFPAview *view = pmFPAviewAlloc(0);// View for iterating over FPA
+
+    pmChip *chip;                       // Chip from FPA
+    while ((chip = pmFPAviewNextChip(view, fpa, 1))) {
+
+        int x0Chip = psMetadataLookupS32(NULL, chip->concepts, "CHIP.X0");
+        int y0Chip = psMetadataLookupS32(NULL, chip->concepts, "CHIP.Y0");
+        int xParityChip = psMetadataLookupS32(NULL, chip->concepts, "CHIP.XPARITY");
+        int yParityChip = psMetadataLookupS32(NULL, chip->concepts, "CHIP.YPARITY");
+
+        pmCell *cell;                   // Cell from chip
+        while ((cell = pmFPAviewNextCell(view, fpa, 1))) {
+
+            // Size, position and orientation of cell
+            int numCols = psMetadataLookupS32(NULL, cell->concepts, "CELL.XSIZE");
+            int numRows = psMetadataLookupS32(NULL, cell->concepts, "CELL.YSIZE");
+            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");
+
+            float gain = psMetadataLookupF32(NULL, cell->concepts, "CELL.GAIN"); // CCD gain, e/ADU
+            if (isnan(gain)) {
+                psWarning("CELL.GAIN is not set; reverting to recipe value.");
+                gain = psMetadataLookupF32(NULL, config->arguments, "GAIN");
+            }
+            float readnoise = psMetadataLookupF32(NULL, cell->concepts, "CELL.RDNOISE"); // CCD read noise, e
+            if (isnan(readnoise)) {
+                psWarning("CELL.RDNOISE is not set; reverting to recipe value.");
+                readnoise = psMetadataLookupF32(NULL, config->arguments, "RDNOISE");
+            }
+
+            // TO DO: Decide if cell is to be windowed, reduce numCols, numRows appropriately
+
+            // TO DO: Decide if cell is to be video readout
+            int numReadouts = 0.0;      // Number of readouts in cell
+
+            for (int i = 0; i < numReadouts; i++) {
+                pmReadout *readout = pmReadoutAlloc(cell); // Readout within cell
+
+                psImage *signal = psImageAlloc(numCols, numRows, PS_TYPE_F32); // Signal in pixels
+                psImage *variance = psImageAlloc(numCols, numRows, PS_TYPE_F32); // Noise in pixels
+
+                for (int y = 0; y < numRows; y++) {
+                    // Adjust bias level for this row
+                    float rowBias = biasLevel += psRandomGaussian(rng) * biasRange;
+                    float yFPA = (y0Chip + yParityChip * (y0Cell + yParityCell * y) - 2 * bounds->y0) /
+                        (bounds->y1 - bounds->y0); // Relative y position in FPA
+
+                    for (int x = 0; x < numCols; x++) {
+                        float xFPA = (x0Chip + xParityChip * (x0Cell + xParityCell * x) - 2 * bounds->x0) /
+                            (bounds->x1 - bounds->x0); // Relative x position in FPA
+
+                        // Bias level
+                        signal->data.F32[y][x] = rowBias;
+                        variance->data.F32[y][x] = PS_SQR(readnoise);
+
+                        if (type == PPSIM_TYPE_BIAS) {
+                            continue;
+                        }
+
+                        // Dark current
+                        float darkCurrent = darkRate * expTime; // Dark current accumulated
+                        signal->data.F32[y][x] += darkCurrent;
+                        variance->data.F32[y][x] += darkCurrent;
+
+                        if (type == PPSIM_TYPE_DARK) {
+                            continue;
+                        }
+
+                        // Shutter: adjust exposure time
+                        float realExpTime = expTime + shutterTime * (yFPA + 1.0) * (xFPA + 1.0) / 4.0;
+
+                        // Gaussian flat-field over the FPA
+                        float flatValue = exp(-0.5 / PS_SQR(flatSigma) *
+                                              (PS_SQR(yFPA) + PS_SQR(xFPA))) / flatSigma / sqrt(M_PI);
+                        if (type == PPSIM_TYPE_FLAT) {
+                            float flatFlux = flatRate * flatValue * realExpTime; // Flux from flat-field
+                            signal->data.F32[y][x] += flatFlux;
+                            variance->data.F32[y][x] += flatFlux;
+                            continue;
+                        }
+
+                        // Sky background
+                        float skyFlux = skyRate * realExpTime * flatValue; // Flux from sky
+                        signal->data.F32[y][x] += skyFlux;
+                        variance->data.F32[y][x] += skyFlux;
+
+                        // TO DO: Add fringes
+
+                        // TO DO: Add stars
+                    }
+                }
+
+                // Add the noise into the image
+                for (int y = 0; y < numRows; y++) {
+                    for (int x = 0; x < numCols; x++) {
+                        signal->data.F32[y][x] /= gain; // Converting to ADU
+                        signal->data.F32[y][x] += sqrtf(variance->data.F32[y][x] / gain) *
+                            psRandomGaussian(rng);
+                    }
+                }
+                readout->image = signal;
+                psFree(variance);
+
+                psFree(readout);        // Drop reference
+            }
+
+            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.EXPTIME", 0, "Exposure time (sec)", expTime);
+            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.DARKTIME", 0, "Dark time (sec)", expTime);
+
+            if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
+                psError(PS_ERR_IO, false, "Unable to write file.");
+                psFree(rng);
+                psFree(view);
+                psFree(bounds);
+                return PS_EXIT_SYS_ERROR;
+            }
+        }
+
+        if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
+            psError(PS_ERR_IO, false, "Unable to write file.");
+            psFree(rng);
+            psFree(view);
+            psFree(bounds);
+            return PS_EXIT_SYS_ERROR;
+        }
+
+    }
+
+    const char *typeStr;                // Exposure type String
+    switch (type) {
+      case PPSIM_TYPE_BIAS:   typeStr = "BIAS";   break;
+      case PPSIM_TYPE_DARK:   typeStr = "DARK";   break;
+      case PPSIM_TYPE_FLAT:   typeStr = "FLAT";   break;
+      case PPSIM_TYPE_OBJECT: typeStr = "OBJECT"; break;
+      default:
+        psAbort("Should never get here.");
+    }
+
+    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.OBSTYPE", 0, "Observation type", typeStr);
+    psMetadataAddF32(fpa->concepts, PS_LIST_TAIL, "FPA.EXPTIME", 0, "Exposure time (sec)", expTime);
+
+            if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
+                psError(PS_ERR_IO, false, "Unable to write file.");
+                psFree(rng);
+                psFree(view);
+                psFree(bounds);
+                return PS_EXIT_SYS_ERROR;
+            }
+
+
+    psFree(rng);
+    psFree(view);
+    psFree(bounds);
+
+    return 0;
+}
