Index: /trunk/archive/scripts/src/phase2/ipprc.config
===================================================================
--- /trunk/archive/scripts/src/phase2/ipprc.config	(revision 5105)
+++ /trunk/archive/scripts/src/phase2/ipprc.config	(revision 5105)
@@ -0,0 +1,25 @@
+### Example .ipprc file
+
+### Database configuration
+DBSERVER	STR	ippdb.ifa.hawaii.edu	# Database host name (for psDBInit)
+DBUSER		STR	ipp			# Database user name (for psDBInit)
+DBPASSWORD	STR	password		# Database password (for psDBInit)
+
+### Setups for each camera system
+CAMERAS		METADATA
+	MEGACAM_RAW	STR	megacam_raw.config
+#	MEGACAM_SPLICE	STR	megacam_splice.config
+#	GPC1_RAW	STR	gpc1_raw.config
+#	LRIS_BLUE	STR	lris_blue.config
+#	LRIS_RED	STR	lris_red.config
+END
+
+### psLib setup
+#TIME		STR	/home/mithrandir/price/pan-starrs/jhroot/i686-pc-linux-gnu/etc/pslib/psTime.config	# Time configuration file
+LOGLEVEL	S32	3			# Logging level; 3=INFO
+LOGFORMAT	STR	THLNM			# Log format
+LOGDEST	STR	STDOUT				# Log destination
+TRACE		METADATA			# Trace levels
+	pmConfigRead		S32	10
+	pmFPARead		S32	10
+END
Index: /trunk/archive/scripts/src/phase2/megacam_raw.config
===================================================================
--- /trunk/archive/scripts/src/phase2/megacam_raw.config	(revision 5105)
+++ /trunk/archive/scripts/src/phase2/megacam_raw.config	(revision 5105)
@@ -0,0 +1,160 @@
+# The raw MegaCam data comes off the telescope with each of the chips stored in extensions of a MEF file.
+
+# How to identify this type
+RULE	METADATA
+	TELESCOP	STR	CFHT 3.6m
+	DETECTOR	STR	MegaCam
+	EXTEND		BOOL	T
+	NEXTEND		S32	72
+END
+
+# How to read this data
+PHU		STR	FPA	# The FITS file represents an entire FPA
+EXTENSIONS	STR	CELL	# The extensions represent cells
+
+# What's in the FITS file?
+CONTENTS	METADATA
+	# Extension name, chip name:type
+	amp00	STR	ccd00:left
+	amp01	STR	ccd00:right
+	amp02	STR	ccd01:left
+	amp03	STR	ccd01:right
+	amp04	STR	ccd02:left
+	amp05	STR	ccd02:right
+	amp06	STR	ccd03:left
+	amp07	STR	ccd03:right
+	amp08	STR	ccd04:left
+	amp09	STR	ccd04:right
+	amp10	STR	ccd05:left
+	amp11	STR	ccd05:right
+	amp12	STR	ccd06:left
+	amp13	STR	ccd06:right
+	amp14	STR	ccd07:left
+	amp15	STR	ccd07:right
+	amp16	STR	ccd08:left
+	amp17	STR	ccd08:right
+	amp18	STR	ccd09:left
+	amp19	STR	ccd09:right
+	amp20	STR	ccd10:left
+	amp21	STR	ccd10:right
+	amp22	STR	ccd11:left
+	amp23	STR	ccd11:right
+	amp24	STR	ccd12:left
+	amp25	STR	ccd12:right
+	amp26	STR	ccd13:left
+	amp27	STR	ccd13:right
+	amp28	STR	ccd14:left
+	amp29	STR	ccd14:right
+	amp30	STR	ccd15:left
+	amp31	STR	ccd15:right
+	amp32	STR	ccd16:left
+	amp33	STR	ccd16:right
+	amp34	STR	ccd17:left
+	amp35	STR	ccd17:right
+	amp36	STR	ccd18:left
+	amp37	STR	ccd18:right
+	amp38	STR	ccd19:left
+	amp39	STR	ccd19:right
+	amp40	STR	ccd20:left
+	amp41	STR	ccd20:right
+	amp42	STR	ccd21:left
+	amp43	STR	ccd21:right
+	amp44	STR	ccd22:left
+	amp45	STR	ccd22:right
+	amp46	STR	ccd23:left
+	amp47	STR	ccd23:right
+	amp48	STR	ccd24:left
+	amp49	STR	ccd24:right
+	amp50	STR	ccd25:left
+	amp51	STR	ccd25:right
+	amp52	STR	ccd26:left
+	amp53	STR	ccd26:right
+	amp54	STR	ccd27:left
+	amp55	STR	ccd27:right
+	amp56	STR	ccd28:left
+	amp57	STR	ccd28:right
+	amp58	STR	ccd29:left
+	amp59	STR	ccd29:right
+	amp60	STR	ccd30:left
+	amp61	STR	ccd30:right
+	amp62	STR	ccd31:left
+	amp63	STR	ccd31:right
+	amp64	STR	ccd32:left
+	amp65	STR	ccd32:right
+	amp66	STR	ccd33:left
+	amp67	STR	ccd33:right
+	amp68	STR	ccd34:left
+	amp69	STR	ccd34:right
+	amp70	STR	ccd35:left
+	amp71	STR	ccd35:right
+END
+
+# Specify the cell data
+CELLS	METADATA
+	left	METADATA	# Left amplifier
+		CELL.BIASSEC.SOURCE	STR	HEADER
+		CELL.TRIMSEC.SOURCE	STR	HEADER
+		CELL.BIASSEC		STR	BIASSEC
+		CELL.TRIMSEC		STR	DATASEC
+		CELL.XPARITY		S32	1 # We could have specified this as a DEFAULT, but this works
+	END
+	right	METADATA	# Right amplifier
+		CELL.BIASSEC.SOURCE	STR	HEADER
+		CELL.TRIMSEC.SOURCE	STR	HEADER
+		CELL.BIASSEC		STR	BIASSEC
+		CELL.TRIMSEC		STR	DATASEC
+		CELL.XPARITY		S32	-1 # This cell is read out in the opposite direction
+	END
+END
+
+# How to translate PS concepts into FITS headers
+TRANSLATION	METADATA
+	FPA.NAME		STR	EXPNUM
+	FPA.AIRMASS		STR	AIRMASS
+	FPA.FILTER		STR	FILTER
+	FPA.POSANGLE		STR	ROTANGLE
+	FPA.RA			STR	RA
+	FPA.DEC			STR	DEC
+	FPA.RADECSYS		STR	RADECSYS
+	CELL.EXPOSURE		STR	EXPTIME
+	CELL.DARKTIME		STR	DARKTIME
+	CELL.GAIN		STR	GAIN
+	CELL.READNOISE		STR	RDNOISE
+	CELL.SATURATION		STR	SATURATE
+	CELL.TIME		STR	MJD-OBS
+	CELL.XBIN		STR	CCDBIN1
+	CELL.YBIN		STR	CCDBIN2
+END
+
+# Default PS concepts that may be specified by value
+DEFAULTS	METADATA
+	CELL.READDIR		S32	2		# Cell is read in x direction
+	CELL.BAD		S32	0
+	CELL.TIMESYS		STR	UTC
+	CELL.YPARITY		S32	1
+END
+
+# How to translation PS concepts into database lookups
+DATABASE	METADATA
+	TYPE		dbEntry		TABLE		COLUMN		GIVENDBCOL	GIVENPS
+#	CELL.GAIN	dbEntry		Camera		gain		chipId,cellId	CHIP.NAME,CELL.NAME
+#	CELL.READNOISE	dbEntry		Camera		readNoise	chipId,cellId	CHIP.NAME,CELL.NAME
+
+# A database entry refers to a particular column (COLUMN) in a
+# particular table (TABLE), given certain PS concepts (GIVENPS) that
+# match certain database columns (GIVENDBCOL).
+END
+
+
+# Where there might be some ambiguity, specify the format
+FORMATS		METADATA
+	FPA.RA		STR	HOURS
+	FPA.DEC		STR	DEGREES
+	CELL.TIME	STR	MJD
+#	CELL.BINNING	STR	SEPARATE
+END
+
+# Recipe options
+RECIPES		METADATA
+	PHASE2		STR	phase2.config		# Phase 2 recipe details
+END
Index: /trunk/archive/scripts/src/phase2/phase2.config
===================================================================
--- /trunk/archive/scripts/src/phase2/phase2.config	(revision 5105)
+++ /trunk/archive/scripts/src/phase2/phase2.config	(revision 5105)
@@ -0,0 +1,62 @@
+### Phase 2 recipe configuration file
+
+# List of tasks to perform
+MASK		BOOL	FALSE		# Mask bad pixels
+NONLIN		BOOL	FALSE		# Non-linearity correction
+BIAS		BOOL	FALSE		# Bias subtraction
+DARK		BOOL	FALSE		# Dark subtraction
+OVERSCAN	BOOL	TRUE		# Overscan subtraction
+FLAT		BOOL	FALSE		# Flat-field normalisation
+FRINGE		BOOL	FALSE		# Fringe subtraction
+SOURCE		BOOL	FALSE		# Source identification and photometry
+ASTROM		BOOL	FALSE		# Astrometry
+
+
+# Non-linearity correction
+NONLIN.SOURCE		STR	CHIP.NAME	# How to determine the source
+# @NONLIN.SOURCE	STR	0.0 1.001 0.001	# A polynomial
+NONLIN.DATA		METADATA		# Source of non-linearity data
+	ccd00		STR	nonlin00.dat	# A lookup table 
+	@ccd01		F32	0.0 1.001 0.001	# A polynomial
+	@ccd02		F32	0.0 1.001 0.001	# A polynomial
+	@ccd03		F32	0.0 1.001 0.001	# A polynomial
+	@ccd04		F32	0.0 1.001 0.001	# A polynomial
+	@ccd05		F32	0.0 1.001 0.001	# A polynomial
+	@ccd06		F32	0.0 1.001 0.001	# A polynomial
+	@ccd07		F32	0.0 1.001 0.001	# A polynomial
+	@ccd08		F32	0.0 1.001 0.001	# A polynomial
+	@ccd09		F32	0.0 1.001 0.001	# A polynomial
+	@ccd10		F32	0.0 1.001 0.001	# A polynomial
+	@ccd11		F32	0.0 1.001 0.001	# A polynomial
+	@ccd12		F32	0.0 1.001 0.001	# A polynomial
+	@ccd13		F32	0.0 1.001 0.001	# A polynomial
+	@ccd14		F32	0.0 1.001 0.001	# A polynomial
+	@ccd15		F32	0.0 1.001 0.001	# A polynomial
+	@ccd16		F32	0.0 1.001 0.001	# A polynomial
+	@ccd17		F32	0.0 1.001 0.001	# A polynomial
+	@ccd18		F32	0.0 1.001 0.001	# A polynomial
+	@ccd19		F32	0.0 1.001 0.001	# A polynomial
+	@ccd10		F32	0.0 1.001 0.001	# A polynomial
+	@ccd21		F32	0.0 1.001 0.001	# A polynomial
+	@ccd22		F32	0.0 1.001 0.001	# A polynomial
+	@ccd23		F32	0.0 1.001 0.001	# A polynomial
+	@ccd24		F32	0.0 1.001 0.001	# A polynomial
+	@ccd25		F32	0.0 1.001 0.001	# A polynomial
+	@ccd26		F32	0.0 1.001 0.001	# A polynomial
+	@ccd27		F32	0.0 1.001 0.001	# A polynomial
+	@ccd28		F32	0.0 1.001 0.001	# A polynomial
+	@ccd29		F32	0.0 1.001 0.001	# A polynomial
+	@ccd30		F32	0.0 1.001 0.001	# A polynomial
+	@ccd31		F32	0.0 1.001 0.001	# A polynomial
+	@ccd32		F32	0.0 1.001 0.001	# A polynomial
+	@ccd33		F32	0.0 1.001 0.001	# A polynomial
+	@ccd34		F32	0.0 1.001 0.001	# A polynomial
+	@ccd35		F32	0.0 1.001 0.001	# A polynomial
+	@ccd36		F32	0.0 1.001 0.001	# A polynomial
+END
+
+# Overscan subtraction
+OVERSCAN.MODE		STR	INDIVIDUAL	# NONE | INDIVIDUAL | ALL
+OVERSCAN.FIT		STR	NONE		# NONE | POLYNOMIAL | SPLINE
+OVERSCAN.BIN		S32	4		# Number of pixels per bin
+OVERSCAN.STAT		STR	MEAN		# MEAN | MEDIAN
Index: /trunk/archive/scripts/src/phase2/pmFPAConceptsGet.c
===================================================================
--- /trunk/archive/scripts/src/phase2/pmFPAConceptsGet.c	(revision 5105)
+++ /trunk/archive/scripts/src/phase2/pmFPAConceptsGet.c	(revision 5105)
@@ -0,0 +1,990 @@
+#include <stdio.h>
+#include <string.h>
+#include "pslib.h"
+
+#include "pmFPA.h"
+
+#include "papStuff.h"
+
+#include "pmFPAConceptsGet.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Private functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+psMetadataItem *p_pmFPAConceptGetFromCamera(pmCell *cell, // The cell
+					    const char *concept // Name of concept
+    )
+{
+    if (cell) {
+	psMetadata *camera = cell->camera;	// Camera data
+	// Need the CELL.NAME first, which should be in the cell->concepts since creation of the cell
+	bool mdStatus = true;		// Status of MD lookup
+	// Finally, the info that we want
+	psMetadataItem *item = psMetadataLookup(camera, concept);
+	return item;
+    }
+    return NULL;
+}   
+
+
+psMetadataItem *p_pmFPAConceptGetFromHeader(pmFPA *fpa, // The FPA that contains the chip
+					    pmChip *chip, // The chip that contains the cell
+					    pmCell *cell, // The cell
+					    const char *concept // Name of concept
+    )
+{
+    bool mdStatus = true;		// Status of MD lookup
+    psMetadata *translation = psMetadataLookupMD(&mdStatus, fpa->camera, "TRANSLATION"); // FITS translation
+    if (! mdStatus) {
+	psError(PS_ERR_IO, false, "Unable to find TRANSLATION in camera configuration.\n");
+	return NULL;
+    }
+
+    // Look for how to translate the concept into a FITS header name
+    const char *keyword = psMetadataLookupString(&mdStatus, translation, concept);
+    if (mdStatus && strlen(keyword) > 0) {
+	// We have a FITS header to look up --- search each level
+	if (cell && cell->private) {
+	    psMetadataItem *cellItem = psMetadataLookup(cell->private->header, keyword);
+	    if (cellItem) {
+		// XXX: Need to clean up before returning
+		return cellItem;
+	    }
+	}
+
+	if (chip && chip->private) {
+	    psMetadataItem *chipItem = psMetadataLookup(chip->private->header, keyword);
+	    if (chipItem) {
+		// XXX: Need to clean up before returning
+		return chipItem;
+	    }
+	}
+
+	if (fpa->private) {
+	    psMetadataItem *fpaItem = psMetadataLookup(fpa->private->header, keyword);
+	    if (fpaItem) {
+		// XXX: Need to clean up before returning
+		return fpaItem;
+	    }
+	}
+
+	if (fpa->phu) {
+	    psMetadataItem *fpaItem = psMetadataLookup(fpa->phu, keyword);
+	    if (fpaItem) {
+		// XXX: Need to clean up before returning
+		return fpaItem;
+	    }
+	}
+    }
+
+    // No header value
+    return NULL;
+}
+
+
+// Look for a default
+psMetadataItem *p_pmFPAConceptGetFromDefault(pmFPA *fpa, // The FPA that contains the chip
+					     pmChip *chip, // The chip that contains the cell
+					     pmCell *cell, // The cell
+					     const char *concept // Name of concept
+    )
+{
+    bool mdOK = true;			// Status of MD lookup
+    psMetadata *defaults = psMetadataLookupMD(&mdOK, fpa->camera, "DEFAULTS");
+    if (! mdOK) {
+	psError(PS_ERR_IO, false, "Unable to find DEFAULTS in camera configuration.\n");
+	return NULL;
+    }
+
+    psMetadataItem *defItem = psMetadataLookup(defaults, concept);
+    if (defItem) {
+	if (defItem->type == PS_META_META) {
+	    // A dependent default
+	    psTrace(__func__, 7, "Evaluating dependent default....\n");
+	    psMetadata *dependents = defItem->data.V; // The list of dependents
+	    // Find out what it depends on
+	    psString dependName = psStringCopy(concept);
+	    psStringAppend(&dependName, ".DEPEND");
+	    psString dependsOn = psMetadataLookupString(&mdOK, defaults, dependName);
+	    if (! mdOK) {
+		psError(PS_ERR_IO, false, "Unable to find %s in camera configuration for dependent default"
+			" --- ignored\n", dependName);
+		// XXX: Need to clean up before returning
+		return NULL;
+	    }
+	    psFree(dependName);
+	    // Find the value of the dependent concept
+	    psMetadataItem *depItem = p_pmFPAConceptGetFromHeader(fpa, chip, cell, dependsOn);
+	    if (! depItem) {
+		psError(PS_ERR_IO, true, "Unable to find value for %s (required for %s)\n", dependsOn,
+			concept);
+		return NULL;
+	    }
+	    if (depItem->type != PS_META_STR) {
+		psError(PS_ERR_IO, true, "Value of %s is not of type string, as required for dependency"
+			" --- ignored.\n", dependsOn);
+	    }
+
+	    defItem = psMetadataLookup(dependents, depItem->data.V);	// This is now what we were after
+	}
+    }
+
+    // XXX: Need to clean up before returning
+    return defItem;			// defItem is either NULL or points to what was desired
+}
+
+
+// Look for a database lookup
+// XXX: Not tested
+psMetadataItem *p_pmFPAConceptGetFromDB(pmFPA *fpa, // The FPA that contains the chip
+					pmChip *chip, // The chip that contains the cell
+					pmCell *cell, // The cell
+					psDB *db,	// DB handle
+					const char *concept // Name of concept
+    )
+{
+    if (! db) {
+	// No database initialised
+	return NULL;
+    }
+
+    bool mdStatus = true;		// Status of MD lookup
+    psMetadata *database = psMetadataLookupMD(&mdStatus, fpa->camera, "DATABASE");
+    if (! mdStatus) {
+	// No error, because not everyone needs to use the DB
+	return NULL;
+    }
+
+    psMetadata *dbLookup = psMetadataLookupMD(&mdStatus, database, concept);
+    if (dbLookup) {
+	const char *tableName = psMetadataLookupString(&mdStatus, dbLookup, "TABLE"); // Name of the table
+	const char *colName = psMetadataLookupString(&mdStatus, dbLookup, "COLUMN"); // Name of the column
+	const char *givenCols = psMetadataLookupString(&mdStatus, dbLookup, "GIVENDBCOL"); // Name of "where"
+											   // columns
+	const char *givenPS = psMetadataLookupString(&mdStatus, dbLookup, "GIVENPS"); // Values for "where"
+										      // columns
+	
+	// Now, need to get the "given"s
+	if (strlen(givenCols) || strlen(givenPS)) {
+	    psList *cols = papSplit(givenCols, ",;"); // List of column names
+	    psList *values = papSplit(givenPS, ",;"); // List of value names for the columns
+	    psMetadata *selection = psMetadataAlloc(); // The stuff to select in the DB
+	    if (cols->n != values->n) {
+		psLogMsg(__func__, PS_LOG_WARN, "The GIVENDBCOL and GIVENPS entries for %s do not have "
+			 "the same number of entries --- ignored.\n", concept);
+	    } else {
+		// Iterators for the lists
+		psListIterator *colsIter = psListIteratorAlloc(cols, PS_LIST_HEAD, false);
+		psListIterator *valuesIter = psListIteratorAlloc(values, PS_LIST_HEAD, false);
+		char *column = NULL;	// Name of the column
+		while (column = psListGetAndIncrement(colsIter)) {
+		    char *name = psListGetAndIncrement(valuesIter); // Name for the value
+		    if (!strlen(column) || !strlen(name)) {
+			psLogMsg(__func__, PS_LOG_WARN, "One of the columns or value names for %s is "
+				 " empty --- ignored.\n", concept);
+		    } else {
+			// Search for the value name
+			psMetadataItem *item = p_pmFPAConceptGetFromHeader(fpa, chip, cell, name);
+			if (! item) {
+			    item = p_pmFPAConceptGetFromDefault(fpa, chip, cell, name);
+			}
+			if (! item) {
+			    psLogMsg(__func__, PS_LOG_ERROR, "Unable to find the value name %s for DB "
+				     " lookup on %s --- ignored.\n", name, concept);
+			} else {
+			    // We need to create a new psMetadataItem.  I don't think we can't simply hack
+			    // the existing one, since that could conceivably cause memory leaks
+			    psMetadataItem *newItem = psMetadataItemAlloc(concept, item->type,
+									  item->comment, item->data.V);
+			    psMetadataAddItem(selection, newItem, PS_LIST_TAIL, PS_META_REPLACE);
+			    psFree(newItem);
+			}
+		    }
+		    psFree(name);
+		    psFree(column);
+		} // Iterating through the columns
+		psFree(colsIter);
+		psFree(valuesIter);
+		
+		psArray *dbResult = psDBSelectRows(db, tableName, selection, 2); // Lookup result
+		// Note that we use limit=2 in order to test if there are multiple rows returned
+		
+		psMetadataItem *result = NULL; // The final result of the DB lookup
+		if (dbResult->n == 0) {
+		    psLogMsg(__func__, PS_LOG_WARN, "Unable to find any rows in DB for %s --- ignored\n", 
+			     concept);
+		} else {
+		    if (dbResult-> n > 1) {
+			psLogMsg(__func__, PS_LOG_WARN, "Multiple rows returned in DB lookup for %s --- "
+				 " using the first one only.\n", concept);
+		    }
+		    result = (psMetadataItem*)dbResult->data[0];
+		}
+		// XXX: Need to clean up before returning
+		return result;
+	    }
+	    psFree(cols);
+	    psFree(values);
+	}
+    } // Doing the "given"s.
+
+    psAbort(__func__, "Shouldn't ever get here.\n");
+}
+
+
+// Concept lookup
+psMetadataItem *p_pmFPAConceptGet(pmFPA *fpa, // The FPA
+				  pmChip *chip,// The chip
+				  pmCell *cell,	// The cell
+				  psDB *db, // DB handle
+				  const char *concept // Concept name
+    )
+{
+    // Try headers, database, defaults in order
+    psMetadataItem *item = p_pmFPAConceptGetFromCamera(cell, concept);
+    if (! item) {
+	item = p_pmFPAConceptGetFromHeader(fpa, chip, cell, concept);
+    }
+    if (! item) {
+        item = p_pmFPAConceptGetFromDB(fpa, chip, cell, db, concept);
+    }
+    if (! item) {
+        item = p_pmFPAConceptGetFromDefault(fpa, chip, cell, concept);
+    }
+    return item; // item is either NULL, or points to what was desired
+}
+
+
+void p_pmFPAConceptGetF32(pmFPA *fpa,	// The FPA
+			  pmChip *chip, // The chip
+			  pmCell *cell, // The cell
+			  psDB *db,	// DB handle
+			  psMetadata *concepts, // The concepts MD
+			  const char *name, // Name of the concept
+			  const char *comment // Comment for concept
+    )
+{
+    psMetadataItem *item = p_pmFPAConceptGet(fpa, chip, cell, db, name);
+    float value = NAN;
+    if (item) {
+	switch (item->type) {
+	  case PS_META_F32:
+	    value = item->data.F32;
+	    break;
+	  case PS_META_F64:
+	    // Assume it's OK to truncate to floating point from double
+	    value = (float)item->data.F64;
+	    break;
+	  case PS_META_S32:
+	    // Promote to float
+	    value = (float)item->data.S32;
+	    break;
+	  default:
+	    psError(PS_ERR_IO, true, "Concept %s (%s) is not of floating point type (%x) --- treating as "
+		    "undefined.\n", name, comment, item->type);
+	}
+    } else {
+	psError(PS_ERR_IO, true, "Concept %s (%s) is not defined.\n", name, comment);
+    }
+    psTrace(__func__, 7, "Adding %s (%s): %f\n", name, comment, value);
+
+    psMetadataAdd(concepts, PS_LIST_TAIL, name, PS_TYPE_F32, comment, value);
+}
+
+void p_pmFPAConceptGetF64(pmFPA *fpa,	// The FPA
+			  pmChip *chip, // The chip
+			  pmCell *cell, // The cell
+			  psDB *db,	// DB handle
+			  psMetadata *concepts, // The concepts MD
+			  const char *name, // Name of the concept
+			  const char *comment // Comment for concept
+    )
+{
+    psMetadataItem *item = p_pmFPAConceptGet(fpa, chip, cell, db, name);
+    double value = NAN;
+    if (item) {
+	switch (item->type) {
+	  case PS_TYPE_F64:
+	    value = item->data.F64;
+	    break;
+	  case PS_TYPE_F32:
+	    // Promote to double
+	    value = (double)item->data.F32;
+	    break;
+	  case PS_TYPE_S32:
+	    // Promote to double
+	    value = (double)item->data.S32;
+	    break;
+	  default:
+	    psError(PS_ERR_IO, true, "Concept %s (%s) is not of double-precision floating point type (%x) "
+		    "--- treating as undefined.\n", name, comment, item->type);
+	}
+    } else {
+	psError(PS_ERR_IO, true, "Concept %s (%s) is not defined.\n", name, comment);
+    }
+    psTrace(__func__, 7, "Adding %s (%s): %f\n", name, comment, value);
+
+    psMetadataAdd(concepts, PS_LIST_TAIL, name, PS_TYPE_F64, comment, value);
+}
+
+void p_pmFPAConceptGetS32(pmFPA *fpa,	// The FPA
+			  pmChip *chip,	// The chip
+			  pmCell *cell,	// The cell
+			  psDB *db,	// DB handle
+			  psMetadata *concepts, // The concepts MD
+			  const char *name, // Name of the concept
+			  const char *comment // Comment for concept
+    )
+{
+    psMetadataItem *item = p_pmFPAConceptGet(fpa, chip, cell, db, name);
+    int value = 0;
+    if (item) {
+	switch (item->type) {
+	  case PS_TYPE_S32:
+	    value = item->data.S32;
+	    break;
+	  default:
+	    psError(PS_ERR_IO, true, "Concept %s (%s) is not of integer type (%x) --- treating as "
+		    "undefined.\n", name, comment, item->type);
+	}
+    } else {
+	psError(PS_ERR_IO, true, "Concept %s (%s) is not defined.\n", name, comment);
+    }
+    psTrace(__func__, 7, "Adding %s (%s): %d\n", name, comment, value);
+    
+    psMetadataAdd(concepts, PS_LIST_TAIL, name, PS_TYPE_S32, comment, value);
+}
+
+void p_pmFPAConceptGetString(pmFPA *fpa, // The FPA
+			     pmChip *chip, // The chip
+			     pmCell *cell, // The cell
+			     psDB *db,	// DB handle
+			     psMetadata *concepts, // The concepts MD
+			     const char *name, // Name of the concept
+			     const char *comment // Comment for concept
+    )
+{
+    psMetadataItem *item = p_pmFPAConceptGet(fpa, chip, cell, db, name);
+    psString value = NULL;
+    if (item) {
+	switch (item->type) {
+	  case PS_META_STR:
+	    value = item->data.V;
+	    break;
+	  default:
+	    psError(PS_ERR_IO, true, "Concept %s (%s) is not of string type (%x) --- treating as "
+		    "undefined.\n", name, comment, item->type);
+	}
+    } else {
+	psError(PS_ERR_IO, true, "Concept %s (%s) is not defined.\n", name, comment);
+	value = psStringCopy("");
+    }
+    psTrace(__func__, 7, "Adding %s (%s): %s\n", name, comment, value);
+
+    psMetadataAdd(concepts, PS_LIST_TAIL, name, PS_META_STR, comment, value);
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Ingest concepts for the FPA
+void pmFPAIngestConcepts(pmFPA *fpa,	// The FPA
+			 psDB *db	// DB handle
+    )
+{
+    if (! fpa->concepts) {
+	fpa->concepts = psMetadataAlloc();
+    }
+
+    // FPA.NAME --- added by pmFPAConstruct
+
+    // FPA.AIRMASS
+    p_pmFPAConceptGetF32(fpa, NULL, NULL, db, fpa->concepts, "FPA.AIRMASS", "Airmass at boresight");
+
+    // FPA.FILTER
+    p_pmFPAConceptGetString(fpa, NULL, NULL, db, fpa->concepts, "FPA.FILTER", "Filter used");
+
+    // FPA.POSANGLE
+    p_pmFPAConceptGetF32(fpa, NULL, NULL, db, fpa->concepts, "FPA.POSANGLE", "Position angle for instrument");
+
+    // FPA.RADECSYS
+    p_pmFPAConceptGetString(fpa, NULL, NULL, db, fpa->concepts, "FPA.RADECSYS", "Celestial coordinate system");
+
+    // These take some extra work
+
+    // FPA.RA
+    {
+	double ra = NAN;		// The RA
+	psMetadataItem *raItem = p_pmFPAConceptGet(fpa, NULL, NULL, db, "FPA.RA"); // The FPA.RA item
+	if (raItem) {
+	    switch (raItem->type) {
+	      case PS_TYPE_F32:
+		ra = raItem->data.F32;
+		break;
+	      case PS_TYPE_F64:
+		ra = raItem->data.F64;
+		break;
+	      case PS_META_STR:
+		// Sexagesimal format
+		{
+		    int big, medium;
+		    float small;
+		    // XXX: Upgrade path is to allow dd:mm.mmm
+		    if (sscanf(raItem->data.V, "%d:%d:%f", &big, &medium, &small) != 3 &&
+			sscanf(raItem->data.V, "%d %d %f", &big, &medium, &small) != 3) {
+			psError(PS_ERR_IO, true, "Cannot interpret FPA.RA: %s\n", raItem->data.V);
+			break;
+		    }
+		    ra = abs(big) + (float)medium/60.0 + small/3600.0;
+		    if (big < 0) {
+			ra *= -1.0;
+		    }
+		}
+		break;
+	      default:
+		psError(PS_ERR_IO, true, "FPA.RA is of an unexpected type: %x\n", raItem->type);
+	    }
+	    
+	    // How to interpret the RA
+	    psMetadata *camera = fpa->camera; // Camera configuration data
+	    bool mdok = true;		// Status of MD lookup
+	    psMetadata *formats = psMetadataLookupMD(&mdok, camera, "FORMATS");
+	    if (mdok && formats) {
+		psString raFormat = psMetadataLookupString(&mdok, formats, "FPA.RA");
+		if (mdok && strlen(raFormat) > 0) {
+		    if (strcasecmp(raFormat, "HOURS") == 0) {
+			ra *= M_PI / 12.0;
+		    } else if (strcasecmp(raFormat, "DEGREES") == 0) {
+			ra *= M_PI / 180.0;
+		    } else if (strcasecmp(raFormat, "RADIANS") == 0) {
+			// No action required
+		    } else {
+			psLogMsg(__func__, PS_LOG_WARN, "Don't understand FPA.RA in FORMATS (%s) --- assuming"
+				 " HOURS.\n");
+			ra *= M_PI / 12.0;
+		    }
+		} else {
+		    psError(PS_ERR_IO, false, "Unable to find FPA.RA in FORMATS --- assuming HOURS.\n");
+		    ra *= M_PI / 12.0;
+		}
+	    } else {
+		psError(PS_ERR_IO, false, "Unable to find FORMAT metadata in camera configuration --- "
+			"assuming format for FPA.RA is HOURS.\n");
+		ra *= M_PI / 12.0;
+	    }
+	} else {
+	    psError(PS_ERR_IO, false, "Couldn't find FPA.RA.\n");
+	}
+
+	psMetadataAdd(fpa->concepts, PS_LIST_TAIL, "FPA.RA", PS_META_F64, 
+		      "Right Ascension of the boresight (radians)", ra);
+
+    }
+
+    // FPA.DEC
+    {
+	double dec = NAN;		// The DEC
+	psMetadataItem *decItem = p_pmFPAConceptGet(fpa, NULL, NULL, db, "FPA.DEC"); // The FPA.DEC item
+	if (decItem) {
+	    switch (decItem->type) {
+	      case PS_TYPE_F32:
+		dec = decItem->data.F32;
+		break;
+	      case PS_TYPE_F64:
+		dec = decItem->data.F64;
+		break;
+	      case PS_META_STR:
+		// Sexagesimal format
+		{
+		    int big, medium;
+		    float small;
+		    // XXX: Upgrade path is to allow dd:mm.mmm
+		    if (sscanf(decItem->data.V, "%d:%d:%f", &big, &medium, &small) != 3 &&
+			sscanf(decItem->data.V, "%d %d %f", &big, &medium, &small) != 3) {
+			psError(PS_ERR_IO, true, "Cannot interpret FPA.DEC: %s\n", decItem->data.V);
+			break;
+		    }
+		    dec = abs(big) + (float)medium/60.0 + small/3600.0;
+		    if (big < 0) {
+			dec *= -1.0;
+		    }
+		}
+		break;
+	      default:
+		psError(PS_ERR_IO, true, "FPA.DEC is of an unexpected type: %x\n", decItem->type);
+	    }
+	    
+	    // How to interpret the DEC
+	    psMetadata *camera = fpa->camera; // Camera configuration data
+	    bool mdok = true;		// Status of MD lookup
+	    psMetadata *formats = psMetadataLookupMD(&mdok, camera, "FORMATS");
+	    if (mdok && formats) {
+		psString decFormat = psMetadataLookupString(&mdok, formats, "FPA.DEC");
+		if (mdok && strlen(decFormat) > 0) {
+		    if (strcasecmp(decFormat, "HOURS") == 0) {
+			dec *= M_PI / 12.0;
+		    } else if (strcasecmp(decFormat, "DEGREES") == 0) {
+			dec *= M_PI / 180.0;
+		    } else if (strcasecmp(decFormat, "RADIANS") == 0) {
+			// No action required
+		    } else {
+			psLogMsg(__func__, PS_LOG_WARN, "Don't understand FPA.DEC in FORMATS (%s) --- "
+				 "assuming DEGREES.\n");
+			dec *= M_PI / 180.0;
+		    }
+		} else {
+		    psError(PS_ERR_IO, false, "Unable to find FPA.DEC in FORMATS --- assuming DEGREES.\n");
+		    dec *= M_PI / 180.0;
+		}
+	    } else {
+		psError(PS_ERR_IO, false, "Unable to find FORMATS metadata in camera configuration --- "
+			"assuming format for FPA.DEC is DEGREES.\n");
+		dec *= M_PI / 180.0;
+	    }
+	} else {
+	    psError(PS_ERR_IO, false, "Couldn't find FPA.DEC.\n");
+	}
+
+	psMetadataAdd(fpa->concepts, PS_LIST_TAIL, "FPA.DEC", PS_META_F64, 
+		      "Declination of the boresight (radians)", dec);
+
+    }
+
+    // Pau.
+}
+
+
+// Ingest concepts for the chip
+bool pmChipIngestConcepts(pmChip *chip,	// The chip
+			  psDB *db	// DB handle
+    )
+{
+    pmFPA *fpa = chip->parent;		// The parent FPA
+
+    if (! chip->concepts) {
+	chip->concepts = psMetadataAlloc();
+    }
+
+    // CHIP.NAME --- added by pmFPAConstruct
+
+    // Pau.
+}
+
+
+// Ingest concepts for the cell
+bool pmCellIngestConcepts(pmCell *cell, // The cell
+			  psDB *db	// DB handle
+    )
+{
+    pmChip *chip = cell->parent;	// The parent chip
+    pmFPA *fpa = chip->parent;		// The parent FPA
+
+    if (! cell->concepts) {
+	cell->concepts = psMetadataAlloc();
+    }
+
+    // CELL.NAME --- added by pmFPAConstruct
+
+    // CELL.GAIN
+    p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.GAIN", "CCD gain (e/count)");
+
+    // CELL.READNOISE
+    p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.READNOISE", "CCD read noise (e)");
+
+    // CELL.SATURATION
+    p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.SATURATION", "Saturation level (ADU)");
+
+    // CELL.BAD
+    p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.BAD", "Bad level (ADU)");
+
+    // CELL.XPARITY
+    p_pmFPAConceptGetS32(fpa, chip, cell, db, cell->concepts, "CELL.XPARITY", "Orientation in x compared to the "
+		  "rest of the FPA");
+
+    // CELL.YPARITY
+    p_pmFPAConceptGetS32(fpa, chip, cell, db, cell->concepts, "CELL.YPARITY", "Orientation in y compared to the "
+		  "rest of the FPA");
+
+    // CELL.READDIR
+    p_pmFPAConceptGetS32(fpa, chip, cell, db, cell->concepts, "CELL.READDIR", "Read direction: 1=row, 2=col");
+
+    // These used to be pmReadoutGetExposure and pmReadoutGetDarkTime, but that doesn't really make sense at
+    // the moment.  Maybe we need to add a "parent" link to the readouts.  But then how are the exposure times
+    // REALLY derived?  They're not in the FITS headers, because a readout is a plane in a 3D image.  We'll
+    // have to dream up some additional suffix to specify these, but for now....
+
+    // CELL.EXPOSURE (used to be READOUT.EXPOSURE)
+    p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.EXPOSURE", "Exposure time (sec)");
+
+    // CELL.DARKTIME (used to be READOUT.DARKTIME)
+    p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.DARKTIME", "Time since CCD flush (sec)");
+
+    // These take some extra work
+
+    // CELL.TRIMSEC
+    {
+	psRegion *trimsec = psAlloc(sizeof(psRegion)); // Make space for a psRegion (usually passed by value)
+
+	psMetadataItem *secItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.TRIMSEC");
+	if (! secItem) {
+	    psError(PS_ERR_IO, false, "Couldn't find CELL.TRIMSEC.\n");
+	    *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
+	} else if (secItem->type != PS_META_STR) {
+	    psError(PS_ERR_IO, true, "CELL.TRIMSEC is not of type STR (%x)\n", secItem->type);
+	    *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
+	} else {
+	    psString section = secItem->data.V;	// The section string
+
+	    psMetadataItem *sourceItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.TRIMSEC.SOURCE");
+	    if (! sourceItem) {
+		psError(PS_ERR_IO, false, "Couldn't find CELL.TRIMSEC.SOURCE.\n");
+		*trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
+	    } else if (sourceItem->type != PS_META_STR) {
+		psError(PS_ERR_IO, true, "CELL.TRIMSEC.SOURCE is not of type STR (%x)\n", sourceItem->type);
+		*trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
+	    } else {
+		psString source = sourceItem->data.V; // The source string
+	    
+		if (strcasecmp(source, "VALUE") == 0) {
+		    *trimsec = psRegionFromString(section);
+		} else if (strcasecmp(source, "HEADER") == 0) {
+		    psMetadata *header = NULL; // The FITS header
+		    if (cell->private) {
+			header = cell->private->header;
+		    } else if (chip->private) {
+			header = chip->private->header;
+		    } else if (fpa->private) {
+			    header = fpa->private->header;
+		    }
+		    if (! header) {
+			psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
+			*trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
+		    } else {
+			bool mdok = true;		// Status of MD lookup
+			psString secValue = psMetadataLookupString(&mdok, header, section);
+			if (! mdok || ! secValue) {
+			    psError(PS_ERR_IO, false, "Unable to locate header %s\n", section);
+			    *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
+			} else {
+			    *trimsec = psRegionFromString(secValue);
+			}
+		    }
+		} else {
+		    psError(PS_ERR_IO, true, "CELL.TRIMSEC.SOURCE (%s) is not HEADER or VALUE --- trying "
+			    "VALUE.\n", source);
+		    *trimsec = psRegionFromString(section);
+		} // Value of CELL.TRIMSEC.SOURCE
+	    } // Looking up CELL.TRIMSEC.SOURCE
+	} // Looking up CELL.TRIMSEC
+
+	psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.TRIMSEC", PS_META_UNKNOWN,
+		      "Trim section", trimsec);
+	psFree(trimsec);
+    }
+
+    // CELL.BIASSEC
+    {
+	psList *biassecs = psListAlloc(NULL); // List of bias sections
+
+	psMetadataItem *secItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.BIASSEC");
+	if (! secItem) {
+	    psError(PS_ERR_IO, false, "Couldn't find CELL.BIASSEC.\n");
+	} else if (secItem->type != PS_META_STR) {
+	    psError(PS_ERR_IO, true, "CELL.BIASSEC is not of type STR (%x)\n", secItem->type);
+	} else {
+	    psString sections = secItem->data.V; // The section string
+
+	    psMetadataItem *sourceItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.BIASSEC.SOURCE");
+	    if (! sourceItem) {
+		psError(PS_ERR_IO, false, "Couldn't find CELL.BIASSEC.SOURCE.\n");
+	    } else if (sourceItem->type != PS_META_STR) {
+		psError(PS_ERR_IO, true, "CELL.BIASSEC.SOURCE is not of type STR (%x)\n", sourceItem->type);
+	    } else {
+		psString source = sourceItem->data.V; // The source string
+
+		psList *secList = papSplit(sections, " ;"); // List of sections
+		psListIterator *secIter = psListIteratorAlloc(secList, PS_LIST_HEAD, false); // Iterator over
+											     // sections
+		psString aSection = NULL; // A section from the list
+		while (aSection = psListGetAndIncrement(secIter)) {
+		    psRegion *region = psAlloc(sizeof(psRegion)); // Make space for a psRegion (usually passed
+								  // by value)
+
+		    if (strcasecmp(source, "VALUE") == 0) {
+			*region = psRegionFromString(aSection);
+		    } else if (strcasecmp(source, "HEADER") == 0) {
+			psMetadata *header = NULL; // The FITS header
+			if (cell->private) {
+			    header = cell->private->header;
+			} else if (chip->private) {
+			    header = chip->private->header;
+			} else if (fpa->private) {
+			    header = fpa->private->header;
+			}
+			if (! header) {
+			    psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
+			    *region = psRegionSet(0.0,0.0,0.0,0.0);
+			} else {
+			    bool mdok = true;		// Status of MD lookup
+			    psString secValue = psMetadataLookupString(&mdok, header, aSection);
+			    if (! mdok || ! secValue) {
+				psError(PS_ERR_IO, false, "Unable to locate header %s\n", aSection);
+				*region = psRegionSet(0.0,0.0,0.0,0.0);
+			    } else {
+				*region = psRegionFromString(secValue);
+			    }
+			}
+		    } else {
+			psError(PS_ERR_IO, true, "CELL.BIASSEC.SOURCE (%s) is not HEADER or VALUE --- trying "
+				"VALUE.\n", source);
+			*region = psRegionFromString(aSection);
+		    } // Value of CELL.BIASSEC.SOURCE
+		    
+		    psListAdd(biassecs, PS_LIST_TAIL, region);
+		    psFree(region);
+		} // Iterating over multiple sections
+		psFree(secIter);
+		psFree(secList);
+	    } // Looking up CELL.BIASSEC.SOURCE
+	} // Looking up CELL.BIASSEC
+
+	psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.BIASSEC", PS_META_LIST, "Bias sections", biassecs);
+    }
+
+    // CELL.XBIN
+    {
+	int xBin = 1;			// Binning factor in x
+	psMetadataItem *binItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.XBIN");
+	if (! binItem) {
+	    psError(PS_ERR_IO, false, "Couldn't find CELL.XBIN.\n");
+	} else if (binItem->type == PS_META_STR) {
+	    psString binString = binItem->data.V; // The string containing the binning
+	    if (sscanf(binString, "%d %*d", &xBin) != 1 &&
+		sscanf(binString, "%d,%*d", &xBin) != 1) {
+		psError(PS_ERR_IO, true, "Unable to read string to get x binning: %s\n", binString);
+	    }
+	} else if (binItem->type == PS_TYPE_S32) {
+	    xBin = binItem->data.S32;
+	} else {
+	    psError(PS_ERR_IO, true, "Note sure how to interpret CELL.XBIN of type %x --- assuming 1.\n",
+		    binItem->type);
+	}
+
+	psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.XBIN", PS_TYPE_S32, "Binning in x", xBin);
+    }
+
+    // CELL.XBIN
+    {
+	int yBin = 1;			// Binning factor in y
+	psMetadataItem *binItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.YBIN");
+	if (! binItem) {
+	    psError(PS_ERR_IO, false, "Couldn't find CELL.YBIN.\n");
+	} else if (binItem->type == PS_META_STR) {
+	    psString binString = binItem->data.V; // The string containing the binning
+	    if (sscanf(binString, "%*d %d", &yBin) != 1 &&
+		sscanf(binString, "%*d,%d", &yBin) != 1) {
+		psError(PS_ERR_IO, true, "Unable to read string to get y binning: %s\n", binString);
+	    }
+	} else if (binItem->type == PS_TYPE_S32) {
+	    yBin = binItem->data.S32;
+	} else {
+	    psError(PS_ERR_IO, true, "Note sure how to interpret CELL.YBIN of type %x --- assuming 1.\n",
+		    binItem->type);
+	}
+
+	psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.YBIN", PS_TYPE_S32, "Binning in y", yBin);
+    }
+
+    // CELL.TIME and CELL.TIMESYS
+    {
+	psTime *time = NULL;		// The time
+	psTimeType timeSys = PS_TIME_UTC; // The time system
+
+	// CELL.TIMESYS
+	psMetadataItem *sysItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.TIMESYS");
+	if (! sysItem) {
+	    psError(PS_ERR_IO, true, "Couldn't find CELL.TIMESYS --- assuming UTC.\n");
+	} else if (sysItem->type != PS_META_STR) {
+	    psError(PS_ERR_IO, true, "CELL.TIMESYS isn't of type STRING --- assuming UTC.\n");
+	} else {
+	    psString sys = sysItem->data.V; // The time system string
+	    if (strcasecmp(sys, "TAI") == 0) {
+		timeSys = PS_TIME_TAI;
+	    } else if (strcasecmp(sys, "UTC") == 0) {
+		timeSys = PS_TIME_UTC;
+	    } else if (strcasecmp(sys, "UT1") == 0) {
+		timeSys = PS_TIME_UT1;
+	    } else if (strcasecmp(sys, "TT") == 0) {
+		timeSys = PS_TIME_TT;
+	    } else {
+		psError(PS_ERR_IO, true, "Can't interpret CELL.TIMESYS --- assuming UTC.\n");
+	    }
+	}
+	psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.TIMESYS", PS_TYPE_S32, "Time system", timeSys);
+
+	psMetadataItem *timeItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.TIME");
+	if (! timeItem) {
+	    psError(PS_ERR_IO, false, "Couldn't find CELL.TIME.\n");
+	} else {
+	    // Get format
+	    psMetadata *camera = fpa->camera; // The camera configuration data
+	    bool mdok = true;		// Status of MD lookup
+	    psMetadata *formats = psMetadataLookupMD(&mdok, camera, "FORMATS");
+	    if (mdok && formats) {
+		psString timeFormat = psMetadataLookupString(&mdok, formats, "CELL.TIME");
+		if (mdok && strlen(timeFormat) > 0) {
+		    switch (timeItem->type) {
+		      case PS_META_STR:
+			{
+			    psString timeString = timeItem->data.V;	// String with the time
+			    if (strcasecmp(timeFormat, "ISO") == 0) {
+				// timeString contains an ISO time
+				time = psTimeFromISO(timeString, timeSys);
+			    } else if (strstr(timeFormat, "SEPARATE")) {
+				// timeString contains headers for the date and time
+				psMetadata *header = NULL; // The FITS header
+				if (cell->private) {
+				    header = cell->private->header;
+				} else if (chip->private) {
+				    header = chip->private->header;
+				} else if (fpa->private) {
+				    header = fpa->private->header;
+				}
+				if (! header) {
+				    psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
+				} else {
+				    // Get the headers
+				    char *stuff1 = strpbrk(timeString, " ,;");
+				    psString dateName = psStringNCopy(timeString,
+								      strlen(timeString) - strlen(stuff1));
+				    char *stuff2 = strpbrk(stuff1,
+							   "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
+				    psString timeName = psStringCopy(stuff2);
+				    
+				    bool mdok = true; // Status of MD lookup
+				    psString dateString = psMetadataLookupString(&mdok, header, dateName);
+				    psFree(dateName);
+				    int day = 0, month = 0, year = 0;
+				    if (sscanf(dateString, "%d-%d-%d", &day, &month, &year) != 3 &&
+					sscanf(dateString, "%d/%d/%d", &day, &month, &year) != 3) {
+					psError(PS_ERR_IO, true, "Unable to read date: %s\n", dateString);
+				    } else {
+					if (strstr(timeFormat, "BACKWARDS")) {
+					    int temp = day;
+					    day = year;
+					    year = temp;
+					}
+					if (strstr(timeFormat, "PRE2000") || year < 2000) {
+					    year += 2000;
+					}
+					
+					psMetadataItem *timeItem = psMetadataLookup(header, timeName);
+					if (! timeItem) {
+					    psError(PS_ERR_IO, false, "Unable to find time header: %s\n",
+						    timeName);
+					} else if (timeItem->type == PS_META_STR) {
+					    // Time is a string, in the usual way:
+					    psStringAppend(&dateString, "T%s", timeItem->data.V);
+					} else {
+					    // Assume that time is specified in Second of Day
+					    double seconds = NAN;
+					    switch (timeItem->type) {
+					      case PS_TYPE_S32:
+						seconds = timeItem->data.S32;
+						break;
+					      case PS_TYPE_F32:
+						seconds = timeItem->data.F32;
+						break;
+					      case PS_TYPE_F64:
+						seconds = timeItem->data.F64;
+						break;
+					      default:
+						psError(PS_ERR_IO, true, "Time header (%s) is not of an "
+							"expected type: %x\n", timeName, timeItem->type);
+					    }
+					    // Now print to timeString as "hh:mm:ss.ss"
+					    int hours = seconds / 3600;
+					    seconds -= (double)hours * 3600.0;
+					    int minutes = seconds / 60;
+					    seconds -= (double)minutes * 60.0;
+					    psStringAppend(&dateString, "T%02d:%02d:%02f", hours, minutes,
+							   seconds);
+					}
+					time = psTimeFromISO(dateString, timeSys);
+				    } // Reading date and time
+				    psFree(timeName);
+				} // Reading headers
+			    } else {
+				psError(PS_ERR_IO, true, "Not sure how to parse CELL.TIME (%s) --- trying "
+					"ISO\n", timeString);
+				time = psTimeFromISO(timeString, timeSys);
+			    } // Interpreting the time string
+			}
+			break;
+		      case PS_TYPE_F32:
+			{
+			    double timeValue = (double)timeItem->data.F32;
+			    if (strcasecmp(timeFormat, "JD") == 0) {
+				time = psTimeFromJD(timeValue);
+			    } else if (strcasecmp(timeFormat, "MJD") == 0) {
+				time = psTimeFromMJD(timeValue);
+			    } else {
+				psError(PS_ERR_IO, true, "Not sure how to parse CELL.TIME (%f) --- trying "
+					"JD\n", timeValue);
+				time = psTimeFromJD(timeValue);
+			    }
+			}
+			break;
+		      case PS_TYPE_F64:
+			{
+			    double timeValue = (double)timeItem->data.F64;
+			    if (strcasecmp(timeFormat, "JD") == 0) {
+				time = psTimeFromJD(timeValue);
+			    } else if (strcasecmp(timeFormat, "MJD") == 0) {
+				time = psTimeFromMJD(timeValue);
+			    } else {
+				psError(PS_ERR_IO, true, "Not sure how to parse CELL.TIME (%f) --- trying "
+					"JD\n", timeValue);
+				time = psTimeFromJD(timeValue);
+			    }
+			}
+			break;
+		      default:
+			psError(PS_ERR_IO, true, "Unable to parse CELL.TIME.\n");
+		    }
+		} else {
+		    psError(PS_ERR_IO, false, "Unable to find CELL.TIME in FORMATS.\n");
+		} // Getting the format
+	    } else {
+		psError(PS_ERR_IO, false, "Unable to find FORMATS in camera configuration.\n");
+	    } // Getting the formats
+	} // Getting CELL.TIME
+
+	psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.TIME", PS_META_UNKNOWN, "Time of exposure", time);
+	psFree(time);
+
+    }
+
+    // Pau.
+}
+
+
+// Retrieve a concept
+psMetadataItem *pmCellGetConcept(pmCell *cell, // The cell
+				 const char *concept // The concept
+    )
+{
+    psMetadataItem *item = psMetadataLookup(cell->concepts, concept);
+    if (! item) {
+	pmChip *chip = cell->parent;
+	item = psMetadataLookup(chip->concepts, concept);
+	if (! item) {
+	    pmFPA *fpa = chip->parent;
+	    item = psMetadataLookup(fpa->concepts, concept);
+	}
+    }
+    return item;			// item is either NULL or is what we want.
+}
Index: /trunk/archive/scripts/src/phase2/pmFPAConceptsGet.h
===================================================================
--- /trunk/archive/scripts/src/phase2/pmFPAConceptsGet.h	(revision 5105)
+++ /trunk/archive/scripts/src/phase2/pmFPAConceptsGet.h	(revision 5105)
@@ -0,0 +1,86 @@
+#ifndef PM_FPA_CONCEPTS_GET_H
+#define PM_FPA_CONCEPTS_GET_H
+
+#include "pmFPA.h"
+#include "pslib.h"
+#include "psAdditionals.h"
+
+psMetadataItem *p_pmFPAConceptGetFromCamera(pmCell *cell, // The cell
+					    const char *concept // Name of concept
+    );
+psMetadataItem *p_pmFPAConceptGetFromHeader(pmFPA *fpa, // The FPA that contains the chip
+					    pmChip *chip, // The chip that contains the cell
+					    pmCell *cell, // The cell
+					    const char *concept // Name of concept
+    );
+psMetadataItem *p_pmFPAConceptGetFromDefault(pmFPA *fpa, // The FPA that contains the chip
+					     pmChip *chip, // The chip that contains the cell
+					     pmCell *cell, // The cell
+					     const char *concept // Name of concept
+    );
+psMetadataItem *p_pmFPAConceptGetFromDB(pmFPA *fpa, // The FPA that contains the chip
+					pmChip *chip, // The chip that contains the cell
+					pmCell *cell, // The cell
+					psDB *db,	// DB handle
+					const char *concept // Name of concept
+    );
+psMetadataItem *p_pmFPAConceptGet(pmFPA *fpa, // The FPA
+				  pmChip *chip,// The chip
+				  pmCell *cell,	// The cell
+				  psDB *db, // DB handle
+				  const char *concept // Concept name
+    );
+void p_pmFPAConceptGetF32(pmFPA *fpa,	// The FPA
+			  pmChip *chip, // The chip
+			  pmCell *cell, // The cell
+			  psDB *db,	// DB handle
+			  psMetadata *concepts, // The concepts MD
+			  const char *name, // Name of the concept
+			  const char *comment // Comment for concept
+    );
+void p_pmFPAConceptGetF64(pmFPA *fpa,	// The FPA
+			  pmChip *chip, // The chip
+			  pmCell *cell, // The cell
+			  psDB *db,	// DB handle
+			  psMetadata *concepts, // The concepts MD
+			  const char *name, // Name of the concept
+			  const char *comment // Comment for concept
+    );
+void p_pmFPAConceptGetS32(pmFPA *fpa,	// The FPA
+			  pmChip *chip,	// The chip
+			  pmCell *cell,	// The cell
+			  psDB *db,	// DB handle
+			  psMetadata *concepts, // The concepts MD
+			  const char *name, // Name of the concept
+			  const char *comment // Comment for concept
+    );
+void p_pmFPAConceptGetString(pmFPA *fpa, // The FPA
+			     pmChip *chip, // The chip
+			     pmCell *cell, // The cell
+			     psDB *db,	// DB handle
+			     psMetadata *concepts, // The concepts MD
+			     const char *name, // Name of the concept
+			     const char *comment // Comment for concept
+    );
+
+
+// Ingest concepts for the FPA
+void pmFPAIngestConcepts(pmFPA *fpa,	// The FPA
+			 psDB *db	// DB handle
+    );
+// Ingest concepts for the chip
+bool pmChipIngestConcepts(pmChip *chip,	// The chip
+			  psDB *db	// DB handle
+    );
+// Ingest concepts for the cell
+bool pmCellIngestConcepts(pmCell *cell, // The cell
+			  psDB *db	// DB handle
+    );
+
+// Retrieve a concept
+psMetadataItem *pmCellGetConcept(pmCell *cell, // The cell
+				 const char *concept // The concept
+    );
+
+
+#endif
Index: /trunk/archive/scripts/src/phase2/pmFPAConceptsSet.c
===================================================================
--- /trunk/archive/scripts/src/phase2/pmFPAConceptsSet.c	(revision 5105)
+++ /trunk/archive/scripts/src/phase2/pmFPAConceptsSet.c	(revision 5105)
@@ -0,0 +1,853 @@
+#include <stdio.h>
+#include <string.h>
+#include "pslib.h"
+
+#include "pmFPA.h"
+
+#include "papStuff.h"
+
+#include "pmFPAConceptsGet.h"
+#include "pmFPAConceptsSet.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#define COMPARE_REGIONS(a,b) (((a)->x0 == (b)->x0 && \
+                               (a)->x1 == (b)->x1 && \
+                               (a)->y0 == (b)->y0 && \
+                               (a)->y1 == (b)->y1) ? true : false)
+
+
+static bool compareConcepts(psMetadataItem *item1, // First item to compare
+			    psMetadataItem *item2 // Second item to compare
+    )
+{
+    // First order checks
+    if (! item1 || ! item2) {
+	return false;
+    }
+    if (strcasecmp(item1->name, item2->name) != 0) {
+	return false;
+    }
+
+    // Check the more boring types
+    switch (item1->type) {
+      case PS_TYPE_S32:
+	switch (item2->type) {
+	  case PS_TYPE_S32:
+	    return (item1->data.S32 == item2->data.S32) ? true : false;
+	  case PS_TYPE_F32:
+	    return (item1->data.S32 == (int)item2->data.F32) ? true : false;
+	  case PS_TYPE_F64:
+	    return (item1->data.S32 == (int)item2->data.F64) ? true : false;
+	  default:
+	    return false;
+	}
+      case PS_TYPE_F32:
+	switch (item2->type) {
+	  case PS_TYPE_S32:
+	    return (item1->data.F32 == (float)item2->data.S32) ? true : false;
+	  case PS_TYPE_F32:
+	    return (item1->data.F32 == item2->data.F32) ? true : false;
+	  case PS_TYPE_F64:
+	    return (item1->data.F32 == (float)item2->data.F64) ? true : false;
+	  default:
+	    return false;
+	}
+      case PS_TYPE_F64:
+	switch (item2->type) {
+	  case PS_TYPE_S32:
+	    return (item1->data.F64 == (double)item2->data.S32) ? true : false;
+	  case PS_TYPE_F32:
+	    return (item1->data.F64 == (double)item2->data.F32) ? true : false;
+	  case PS_TYPE_F64:
+	    return (item1->data.F64 == item2->data.F64) ? true : false;
+	  default:
+	    return false;
+	}
+	return (item1->data.F64 == item2->data.F64) ? true : false;
+      case PS_META_STR:
+	if (item2->type != PS_META_STR) {
+	    return false;
+	}
+	return (strcasecmp(item1->data.V, item2->data.V) == 0) ? true : false;
+      default:
+	return false;
+    }
+    psAbort(__func__, "Should never get here.\n");
+}
+
+
+// Well, not really "set", but check to make sure it's there and matches
+static bool setConceptInCamera(pmCell *cell, // The cell
+			       psMetadataItem *concept // Concept
+    )
+{
+    if (cell) {
+	psMetadataItem *item = psMetadataLookup(cell->camera, concept->name); // Info we want
+	return compareConcepts(item, concept);
+    }
+}   
+
+
+static bool setConceptInHeader(pmFPA *fpa, // The FPA that contains the chip
+			       pmChip *chip, // The chip that contains the cell
+			       pmCell *cell, // The cell
+			       psMetadataItem *concept // Concept
+    )
+{
+    bool mdok = true;			// Status of MD lookup
+    bool status = false;		// Status of setting header
+    psMetadata *translation = psMetadataLookupMD(&mdok, fpa->camera, "TRANSLATION"); // FITS translation
+    if (! mdok) {
+	psError(PS_ERR_IO, false, "Unable to find TRANSLATION in camera configuration.\n");
+	return false;
+    }
+
+    // Look for how to translate the concept into a FITS header name
+    const char *keyword = psMetadataLookupString(&mdok, translation, concept->name);
+    if (mdok && strlen(keyword) > 0) {
+	psMetadataItem *headerItem = NULL; // Item to add to header
+	// XXX: Need to expand range of types
+	switch (concept->type) {
+	  case PS_META_STR:
+	    headerItem = psMetadataItemAllocStr(keyword, concept->comment, concept->data.V);
+	    break;
+	  case PS_META_S32:
+	    headerItem = psMetadataItemAllocS32(keyword, concept->comment, concept->data.S32);
+	    break;
+	  case PS_META_F32:
+	    headerItem = psMetadataItemAllocF32(keyword, concept->comment, concept->data.F32);
+	    break;
+	  case PS_META_F64:
+	    headerItem = psMetadataItemAllocF64(keyword, concept->comment, concept->data.F64);
+	    break;
+	  default:
+	    headerItem = psMetadataItemAlloc(keyword, concept->type, concept->comment,
+					     concept->data.V); // Item for the header
+	}
+
+	// We have a FITS header to look up --- search each level
+	if (cell && cell->private) {
+	    psMetadataItem *cellItem = psMetadataLookup(cell->private->header, keyword);
+	    if (cellItem) {
+		// XXX: Need to clean up before returning
+		psMetadataAddItem(cell->private->header, headerItem, PS_LIST_TAIL, PS_META_REPLACE);
+		status = true;
+	    }
+	} else if (chip && chip->private) {
+	    psMetadataItem *chipItem = psMetadataLookup(chip->private->header, keyword);
+	    if (chipItem) {
+		// XXX: Need to clean up before returning
+		psMetadataAddItem(chip->private->header, headerItem, PS_LIST_TAIL, PS_META_REPLACE);
+		status = true;
+	    }
+	} else if (fpa->private) {
+	    psMetadataItem *fpaItem = psMetadataLookup(fpa->private->header, keyword);
+	    if (fpaItem) {
+		// XXX: Need to clean up before returning
+		psMetadataAddItem(fpa->private->header, headerItem, PS_LIST_TAIL, PS_META_REPLACE);
+		status = true;
+	    }
+	} else if (fpa->phu) {
+	    psMetadataItem *fpaItem = psMetadataLookup(fpa->phu, keyword);
+	    if (fpaItem) {
+		// XXX: Need to clean up before returning
+		psMetadataAddItem(fpa->phu, headerItem, PS_LIST_TAIL, PS_META_REPLACE);
+		status = true;
+	    }
+	}
+	psFree(headerItem);
+    }
+
+    // No header value
+    return status;
+}
+
+
+// Well, not really "set", but check to see if it's there, and matches
+static bool setConceptInDefault(pmFPA *fpa, // The FPA that contains the chip
+				pmChip *chip, // The chip that contains the cell
+				pmCell *cell, // The cell
+				psMetadataItem *concept // Concept
+    )
+{
+    bool mdOK = true;			// Status of MD lookup
+    psMetadata *defaults = psMetadataLookupMD(&mdOK, fpa->camera, "DEFAULTS");
+    if (! mdOK || ! defaults) {
+	psError(PS_ERR_IO, false, "Unable to find DEFAULTS in camera configuration.\n");
+	return false;
+    }
+
+    psMetadataItem *defItem = psMetadataLookup(defaults, concept->name);
+    if (defItem) {
+	if (defItem->type == PS_META_META) {
+	    // A dependent default
+	    psTrace(__func__, 7, "Evaluating dependent default....\n");
+	    psMetadata *dependents = defItem->data.V; // The list of dependents
+	    // Find out what it depends on
+	    psString dependName = psStringCopy(concept->name);
+	    psStringAppend(&dependName, ".DEPEND");
+	    psString dependsOn = psMetadataLookupString(&mdOK, defaults, dependName);
+	    if (! mdOK) {
+		psError(PS_ERR_IO, false, "Unable to find %s in camera configuration for dependent default"
+			" --- ignored\n", dependName);
+		// XXX: Need to clean up before returning
+		return false;
+	    }
+	    psFree(dependName);
+	    // Find the value of the dependent concept
+	    psMetadataItem *depItem = p_pmFPAConceptGetFromHeader(fpa, chip, cell, dependsOn);
+	    if (! depItem) {
+		psError(PS_ERR_IO, true, "Unable to find value for %s (required for %s)\n", dependsOn,
+			concept->name);
+		return false;
+	    }
+	    if (depItem->type != PS_META_STR) {
+		psError(PS_ERR_IO, true, "Value of %s is not of type string, as required for dependency"
+			" --- ignored.\n", dependsOn);
+	    }
+
+	    defItem = psMetadataLookup(dependents, depItem->data.V);	// This is now what we were after
+	}
+    }
+
+    // XXX: Need to clean up before returning
+    return compareConcepts(defItem, concept);
+}
+
+
+// XXX: Not tested at all
+static bool setConceptInDB(pmFPA *fpa, // The FPA that contains the chip
+			   pmChip *chip, // The chip that contains the cell
+			   pmCell *cell, // The cell
+			   psDB *db,	// DB handle
+			   psMetadataItem *concept // Concept
+    )
+{
+    if (! db) {
+	// No database initialised
+	return false;
+    }
+
+    bool mdStatus = true;		// Status of MD lookup
+    psMetadata *database = psMetadataLookupMD(&mdStatus, fpa->camera, "DATABASE");
+    if (! mdStatus) {
+	// No error, because not everyone needs to use the DB
+	return NULL;
+    }
+
+    psMetadata *dbLookup = psMetadataLookupMD(&mdStatus, database, concept->name);
+    if (dbLookup) {
+	const char *tableName = psMetadataLookupString(&mdStatus, dbLookup, "TABLE"); // Name of the table
+	const char *colName = psMetadataLookupString(&mdStatus, dbLookup, "COLUMN"); // Name of the column
+	const char *givenCols = psMetadataLookupString(&mdStatus, dbLookup, "GIVENDBCOL"); // Name of "where"
+											   // columns
+	const char *givenPS = psMetadataLookupString(&mdStatus, dbLookup, "GIVENPS"); // Values for "where"
+										      // columns
+	
+	// Now, need to get the "given"s
+	if (strlen(givenCols) || strlen(givenPS)) {
+	    psList *cols = papSplit(givenCols, ",;"); // List of column names
+	    psList *values = papSplit(givenPS, ",;"); // List of value names for the columns
+	    psMetadata *selection = psMetadataAlloc(); // The stuff to select in the DB
+	    if (cols->n != values->n) {
+		psLogMsg(__func__, PS_LOG_WARN, "The GIVENDBCOL and GIVENPS entries for %s do not have "
+			 "the same number of entries --- ignored.\n", concept);
+	    } else {
+		// Iterators for the lists
+		psListIterator *colsIter = psListIteratorAlloc(cols, PS_LIST_HEAD, false);
+		psListIterator *valuesIter = psListIteratorAlloc(values, PS_LIST_HEAD, false);
+		char *column = NULL;	// Name of the column
+		while (column = psListGetAndIncrement(colsIter)) {
+		    char *name = psListGetAndIncrement(valuesIter); // Name for the value
+		    if (!strlen(column) || !strlen(name)) {
+			psLogMsg(__func__, PS_LOG_WARN, "One of the columns or value names for %s is "
+				 " empty --- ignored.\n", concept);
+		    } else {
+			// Search for the value name
+			psMetadataItem *item = p_pmFPAConceptGetFromHeader(fpa, chip, cell, name);
+			if (! item) {
+			    item = p_pmFPAConceptGetFromDefault(fpa, chip, cell, name);
+			}
+			if (! item) {
+			    psLogMsg(__func__, PS_LOG_ERROR, "Unable to find the value name %s for DB "
+				     " lookup on %s --- ignored.\n", name, concept);
+			} else {
+			    // We need to create a new psMetadataItem.  I don't think we can't simply hack
+			    // the existing one, since that could conceivably cause memory leaks
+			    psMetadataItem *newItem = psMetadataItemAlloc(concept->name, item->type,
+									  item->comment, item->data.V);
+			    psMetadataAddItem(selection, newItem, PS_LIST_TAIL, PS_META_REPLACE);
+			    psFree(newItem);
+			}
+		    }
+		    psFree(name);
+		    psFree(column);
+		} // Iterating through the columns
+		psFree(colsIter);
+		psFree(valuesIter);
+
+		// Check first to make sure we're only going to touch one row
+		psArray *dbResult = psDBSelectRows(db, tableName, selection, 2); // Lookup result
+		// Note that we use limit=2 in order to test if there are multiple rows returned
+
+		psMetadataItem *result = NULL; // The final result of the DB lookup
+		if (! dbResult || dbResult->n == 0) {
+		    psLogMsg(__func__, PS_LOG_WARN, "Unable to find any rows in DB for %s --- ignored\n", 
+			     concept->name);
+		    return false;
+		} else {
+		    if (dbResult->n > 1) {
+			psLogMsg(__func__, PS_LOG_WARN, "Multiple rows returned in DB lookup for %s --- "
+				 " ignored.\n", concept->name);
+		    }
+		    // Update the DB
+		    psMetadata *update = psMetadataAlloc();
+		    psMetadataAddItem(update, concept, PS_LIST_HEAD, 0);
+		    psDBUpdateRows(db, tableName, selection, update);
+		    psFree(update);
+		    return true;
+		}
+	    }
+	    psFree(cols);
+	    psFree(values);
+	}
+    } // Doing the "given"s.
+
+    psAbort(__func__, "Shouldn't ever get here?\n");
+}
+
+
+// Concept set from item
+static bool setConceptItem(pmFPA *fpa, // The FPA
+			   pmChip *chip,// The chip
+			   pmCell *cell,	// The cell
+			   psDB *db, // DB handle
+			   psMetadataItem *concept // Concept item
+    )
+{
+    // Try headers, database, defaults in order
+    bool status = setConceptInCamera(cell, concept); // Status for return
+    if (! status) {
+	status = setConceptInHeader(fpa, chip, cell, concept);
+    }
+    if (! status) {
+        status = setConceptInDB(fpa, chip, cell, db, concept);
+    }
+    if (! status) {
+        status = setConceptInDefault(fpa, chip, cell, concept);
+    }
+
+    if (! status) {
+	psError(PS_ERR_IO, true, "Unable to set %s (%s).\n", concept->name, concept->comment);
+    }
+
+    return status;
+}
+
+
+// Concept set
+static bool setConcept(pmFPA *fpa, // The FPA
+		       pmChip *chip,// The chip
+		       pmCell *cell,	// The cell
+		       psDB *db, // DB handle
+		       psMetadata *concepts, // Concepts MD from which to set
+		       const char *name // Name of the concept
+    )
+{
+    psMetadataItem *concept = psMetadataLookup(concepts, name);
+    if (! concept) {
+	psError(PS_ERR_IO, true, "No such concept as %s\n", name);
+	return false;
+    }
+    return setConceptItem(fpa, chip, cell, db, concept);
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Outget concepts from the FPA
+void pmFPAOutgestConcepts(pmFPA *fpa,	// The FPA
+			  psDB *db	// DB handle
+    )
+{
+    // FPA.NAME --- don't need to worry about this
+
+    // FPA.AIRMASS
+    setConcept(fpa, NULL, NULL, db, fpa->concepts, "FPA.AIRMASS");
+
+    // FPA.FILTER
+    setConcept(fpa, NULL, NULL, db, fpa->concepts, "FPA.FILTER");
+
+    // FPA.POSANGLE
+    setConcept(fpa, NULL, NULL, db, fpa->concepts, "FPA.POSANGLE");
+
+    // FPA.RADECSYS
+    setConcept(fpa, NULL, NULL, db, fpa->concepts, "FPA.RADECSYS");
+
+    // These take some extra work
+
+    // FPA.RA
+    {
+	double ra = psMetadataLookupF64(NULL, fpa->concepts, "FPA.RA");	// The RA
+
+	// How to interpret the RA
+	psMetadata *camera = fpa->camera; // Camera configuration data
+	bool mdok = true;		// Status of MD lookup
+	psMetadata *formats = psMetadataLookupMD(&mdok, camera, "FORMATS");
+	if (mdok && formats) {
+	    psString raFormat = psMetadataLookupString(&mdok, formats, "FPA.RA");
+	    if (mdok && strlen(raFormat) > 0) {
+		if (strcasecmp(raFormat, "HOURS") == 0) {
+		    ra /= M_PI / 12.0;
+		} else if (strcasecmp(raFormat, "DEGREES") == 0) {
+		    ra /= M_PI / 180.0;
+		} else if (strcasecmp(raFormat, "RADIANS") == 0) {
+		    // No action required
+		} else {
+		    psLogMsg(__func__, PS_LOG_WARN, "Don't understand FPA.RA in FORMATS (%s) --- assuming"
+			     " HOURS.\n");
+		    ra /= M_PI / 12.0;
+		}
+	    } else {
+		psError(PS_ERR_IO, false, "Unable to find FPA.RA in FORMATS --- assuming HOURS.\n");
+		ra /= M_PI / 12.0;
+	    }
+	} else {
+	    psError(PS_ERR_IO, false, "Unable to find FORMAT metadata in camera configuration --- "
+		    "assuming format for FPA.RA is HOURS.\n");
+	    ra /= M_PI / 12.0;
+	}
+
+	// We choose to write sexagesimal format
+	int big, medium;
+	float small;
+	big = (int)ra;
+	medium = (int)(60.0*(ra - (double)big));
+	small = 3600.0*(ra - (double)big - 60.0 * (double)medium);
+	psString raString = psStringCopy("");
+	psStringAppend(&raString, "%d:%d:%.2f", big, medium, small);
+	psMetadataItem *raItem = psMetadataItemAllocStr("FPA.RA", "Right Ascension of the boresight",
+							raString);
+	setConceptItem(fpa, NULL, NULL, db, raItem);
+	psFree(raItem);
+	psFree(raString);
+    }
+
+    // FPA.DEC
+    {
+	double dec = psMetadataLookupF64(NULL, fpa->concepts, "FPA.DEC"); // The DEC
+
+	// How to interpret the DEC
+	psMetadata *camera = fpa->camera; // Camera configuration data
+	bool mdok = true;		// Status of MD lookup
+	psMetadata *formats = psMetadataLookupMD(&mdok, camera, "FORMATS");
+	if (mdok && formats) {
+	    psString decFormat = psMetadataLookupString(&mdok, formats, "FPA.DEC");
+	    if (mdok && strlen(decFormat) > 0) {
+		if (strcasecmp(decFormat, "HOURS") == 0) {
+		    dec /= M_PI / 12.0;
+		} else if (strcasecmp(decFormat, "DEGREES") == 0) {
+		    dec /= M_PI / 180.0;
+		} else if (strcasecmp(decFormat, "RADIANS") == 0) {
+		    // No action required
+		} else {
+		    psLogMsg(__func__, PS_LOG_WARN, "Don't understand FPA.DEC in FORMATS (%s) --- assuming"
+			     " DEGREES.\n");
+		    dec /= M_PI / 180.0;
+		}
+	    } else {
+		psError(PS_ERR_IO, false, "Unable to find FPA.DEC in FORMATS --- assuming DEGREES.\n");
+		dec /= M_PI / 180.0;
+	    }
+	} else {
+	    psError(PS_ERR_IO, false, "Unable to find FORMAT metadata in camera configuration --- "
+		    "assuming format for FPA.DEC is HOURS.\n");
+	    dec /= M_PI / 12.0;
+	}
+
+	// We choose to write sexagesimal format
+	int big, medium;
+	float small;
+	big = (int)dec;
+	medium = (int)(60.0*(dec - (double)big));
+	small = 3600.0*(dec - (double)big - 60.0 * (double)medium);
+	psString decString = psStringCopy("");
+	psStringAppend(&decString, "%d:%d:%.2f", big, medium, small);
+	psMetadataItem *decItem = psMetadataItemAllocStr("FPA.DEC", "Right Ascension of the boresight",
+							decString);
+	setConceptItem(fpa, NULL, NULL, db, decItem);
+	psFree(decItem);
+	psFree(decString);
+ 
+    }
+
+    // Pau.
+}
+
+
+// Outgest concepts for the chip
+void pmChipOutgestConcepts(pmChip *chip, // The chip
+			   psDB *db	// DB handle
+    )
+{
+    pmFPA *fpa = chip->parent;		// The parent FPA
+
+    // CHIP.NAME --- no need to do anything
+
+    // Pau.
+}
+
+
+// Outgest concepts for the cell
+void pmCellOutgestConcepts(pmCell *cell, // The cell
+			   psDB *db	// DB handle
+    )
+{
+    pmChip *chip = cell->parent;	// The parent chip
+    pmFPA *fpa = chip->parent;		// The parent FPA
+
+    // CELL.NAME --- no need to do anything
+
+    // CELL.GAIN
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.GAIN");
+
+    // CELL.READNOISE
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.READNOISE");
+
+    // CELL.SATURATION
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.SATURATION");
+
+    // CELL.BAD
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.BAD");
+
+    // CELL.XPARITY
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.XPARITY");
+
+    // CELL.YPARITY
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.YPARITY");
+
+    // CELL.READDIR
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.READDIR");
+
+    // These used to be pmReadoutGetExposure and pmReadoutGetDarkTime, but that doesn't really make sense at
+    // the moment.  Maybe we need to add a "parent" link to the readouts.  But then how are the exposure times
+    // REALLY derived?  They're not in the FITS headers, because a readout is a plane in a 3D image.  We'll
+    // have to dream up some additional suffix to specify these, but for now....
+
+    // CELL.EXPOSURE (used to be READOUT.EXPOSURE)
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.EXPOSURE");
+
+    // CELL.DARKTIME (used to be READOUT.DARKTIME)
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.DARKTIME");
+
+    // These take some extra work
+
+    // CELL.TRIMSEC
+    {
+	psMetadataItem *trimsecItem = psMetadataLookup(cell->concepts, "CELL.TRIMSEC");
+	psRegion *trimsec = trimsecItem->data.V; // The trimsec region
+	psMetadataItem *sourceItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.TRIMSEC.SOURCE");
+	if (! sourceItem) {
+	    psError(PS_ERR_IO, false, "Couldn't find CELL.TRIMSEC.SOURCE.\n");
+	} else if (sourceItem->type != PS_META_STR) {
+	    psError(PS_ERR_IO, true, "CELL.TRIMSEC.SOURCE is not of type STR (%x)\n", sourceItem->type);
+	} else {
+	    psString source = sourceItem->data.V; // The source string
+	    
+	    if (strcasecmp(source, "VALUE") == 0) {
+		// Check that it's the same value as stored in the camera
+		psString checkString = psMetadataLookupString(NULL, cell->camera, "CELL.TRIMSEC");
+		psRegion checkRegion = psRegionFromString(checkString);
+		if (! COMPARE_REGIONS(&checkRegion, trimsec)) {
+		    psError(PS_ERR_IO, true, "Target CELL.TRIMSEC is specified by value, and values don't "
+			    "match.\n");
+		}
+	    } else if (strcasecmp(source, "HEADER") == 0) {
+		psString keyword = psMetadataLookupString(NULL, cell->camera, "CELL.TRIMSEC");
+		psMetadata *header = NULL; // The FITS header
+		if (cell->private) {
+		    header = cell->private->header;
+		} else if (chip->private) {
+		    header = chip->private->header;
+		} else if (fpa->private) {
+		    header = fpa->private->header;
+		}
+		if (! header) {
+		    psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
+		} else {
+		    psMetadataAddItem(header, trimsecItem, PS_LIST_TAIL, PS_META_REPLACE);
+		}
+	    } else {
+		psError(PS_ERR_IO, true, "CELL.TRIMSEC.SOURCE (%s) is not HEADER or VALUE.\n", source);
+	    }
+	}
+    }
+
+    // CELL.BIASSEC
+    {
+	psMetadataItem *biassecItem = psMetadataLookup(cell->concepts, "CELL.BIASSEC");
+	psList *biassecs = biassecItem->data.V; // The biassecs region list
+	psMetadataItem *sourceItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.BIASSEC.SOURCE");
+	if (! sourceItem) {
+	    psError(PS_ERR_IO, false, "Couldn't find CELL.BIASSEC.SOURCE.\n");
+	} else if (sourceItem->type != PS_META_STR) {
+	    psError(PS_ERR_IO, true, "CELL.BIASSEC.SOURCE is not of type STR (%x)\n", sourceItem->type);
+	} else {
+	    psString source = sourceItem->data.V; // The source string
+	    
+	    if (strcasecmp(source, "VALUE") == 0) {
+		// Check that it's the same value as stored in the camera
+		psString checkString = psMetadataLookupString(NULL, cell->camera, "CELL.BIASSEC");
+		psList *checkList = papSplit(checkString, " ;");
+		if (biassecs->n != checkList->n) {
+		    psError(PS_ERR_IO, true, "Target CELL.BIASSEC is specified by value, but number of "
+			    "entries doesn't match.\n");
+		} else {
+		    // We don't care if the order matches or not
+		    psVector *check = psVectorAlloc(biassecs->n, PS_TYPE_U8); // Vector to mark regions off
+		    for (int i = 0; i < check->n; i++) {
+			check->data.U8[i] = 0;
+		    }
+		    psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD,
+								       false); // Iterator
+		    psListIterator *checkListIter = psListIteratorAlloc(checkList, PS_LIST_HEAD, false);
+		    psRegion *biassec = NULL; // Region from iteration
+		    while (biassec = psListGetAndIncrement(biassecsIter)) {
+			psListIteratorSet(checkListIter, PS_LIST_HEAD);
+			psRegion *checkRegion = NULL; // Region from iteration
+			int i = 0;		// Counter
+			while (checkRegion = psListGetAndIncrement(checkListIter)) {
+			    if (COMPARE_REGIONS(biassec, checkRegion)) {
+				check->data.U8[i] = 1;
+				i++;
+				break;
+			    }
+			    i++;
+			}
+		    }
+		    for (int i = 0; i < check->n; i++) {
+			if (check->data.U8[i] == 0) {
+			    psError(PS_ERR_IO, true, "Target CELL.BIASSEC is specified by value, but values "
+				    "don't match.\n");
+			    break;
+			}
+		    }
+		    psFree(checkListIter);
+		    psFree(biassecsIter);
+		    psFree(check);
+		}
+		psFree(checkList);
+	    } else if (strcasecmp(source, "HEADER") == 0) {
+		psString keywordsString = psMetadataLookupString(NULL, cell->camera, "CELL.BIASSEC");
+		psList *keywords = papSplit(keywordsString, " ,;");
+		if (biassecs->n != keywords->n) {
+		    psError(PS_ERR_IO, true, "Target CELL.BIASSEC is sepcified by headers, but the number "
+			    "of headers doesn't match.\n");
+		} else {
+		    psMetadata *header = NULL; // The FITS header
+		    if (cell->private) {
+			header = cell->private->header;
+		    } else if (chip->private) {
+			header = chip->private->header;
+		    } else if (fpa->private) {
+			header = fpa->private->header;
+		    }
+		    if (! header) {
+			psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
+		    } else {
+			psListIterator *keywordsIter = psListIteratorAlloc(keywords, PS_LIST_HEAD, false);
+			psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, false);
+			psString keyword = NULL; // Header keyword from list
+			while (keyword = psListGetAndIncrement(keywordsIter)) {
+			    // Update the header
+			    psRegion *biassec = psListGetAndIncrement(biassecsIter);
+			    psString biassecString = psRegionToString(*biassec);
+			    psMetadataAdd(header, PS_LIST_TAIL, keyword, PS_META_STR | PS_META_REPLACE,
+					  "Bias section", biassecString);
+			    psFree(biassecString);
+			}
+			psFree(keywordsIter);
+			psFree(biassecsIter);
+		    }
+		}
+		psFree(keywords);
+	    } else {
+		psError(PS_ERR_IO, true, "CELL.BIASSEC.SOURCE (%s) is not HEADER or VALUE.\n", source);
+	    }
+	}
+    }
+
+    // CELL.XBIN and CELL.YBIN
+    {
+	psMetadataItem *xBinItem = psMetadataLookup(cell->concepts, "CELL.XBIN"); // Binning factor in x
+	psMetadataItem *yBinItem = psMetadataLookup(cell->concepts, "CELL.YBIN"); // Binning factor in y
+
+	// If these are specified by the header, we need to check to see if it's the same header keyword
+	psMetadata *camera = fpa->camera;
+	psMetadata *translation = psMetadataLookupMD(NULL, camera, "TRANSLATION");
+	psString xKeyword = psMetadataLookupString(NULL, translation, "CELL.XBIN");
+	psString yKeyword = psMetadataLookupString(NULL, translation, "CELL.YBIN");
+	if (strlen(xKeyword) > 0 && strcasecmp(xKeyword, yKeyword) != 0) {
+	    setConceptInHeader(fpa, chip, cell, xBinItem);
+	    xBinItem = NULL;
+	}
+	if (strlen(yKeyword) > 0 && strcasecmp(xKeyword, yKeyword) != 0) {
+	    setConceptInHeader(fpa, chip, cell, yBinItem);
+	    yBinItem = NULL;
+	}
+	if (strlen(xKeyword) > 0 && strlen(yKeyword) > 0 && strcasecmp(xKeyword, yKeyword) == 0) {
+	    psString binString = psStringCopy("");
+	    psStringAppend(&binString, "%d,%d", xBinItem->data.S32, yBinItem->data.S32);
+	    psMetadataItem *binItem = psMetadataItemAllocStr(xKeyword, "Binning factor in x and y",
+							     binString);
+	    psMetadata *header = NULL; // The FITS header
+	    if (cell->private) {
+		header = cell->private->header;
+	    } else if (chip->private) {
+		header = chip->private->header;
+	    } else if (fpa->private) {
+		header = fpa->private->header;
+	    }
+	    if (! header) {
+		psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
+	    } else {
+		psMetadataAddItem(header, binItem, PS_LIST_TAIL, PS_META_REPLACE);
+		xBinItem = NULL;
+		yBinItem = NULL;
+	    }
+	    psFree(binItem);
+	}
+
+	// Do it in the usual way if we have to
+	if (xBinItem) {
+	    setConceptItem(fpa, chip, cell, db, xBinItem);
+	}
+	if (yBinItem) {
+	    setConceptItem(fpa, chip, cell, db, yBinItem);
+	}
+    }
+
+    // CELL.TIMESYS
+    {
+	psMetadataItem *sysItem = psMetadataLookup(cell->concepts, "CELL.TIMESYS");
+	psString sys = NULL;		// String to store
+	switch (sysItem->data.S32) {
+	  case PS_TIME_TAI:
+	    sys = psStringCopy("TAI");
+	    break;
+	  case PS_TIME_UTC:
+	    sys = psStringCopy("UTC");
+	    break;
+	  case PS_TIME_UT1:
+	    sys = psStringCopy("UT1");
+	    break;
+	  case PS_TIME_TT:
+	    sys = psStringCopy("TT");
+	    break;
+	  default:
+	    sys = psStringCopy("Unknown");
+	}
+	psMetadataItem *newItem = psMetadataItemAllocStr("CELL.TIMESYS", "Time system", sys);
+	setConceptItem(fpa, chip, cell, db, newItem);
+	psFree(newItem);
+	psFree(sys);
+    }
+
+    // CELL.TIME
+    {
+	psMetadataItem *timeItem = psMetadataLookup(cell->concepts, "CELL.TIME");
+	psTime *time = timeItem->data.V; // The time
+	psString dateTimeString = psTimeToISO(time); // String representation
+
+	psMetadata *formats = psMetadataLookupMD(NULL, fpa->camera, "FORMATS");
+	psString format = psMetadataLookupString(NULL, formats, "CELL.TIME");
+
+	if (strlen(format) == 0) {
+	    // No format specified --> do it in the usual way (maybe it's a DB lookup)
+	    psMetadataItem *newTimeItem = psMetadataItemAllocStr("CELL.TIME", "Time of observation",
+								 dateTimeString);
+	    setConceptItem(fpa, chip, cell, db, newTimeItem);
+	    psFree(newTimeItem);
+	} else {
+	    if (strcasecmp(format, "ISO") == 0) {
+		// dateTimeString contains an ISO time
+		psMetadataItem *newTimeItem = psMetadataItemAllocStr("CELL.TIME", "Time of observation",
+								     dateTimeString);
+		setConceptItem(fpa, chip, cell, db, newTimeItem);
+		psFree(newTimeItem);
+	    } else if (strstr(format, "SEPARATE")) {
+		// We're working with two separate headers
+		psMetadata *header = NULL; // The FITS header
+		if (cell->private) {
+		    header = cell->private->header;
+		} else if (chip->private) {
+		    header = chip->private->header;
+		} else if (fpa->private) {
+		    header = fpa->private->header;
+		}
+		if (! header) {
+		    psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
+		} else {
+		    // Get the headers
+		    psMetadata *camera = fpa->camera;
+		    psMetadata *translation = psMetadataLookupMD(NULL, camera, "TRANSLATION");
+		    psString keywords = psMetadataLookupString(NULL, translation, "CELL.TIME");
+		    psList *dateTimeKeywords = papSplit(keywords, " ,;");
+		    psString dateKeyword = psListGet(dateTimeKeywords, PS_LIST_HEAD);
+		    psString timeKeyword = psListGet(dateTimeKeywords, PS_LIST_TAIL);
+
+		    psList *dateTime = papSplit(dateTimeString, " T"); // Find the middle T
+		    psString dateString = psListGet(dateTime, PS_LIST_HEAD);
+		    psString timeString = psListGet(dateTime, PS_LIST_TAIL);
+
+		    // XXX: Couldn't be bothered doing these right now
+		    if (strstr(format, "PRE2000")) {
+			psError(PS_ERR_IO, true, "Don't you realise it's the twenty-first century?\n");
+		    }
+		    if (strstr(format, "BACKWARDS")) {
+			psError(PS_ERR_IO, true, "You want it BACKWARDS?  Not right now, thanks.\n");
+		    }
+
+		    psMetadataItem *dateItem = psMetadataItemAllocStr(dateKeyword, "Date of observation",
+								      dateString);
+		    psMetadataItem *timeItem = psMetadataItemAllocStr(timeKeyword, "Time of observation",
+								      timeString);
+		    psMetadataAddItem(header, dateItem, PS_LIST_TAIL, PS_META_REPLACE);
+		    psMetadataAddItem(header, timeItem, PS_LIST_TAIL, PS_META_REPLACE);
+
+		    psFree(dateTimeKeywords);
+		    psFree(dateTime);
+		    psFree(dateItem);
+		    psFree(timeItem);
+		}
+	    } else if (strcasecmp(format, "MJD") == 0) {
+		double mjd = psTimeToMJD(time);
+		psMetadataItem *newTimeItem = psMetadataItemAllocF64("CELL.TIME", "MJD of observation", mjd);
+		setConceptItem(fpa, chip, cell, db, newTimeItem);
+		psFree(newTimeItem);
+	    } else if (strcasecmp(format, "JD") == 0) {
+		double jd = psTimeToMJD(time);
+		psMetadataItem *newTimeItem = psMetadataItemAllocF64("CELL.TIME", "JD of observation", jd);
+		setConceptItem(fpa, chip, cell, db, newTimeItem);
+		psFree(newTimeItem);
+	    } else {
+		psError(PS_ERR_IO, true, "Not sure how to outgest CELL.TIME (%s) --- trying "
+			"ISO\n", dateTimeString);
+		psMetadataItem *newTimeItem = psMetadataItemAllocStr("CELL.TIME", "Time of observation",
+								     dateTimeString);
+		setConceptItem(fpa, chip, cell, db, newTimeItem);
+		psFree(newTimeItem);
+	    }
+	}
+
+	psFree(dateTimeString);
+    }
+
+    // Pau.
+}
+
Index: /trunk/archive/scripts/src/phase2/pmFPAConceptsSet.h
===================================================================
--- /trunk/archive/scripts/src/phase2/pmFPAConceptsSet.h	(revision 5105)
+++ /trunk/archive/scripts/src/phase2/pmFPAConceptsSet.h	(revision 5105)
@@ -0,0 +1,17 @@
+#ifndef PM_FPA_CONCEPTS_SET_H
+#define PM_FPA_CONCEPTS_SET_H
+
+void pmFPAOutgestConcepts(pmFPA *fpa,	// The FPA
+			  psDB *db	// DB handle
+    );
+void pmChipOutgestConcepts(pmChip *chip,	// The chip
+			   psDB *db	// DB handle
+    );
+void pmCellOutgestConcepts(pmCell *cell, // The cell
+			   psDB *db	// DB handle
+    );
+
+
+
+
+#endif
Index: /trunk/archive/scripts/src/phase2/pmFPARead.c
===================================================================
--- /trunk/archive/scripts/src/phase2/pmFPARead.c	(revision 5105)
+++ /trunk/archive/scripts/src/phase2/pmFPARead.c	(revision 5105)
@@ -0,0 +1,232 @@
+#include <stdio.h>
+#include <string.h>
+#include "pslib.h"
+
+#include "pmFPA.h"
+#include "pmFPAConceptsGet.h"
+#include "pmFPARead.h"
+
+#include "papStuff.h"			// For "split"
+
+// NOTE: Need to deal with header inheritance
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+// Read a FITS extension into a chip
+static bool readExtension(p_pmHDU *hdu,	// HDU to read
+			  psFits *fits	// The FITS file from which to read
+    )
+{
+    const char *extName = hdu->extname;	// Extension name
+
+    psTrace(__func__, 7, "Moving to extension %s...\n", extName);
+    if (strncmp(extName, "PHU", 3) == 0) {
+	if (!psFitsMoveExtNum(fits, 0, false)) {
+	    psError(PS_ERR_IO, false, "Unable to find PHU in FITS file!\n");
+	    return false;
+	}
+    } else if (! psFitsMoveExtName(fits, extName)) {
+	psError(PS_ERR_IO, false, "Unable to find extension %s in FITS file!\n", extName);
+	return false;
+    }
+    psTrace(__func__, 7, "Reading header....\n");
+    psMetadata *header = psFitsReadHeader(NULL, fits); // Header
+    if (! header) {
+	psError(PS_ERR_IO, false, "Unable to read FITS header!\n");
+	return false;
+    }
+
+    psTrace(__func__, 7, "Checking NAXIS....\n");
+    bool mdStatus = false;
+    int nAxis = psMetadataLookupS32(&mdStatus, header, "NAXIS");
+    if (!mdStatus) {
+	psLogMsg(__func__, PS_LOG_WARN, "There is no NAXIS keyword in the FITS header of extension %s!\n",
+		 extName);
+    }
+    if (nAxis != 2 && nAxis != 3) {
+	psLogMsg(__func__, PS_LOG_WARN, "Image is not 2- or 3-dimensional --- reading into a single image "
+		 "anyway.\n");
+    }
+    psTrace(__func__, 9, "NAXIS = %d\n", nAxis);
+
+    int numPlanes = 1;			// Number of planes
+    if (nAxis == 3) {
+	numPlanes = psMetadataLookupS32(&mdStatus, header, "NAXIS3");
+	if (!mdStatus) {
+	    psError(PS_ERR_IO, false, "Unable to read NAXIS3 for 3-dimensional image!\n");
+	    return false;
+	}
+    }
+    psRegion region = {0, 0, 0, 0};	// Region to read is everything
+
+    // Read each plane into the array
+    psTrace(__func__, 7, "Reading %d planes into array....\n", numPlanes);
+    psArray *pixels = psArrayAlloc(numPlanes); // Array of images
+    for (int i = 0; i < numPlanes; i++) {
+	psTrace(__func__, 9, "Reading plane %d\n", i);
+	psMemCheckCorruption(true);
+	psImage *image = psFitsReadImage(NULL, fits, region, i);
+
+	// XXX: Type conversion here to support the modules, which don't have multiple type support yet
+	if (image->type.type != PS_TYPE_F32) {
+	    pixels->data[i] = psImageCopy(NULL, image, PS_TYPE_F32);
+	    psFree(image);
+	} else {
+	    pixels->data[i] = image;
+	}
+	psTrace(__func__, 10, "Done\n");
+        if (! pixels->data[i]) {
+	    psError(PS_ERR_IO, false, "Unable to read image for extension %s, plane %d\n", extName, i);
+	    return false;
+	}
+    }
+
+    // Update the HDU with the new data
+    hdu->pixels = pixels;
+    hdu->header = header;
+
+    return true;
+}
+
+
+// Portion out an image into the cell
+static bool portionCell(pmCell *cell,	// The cell that gets its bits
+			psArray *images // Array of images from which the readouts are assigned
+    )
+{
+    bool mdStatus = false;		// Status of MD lookup
+
+    // Get the trim and bias sections
+    psMetadataItem *mdItem = psMetadataLookup(cell->concepts, "CELL.TRIMSEC");
+    psRegion *trimRegion = mdItem->data.V;
+    mdItem = psMetadataLookup(cell->concepts, "CELL.BIASSEC");
+    psList *biasRegionList = mdItem->data.V;
+
+    // Iterate over each of the image planes
+    for (int i = 0; i < images->n; i++) {
+	psImage *image = images->data[i]; // The i-th plane
+
+	// Convert from FITS standard to PS standard
+	// We don't touch the x1 and y1 values because they aren't included by psImageSubset.
+	//trimRegion->x0 -= 1;
+	//trimRegion->y0 -= 1;
+
+	psImage *trimImage = psImageSubset(image, *trimRegion); // Image from trim section
+
+	psList *biasImages = psListAlloc(NULL);	// List of images from bias sections
+	psListIterator *biasRegionIter = psListIteratorAlloc(biasRegionList, PS_LIST_HEAD, true); // Iterator
+	psRegion *biasRegion = NULL;	// Bias region from list
+	while (biasRegion = psListGetAndIncrement(biasRegionIter)) {
+	    // Convert from FITS standard to PS standard
+	    // We don't touch the x1 and y1 values because they aren't included by psImageSubset.
+	    //biasRegion->x0 -= 1;
+	    //biasRegion->y0 -= 1;
+	    
+	    psImage *biasImage = psImageSubset(image, *biasRegion); // Image from bias section
+	    psListAdd(biasImages, PS_LIST_TAIL, biasImage);
+	    psFree(biasImage);
+	}
+	psFree(biasRegionIter);
+
+	pmReadout *readout = pmReadoutAlloc(cell, trimImage, biasImages, 0,0,0,0,1,1);
+	psFree(readout);		// This seems silly, but the alloc registers it with the cell, so we
+					// don't have to do anything with it.  Perhaps we should change
+					// pmReadoutAlloc to pmReadoutAdd?
+	psFree(trimImage);
+	psFree(biasImages);
+    }
+    psFree(biasRegionList);
+
+    return true;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmFPARead(pmFPA *fpa,		// FPA to read into
+	       psFits *fits,		// FITS file from which to read
+	       psMetadata *phu,		// Primary header
+	       psDB *db			// Database handle, for concept ingest
+    )
+{
+    p_pmHDU *hdu = NULL;		// Current HDU from FITS file
+
+    // Read the PHU, if required
+    if (! phu) {
+	if (! psFitsMoveExtNum(fits, 0, false)) {
+	    psError(PS_ERR_IO, false, "Unable to find PHU in FITS file!\n");
+	    return false;
+	}
+	psTrace(__func__, 7, "Reading PHU....\n");
+	psMetadata *phu = psFitsReadHeader(NULL, fits); // Primary header
+    }
+    fpa->phu = phu;
+
+    // Read in....
+    psTrace(__func__, 1, "Working on FPA...\n");
+    if (fpa->private) {
+	hdu = fpa->private;
+	psTrace(__func__, 3, "Reading pixels from extension %s into FPA.\n", hdu->extname);
+	if (! readExtension(hdu, fits)) {
+	    psError(PS_ERR_IO, false, "Unable to read pixels from extension %s\n", hdu->extname);
+	    return false;
+	}
+    }
+    pmFPAIngestConcepts(fpa, db);
+
+    psArray *chips = fpa->chips;	// Array of chips
+    // Iterate over the FPA
+    for (int i = 0; i < chips->n; i++) {
+	pmChip *chip = chips->data[i]; // The chip
+
+	// Only read chips marked "valid"
+	if (! chip->valid) {
+	    psTrace(__func__, 2, "Ignoring chip %d...\n", i);
+	    continue;
+	}
+	psTrace(__func__, 2, "Reading in chip %d...\n", i);
+
+	if (chip->private) {
+	    hdu = chip->private;
+	    psTrace(__func__, 3, "Reading pixels from extension %s into chip %d.\n", hdu->extname, i);
+	    if (! readExtension(hdu, fits)) {
+		psError(PS_ERR_IO, false, "Unable to read pixels from extension %s\n", hdu->extname);
+		return false;
+	    }
+	}
+	pmChipIngestConcepts(chip, db);
+
+	// Iterate over the chip
+	psArray *cells = chip->cells;	// Array of cells
+	for (int j = 0; j < cells->n; j++) {
+	    pmCell *cell = cells->data[j]; // The cell
+
+	    // Only read cells marked "valid"
+	    if (! cell->valid) {
+		psTrace(__func__, 3, "Ignoring chip %d...\n", i);
+		continue;
+	    }
+	    psTrace(__func__, 3, "Reading in cell %d...\n", j);
+
+	    if (cell->private) {
+		hdu = cell->private;
+		psTrace(__func__, 5, "Reading pixels from extension %s into cell %d.\n", hdu->extname, j);
+		if (! readExtension(hdu, fits)) {
+		    psError(PS_ERR_IO, false, "Unable to read pixels from extension %s\n", hdu->extname);
+		    return false;
+		}
+	    }
+	    pmCellIngestConcepts(cell, db);
+
+	    psTrace(__func__, 5, "Allocating readouts for chip %d cell %d...\n",
+		    i, j);
+	    portionCell(cell, hdu->pixels);
+	}
+    }
+
+    return true;
+}
Index: /trunk/archive/scripts/src/phase2/pmFPARead.h
===================================================================
--- /trunk/archive/scripts/src/phase2/pmFPARead.h	(revision 5105)
+++ /trunk/archive/scripts/src/phase2/pmFPARead.h	(revision 5105)
@@ -0,0 +1,12 @@
+#ifndef PM_FPA_READ_H
+#define PM_FPA_READ_H
+
+#include "pmFPA.h"
+
+bool pmFPARead(pmFPA *fpa,		// FPA to read into
+	       psFits *fits,		// FITS file from which to read
+	       psMetadata *phu,		// Primary header
+	       psDB *db			// Database handle, for concept ingest
+    );
+
+#endif
Index: /trunk/archive/scripts/src/phase2/pmFPAWrite.c
===================================================================
--- /trunk/archive/scripts/src/phase2/pmFPAWrite.c	(revision 5105)
+++ /trunk/archive/scripts/src/phase2/pmFPAWrite.c	(revision 5105)
@@ -0,0 +1,66 @@
+#include <stdio.h>
+#include <string.h>
+#include "pslib.h"
+
+#include "pmFPA.h"
+#include "pmFPAConceptsSet.h"
+
+static bool writeHDU(psFits *fits,	// FITS file to which to write
+		     p_pmHDU *hdu	// HDU to write
+    )
+{
+    bool status = true;			// Status of write, to return
+    for (int i = 0; i < hdu->pixels->n; i++) {
+	status &= psFitsWriteImage(fits, hdu->header, hdu->pixels->data[i], i);
+    }
+
+    return status;
+}
+
+
+bool pmFPAWrite(psFits *fits,		// FITS file to which to write
+		pmFPA *fpa,		// FPA to write
+		psDB *db		// Database to update
+    )
+{
+    bool status = true;			// Status of writing, to return
+
+    pmFPAOutgestConcepts(fpa, db);
+
+    // Write the primary header
+    if (fpa->phu) {
+	status &= psFitsMoveExtNum(fits, 0, false);
+	status &= psFitsWriteHeader(fpa->phu, fits);
+    }
+
+    psArray *chips = fpa->chips;	// Array of component chips
+    for (int i = 0; i < chips->n; i++) {
+	pmChip *chip = chips->data[i];	// The component chip
+	if (chip->valid) {
+	    pmChipOutgestConcepts(chip, db);
+
+	    psArray *cells = chip->cells;	// Array of component cells
+	    for (int j = 0; j < cells->n; j++) {
+		pmCell *cell = cells->data[j]; // The component cell
+		if (cell->valid) {
+		    pmCellOutgestConcepts(cell, db);
+
+		    if (cell->private && strlen(cell->private->extname) > 0) {
+			status &= writeHDU(fits, cell->private);
+		    }
+		}
+	    }
+	    
+	    if (chip->private && strlen(chip->private->extname) > 0) {
+		status &= writeHDU(fits, chip->private);
+	    }
+	}
+
+    }
+
+    if (fpa->private && strlen(fpa->private->extname) > 0) {
+	status &= writeHDU(fits, fpa->private);
+    }
+
+    return status;
+}
Index: /trunk/archive/scripts/src/phase2/pmFPAWrite.h
===================================================================
--- /trunk/archive/scripts/src/phase2/pmFPAWrite.h	(revision 5105)
+++ /trunk/archive/scripts/src/phase2/pmFPAWrite.h	(revision 5105)
@@ -0,0 +1,12 @@
+#ifndef PM_FPA_WRITE_H
+#define PM_FPA_WRITE_H
+
+#include "pmFPA.h"
+
+bool pmFPAWrite(psFits *fits,		// FITS file to which to write
+		pmFPA *fpa,		// FPA to write
+		psDB *db		// Database to update
+    );
+
+
+#endif
