import java.io.*;
import java.net.*;
import java.text.*;
import java.util.*;

import nom.tam.fits.*;
import nom.tam.util.*;

public class SharcScan implements Cloneable {
    SharcFrame[] frame;
    ReducedData reduced;
    Bolometer[] pixel;

    BinaryTableHDU HDU;

    double delay = 0.0;

    final static int[] noisy13HzColumn = { 3, 11, 16, 24 };
    
    static String dataPath = "/home/sharcii/data0/fits";
    static String filePrefix = "sharc2-";
    String configName;
    String maskName;

    // Scan descriptors
    String descriptor;
    long fileSize;
    int serialNo;
    int frames = 0, pixels = 384, activePixels, activeFrames;
    String sourceName, creator, observer, project;
    String date;
    int year, month, day;
    double UT;
    boolean isTotalPower = false;
    double dspTimeOffset = Double.NaN;
    int firstFrameNumber;

    // Instrument data
    float RA, DEC, AZ, EL, PA, coordinateEpoch = Float.NaN;
    float origRA, origDEC, origEpoch;
    float RAO, DECO, AZO, ELO;
    double rotatorAngle = Double.NaN, rotatorZeroAngle = Double.NaN, rotatorOffset, secondaryAngle, focusX, focusY, focusZ;
    double focusYOffset, focusZOffset;
    int MJD;
    String focusMode, filterName;

    double FAZO = Double.NaN, FZAO = Double.NaN;
    double tau = Double.NaN;
    double scale = 1.0;
    String tauID;

    // Telescope Settings
    double temperature, pressure, humidity;
    String rotatorMode, dsosVersion;
    boolean dsos, isNasmyth;

    // Scan Settings
    String pixelFileName;
    boolean taufit=true, lookup = false, autoLookup=true, autoHighPass = false, useSharcTau=false;
    boolean fitsGains = true, fitsWeights = true, fitsFlags = true, fitsOffsets = true;
    boolean directGains=true, directTau=false;
    boolean chopped = false, filter13Hz=true, flag13Hz=true;
    boolean autoChopRecognition = true;
    String gapOKRule = "JSharc", tauLookupID = "350um";
    int downSample = 1, excludeFlags = 0x03;
    double vclip = Double.NaN, aclip = -1.0;
    double highPass = 0.0, width13Hz = 0.25 * Unit.Hz;
    int highPassFrames = -1;
    int topEdge=0, bottomEdge=0, leftEdge=0, rightEdge=0;
    float chopTolerance = 8.0F * Unit.ArcSECOND;
    double Jy = ReductionTool.defaultVperJy;
    double minIntTime = 60.0 * Unit.SECOND;

    double samplingInterval = 0.0;
    double chopperThrow = 0.0, chopperFrequency = 0.0;
    
    int velocityT = 5;
    
    // Derived Quantities
    float cosDEC = 1.0F;
    short mapIndexTable[][][]; // t, bol, {i,j}
    int masks = 0, flags = 0, vclipped = 0, aclipped = 0;
    float gain = 1.0F;
    float transmission = 1.0F;
    float excessLoadT = 0.0F;
    float dIdT = 0.0F, I0 = SharcHeader.I0;
    float loadT = 0.0F;    
    float weight = 1.0F;

    short[][] DAC;
    float[] rowGain;
    boolean[] isHiGain;

    boolean[] isFiltered13Hz;
    int filtered13HzChannels = 0;

    public SharcScan() {
	pixel = new Bolometer[pixels];
	for(int bol=0; bol<pixels; bol++) pixel[bol] = new Bolometer(bol%32,bol/32);	
    }
    
    public Object clone() { 
	try { return super.clone(); }
	catch(CloneNotSupportedException e) { return null; }
    }

    public SharcScan duplicate() {
	SharcScan copy = (SharcScan) clone();

	copy.frame = new SharcFrame[frames];
	copy.pixel = new Bolometer[pixels];

	for(int t=0; t<frames; t++) copy.frame[t] = frame[t].duplicate();
	for(int bol=0; bol<pixels; bol++) copy.pixel[bol] = pixel[bol].duplicate();

	return copy;
    }

    public void init() {
	frames = frame.length;
	pixels = frame[0].readout.length;
	
	configName = Crush.currentConfig;

	// Initialize the bolometers, and make a census of them
	activePixels = 0;
	activeFrames = frames;

	isFiltered13Hz = new boolean[pixels];

	if(directGains) calcPixelGains();

	for(int bol=0; bol<pixels; bol++) {
	    if(filter13Hz) for(int c=0; c<noisy13HzColumn.length; c++) if(noisy13HzColumn[c] == bol%32) isFiltered13Hz[bol] = true; 
	    pixel[bol].excludeFlags = excludeFlags;
	    pixel[bol].init(samplingInterval);
	    if(pixel[bol].isActive) activePixels++;
	}

	// Flag pixels as required
	for(int row=0,bol=0; row<12; row++) for(int col=0; col<32; col++,bol++) {
	    if(col < leftEdge || col >= 32-rightEdge) flag(bol, Bolometer.EDGE);
	    if(row < topEdge || row >= 12-bottomEdge) flag(bol, Bolometer.EDGE);
	}

	if(flag13Hz) flagNoisyColumns();

	// Calculate the derived scan constants
	flagInvalidGains();
	if(directTau) calcDirectTau();
	else calcExcessLoad();
	calcI0();
	calcdIdT();
	if(directGains) calcPixelGains();
	else calcOverallGain();

	// Recalculate the frames
	for(int t=0; t<frames; t++) frame[t].calc();

	// Clip frames with unsuitable velocities and acceleration
	// Assume fastest varying physical is on 1s scale.
	if(Double.isNaN(vclip)) vclip = ReductionTool.resolution / Unit.SECOND;

	if(vclip > 0.0 || aclip > 0.0) {	    
	    if(vclip > 0.0) velocityClip(vclip);
	    if(aclip > 0.0) accelerationClip(aclip);
	}

	// Apply high-pass filter if requested	
	if(highPass > 0.0) highPassFilter(highPass);	
    }

    public void makeCensus() {
	activePixels = 0;
	for(int bol=0; bol<pixels; bol++) if(pixel[bol].isActive) activePixels++;
    }

    public void flagInvalidGains() {
	normalizeGains();
	for(int bol=0; bol<pixels; bol++) if(!pixel[bol].isExcluded()) {
	    double gain = pixel[bol].gain;
	    if(gain < 0.25 || gain > 2.0) flag(bol, Bolometer.INVALID_GAIN);
	}
    }	    

    // Calculate the total gain factor in the case when only
    // relative gains are available. E.g. read from FITS data.
    public void calcOverallGain() {
	makeCensus();
	calcLoadTemperature();
	double sumGain = 0.0;
	for(int bol=0; bol<pixels; bol++) if(pixel[bol].isActive) {
	    sumGain += pixel[bol].G0 * (1.0 - loadT / pixel[bol].T0);
	}
	
	gain = (float) (sumGain / activePixels / scale);
    }

    // Call this to bring gains to normalized form, in which
    // <pixel.gain> = 1, and the total gain factor is fully absorbed
    // into scan.gain.
    public void normalizeGains() {
	makeCensus();
	double aveGain = 0.0;

	for(int bol=0; bol<pixels; bol++) if(pixel[bol].isActive) aveGain += pixel[bol].gain;
    	if(activePixels > 0) {
	    aveGain /= activePixels;
	    for(int bol=0; bol<pixels; bol++) pixel[bol].gain /= aveGain;
	}
	if(directGains) gain *= aveGain;
    }

    public void calcdIdT() {
	makeCensus();
	double sumdIdT = 0.0, sumw=0.0;
	for(int bol=0; bol<pixels; bol++) if(pixel[bol].isActive) {
	    sumdIdT += pixel[bol].weight * pixel[bol].I0 / pixel[bol].T0;
	    sumw += pixel[bol].weight;
	}
	
	dIdT = sumw > 0.0 ? (float) (sumdIdT / sumw) : 0.0F;
    }

    public void calcI0() {
	makeCensus();
	double sumI0=0.0, sumw=0.0;
	for(int bol=0; bol < pixels; bol++) if(pixel[bol].isActive) {
	    sumI0 += pixel[bol].weight * pixel[bol].I0 * (temperature + 273.16) / pixel[bol].T0;
	    sumw += pixel[bol].weight;
	}
	I0 = (float) (sumI0/sumw);
    }

    public void calcPixelGains() {
	makeCensus();
	calcLoadTemperature();
	for(int bol=0; bol<pixels; bol++) {
	    pixel[bol].gain = pixel[bol].G0 * (1.0F - loadT / pixel[bol].T0);
	}
	gain = (float) (1.0 / scale);
	normalizeGains();
    }

    public double getDirectTransmission() {
	makeCensus();
	double dI=0.0, dIdT=0.0; 
	float areaFactor = pixelArea / SharcHeader.pixelArea;	
	double eps = (getActualLoadTemperature() - excessLoadT) / (areaFactor * (temperature + 273.16)); 	
	return 1.0 - eps;
    }
    
    // Calculate the direct transmission, and apply it to the scan as
    // well as all contained frames within.
    public void calcDirectTransmission() { transmission = (float) getDirectTransmission(); }

    public double getDirectTau() { return getDirectTau(getDirectTransmission()); }

    public double getDirectTau(double trans) { return -Math.log(trans) * Math.sin(EL); }
    
    public void calcDirectTau() {
	calcDirectTransmission();
	tau = getDirectTau(transmission);
	tauID = "Sharc2";
	if(Double.isNaN(tau)) {
	    System.out.println("ERROR! Invalid Direct Tau Value. Exiting.");
	    System.exit(1);
	}
    }

    // Reminder: eps_obs = eps * areaFactor
    public void calcLoadTemperature() {
	float areaFactor = pixelArea / SharcHeader.pixelArea;
	loadT = areaFactor * (1.0F - transmission) * ((float)temperature + 273.16F) + excessLoadT;
    }


    public double getActualLoadTemperature() {
	makeCensus();
	double dI=0.0, dIdT=0.0; 
	
	for(int bol=0; bol<pixels; bol++) if(pixel[bol].isActive) {
	    final float w = pixel[bol].weight;
	    dI += w * (pixel[bol].I0 + pixel[bol].offset);
	    dIdT += w * pixel[bol].I0 / pixel[bol].T0;
	}

	return (dIdT > 0.0 ? dI / dIdT : 0.0);
    }

    // Reminder: eps_obs = eps * areaFactor
    public void calcExcessLoad() {
	makeCensus();

	float areaFactor = pixelArea / SharcHeader.pixelArea;
	double opticalLoadT = areaFactor * (1.0 - transmission) * (temperature + 273.16);

	excessLoadT = (float)(getActualLoadTemperature() - opticalLoadT); 
    }

    // Try guess the loading from the relative gains alone
    public void guessLoad() {
	if(activePixels <= 0) return;

	normalizeGains();
	
	float[][] relGain = new float[pixels][2];
	double sum0 = 0.0, sum1 = 0.0;
	for(int bol=0; bol < pixels; bol++) if(pixel[bol].isActive) {
	    relGain[bol][0] = pixel[bol].G0;
	    relGain[bol][1] = pixel[bol].G0 * (1.0F - 300.0F / pixel[bol].T0);

	    sum0 += relGain[bol][0];
	    sum1 += relGain[bol][1];
	}

	sum0 /= activePixels; sum1 /= activePixels;
	
	double T = 0.0, sumw = 0.0;

	for(int bol=0; bol < pixels; bol++) if(pixel[bol].isActive) {
	    double gainSlope = relGain[bol][1] / sum1 - relGain[bol][0] / sum0;
	    
	    if(gainSlope != 0.0) {
		T += pixel[bol].weight * gainSlope * (pixel[bol].gain - relGain[bol][0]);
		sumw += pixel[bol].weight * gainSlope * gainSlope;
	    }
	}
	
	if(sumw > 0.0) T *= 300.0 / sumw;

	System.out.print(" T = " + T + " K ");
	
    }

    public double adjustTransparencies() {
	double dtrans = getDirectTransmission() - transmission;
	tau = getDirectTau(transmission);

	transmission += dtrans;
	for(int t=0; t < frames; t++) frame[t].transmission += dtrans;

	return dtrans;
    }


    public void getGainCoefficients() throws IOException {
	RandomAccessFile gainFile = new RandomAccessFile("nonlinearity.dat", "r");

	for(int bol=0; bol<pixels; bol++) {
	    String line;

	    do { line = gainFile.readLine(); } while(line.charAt(0) == '#');	    

	    StringTokenizer tokens = new StringTokenizer(line);

	    tokens.nextToken();
	    tokens.nextToken();
	    
	    pixel[bol].G0 = Float.parseFloat(tokens.nextToken());
	    pixel[bol].I0 = Float.parseFloat(tokens.nextToken());
	    pixel[bol].T0 = Float.parseFloat(tokens.nextToken());
	}

	gainFile.close();
    }

    public void writeGainCoefficients() throws IOException {
	File gainFile = new File(Crush.workPath + "nonlinearity-" + serialNo + ".dat");
	if(gainFile.exists()) gainFile.delete();
	RandomAccessFile out = new RandomAccessFile(gainFile, "rw");

	DecimalFormat df = new DecimalFormat("0.000000");
	DecimalFormat ef = new DecimalFormat("0.0000E0");

	out.writeBytes("# Coefficients determined from scan " + serialNo + "\n");
	out.writeBytes("#\n");
	out.writeBytes("# row\tcol\tG(eps=0)\tI(eps=0)\tT0 (K))\n");
	out.writeBytes("# ----- ------- --------------  --------------- ----------\n");

	for(int bol=0; bol<pixels; bol++) {
	    out.writeBytes((bol/32+1) + "\t" + (bol%32+1) + "\t");
	    out.writeBytes(df.format(pixel[bol].G0) + "\t");
	    out.writeBytes(ef.format(pixel[bol].I0) + "\t");
	    out.writeBytes(df.format(pixel[bol].T0) + "\n");
	}

	out.close();
    }

    public void calcGainCoefficients() {
	double eps = 1.0 - transmission;
	for(int bol=0; bol<pixels; bol++) {
	    pixel[bol].calcI0(this);
	    pixel[bol].calcG0(this);
	}
    }

    public void calcSharcTau() {
	double sinEL = Math.sin(EL);
	tau = ReductionTool.sharcTauOf(tauID, tau);
	tauID = "Sharc2";
    }

    public void getFitTau() throws IOException {
	// Return immediately if ID does not match 225GHz or 350um, which are the only values in
	// the Mai-Tau lookup at present
	taufit = false;
	
	if(!tauLookupID.equalsIgnoreCase("225GHz") && !tauLookupID.equalsIgnoreCase("350um")) {
	    System.out.println("   No Mai-Tau Lookup for " + tauLookupID + ". Using default.");
	    tauLookupID = "off"; 
	    return;
	}
	    
	Socket tauserver = new Socket(Crush.tauServer, 63225);
	PrintWriter out = new PrintWriter(tauserver.getOutputStream(), true);
	BufferedReader in = new BufferedReader(new InputStreamReader(tauserver.getInputStream()));
	
	while(in.read() != '>'); // Seek for prompt

	out.println("set noexit"); // Enter into interactive mode (do not disconnect after first command).
	
	while(in.read() != '>'); // Seek for prompt

	out.println("set " + tauLookupID); // Specify which tau value to look up. 

	while(in.read() != '>'); // Seek for prompt

	out.println("get tau " + date); // Request tau for the specified date	
	String reply = in.readLine();		

	out.println("exit"); // Disconnect from Mai-Tau server 

	try {	    
	    tau = Double.parseDouble(reply);
	    tauID = tauLookupID;
 	    taufit = true;
	    System.out.println("   Got Mai-Tau! tau(" + tauLookupID + ") = " + reply); 
	}
	catch(NumberFormatException e) { 
	    System.out.println("   Mai-Tau Error: " + reply); 
	    tauLookupID = "off";
	}
    }

    
    double aveScanSpeed = 0.0, aveAcceleration = 0.0, maxAcceleration = 0.0;

    public void getPositionDerivatives() {
	final int vT = (int)Math.ceil((float)velocityT / downSample);
	final int N = (int)Math.ceil((float)frames/vT);

	double[] blockUT = new double[N]; 
	Vector2D[] position = new Vector2D[N];
	Vector2D[] offset = new Vector2D[N];
	Vector2D[] velocity = new Vector2D[N];
	Vector2D[] acceleration = new Vector2D[N];
	Vector2D[] sweepVelocity = new Vector2D[N];

	for(int T=0; T<N; T++) {
	    position[T] = new Vector2D();
	    offset[T] = new Vector2D();
	    int n = 0;

	    for(int blockt=0,t=T*vT; blockt<vT; blockt++,t++) if(t < frames) { 
		position[T].add(frame[t].getTrackingALTAZ(pixel[0]));
		offset[T].add(frame[t].getdAZdEL(pixel[0]));
		blockUT[T] += frame[t].UT;
		n++;
	    }
	    position[T].scale(1.0/n);
	    offset[T].scale(1.0/n);
	    blockUT[T] /= n;
	}
	
	// First calculate velocities as chords between consecutive points
	for(int T=0; T<N-1; T++) {
	    final double dt = blockUT[T+1] - blockUT[T];

	    Vector2D chord = Vector2D.difference(position[T+1], position[T]);
	    chord.x *= frame[T*vT].cosEL;
	    chord.scale(1.0/dt);
	    velocity[T+1] = chord;

	    chord = Vector2D.difference(offset[T+1], offset[T]);
	    chord.x *= frame[T*vT].cosEL;
	    chord.scale(1.0/dt);
	    sweepVelocity[T+1] = chord;
	}
	
	// Make a weighted average of neighbouring velocities to get point velocities
	Vector2D avev = new Vector2D();
	for(int T=1; T<N-1; T++) {
	    final double dt1 = blockUT[T] - blockUT[T-1];
	    final double dt2 = blockUT[T+1] - blockUT[T];
	    final double dt = dt1 + dt2;
	   
	    velocity[T].x = dt2/dt * velocity[T].x + dt1/dt * velocity[T+1].x; 
	    velocity[T].y = dt2/dt * velocity[T].y + dt1/dt * velocity[T+1].y;

	    sweepVelocity[T].x = dt2/dt * sweepVelocity[T].x + dt1/dt * sweepVelocity[T+1].x; 
	    sweepVelocity[T].y = dt2/dt * sweepVelocity[T].y + dt1/dt * sweepVelocity[T+1].y;
	}
	velocity[0] = velocity[1];
	velocity[N-1] = velocity[N-2];

	sweepVelocity[0] = sweepVelocity[1];
	sweepVelocity[N-1] = sweepVelocity[N-2];

	// Estimate acceleration based on chord velocities
	for(int T=1; T<N-1; T++) {
	    final double dt = (blockUT[T+1] - blockUT[T-1]) / 2.0;
	    acceleration[T] = Vector2D.difference(velocity[T+1],velocity[T-1]);
	    acceleration[T].math('/', dt);
	}
	acceleration[0] = acceleration[1];
	acceleration[N-1] = acceleration[N-2];

	// now fill in these value for the frames
	maxAcceleration = 0.0;
	aveAcceleration = 0.0;
	aveScanSpeed = 0.0;

	for(int T=0,t=0; T<N; T++) {
	    final float v = (float) sweepVelocity[T].length(); 
	    final float a = (float) acceleration[T].length();
	    for(int blockt=0; blockt < vT; blockt++,t++) if(t < frames) {
		frame[t].velocity = v;
		frame[t].acceleration = a;
	    }
	    aveScanSpeed += v;
	    aveAcceleration += a;
	    if(a > maxAcceleration) maxAcceleration = a; 
	}
	aveScanSpeed /= N;
	aveAcceleration /= N;

	DecimalFormat df =  new DecimalFormat("0.0");
	System.out.println("   Sweep Average Velocity: " + df.format(aveScanSpeed / (Unit.ArcSECOND / Unit.SECOND)) + " \"/s. ");  
	System.out.println("   Telescope Average Acceletation: " + 
			 df.format(aveAcceleration / (Unit.ArcSECOND / (Unit.SECOND*Unit.SECOND))) + " \"/s^2. ");  
    }	

    public void velocityClip(double minv) {
	if(minv < 0.0) return;

	vclipped = 0;
	for(int t=0; t<frames; t++) if(frame[t].velocity < minv) {
	    vclipped++;
	    for(int bol=0; bol<pixels; bol++,masks++) frame[t].masked[bol] = true;       
	}
    }

    public void accelerationClip(double maxa) { 
	if(aclip <= 0.0) return;

	aclipped = 0;
	for(int t=0; t<frames; t++) if(frame[t].acceleration > maxa) {
	    aclipped++;
	    for(int bol=0; bol<pixels; bol++,masks++) frame[t].masked[bol] = true; 	
	}
    }
    
    // Recycle the index array
    private short[] mapIndex = new short[2];
    public short[] getMapIndexes(int t, int bol) {
	if(lookup) { return mapIndexTable[t][bol]; }
      
	Vector2D coord = getMapOffsets(t, bol);
	
	mapIndex[0] = (short)reduced.indexOfdRA(coord.x);
	mapIndex[1] = (short)reduced.indexOfdDEC(coord.y);
	
	return mapIndex;	
    }

    public Vector2D getMapOffsets(int t, int bol) {
	Vector2D mapOffset;

	if(!ReductionTool.altaz) {
	    if(ReductionTool.planetary) mapOffset = frame[t].getdRAdDEC(pixel[bol]);
	    else { 
		mapOffset = frame[t].getRADEC(pixel[bol]);
		mapOffset.x -= reduced.RA;
		mapOffset.x *= cosDEC;
		mapOffset.y -= reduced.DEC;
	    }
	}
	else mapOffset = frame[t].getdAZdEL(pixel[bol]);
	
	return mapOffset;
    }	

    public void autoLookup() throws OutOfMemoryError {
	if(!autoLookup || mapIndexTable != null) {
	    long neededMemory = 4 * frames * pixels;
	    Runtime VM = Runtime.getRuntime();
	    makeIndexLookup();
	}
    }


    public void makeIndexLookup() throws OutOfMemoryError {
	lookup = false;

	mapIndexTable = new short[frames][pixels][2];
	    
	for(int t=0; t<frames; t++) for(int bol=0; bol<pixels; bol++) {
	    getMapIndexes(t, bol);
	    mapIndexTable[t][bol][0] = mapIndex[0];
	    mapIndexTable[t][bol][1] = mapIndex[1];
	}
  
	lookup = true;
    }

    public void writeIndex() {
	try {
	    Fits indexFITS = new Fits();
	    
	    BasicHDU hdu = Fits.makeHDU(mapIndexTable);

	    hdu.addValue("ALTAZ", ReductionTool.altaz, "Whether grid is in Alt/AZ vs the default RA/DEC system.");
	    hdu.addValue("NX", reduced.sizeX, "The number of grid positions on the x (RA/AZ) direction.");
	    hdu.addValue("NY", reduced.sizeY, "The number of grid positions on the y (DEC/Alt) direction.");
	    hdu.addValue("DELTA", reduced.delta / Unit.ArcSECOND, "The map resolution in arcsec.");
	    
	    reduced.addCoordinateInfo(hdu);
	    editHeader(hdu.getHeader());

	    hdu.addValue("EQUINOX", coordinateEpoch, "The catalog coordinate epoch");

	    indexFITS.addHDU(hdu);
	    indexFITS.write(new BufferedDataOutputStream(new FileOutputStream(Crush.workPath + "index-" + serialNo + ".fits")));
	}
	catch(Exception e) { e.printStackTrace(); }
    }

    public void writeResiduals() {
	try {
	    Fits residualFITS = new Fits();
	    
	    float[][] residual = new float[frames][];
	    for(int t=0; t<frames; t++) residual[t] = frame[t].readout;

	    BasicHDU hdu = Fits.makeHDU(residual);

	    editHeader(hdu.getHeader());

	    residualFITS.addHDU(hdu);
	    residualFITS.write(new BufferedDataOutputStream(new FileOutputStream(Crush.workPath + "residual-" + serialNo + ".fits")));
	}
	catch(Exception e) { e.printStackTrace(); }
    }

    void writeGains() {
	// pixel weights averaged on good pixels
	double norm = 0.0; 
	int n=0;
	for(int i=0; i<pixels; i++) if(pixel[i].isActive) {
	    norm += pixel[i].weight;
	    n++;
	}
        norm /= n;
		
	DecimalFormat df = new DecimalFormat("0.000");

	try {
	    File file = new File(Crush.workPath + "gain-" + serialNo + ".dat");
	    if(file.exists()) file.delete();
	    RandomAccessFile gainFile = new RandomAccessFile(file, "rw");
	
	    for(int i=0; i<pixels; i++) {
		gainFile.writeBytes((i/32+1) + "\t" + (i%32+1) + "\t");
		gainFile.writeBytes(df.format(pixel[i].gain) + "\t");
		gainFile.writeBytes(pixel[i].flag + "\t");
		gainFile.writeBytes(df.format(pixel[i].weight / norm) + "\n");
	    }
	    gainFile.close();
	} catch(Exception e) { e.printStackTrace(); } 
    }

    public void writeCovariance() throws FitsException, HeaderCardException, IOException {
	Fits fits = new Fits();
	BasicHDU hdu = Fits.makeHDU(getCovariance());
	editHeader(hdu.getHeader());
	fits.addHDU(hdu);
	fits.addHDU(Fits.makeHDU(n));
	fits.write(new BufferedDataOutputStream(new FileOutputStream(Crush.workPath + "covar-" + serialNo + ".fits")));
    }

     public void writeCovariance(int bol) throws FitsException, HeaderCardException, IOException {
	Fits fits = new Fits();
	BasicHDU hdu = Fits.makeHDU(getCovariance(bol));
	editHeader(hdu.getHeader());
	fits.addHDU(hdu);
	fits.write(new BufferedDataOutputStream(new FileOutputStream(Crush.workPath + "pcovar-" + serialNo + "-"
								     + (bol/32+1) + ":" + (bol%32+1) + ".fits")));
    }

    public void writeResiduals(String fileName) {
	//Fits original = new Fits(getFile(descriptor));

	// Non-data HDU's are simply copied.
	//
	// The pixel HDU is updated to hold the new offsets (0.0), weights, gains, and flags
	// Adjust sampling time to correct for down-sampling
 
	// Construct new data HDU's
	// scale values back by row-gain
	// Make rms correspond to frame and pixel weights
	// Fill out the headers as much as possible
	// If possible write bit by bit to save memory.

	// Finally, write the residuals to the data directory
    }

    public void makeMask(ReducedData reduced) {
	for(int t=0; t<frames; t++) for(int bol=0; bol<pixels; bol++) {
	    short[] index = getMapIndexes(t, bol);
	    
	    if(reduced.isInsideRegions(index[0], index[1])) {
		frame[t].masked[bol] = true;
		frame[t].flagged[bol] = true;
		masks++;
	    }
	}
    }	 

    public void flag(int bol, int pattern) {
	pixel[bol].flag |= pattern;

	if(pixel[bol].isActive) {
	    pixel[bol].isActive = false;
	    activePixels--;
	}
    }

    public void unflag(int bol, int pattern) {
	pixel[bol].flag &= ~pattern;

	if(!pixel[bol].isActive) if(!pixel[bol].isFlagged()) {
	    pixel[bol].isActive = true;
	    activePixels++;
	}
    }

    public void parsePixelFile(String fileName) throws IOException {       
	if(fileName.equals("*") || fileName.equalsIgnoreCase("none")) {
	    pixelFileName = null;
	    return;
	}
	if(fitsGains && fitsFlags && fitsWeights) return;

	RandomAccessFile pixelFile = new RandomAccessFile(fileName, "r");
	pixelFileName = fileName;

	while(pixelFile.getFilePointer() < pixelFile.length()) {
	    StringTokenizer tokens = new StringTokenizer(pixelFile.readLine());

	    final int row = Integer.parseInt(tokens.nextToken())-1;
	    final int col = Integer.parseInt(tokens.nextToken())-1;
	    final Bolometer bolometer = pixel[32 * row + col];

	    float pixelGain = Float.parseFloat(tokens.nextToken());
	    int pixelFlag = Integer.parseInt(tokens.nextToken());
	    float pixelWeight = Float.parseFloat(tokens.nextToken());

	    if(!fitsGains) bolometer.gain = pixelGain;
	    if(!fitsFlags) bolometer.flag = pixelFlag;
	    if(!fitsWeights) bolometer.weight = pixelWeight;
	}
    }


    private static String getFileName(int scanNo) {
	DecimalFormat sixDigits = new DecimalFormat("000000");
	String fileName = filePrefix + sixDigits.format(scanNo) + ".fits";
	return fileName;
    }

    public File getFile(String scanDescriptor) {
	File scanFile;

	descriptor = scanDescriptor;

	try { 
	    int scanNo = Integer.parseInt(scanDescriptor);
	    System.out.println(" Reading " + dataPath + getFileName(scanNo) + "...");
	    scanFile = new File(dataPath + getFileName(scanNo));
	} catch(NumberFormatException e) {
	    
	    scanFile= new File(scanDescriptor) ;	
	    if(scanFile.exists()) { System.out.println(" Reading " + scanDescriptor + "..."); }
	    
	    else {
		scanFile = new File(dataPath + scanDescriptor);
		if(scanFile.exists()) { System.out.println(" Reading " + dataPath + scanDescriptor + "..."); }
		
		else { 
		    System.out.println("ERROR! Could Not read scan " + scanDescriptor); 
		    System.exit(1);
		}
	    } 
	}	

	fileSize = scanFile.length();

	return scanFile;
    }

    public void readScanInfo(String scanDescriptor) throws IOException, HeaderCardException, FitsException {
	readScanInfo(new Fits(getFile(scanDescriptor)));
    }

    public void readScanInfo(int scanNo) throws IOException, HeaderCardException, FitsException {
 	readScanInfo(dataPath + getFileName(scanNo));
	descriptor = scanNo + "";
    }

    protected void readScanInfo(Fits fits) throws IOException, HeaderCardException, FitsException {
	parseScanPrimaryHDU(fits.readHDU());
	parseHardwareHDU((BinaryTableHDU) fits.readHDU());
	parseDSPHDU((BinaryTableHDU) fits.readHDU());
	parsePixelHDU((BinaryTableHDU) fits.readHDU());

	BasicHDU nextHDU = fits.readHDU();
	while( ! nextHDU.getHeader().getStringValue("EXTNAME").equalsIgnoreCase("SHARC2 Data") ) nextHDU = fits.readHDU();
	parseDataHeader((BinaryTableHDU) nextHDU);

	calcSharcTau();
	transmission = (float) Math.exp(-tau / Math.sin(EL));
    }


    public void read(String scanDescriptor) throws HeaderCardException, FitsException {
	read(new Fits(getFile(scanDescriptor)));
    }


    public void read(int scanNo) throws HeaderCardException, FitsException {
 	read(dataPath + getFileName(scanNo));
	descriptor = scanNo + "";
    }


    protected void read(Fits fits) throws IllegalStateException, HeaderCardException, FitsException {
	// Read in entire FITS file
	BasicHDU[] HDU = fits.read();

	parseScanPrimaryHDU(HDU[0]);
	parseHardwareHDU((BinaryTableHDU) HDU[1]);
	parseDSPHDU((BinaryTableHDU) HDU[2]);
	parsePixelHDU((BinaryTableHDU) HDU[3]);

	int firstDataHDU = 4; 
	while( ! HDU[firstDataHDU].getHeader().getStringValue("EXTNAME").equalsIgnoreCase("SHARC2 Data") ) firstDataHDU++;
	parseDataHeader((BinaryTableHDU) HDU[firstDataHDU]);

	int nDataHDUs = HDU.length - firstDataHDU, records = 0;
	for(int datahdu=0; datahdu<nDataHDUs; datahdu++) records += HDU[firstDataHDU + datahdu].getAxes()[0];
	    
	DecimalFormat f1 = new DecimalFormat("0.0");
        System.out.println("   " + nDataHDUs + " HDUs,  " + records + " x " +
			   (int)(samplingInterval/Unit.MILLISECOND) + "ms frames" + " -> " + 
			   f1.format(records*samplingInterval/Unit.MINUTE) + " minutes total."); 
	

	Vector frameBuffer = new Vector(records);

	SharcFrame blockFirstFrame = null;	
	int blockStart = 0;
	boolean gap = false, mismatch = false;

	boolean tolerateGaps = false;
	try { tolerateGaps = Util.parseBoolean(gapOKRule); }
	catch(NumberFormatException e) { tolerateGaps = gapOKRule.equalsIgnoreCase(creator); } 

	for(int n=0; n<nDataHDUs; n++) {
	    BinaryTableHDU dataHDU = (BinaryTableHDU) HDU[firstDataHDU+n]; 
	    records = dataHDU.getAxes()[0];
	    
	    for(int i=0; i<records; i++) {		
		SharcFrame entry = new SharcFrame(this);
		entry.rowGain = rowGain;
		entry.parse(dataHDU.getRow(i));

		// Check for inconsistent gaps
		if(blockFirstFrame == null) {
		    blockFirstFrame = entry;
		    if(entry.hasExtraTimingInfo) dspTimeOffset = blockFirstFrame.UT - blockFirstFrame.dspTime;
		}
		
		else {
		    if(entry.hasExtraTimingInfo) {
       			double detectorMismatch = entry.UT - entry.dspTime - dspTimeOffset;
			double sequenceMismatch = entry.UT - blockFirstFrame.UT  
			    - (entry.frameNumber - blockFirstFrame.frameNumber) * Unit.MILLISECOND;
			
			if(Math.abs(detectorMismatch) > samplingInterval) {
			    System.out.println("   WARNING! Detector Time mismatch of " + 
					       (int)(detectorMismatch / Unit.MILLISECOND) + " ms found at HDU " +
					       + (firstDataHDU + n) + ", row " + (i+1));
			    
			    mismatch = true;
			}

			if(Math.abs(sequenceMismatch) > samplingInterval) {
			    System.out.println("   WARNING! Sequence Number mismatch of " +
					       (int)(sequenceMismatch / Unit.MILLISECOND) + " ms found at HDU " +
					       + (firstDataHDU + n) + ", row " + (i+1));
			    mismatch = true;
			}
			
			if(mismatch) throw new IllegalStateException("A Timing Mismatch invalidates scan " + serialNo + "."); 
		    }

		    double dT = entry.startUT - blockFirstFrame.startUT;
		    dT -= (frameBuffer.size() - blockStart) * samplingInterval;
		    		    
       		    if(dT > samplingInterval) {
			System.out.println("   WARNING! Gap of " + (int)(dT/Unit.MILLISECOND) 
					   + " ms found in data at HDU " + (firstDataHDU + n) + ", row " + (i+1));
			gap = true;
			if(mismatch) throw new IllegalStateException("A Timing Gap invalidates scan " + serialNo + "."); 
		    }

		    if(gap || mismatch) {
			if(filter13Hz) filter13Hz(frameBuffer, blockStart, frameBuffer.size());

			// Trim the frames at the end that do not down-sample.
			while(frameBuffer.size() % downSample != 0) frameBuffer.remove(frameBuffer.size() - 1);

			// Start a new block
			blockFirstFrame = entry;
			blockStart = frameBuffer.size();
			
			dspTimeOffset = entry.UT - entry.dspTime;

			// Commence reading if gap-free data is desired.
			if((mismatch || gap) && !tolerateGaps) { 
			    System.out.println("   WARNING! Commencing scan reading at " + frameBuffer.size() + " frames.");
			    i = records; n = nDataHDUs; 
			}

			gap = false;
			mismatch = false;
		    }
		    
		}
		
		frameBuffer.add(entry);
	    }
	}

	if(samplingInterval * frameBuffer.size() < minIntTime) 
	    throw new IllegalStateException("Not enough valid frames in scan " + serialNo + ".");

	// This is for understanding Jan 2003 data where there were
	// timing problems.
	shiftData(frameBuffer, delay);
	
	if(filter13Hz) {
	    System.out.println("   Filtering 13.4Hz on noisy pixels.");	    
	    filter13Hz(frameBuffer, blockStart, frameBuffer.size());
	}

	// Do the down-sampling here
	if(downSample > 1) {
	    System.out.print("   DownSampling -> ");	

	    // Once down sampled the 13.4Hz filter has no effect
	    filtered13HzChannels = 0;

	    Vector downSampledBuffer = new Vector();
	    frames = frameBuffer.size();

	    SharcFrame newFrame = null;

	    for(int t=0; t<frames; t++) {
		if(newFrame == null) newFrame = (SharcFrame) frameBuffer.elementAt(t);

		else {
		    try { newFrame.add((SharcFrame) frameBuffer.elementAt(t)); }
		    catch(IllegalStateException e) {
			newFrame = newFrame = (SharcFrame) frameBuffer.elementAt(t);
		    }
		}
 
		if(newFrame.samples == downSample) {
		    downSampledBuffer.add(newFrame);
		    newFrame = null;
		}
	    }
	    frameBuffer = downSampledBuffer;

	    System.out.println(frameBuffer.size() + " frames");	
	}

	samplingInterval *= downSample;
	    
	frames = frameBuffer.size();
	frame = new SharcFrame[frames];
	for(int t=0; t<frames; t++) {
	    frame[t] = (SharcFrame) frameBuffer.elementAt(t);
	    frame[t].calc();
	    frame[t].validate();
	}

	processChopperSignal();

	// Add the subtracted DAC values back onto the offsets
	// The total offset should be negative reflecting the sign inversion
	// of all bolometer signals...
       	for(int row=0,bol=0; row<12; row++) for(int col=0; col < 32; col++,bol++) 
	    pixel[bol].offset -= (float) ((isHiGain[row] ? 48.83e-3 : 4.439e-3) * Unit.VOLT * DAC[row][col] /  rowGain[row]);


	// Clean up a little
	frameBuffer = null;

	// Always get the position derivatives
	getPositionDerivatives();

	// Set the high-pass filter
	if(autoHighPass) {
	    if(ReductionTool.sourceDiameter > 0.0) highPass = 10.0 * ReductionTool.sourceDiameter / aveScanSpeed;
	    else highPass = 0.0;
	}
	
	if(highPass > 0.0) System.out.println("   Will High-Pass Filter with " + f1.format(highPass / Unit.SECOND) + "s boxcar.");

	// Fill in the mean values here...
	AZ = (frame[0].AZ + frame[frames-1].AZ) / 2.0F;
	EL = (frame[0].EL + frame[frames-1].EL) / 2.0F;
	PA = (frame[0].PA + frame[frames-1].PA) / 2.0F;

	calcSharcTau();
	transmission = (float) Math.exp(-tau / Math.sin(EL));
    }
   
    protected void parseScanPrimaryHDU(BasicHDU hdu) throws HeaderCardException, FitsException {
	Header mainHeader = hdu.getHeader();

	// Scan Info
	serialNo = mainHeader.getIntValue("SCANNO");
	creator = mainHeader.getStringValue("CREATOR");
	observer = mainHeader.getStringValue("OBSERVER");
	project = mainHeader.getStringValue("PROJECT");

	if(creator == null) creator = "Unknown";
	if(observer == null) observer = "Unknown";
	if(project == null) project = "Unknown";

	date = mainHeader.getStringValue("DATE-OBS");
	MJD = mainHeader.getIntValue("JUL_DAY");

	// Weather
	if(Double.isNaN(tau)) {
	    tau = mainHeader.getDoubleValue("TAU225GH");
	    tauID = "225GHz";
	}

	temperature = mainHeader.getFloatValue("TEMPERAT");
	pressure = mainHeader.getFloatValue("PRESSURE");
	humidity = mainHeader.getFloatValue("HUMIDITY");

	// Source Information
	sourceName = mainHeader.getStringValue("OBJECT");
	if(sourceName == null) sourceName = "Unknown";

	RA  = mainHeader.getFloatValue("RAEP") * 15.0F * Unit.DEGREE;
	DEC = mainHeader.getFloatValue("DECEP") * Unit.DEGREE;

	if(Float.isNaN(coordinateEpoch)) coordinateEpoch = mainHeader.getFloatValue("EQUINOX");

	origRA = RA; origDEC = DEC; origEpoch = coordinateEpoch;		
	precess(Crush.reducedEpoch);
	
	// Evaluate the cosDEC shorthand
	cosDEC = (float) Math.cos(DEC);

	// Add on the various additional offsets
	AZO = mainHeader.getFloatValue("AZO") * Unit.ArcSECOND;
	ELO = -mainHeader.getFloatValue("ZAO") * Unit.ArcSECOND;
	AZO += mainHeader.getFloatValue("AZO_MAP") * Unit.ArcSECOND;
	ELO -= mainHeader.getFloatValue("ZAO_MAP") * Unit.ArcSECOND;
	AZO += mainHeader.getFloatValue("AZO_CHOP") * Unit.ArcSECOND;		
	ELO -= mainHeader.getFloatValue("ZAO_CHOP") * Unit.ArcSECOND;

	AZO += mainHeader.getFloatValue("CHPOFFST") * Unit.ArcSECOND;
	
	RAO  = mainHeader.getFloatValue("RAO") * Unit.ArcSECOND;
	DECO = mainHeader.getFloatValue("DECO") * Unit.ArcSECOND;		
	RAO  += mainHeader.getFloatValue("RAO_MAP") * Unit.ArcSECOND;
	DECO += mainHeader.getFloatValue("DECO_MAP") * Unit.ArcSECOND;
	RAO  += mainHeader.getFloatValue("RAO_FLD") * Unit.ArcSECOND;
	DECO += mainHeader.getFloatValue("DECO_FLD") * Unit.ArcSECOND;
	
	// Platform
	String platform = mainHeader.getStringValue("PLATFORM");
	if(platform == null) platform = "Cassegrain";
	isNasmyth = platform.equalsIgnoreCase("NASMYTH");

	// Set the default pixel scales if not explicitly set already.
	if(Float.isNaN(pixelXSize) || Float.isNaN(pixelYSize)) {
	    pixelXSize = isNasmyth ? SharcHeader.nasmythPixelXSize : SharcHeader.pixelXSize;
	    pixelYSize = isNasmyth ? SharcHeader.nasmythPixelYSize : SharcHeader.pixelYSize;
	}
	pixelSize = (pixelXSize + pixelYSize) / 2.0F;
	pixelArea = pixelXSize * pixelYSize;

	// Rotation Center
	if(rotationCenter == null) rotationCenter = isNasmyth ? nasmythRotationCenter : cassegrainRotationCenter;

	// Rotator
	if(isNasmyth) {
	    if(Double.isNaN(rotatorZeroAngle)) rotatorZeroAngle = mainHeader.getDoubleValue("ROTZERO", 46.0) * Unit.DEGREE;
	    rotatorAngle = Double.NaN;
	}
	else {
	    if(Double.isNaN(rotatorZeroAngle)) rotatorZeroAngle = 16.6 * Unit.DEGREE;
	    rotatorAngle = mainHeader.getDoubleValue("ROTATOR", 16.6) * Unit.DEGREE;
	}

	rotatorOffset = mainHeader.getFloatValue("ROTOFFST") * Unit.DEGREE;
	rotatorMode = mainHeader.getStringValue("ROTMODE");
	if(rotatorMode == null) rotatorMode = "Unknown";
	
	// Focus
	focusX =  mainHeader.getFloatValue("FOCUS_X");
	focusY =  mainHeader.getFloatValue("FOCUS_Y");
	focusZ =  mainHeader.getFloatValue("FOCUS_Z");

	focusYOffset =  mainHeader.getFloatValue("FOCUS_YO");
	focusZOffset =  mainHeader.getFloatValue("FOCUS_ZO");

	focusMode = mainHeader.getStringValue("FOCMODE");
	if(focusMode == null) focusMode = "Unknown";

	// DSOS
	dsos = mainHeader.getBooleanValue("DSOS");
	dsosVersion = mainHeader.getStringValue("DSOSVER");

	if(Double.isNaN(FAZO)) FAZO = mainHeader.getFloatValue("FAZO") * Unit.ArcSECOND;
	else { 
	    float value = mainHeader.getFloatValue("FAZO") * Unit.ArcSECOND;
	    AZO += value - FAZO;
	}
	if(Double.isNaN(FZAO)) FZAO = mainHeader.getFloatValue("FZAO") * Unit.ArcSECOND;
	else { 
	    float value = mainHeader.getFloatValue("FZAO") * Unit.ArcSECOND;
	    ELO -= value - FZAO;
	}

	// increment month by 1 to correct JSharc FITS bug
	if(creator.equalsIgnoreCase("JSharc")) {
	    System.out.print("   Fixing JSharc Time Stamp. -> ");
	    DecimalFormat mf = new DecimalFormat("00");
	    String modDate = date.substring(0, 5);
	    modDate += mf.format( Integer.parseInt(date.substring(5,7)) + 1 );
	    modDate += date.substring(7);
	    date = modDate;
	    System.out.println(modDate);
	}

	// Break up the date information into pieces for easier access
	StringTokenizer tokens = new StringTokenizer(date, "T \t");
	StringTokenizer dateBits = new StringTokenizer(tokens.nextToken(), "-.");
	StringTokenizer timeBits = new StringTokenizer(tokens.nextToken(), ":");

	year = Integer.parseInt(dateBits.nextToken());
	month = Integer.parseInt(dateBits.nextToken());
	day = Integer.parseInt(dateBits.nextToken());
	
	UT = Integer.parseInt(timeBits.nextToken()) * Unit.HOUR;
	UT += Integer.parseInt(timeBits.nextToken()) * Unit.MINUTE;
	UT += Double.parseDouble(timeBits.nextToken()) * Unit.SECOND;
	
	// Update the tau value (if possible)
	if(taufit) {
	    try { getFitTau();}
	    catch(Exception e) { System.out.println(" WARNING! Mai-Tau could not be obtained. Using default."); }
	}
    }

    protected void parseHardwareHDU(BinaryTableHDU hardwareHDU) throws HeaderCardException, FitsException {
	// Get Vital information from Descriptive HDU's
	float hiGain = hardwareHDU.getHeader().getFloatValue("GAIN_HI", 10521.0F);
	float loGain = hardwareHDU.getHeader().getFloatValue("GAIN_LO", 1412.0F);

	// Filter
	filterName = hardwareHDU.getHeader().getStringValue("FILTER");

	// Get the electronic gains for the rows
	rowGain = new float[12];
	isHiGain = (boolean[]) hardwareHDU.getColumn("Gain Mode");		      
	for(int row=0,bol=0; row<12; row++) rowGain[row] = isHiGain[row] ? hiGain : loGain;

	// Read in the DAC values...
	DAC = new short[12][];
       	for(int row=0,bol=0; row<12; row++) {
	    DAC[row] = (short[]) hardwareHDU.getRow(row)[0];
	}
    }    

    protected void parseDSPHDU(BinaryTableHDU dspHDU) throws HeaderCardException, FitsException {
	samplingInterval = dspHDU.getHeader().getDoubleValue("FRAMESPC", 36.0) * Unit.MILLISECOND;
    }
    
    protected void parsePixelHDU(BinaryTableHDU pixelHDU) throws HeaderCardException, FitsException {
	// read in the pixel data unless explicitly defined by a pixel file
	if(fitsGains || fitsWeights || fitsFlags || fitsOffsets) {
	    for(int r=0,bol0=0; r<12; r++,bol0+=32) {
		Object[] row = pixelHDU.getRow(r);
		int c; 

		if(fitsGains) {
		    if((c = pixelHDU.findColumn("Relative Pixel Gains")) >= 0) {
			float[] gain = (float[]) row[c];
			for(int col=0; col<32; col++) pixel[bol0+col].gain = gain[col];
		    }
		    else {
			System.out.println(" WARNING! Cannot parse pixel gains. Using defaults...");
			fitsGains = false;
		    }
		}

		if(fitsFlags) {
		    if((c = pixelHDU.findColumn("Pixel Flags")) >= 0) {
			int[] flag = (int[]) row[c];
			for(int col=0; col<32; col++) pixel[bol0+col].flag = flag[col];
		    }
		    else {
			System.out.println(" WARNING! Cannot parse pixel flags. Using defaults...");
			fitsFlags = false;
		    }
		}

		if(fitsWeights) {
		    if((c = pixelHDU.findColumn("Pixel Weights")) >= 0) {
			float[] weight = (float[]) row[c];
			for(int col=0; col<32; col++) pixel[bol0+col].weight = weight[col];
		    }
		    else {
			System.out.println(" WARNING! Cannot parse pixel weights. Using defaults...");
			fitsWeights = false;
		    }
		}	    

		// The offsets go through the same sign inversion as signals...
		// If the offsets are not meant ot be read from the FITS file
		// default the values to zero.
		if(fitsOffsets) {
		    if((c = pixelHDU.findColumn("Pixel Offsets")) >= 0) {
			float[] offset = (float[]) row[c];
			for(int col=0; col<32; col++) pixel[bol0+col].offset = -offset[col] / rowGain[r];
		    }
		    else {
			System.out.println(" WARNING! Cannot parse pixel offsets. Using defaults...");
			fitsOffsets = false;
		    }
		}

	    }
	}

	// Check for identical zeros in gains and weights
	boolean gainValid = false, weightValid = false, combinedValid = false;
	for(int bol=0; bol < pixels; bol++) if(!pixel[bol].isFlagged()) {
	    if(pixel[bol].gain != 0.0) gainValid = true;
	    if(pixel[bol].weight != 0.0) weightValid = true;
	    if(pixel[bol].weight * pixel[bol].gain != 0.0) combinedValid = true;
	}	
	if(!gainValid) System.out.println(" WARNING! All gains appear to be zero.");
	if(!weightValid) System.out.println(" WARNING! All weights appear to be zero.");
	if(!combinedValid) System.out.println(" WARNING! No valid pixels registered.");
    } 
	
    protected void parseDataHeader(BinaryTableHDU dataHDU) throws HeaderCardException, FitsException {
	// Pointing Center
	Header dataHeader = dataHDU.getHeader();
	if(pointingCenter == null) {
	    pointingCenter = new Vector2D();
	    pointingCenter.x = dataHeader.getDoubleValue("CRPIX2", 16.5) - 1.0;
	    pointingCenter.y = dataHeader.getDoubleValue("CRPIX3", 6.5) - 1.0;
	}

	SharcFrame.findColumns(dataHDU);

	SharcFrame firstFrame = new SharcFrame(this);
	firstFrame.parse(dataHDU.getRow(0));

	AZ = firstFrame.AZ;
	EL = firstFrame.EL;
	PA = firstFrame.PA;
    }


    public void totalPower() {
	if(isTotalPower) return;
	for(int bol=0; bol<pixels; bol++) {
	    float offset = pixel[bol].offset;
	    for(int t=0; t<frames; t++) frame[t].readout[bol] = offset - frame[t].readout[bol];
	}
	isTotalPower = true;
    }

    private void flagNoisyColumns() {
	for(int row=0; row<12; row++) for(int i=0; i<noisy13HzColumn.length; i++) {
	    flag(32*row+noisy13HzColumn[i], Bolometer.NOISY);
	}
    }

    private void filter13Hz(Vector frameBuffer, int startingAt, int endingAt) {
	final int nt = endingAt - startingAt;
	if(nt <= 1) return;

	int nFFT = ArrayUtil.getPaddedFFTSize(nt);
	double[][] signal = new double[12 * noisy13HzColumn.length][nFFT];

	// Rearrange the data to make it suitable for filtering
	for(int t=0; t<nt; t++) {
	    SharcFrame exposure = (SharcFrame) frameBuffer.elementAt(startingAt + t);
	    for(int row=0,i=0; row<12; row++) for(int n=0; n<4; n++,i++) {
		final int bol = 32 * row + noisy13HzColumn[n];
		signal[i][t] = exposure.readout[bol];
	    }
	}

	int nf = nFFT/2;
	double df = 1.0 / (nFFT * samplingInterval);

	int fromf = (int) Math.floor((13.38 * Unit.Hz - 0.5 * width13Hz) / df);
	int tof = (int) Math.ceil((13.38 * Unit.Hz + 0.5 * width13Hz) / df);
	if(tof >= nf) tof = nf - 1;

	if(tof <= fromf) return;
	
	filtered13HzChannels = tof - fromf + 1;	

	// Perform 13.4 Hz notch filter on noisy columns
	for(int i=0; i<signal.length; i++) {
	    Complex[] spectrum = ArrayUtil.FFT(signal[i]);
	    for(int f=fromf; f<=tof; f++) spectrum[f].zero();
	    signal[i] = ArrayUtil.backFFT(spectrum);
	}
       
	 	 
	// Replace the noisy channels with their filtered substitutes
	for(int t=0; t<nt; t++) {	    
	    SharcFrame exposure  = (SharcFrame) frameBuffer.elementAt(startingAt + t);	    
	    for(int row=0,i=0; row<12; row++) for(int n=0; n<4; n++,i++) {
		final int bol = 32 * row + noisy13HzColumn[n];		
		exposure.readout[bol] = (float) signal[i][t];
	    }
	}
    }

    private void processChopperSignal() {
	if(autoChopRecognition) System.out.print("   Detecting Chopper. ");	
	else if(!chopped) return;
	else System.out.print("   Chopped Reduction. ");

	// First calculate the mean
	double mean = 0.0;
	for(int t=0; t < frames; t++) mean += frame[t].chopOffset;
	mean /= frames;

	// Now calculate the mean hi and low windows
	double hi=0.0, lo=0.0;
	int nhi=0, nlo=0;

	for(int t=0; t < frames; t++) {
	    float chopOffset = frame[t].chopOffset;
	    if(chopOffset > mean) { hi += chopOffset; nhi++; }
	    else if(chopOffset < mean) { lo += chopOffset; nlo++; }
	}

	// nhi and nlo will be 0 if chopOffset is identical
	// in that case, the hi and lo windows are simply the identical chopper values.
	if(nhi > 0) hi /= nhi; else hi=mean;
	if(nlo > 0) lo /= nlo; else lo=mean;	


	// Measure the chop throw
	chopperThrow = hi - lo;

	DecimalFormat df = new DecimalFormat("0.0");

	if(autoChopRecognition) {
	    if(chopperThrow > 5.0 * Unit.ArcSECOND) {
		chopped = true;
		System.out.print("OK. ");
	    }
	    else {
		chopped = false;
		System.out.println("No Chopper.");
		chopperThrow = 0.0;
		return;
	    }
	}
	

	// Remove the center from the chopper offsets
	System.out.print("Centering. "); 
	final float center = (float)(hi + lo) / 2.0F;
	for(int t=0; t < frames; t++) frame[t].chopOffset -= center;

	hi -= center;
	lo -= center;


	double firstUT = Double.NaN, lastUT = Double.NaN;
	int transitions = 0;
	boolean isHi = frame[0].chopOffset > center;

	for(int t=1; t < frames; t++) {
	    float chopOffset = frame[t].chopOffset;

	    // Check for transition
	    if((isHi && chopOffset < lo / 2.0) || (!isHi && chopOffset > hi / 2.0)) {
		if(transitions == 0) firstUT = frame[t].UT;
		else lastUT = frame[t].UT;
   
		transitions++;
		isHi = !isHi;
	    }
	}

	chopperFrequency =  (transitions-1) / 2 / (lastUT - firstUT);

	System.out.print("Throw " + df.format(chopperThrow / Unit.ArcSECOND) + " \". ");
	System.out.print("Frequency " + df.format(chopperFrequency * Unit.SECOND) + " Hz. "); 

	System.out.println();
    }
	    

    public ChopperState[] getChopperStates() {	 
	Vector chopperStates = new Vector();

	ChopperState currentState = new ChopperState(0, frame[0].chopOffset > 0);

	for(int t=0; t<frames; t++) if((frame[t].chopOffset > 0) != currentState.isHi) {
	    currentState.to = t;
	    chopperStates.add(currentState);
	    currentState = new ChopperState(t, frame[t].chopOffset > 0);
	}
	 
	currentState.to = frames;
	chopperStates.add(currentState);

	ChopperState[] state = new ChopperState[chopperStates.size()];
	for(int i=0; i<state.length; i++) state[i] = (ChopperState) chopperStates.get(i);

	return state;
    }

 
    public void highPassFilter(double timeScale) {
	int nFFT = ArrayUtil.getPaddedFFTSize(frames);
	int nf = nFFT/2;
	double[] signal = new double[nFFT];
	double[] temp = new double[2*nFFT];

	int fmin = (int) Math.round(samplingInterval/(2.0 * timeScale));
	
	Complex[] spectrum = new Complex[nf];
	for(int f=0; f<nf; f++) spectrum[f] = new Complex();

	for(int bol=0; bol<pixels; bol++) {
	    for(int t=0; t<frames; t++) signal[t] = frame[t].readout[bol];
	    Arrays.fill(signal, frames, signal.length, 0.0);
	    ArrayUtil.unsecureFFT(signal, spectrum);
	    
	    for(int f=fmin; f<nf; f++) spectrum[f].zero();

	    ArrayUtil.unsecureBackFFT(signal, spectrum);

	    for(int t=0; t<frames; t++) frame[t].readout[bol] = (float) signal[t];
	}

	System.out.println();
    }	   

    // Shift the frame data by the desired amount
    // This is only for diagnostics of the Jan. 2003 run really
    // Properly deals with gaps.
    public void shiftData(Vector frameBuffer, double dt) {
	int dN = (int)Math.round(dt/samplingInterval);
	if(dN == 0) return;

	System.out.println("   Shifting Frame Data by " + dN + " frames.");

	dt = (Math.abs(dN) + 1) * samplingInterval;
	int from, to, step; 
	
	if(dN > 0) {
	    to=frameBuffer.size()-1;
	    from=to-dN;
	    step=-1;
	}
	else {
	    to=0;
	    from=to-dN;
	    step=1;
	}

	for(; from >= 0 && from < frameBuffer.size(); to += step, from += step) {
	    SharcFrame fromFrame = (SharcFrame) frameBuffer.get(from);
	    SharcFrame toFrame = (SharcFrame) frameBuffer.get(to);
	    
	    toFrame.readout = fromFrame.readout;
	    
	    // Gap Check.
	    toFrame.weight = Math.abs(toFrame.UT - fromFrame.UT) > dt ? 0.0F : 1.0F;
	    fromFrame.weight = 0.0F;
	}
	
	// Clean up the unused frames.
	for(int i=0; i<frameBuffer.size(); ) {
	    SharcFrame frame = (SharcFrame) frameBuffer.get(i);
	    if(frame.weight == 0.0F) frameBuffer.remove(i);
	    else i++;
	}
    }


    public void scramble() {
	int nshuffle = pixels * (int)(Math.log(pixels) / Math.log(2.0));
	for(int i=0; i<nshuffle; i++) {
	    int b1 = (int) (pixels * Math.random());
	    int b2 = (int) (pixels * Math.random());
	    Vector2D temp = pixel[b1].position;
	    pixel[b1].position = pixel[b2].position;
	    pixel[b2].position = temp;
	}
    }
    
    public void invert() {
	for(int t=0; t<frames; t++) {
	    float[] readout = frame[t].readout;
	    for(int bol=0; bol<pixels; bol++) readout[bol] *= -1.0F;
	}
    }

    public BinaryTableHDU getHDU() throws FitsException, HeaderCardException, IOException {
	if(HDU == null) makeHDU();
	else {
	    // Reuse the data but update the header
	    HDU = (BinaryTableHDU) Fits.makeHDU(HDU.getData());
	    editHeader(HDU.getHeader());
	}

	return HDU;
    }

    public void makeHDU() throws FitsException, HeaderCardException, IOException {
	// Make hdu out of pixels gains, weights, flags, covariances, and histograms
	// then call editHeader
    }

    public void read(BinaryTableHDU hdu) throws FitsException, HeaderCardException, IOException {
	HDU = hdu;
	// Read what was written...
	parseHeader(hdu.getHeader());
    }

    public void editHeader(Header header) throws FitsException, HeaderCardException, IOException {
	Cursor cursor = header.iterator();
	while(cursor.hasNext()) cursor.next();
	editHeader(cursor);
    }

    public void editHeader(Cursor cursor) throws FitsException, HeaderCardException, IOException {
	cursor.add(new HeaderCard("EXTNAME", "Scan " + serialNo, "Modelled data for the scan"));
	cursor.add(new HeaderCard("SCANNO", serialNo, "The SHARC2 serial number for the scan"));
	cursor.add(new HeaderCard("SCANNAME", descriptor, "The name by which the scan was invoked."));
	cursor.add(new HeaderCard("OBSERVER", observer, "The Name(s) of the Observer(s)."));
	cursor.add(new HeaderCard("PROJECT", project, "A description of the project"));
	cursor.add(new HeaderCard("CREATOR", creator, "The creator of the scan data."));
	cursor.add(new HeaderCard("DATE-OBS", date, "The observation date."));
	cursor.add(new HeaderCard("MJD", MJD, "Modified Julian Day."));

	// The Source Descriptors
	cursor.add(new HeaderCard("OBJECT", sourceName, "The observed object's name as in the UIP catalog."));
	cursor.add(new HeaderCard("RA", origRA/(15.0*Unit.DEGREE), "The RA of the object (h)."));
	cursor.add(new HeaderCard("DEC", origDEC/Unit.DEGREE, "The DEC of the object (deg)."));
	cursor.add(new HeaderCard("EQUINOX", origEpoch, "The epoch of the qouted coordinates"));
	cursor.add(new HeaderCard("AZ", AZ/Unit.DEGREE, "The Azymuth of the object (deg)."));
	cursor.add(new HeaderCard("EL", EL/Unit.DEGREE, "The Elevation of the object (deg)."));
	cursor.add(new HeaderCard("PA", PA/Unit.DEGREE, "The Parallactic Angle of the object (deg)."));	
	cursor.add(new HeaderCard("RAO", RAO/Unit.ArcSECOND, "The Aggregate Static RA Offset (arcsec)."));
	cursor.add(new HeaderCard("DECO", DECO/Unit.ArcSECOND, "The Aggregate Static DEC Offset (arcsec) ."));
	cursor.add(new HeaderCard("AZO", AZO/Unit.ArcSECOND, "The Aggregate Static AZ Offset (arcsec)."));
	cursor.add(new HeaderCard("ZAO", -ELO/Unit.ArcSECOND, "The Aggregate Static EL Offset (arcsec)."));
	
	// The telescope/instrument Descriptors
	cursor.add(new HeaderCard("PLATFORM", isNasmyth ? "Nasmyth" : "Cassegrain", "The focus mount platform."));

	cursor.add(new HeaderCard("PIXSIZEX", pixelXSize / Unit.ArcSECOND, "The detector pixel size along the rows (arcsec)."));
	cursor.add(new HeaderCard("PIXSIZEY", pixelYSize / Unit.ArcSECOND, "The detector pixel size along the columns (arcsec)."));
	cursor.add(new HeaderCard("PIXAREA", pixelArea / (Unit.ArcSECOND * Unit.ArcSECOND), 
				  "The detector pixel area (arcsec^2)."));

	cursor.add(new HeaderCard("ROTATOR", rotatorAngle/Unit.DEGREE, "The instrument rotator value (deg)."));
	cursor.add(new HeaderCard("ROTZERO", rotatorZeroAngle/Unit.DEGREE, "The instrument rotator reference value (deg)."));
	cursor.add(new HeaderCard("ROTOFFST", rotatorOffset/Unit.DEGREE, "The instrument rotator offset value (deg)."));
	cursor.add(new HeaderCard("ROTMODE", rotatorMode, "The instrument rotator mode."));

	cursor.add(new HeaderCard("FAZO", FAZO/Unit.ArcSECOND, "Instrument FAZO setting."));
	cursor.add(new HeaderCard("FZAO", FZAO/Unit.ArcSECOND, "Instrument FZAO setting."));

	cursor.add(new HeaderCard("FOCUS_X", focusX, "X focus value (mm)."));
	cursor.add(new HeaderCard("FOCUS_Y", focusY, "Y focus value (mm)."));
	cursor.add(new HeaderCard("FOCUS_Z", focusZ, "Z focus value (mm)."));
	cursor.add(new HeaderCard("FOCUS_YO", focusYOffset, "Y focus offset value (mm)."));
	cursor.add(new HeaderCard("FOCUS_ZO", focusZOffset, "Z focus offset value (mm)."));
	cursor.add(new HeaderCard("FOCMODE", focusMode, "The focus mode."));

	cursor.add(new HeaderCard("FILTER", filterName, "The filter setting."));

	cursor.add(new HeaderCard("DSOS", dsos, "Whether the Dish Surface Optimization System was on."));
	if(dsos) cursor.add(new HeaderCard("DSOSVER", dsosVersion, "The Vesrion of the DSOS used."));	
	
	// The Weather
	cursor.add(new HeaderCard("TAU", tau, "The SHARC2 pass-band tau value used."));
	cursor.add(new HeaderCard("TEMPERAT", temperature, "Ambivalent Temperature (C)."));
	cursor.add(new HeaderCard("PRESSURE", pressure, "Atmospheric Pressure (kPa)."));
	cursor.add(new HeaderCard("HUMIDITY", humidity, "Outside Relative Humidity"));

	// Reduction Settings
	cursor.add(new HeaderCard("VPERJY", Jy / Unit.VOLT, "The Jy-to-Volts conversion factor to use."));
	cursor.add(new HeaderCard("GAIN", gain, "The absolute gain factor (normalized for unloaded bolometers)."));
	cursor.add(new HeaderCard("WEIGHT", weight, "Weighting value used for the scan."));
	cursor.add(new HeaderCard("SCALE", scale, "Scaling factor (e.g.) beam efficiency for the scan."));
	cursor.add(new HeaderCard("DIRTAU", directTau, "Whether tau is directly calculated from detector offsets."));
	cursor.add(new HeaderCard("MAITAU", tauLookupID, "Which tau value is obtained from Mai-Tau server."));
	cursor.add(new HeaderCard("LOOKUP", lookup, "Whether an index lookup table was used for the reduction."));
	cursor.add(new HeaderCard("FLT13HZ", filter13Hz, "Whether 13.4 Hz filter was applied to noisy pixels."));
	cursor.add(new HeaderCard("WDTH13HZ", width13Hz / Unit.Hz, "The width of the 13.4Hz filter in Hz."));
	cursor.add(new HeaderCard("FLAG13HZ", flag13Hz, "Whether to flag 13.4Hz noisy pixels."));
	cursor.add(new HeaderCard("GAPOK", gapOKRule, "The rule under which timing gaps are acceptable."));
	cursor.add(new HeaderCard("CHOPPED", chopped, "Whether the data was taken in chopping mode."));
	if(chopped) {
	    cursor.add(new HeaderCard("CHOP_TOL", chopTolerance / Unit.ArcSECOND, "Chopper smearing tolerance (arcsec)."));
	    cursor.add(new HeaderCard("CHOPTHRW", chopperThrow / Unit.ArcSECOND, "Estimated Chopper Throw (arcsec)."));
	    cursor.add(new HeaderCard("CHOPFREQ", chopperFrequency * Unit.SECOND, "Estimated Chopper Frequency (Hz)."));
	}	
	cursor.add(new HeaderCard("AVERAGE", downSample, "The number of raw frames averaged in the reduction."));
	cursor.add(new HeaderCard("XFLAGS", excludeFlags, "Pixel flags that are excluded from the reduction."));
	cursor.add(new HeaderCard("DIRGAIN", directGains, "Are pixel gains calculated from detector offsets?"));
	cursor.add(new HeaderCard("EXTGAIN", (!fitsGains && !directGains), "Are pixel gains read externally instead of FITS?"));
	cursor.add(new HeaderCard("EXTFLAG", !fitsFlags, "Are pixel flags read externally instead of FITS?"));
	cursor.add(new HeaderCard("EXTWGHT", !fitsWeights, "Are pixel weights read externally instead of FITS?"));
	cursor.add(new HeaderCard("EXTOFFST", !fitsOffsets, "Are pixel offsets read externally instead of FITS?"));
	cursor.add(new HeaderCard("EDGES", leftEdge + ":" + rightEdge + ":" + topEdge + ":" + bottomEdge, 
				  "Pixel flags that are excluded from the reduction."));
	
	cursor.add(new HeaderCard("XSLOAD", excessLoadT, "The Excess Optical Load (Kelvin)."));
	
	cursor.add(new HeaderCard("FRAME_T", samplingInterval/Unit.SECOND, "The frame interval (s) in the reduction."));
	cursor.add(new HeaderCard("FRAMES", frames, "The number of frames."));
	cursor.add(new HeaderCard("INT_TIME", samplingInterval * frames / Unit.SECOND, "The total integration time in seconds."));
	cursor.add(new HeaderCard("OKPIXELS", activePixels, "The number of active detector pixels."));

	cursor.add(new HeaderCard("POS_T", velocityT, "Target number of raw frames per telescope position data."));

	if(highPass > 0.0) cursor.add(new HeaderCard("HIPASS", highPass / Unit.SECOND, "Time scale of the high pass filter."));
	if(vclip > 0.0) cursor.add(new HeaderCard("MIN_SPD", vclip / (Unit.ArcSECOND/Unit.SECOND), 
						  "The minimum required scanning speed (\"/s)."));
	if(aclip > 0.0) cursor.add(new HeaderCard("MAX_ACCL", aclip / (Unit.ArcSECOND/(Unit.SECOND*Unit.SECOND)), 
						 "The maximum tolerated telescope acceleration (\"/s@)."));


	if(maskName != null) cursor.add(new HeaderCard("MASK", maskName, "The source mask file used for the reduction."));
	if(pixelFileName != null) cursor.add(new HeaderCard("PIXFILE", pixelFileName, "The file used for the pixel data."));

	Bolometer.editHeader(cursor);
    }

    public void parseHeader(Header header) throws FitsException, HeaderCardException {
	serialNo = header.getIntValue("SCANNO");
	descriptor = header.getStringValue("SCANNAME");
	observer = header.getStringValue("OBSERVER");
	project = header.getStringValue("PROJECT");
	creator = header.getStringValue("CREATOR");
	date = header.getStringValue("DATE-OBS");
	MJD = header.getIntValue("MJD");

	sourceName = header.getStringValue("OBJECT");
	origRA = header.getFloatValue("RA") * 15.0F * Unit.DEGREE;
	origDEC = header.getFloatValue("DEC") * Unit.DEGREE;
	origEpoch = header.getFloatValue("EQUINOX");
	AZ = header.getFloatValue("AZ") * Unit.DEGREE;
	EL = header.getFloatValue("EL") * Unit.DEGREE;
	PA = header.getFloatValue("PA") * Unit.DEGREE;

	RAO = header.getFloatValue("RAO") * Unit.ArcSECOND;
	DECO = header.getFloatValue("DECO") * Unit.ArcSECOND;
	RAO += header.getFloatValue("RAO_MAP") * Unit.ArcSECOND;
	DECO += header.getFloatValue("DECO_MAP") * Unit.ArcSECOND;
	RAO += header.getFloatValue("RAO_FLD") * Unit.ArcSECOND;
	DECO += header.getFloatValue("DECO_FLD") * Unit.ArcSECOND;

	AZO = header.getFloatValue("AZO") * Unit.ArcSECOND;
	ELO = -header.getFloatValue("ZAO") * Unit.ArcSECOND;
	AZO += header.getFloatValue("AZO_MAP") * Unit.ArcSECOND;
	ELO -= header.getFloatValue("ZAO_MAP") * Unit.ArcSECOND;

	String platform = header.getStringValue("PLATFORM");
	if(platform == null) platform = "Cassegrain";
	isNasmyth = platform.equalsIgnoreCase("Nasmyth");

	pixelXSize = header.getFloatValue("PIXSIZEX", SharcHeader.pixelArea / Unit.ArcSECOND) * Unit.ArcSECOND;
	pixelYSize = header.getFloatValue("PIXSIZEY", SharcHeader.pixelArea / Unit.ArcSECOND) * Unit.ArcSECOND;
	pixelArea = pixelXSize * pixelYSize;
	
	rotatorAngle = header.getDoubleValue("ROTATOR") * Unit.DEGREE;
	rotatorZeroAngle = header.getDoubleValue("ROTZERO") * Unit.DEGREE;
	rotatorOffset = header.getDoubleValue("ROTOFFST") * Unit.DEGREE;
	rotatorMode = header.getStringValue("ROTMODE");
	if(rotatorMode == null) rotatorMode = "unknown";

	FAZO = header.getDoubleValue("FAZO") * Unit.ArcSECOND;
	FZAO = header.getDoubleValue("FZAO") * Unit.ArcSECOND;

	focusX = header.getDoubleValue("FOCUS_X");
	focusY = header.getDoubleValue("FOCUS_Y");
	focusZ = header.getDoubleValue("FOCUS_Z");
	focusYOffset =  header.getDoubleValue("FOCUS_YO");
	focusZOffset =  header.getDoubleValue("FOCUS_ZO");
	focusMode = header.getStringValue("FOCMODE");

	filterName = header.getStringValue("FILTER");
	if(filterName == null) filterName = "uknown";

	dsos = header.getBooleanValue("DSOS");
	if(dsos) dsosVersion = header.getStringValue("DSOSVER");

	tau = header.getDoubleValue("TAU");
	tauID = "Sharc2";
	temperature = header.getDoubleValue("TEMPERAT");
	pressure = header.getDoubleValue("PRESSURE");
	humidity = header.getDoubleValue("HUMIDITY");
	
	Jy = header.getFloatValue("VPERJY");
	gain = header.getFloatValue("GAIN", 1.0F);
	weight = header.getFloatValue("WEIGHT", 1.0F);
	scale = header.getDoubleValue("SCALE", 1.0);
	directTau = header.getBooleanValue("DIRTAU");
	tauLookupID = header.getStringValue("MAITAU");

	// For backward compatibility with old MAITAU key, translate possible boolean values...
	if(tauLookupID == null) {
	    taufit = header.getBooleanValue("MAITAU");
	    tauLookupID = taufit ? "350um" : "off";
	}

	lookup = header.getBooleanValue("LOOKUP");
	filter13Hz = header.getBooleanValue("FLT13HZ");
	width13Hz = header.getDoubleValue("WDTH13HZ", 1.0) * Unit.Hz;
	flag13Hz = header.getBooleanValue("FLAG13HZ");
	gapOKRule = header.getStringValue("GAPOK");
	chopped = header.getBooleanValue("CHOPPED");
	if(chopped) {
	    chopTolerance = header.getFloatValue("CHOP_TOL") * Unit.ArcSECOND;
	    chopperFrequency = header.getDoubleValue("CHOPFREQ") / Unit.SECOND;
	    chopperThrow = header.getDoubleValue("CHOPTHRW") * Unit.ArcSECOND;
	}
	downSample = header.getIntValue("AVERAGE", 1);
	excludeFlags = header.getIntValue("XFLAGS");
	directGains = header.getBooleanValue("DIRGAIN");
	fitsGains = !header.getBooleanValue("EXTGAIN");
	fitsFlags = !header.getBooleanValue("EXTFLAG");
	fitsWeights = !header.getBooleanValue("EXTWGHT");
	fitsOffsets = !header.getBooleanValue("EXTOFFST");

	directTau = header.getBooleanValue("DRCTTAU");
	excessLoadT = header.getFloatValue("XSLOAD");
	
	String edges = header.getStringValue("EDGES");
	if(edges != null) {
	    StringTokenizer tokens = new StringTokenizer(edges,",:");
	    leftEdge = Integer.parseInt(tokens.nextToken());
	    if(tokens.hasMoreTokens()) {
		rightEdge = Integer.parseInt(tokens.nextToken());
		topEdge = Integer.parseInt(tokens.nextToken());
		bottomEdge = Integer.parseInt(tokens.nextToken());
	    }
	    else { rightEdge = leftEdge; topEdge = leftEdge; bottomEdge = leftEdge; }
	}

	samplingInterval = header.getDoubleValue("FRAME_T", 36.0) * Unit.SECOND;
	frames = header.getIntValue("FRAMES");
	activePixels = header.getIntValue("OKPIXELS", 384 - 17);

	velocityT = header.getIntValue("POS_T");

	highPass = header.getFloatValue("HIPASS", -1.0F);
	vclip = header.getFloatValue("MIN_SPD", -1.0F * Unit.ArcSECOND / Unit.SECOND);
	aclip = header.getFloatValue("MAX_ACCL", -1.0F * Unit.ArcSECOND / (Unit.SECOND * Unit.SECOND));
	
	maskName = header.getStringValue("MASK");
	pixelFileName = header.getStringValue("PIXFILE");
    }


    int[][] n;
    public double[][] getCovariance() {
	double[][] covar = new double[pixels][pixels];
	n = new int[pixels][pixels];
	
	for(int t=0; t<frames; t++) {
	    final float[] value = frame[t].readout;
	    
	    for(int b1=0; b1<pixels; b1++) if(pixel[b1].isActive) if(!frame[t].flagged[b1])
		for(int b2=b1; b2<pixels; b2++) if(pixel[b2].isActive) if(!frame[t].flagged[b2]) {
		    covar[b1][b2] += value[b1] * value[b2];
		    n[b1][b2]++;
		}
	}
	    
	for(int b1=0; b1<pixels; b1++) for(int b2=b1+1; b2<pixels; b2++) {
	    if(pixel[b1].isActive && pixel[b2].isActive) {
		covar[b1][b2] /= n[b1][b2] * Math.sqrt(covar[b1][b1]/n[b1][b1] * covar[b2][b2]/n[b2][b2]);
		covar[b2][b1] = covar[b1][b2];
		n[b2][b1] = n[b1][b2];
	    }
	    else {
		covar[b1][b2] = Double.NaN;
		covar[b2][b1] = Double.NaN;
	    }
	}

	for(int bol=0; bol<pixels; bol++) covar[bol][bol] = 0.0;
	
	return covar;
    }

    public double[][] getCovariance(int b1) {
	double[][] covar = new double[12][32];
	double[] norm = new double[pixels];
	
	for(int t=0; t<frames; t++) {
	    final float[] value = frame[t].readout;
	    
	    for (int row=0,b2=0; row<12; row++) for(int col=0; col<32; col++,b2++) if(pixel[b2].isActive) if(!frame[t].flagged[b2]) {
		covar[row][col] += value[b1] * value[b2];
		norm[b2] += value[b2] * value[b2];
	    }
	}
	    
	for(int row=0,b2=0; row<12; row++) for(int col=0; col<32; col++,b2++) {
	    if(pixel[b2].isActive)
		covar[row][col] /= Math.sqrt(norm[b1] * norm[b2]);
	    else covar[row][col] = Double.NaN;
	}
	return covar;
    }


    public int[][] getHistogram(double range, double resolution) {
	final int bins = (int)Math.ceil(range/resolution) + 2; // first and last are overflow bins
	
	int[][] histogram = new int[pixels][bins];
	
	for(int bol=0; bol<pixels; bol++) {
	    double binWidth = pixel[bol].rms * resolution;
	    
	    for(int t=0; t<frames; t++) if(!frame[t].flagged[bol]) {
		int bin = (int)Math.round(frame[t].readout[bol] / binWidth + (bins-1) / 2.0);
		if(bin <= 0) bin = 0;
		if(bin >= bins) bin = bins-1;
		
		histogram[bol][bin]++;
	    }
	}	
	
	return histogram;
    }

    public void precess(double epoch) {
	EquatorialCoordinates coords = new EquatorialCoordinates(RA, DEC, coordinateEpoch);
	coords.precess(epoch);

	RA = (float) coords.RA;
	DEC = (float) coords.DEC;
	coordinateEpoch = (float) coords.epoch;
    }

    void printInfo() {
	
	DecimalFormat f1 = new DecimalFormat("0.0");
	DecimalFormat f2 = new DecimalFormat("0.00");
	DecimalFormat f3 = new DecimalFormat("0.000");
	DecimalFormat f3_1 = new DecimalFormat(" 0.0;-0.0");
	
	System.out.println();
	
	StringTokenizer tokens = new StringTokenizer(date, ":T");
	String dateString = tokens.nextToken();
	String timeString = tokens.nextToken() + ":" + tokens.nextToken() + " UT";

	System.out.println("   [" + sourceName + "] ");
       
	System.out.println("    Observed on " + dateString + " at " + timeString + " by " + observer);

	System.out.println("    Project: " + project);
	
	System.out.println("    RA = " + Util.HMS(12.0*Unit.HOUR/Math.PI * origRA)
			   + "\t\tDEC = " + Util.DMS(origDEC)
			   + "  (" + f1.format(origEpoch) + ")");
	
	if(coordinateEpoch != origEpoch)
	    System.out.println("       = " + Util.HMS(12.0*Unit.HOUR/Math.PI * RA)
			   + "\t\t    = " + Util.DMS(DEC)
			   + "  (" + f1.format(coordinateEpoch) + ")");

	System.out.println("    AZ = " + Util.DMS(AZ) 
			   + "\t\tEL  = " + Util.DMS(EL));

	System.out.println("    RAO =" + f3_1.format(RAO/Unit.ArcSECOND)
			   + "\tDECO=" + f3_1.format(DECO/Unit.ArcSECOND)
			   + "\tAZO =" + f3_1.format(AZO/Unit.ArcSECOND)
			   + "\tZAO =" + f3_1.format(-ELO/Unit.ArcSECOND));
	
	System.out.println("    FAZO=" + f3_1.format(FAZO/Unit.ArcSECOND)
			   + "\tFZAO=" + f3_1.format(FZAO/Unit.ArcSECOND)
			   + (isNasmyth ? 
			      "\tNasmyth Mount" +
			      "\tRefAngle = " + f1.format(rotatorZeroAngle/Unit.DEGREE)
			      :
			      "\tRotator = " + f1.format(rotatorAngle/Unit.DEGREE)+
			      "\tRotZero = " + f1.format(rotatorZeroAngle/Unit.DEGREE)
			      )
			   );
	
	System.out.println("    Pointing Center = " + f1.format(pointingCenter.x+1.0) + "," + f1.format(pointingCenter.y+1.0)
			   + "\tRotation Center = " + f1.format(rotationCenter.x+1.0) + "," + f1.format(rotationCenter.y+1.0)
			   );


	System.out.println("    Focus Mode: " + focusMode
			   + "\tY Offset=" + f2.format(focusYOffset) 
			   + "\tZ Offset=" + f2.format(focusZOffset)
			   );


	if(!directTau) System.out.println("    Parallactic Angle = " + f1.format(PA/Unit.DEGREE)
			   + "\ttau225 = " + f3.format(ReductionTool.getTau("225GHz", tau))
			   + "\ttau = " + f3.format(tau/Math.sin(EL))
			   );

	System.out.println("    Direct Tau Estimates  "
			   + "\t-------> " + f3.format(ReductionTool.getTau("225GHz", getDirectTau()))
			   + "\t----> " + f3.format(getDirectTau()/Math.sin(EL))
			   );	    

	System.out.println("    Filter Setting: " + (filterName == null ? "Unknown" : filterName) +
			   "\tDSOS " + (dsos ? "ON" + "\tversion " + dsosVersion : "OFF"));

	System.out.println("    Plate Scale = " + f2.format(pixelXSize / Unit.ArcSECOND) + "\"x" + f2.format(pixelYSize/Unit.ArcSECOND)
			   + "\"");
	
	System.out.println();
    }

    public void logEntry(String fileName) throws IOException {
	File file = new File(fileName);
	boolean isNew = !file.exists();
	RandomAccessFile out = new RandomAccessFile(file, "rw");
	
	DecimalFormat f1 = new DecimalFormat("0.0");
	DecimalFormat f2 = new DecimalFormat("0.00");
	DecimalFormat f3 = new DecimalFormat("0.000");

	if(isNew) {
	    String header = "Object\tScan\ttau225\tDate\t\tUT\tSize\tRA\tDEC\tAZ\tEL\tFAZO\tFZAO\tFoc.X\tFoc.dY\tFoc.dZ\tObserver"; 
	    
	    out.writeBytes("! SHARC2 Log File\n");
	    out.writeBytes("! Created by Crush " + Crush.version + "\n");
	    out.writeBytes("!\n");
	    out.writeBytes("! " + header + "\n");
	}
	
	out.seek(out.length());

	String info = new String();

	info += sourceName + "\t";
	if(sourceName.length() < 8) info += "\t";

	info += serialNo + "\t";
	info += f3.format(ReductionTool.getTau("225GHz", tau)) + "\t";

	info += year + "-" + month + "-" + day + "\t";
	info += f1.format(UT / Unit.HOUR) + "\t";

	info += Math.round(fileSize / (1024.0 * 1024.0)) + "M\t";	

	info += f1.format(12.0 * RA / Math.PI) + "\t";
	info += f1.format(180.0 * DEC / Math.PI) + "\t";	
	info += f1.format(180.0 * AZ / Math.PI) + "\t";	
	info += f1.format(180.0 * EL / Math.PI) + "\t";	

	info += f1.format(FAZO / Unit.ArcSECOND) + "\t";
	info += f1.format(FZAO / Unit.ArcSECOND) + "\t";
	
	info += f2.format(focusX) + "\t";
	info += f2.format(focusYOffset) + "\t";
	info += f2.format(focusZOffset) + "\t";

	info += observer;

	out.writeBytes(info + "\n");
	out.close();
    }
    
    public boolean key(String key, String value) {
	if(key.equalsIgnoreCase("RAW_DATA_PATH")) dataPath = value + "/";
	else if(key.equalsIgnoreCase("RAW_DATA_PREFIX")) filePrefix = value;
	else if(key.equalsIgnoreCase("MIN_INTEGRATION_TIME")) minIntTime = Double.parseDouble(value) * Unit.SECOND;
	else if(key.equalsIgnoreCase("COORDINATE_LOOKUP")) {
	    if(value.equalsIgnoreCase("auto")) autoLookup = true;
	    else { autoLookup = false; lookup = Util.parseBoolean(value); }		   
	}
	else if(key.equalsIgnoreCase("NOISY_13HZ_ACTION")) {
	    filter13Hz = false; flag13Hz = false;
	    if(value.equalsIgnoreCase("filter")) filter13Hz = true;
	    else if(value.equalsIgnoreCase("flag")) flag13Hz = true;
	}
	else if(key.equalsIgnoreCase("FILTER_13HZ_WIDTH")) width13Hz = Double.parseDouble(value) * Unit.Hz;
	else if(key.equalsIgnoreCase("IGNORE_ARRAY_EDGES")) {
	    if(value.equalsIgnoreCase("off") || value.equalsIgnoreCase("none")) {
		leftEdge = 0; rightEdge = 0; topEdge = 0; bottomEdge = 0;
	    }
	    else { 
		StringTokenizer tokens = new StringTokenizer(value,",:");
		leftEdge = Integer.parseInt(tokens.nextToken());
		if(tokens.hasMoreTokens()) {
		    rightEdge = Integer.parseInt(tokens.nextToken());
		    topEdge = Integer.parseInt(tokens.nextToken());
		    bottomEdge = Integer.parseInt(tokens.nextToken());
		}
		else { rightEdge = leftEdge; topEdge = leftEdge; bottomEdge = leftEdge; }
	    }
	}
	else if(key.equalsIgnoreCase("TAU_SOURCE")) {
	    option("-tau", value);
	    tau = Double.NaN;
	}
	else if(key.equalsIgnoreCase("TOLERATE_TIMING_GAPS")) {
	    gapOKRule = value;
	}
	else if(key.equalsIgnoreCase("CHOPPED_OBSERVATION")) {
	    if(value.equalsIgnoreCase("auto")) autoChopRecognition = true;
	    else {
		autoChopRecognition = false;
		chopped = Util.parseBoolean(value);
	    }
	}
	else if(key.equalsIgnoreCase("CHOPPER_MAX_SMEAR")) chopTolerance = Float.parseFloat(value) * Unit.ArcSECOND;
	else if(key.equalsIgnoreCase("AVERAGE_FRAMES")) downSample = Integer.parseInt(value);
	else if(key.equalsIgnoreCase("EXCLUDE_FLAGS")) excludeFlags = Integer.decode(value).intValue();
	else if(key.equalsIgnoreCase("HIGH_PASS_FILTER")) {
	    autoHighPass = false;
	    if(value.equalsIgnoreCase("auto")) autoHighPass = true;
	    else if(value.equalsIgnoreCase("off")) highPass = 0.0;
	    else highPass = Double.parseDouble(value) * Unit.SECOND;		    
	}
	else if(key.equalsIgnoreCase("MIN_SCAN_SPEED")) {
	    if(value.equalsIgnoreCase("auto")) vclip = Double.NaN;
	    else if(value.equalsIgnoreCase("off")) vclip = -1.0;
	    else vclip = Double.parseDouble(value) * Unit.ArcSECOND/Unit.SECOND;		    
	}	    
	else if(key.equalsIgnoreCase("MAX_ACCELERATION")) {
	    if(value.equalsIgnoreCase("off")) aclip = -1.0;
	    else aclip = Double.parseDouble(value) * Unit.ArcSECOND/(Unit.SECOND*Unit.SECOND);
	}
	else if(key.equalsIgnoreCase("SHARC2_PIXEL_SIZE")) {
	    if(value.equalsIgnoreCase("auto") || value.equalsIgnoreCase("*")) {
		pixelXSize = Float.NaN; 
		pixelYSize = Float.NaN;
	    }
	    else {
		StringTokenizer tokens = new StringTokenizer(value, ",xX");
		
		pixelXSize = Float.parseFloat(tokens.nextToken()) * Unit.ArcSECOND;
		pixelYSize = tokens.hasMoreTokens() ? Float.parseFloat(tokens.nextToken()) * Unit.ArcSECOND : pixelXSize;
	    }
	}
	else if(key.equalsIgnoreCase("POINTING_CENTER")) {
	    if(value.equalsIgnoreCase("auto") || value.equals("*")) pointingCenter = null;
	    else {
		pointingCenter = new Vector2D();
		StringTokenizer tokens = new StringTokenizer(value, ",:");
		pointingCenter.x = Double.parseDouble(tokens.nextToken()) - 1.0;
		pointingCenter.y = Double.parseDouble(tokens.nextToken()) - 1.0;
	    }
	}
	else if(key.equalsIgnoreCase("ROTATION_CENTER")) {
	    if(value.equalsIgnoreCase("auto") || value.equals("*")) rotationCenter = null;
	    else {
		pointingCenter = new Vector2D();
		StringTokenizer tokens = new StringTokenizer(value, ",:");
		rotationCenter.x = Double.parseDouble(tokens.nextToken()) - 1.0;
		rotationCenter.y = Double.parseDouble(tokens.nextToken()) - 1.0;
	    }
	}
	else if(key.equalsIgnoreCase("ROTATOR_ZERO")) {
	    if(value.equalsIgnoreCase("auto")) rotatorZeroAngle = Double.NaN;
	    else rotatorZeroAngle = Double.parseDouble(value) * Unit.DEGREE; 
	}
	else if(key.equalsIgnoreCase("PIXEL_DATA")) option("-pixeldata", value);
	else if(key.equalsIgnoreCase("DIRECT_GAINS")) option("-directGains", value);
	else if(key.equalsIgnoreCase("DEFAULT_EXCESS_LOAD")) excessLoadT = Float.parseFloat(value);
	else return false;
	
	return true;
    }


    public boolean option(String key, String value) {
	if(key.equalsIgnoreCase("-path")) dataPath = value + "/";
	else if(key.equalsIgnoreCase("-prefix")) filePrefix = value;
	else if(key.equalsIgnoreCase("-minIntT")) key("MIN_INTEGRATION_TIME", value);
	else if(key.equalsIgnoreCase("-Jy")) Jy = Double.parseDouble(value) * Unit.VOLT;
	else if(key.equalsIgnoreCase("-directGains")) {
	    directGains = true;
	    fitsGains = false;
	}
	else if(key.equalsIgnoreCase("-pixeldata")) {
	    StringTokenizer tokens = new StringTokenizer(value, ":");
	    String source = tokens.nextToken();
	    String directive = tokens.hasMoreTokens() ? tokens.nextToken() : "gfwo";

	    boolean useFits = source.equalsIgnoreCase("auto") || source.equalsIgnoreCase("fits");

	    if(directive.equalsIgnoreCase("gains")) { directGains = false; fitsGains = useFits; }
	    else if(directive.equalsIgnoreCase("flags")) fitsFlags = useFits;
	    else if(directive.equalsIgnoreCase("weights")) fitsWeights = useFits;
	    else if(directive.equalsIgnoreCase("offsets")) fitsOffsets = useFits;
	    else if(directive.equalsIgnoreCase("all")) {
		directGains = false;
		fitsGains = useFits;
		fitsFlags = useFits;
		fitsWeights = useFits;
		fitsOffsets = useFits;
	    }
	    else for(int i=0; i<directive.length(); i++) 
		switch(directive.charAt(i)) {
		case 'g': directGains=false; fitsGains = useFits; break;
		case 'f': fitsFlags = useFits; break;
		case 'w': fitsWeights = useFits; break;
		case 'o': fitsOffsets = useFits; break;
		}
	
	    if(!useFits) {
		try { parsePixelFile(source); }
		catch(IOException e) { 
		    System.err.println(" WARNING! Cannot parse pixel data file " + source + ".");
		    fitsGains = !directGains; fitsWeights = true; fitsFlags = true;
		}
	    }
	}
	else if(key.equalsIgnoreCase("-lookup")) {
	    lookup = false;
	    if(value.equalsIgnoreCase("auto")) autoLookup = true;
	    else { autoLookup = false; lookup = Util.parseBoolean(value); }
	}
	else if(key.equalsIgnoreCase("-13Hz")) {
	    filter13Hz = false; flag13Hz = false;
	    if(value.equalsIgnoreCase("filter")) filter13Hz = true;
	    else if(value.equalsIgnoreCase("flag")) flag13Hz = true;
	}
	else if(key.equalsIgnoreCase("-width13Hz")) width13Hz = Double.parseDouble(value) * Unit.Hz;
	else if(key.equalsIgnoreCase("-edge")) {
	    if(value.equalsIgnoreCase("off") || value.equalsIgnoreCase("none")) {
		leftEdge = 0; rightEdge = 0; topEdge = 0; bottomEdge = 0;
	    }
	    else { 
		StringTokenizer tokens = new StringTokenizer(value,",:");
		leftEdge = Integer.parseInt(tokens.nextToken());
		if(tokens.hasMoreTokens()) {
		    rightEdge = Integer.parseInt(tokens.nextToken());
		    topEdge = Integer.parseInt(tokens.nextToken());
		    bottomEdge = Integer.parseInt(tokens.nextToken());
		}
		else { rightEdge = leftEdge; topEdge = leftEdge; bottomEdge = leftEdge; }
	    }
	}
	else if(key.equalsIgnoreCase("-gapOK")) key("TOLERATE_TIMING_GAPS", value);
	else if(key.equalsIgnoreCase("-chopped")) key("CHOPPED_OBSERVATION", value);

	else if(key.equalsIgnoreCase("-smear")) chopTolerance = Float.parseFloat(value) * Unit.ArcSECOND;
	else if(key.equalsIgnoreCase("-average")) downSample = Integer.decode(value).intValue();
	else if(key.equalsIgnoreCase("-flags")) excludeFlags = Integer.parseInt(value);
	else if(key.equalsIgnoreCase("-vclip")) key("MIN_SCAN_SPEED", value);
	else if(key.equalsIgnoreCase("-aclip")) key("MAX_ACCELERATION", value);
	else if(key.equalsIgnoreCase("-hipass")) {
	    autoHighPass = false;
	    if(value.equalsIgnoreCase("auto")) autoHighPass = true;
	    else if(value.equalsIgnoreCase("off")) highPass = 0.0;
	    else highPass = Double.parseDouble(value) * Unit.SECOND;
	}
	else if(key.equalsIgnoreCase("-scale")) scale = Double.parseDouble(value);
	else if(key.equalsIgnoreCase("-platescale")) key("SHARC2_PIXEL_SIZE", value);
	else if(key.equalsIgnoreCase("-pcenter")) key("POINTING_CENTER", value);
	else if(key.equalsIgnoreCase("-rcenter")) key("ROTATION_CENTER", value);
	else if(key.equalsIgnoreCase("-rotator")) {
	    if(value.equalsIgnoreCase("auto")) rotatorAngle = Double.NaN;
	    else rotatorAngle = Double.parseDouble(value) * Unit.DEGREE; 
	}
	else if(key.equalsIgnoreCase("-rot0")) {
	    if(value.equalsIgnoreCase("auto")) rotatorZeroAngle = Double.NaN;
	    else rotatorZeroAngle = Double.parseDouble(value) * Unit.DEGREE; 
	}
	else if(key.equalsIgnoreCase("-fazo")) {
	    if(value.equalsIgnoreCase("auto"))  FAZO = Double.NaN;
	    else FAZO = Double.parseDouble(value) * Unit.ArcSECOND; 
	}
	else if(key.equalsIgnoreCase("-fzao")) {
	    if(value.equalsIgnoreCase("auto")) FZAO = Double.NaN;
	    else FZAO = Double.parseDouble(value) * Unit.ArcSECOND;
	}
	else if(key.equalsIgnoreCase("-tau")) {
	    StringTokenizer tokens = new StringTokenizer(value, ":");
	    String mode = tokens.nextToken();

	    taufit = false; directTau = false;

	    // set the default behavour to be lookup.
	    if(mode.equalsIgnoreCase("auto")) mode = "MaiTau";

	    if(mode.equalsIgnoreCase("fit") || mode.equalsIgnoreCase("MaiTau")) {
		tauLookupID = tokens.hasMoreTokens() ? tokens.nextToken() : "350um";
		taufit = true;
	    }
	    else if(mode.equalsIgnoreCase("direct")) directTau = true;
	    else if(mode.equalsIgnoreCase("FITS") || mode.equalsIgnoreCase("data")) tau = Double.NaN;	    
	    else {
		if(tokens.hasMoreTokens()) {
		    tauID = mode; 
		    mode = tokens.nextToken();
		}
		tau = Double.parseDouble(mode);
	    }
	}
	else if(key.equalsIgnoreCase("-excessLoad")) excessLoadT = Float.parseFloat(value);
	else if(key.equalsIgnoreCase("-epoch")) {
	    if(value.equalsIgnoreCase("auto")) coordinateEpoch = Float.NaN;
	    else coordinateEpoch = Float.parseFloat(value);
	}
	else if(key.equalsIgnoreCase("-shift")) {
	    delay = Double.parseDouble(value) * Unit.SECOND;
	}

	else return false;
	
	return true;
    }

    public void usage() {
	DecimalFormat tf = new DecimalFormat("0.000");
	DecimalFormat pos = new DecimalFormat("0.0");
	DecimalFormat f2 = new DecimalFormat("0.00");

	String fitsPixelData = "";
	String readPixelData = "";
	
	if(fitsGains) fitsPixelData += "g"; else if(!directGains) readPixelData += "g";
	if(fitsFlags) fitsPixelData += "f"; else readPixelData += "f";
	if(fitsWeights) fitsPixelData += "w"; else readPixelData += "w";
	if(fitsOffsets) fitsPixelData += "o";	

	String tauSetting;
	if(taufit) tauSetting = "MaiTau:" + tauLookupID;
	else if(directTau) tauSetting = "direct";
	else if(Double.isNaN(tau)) tauSetting = "FITS";
	else tauSetting = tauID + ":" + tf.format(tau);

	String info = "\n" 
	    + " Scan Specific Options: " + "\n"
	    + "\n"
	    
	    + " (These options apply to all subsequently listed scans on the command line.)" + "\n"
	    +"\n"

	    + "  -path=          The location of the SHARC-2 data. (" + dataPath + ")\n"
	    + "  -prefix=        The string preceding the scan serial no. (" + filePrefix + ")\n"	    
	    + "  -tau=           Define how tau is obtained. (" + tauSetting + ").\n"
	    + "  -Jy=            Conversion factor from bolometer V to Jy/pixel. (" + Jy/Unit.VOLT + ")\n"
	    + "  -directGains    Calculate gains directly from detector offsets." + (directGains ? "(*)" : "") + "\n"
	    + "  -pixeldata=     The way the pixel data is obtained. " +
	    (fitsPixelData.length() > 0 ? "(fits:" + fitsPixelData + ")" : "") +
	    (readPixelData.length() > 0 && pixelFileName != null ? "(" + pixelFileName + ":" + readPixelData + ")" : "") + "\n"
	    + "  -lookup=        Boolean or 'auto'. Whether to use lookups internally. (" + 
	    (autoLookup ? "auto" : lookup ? "on" : "off") + ")\n"
	    + "  -13Hz           Action on 13.4Hz noisy pixels. (" + (filter13Hz ? "filter" : (flag13Hz ? "flag" : "none")) + ")\n"
	    + "  -width13Hz=     13.4Hz filter width in Hz. (" + width13Hz / Unit.Hz + ")\n"
	    + "  -edge           Array edges to ignore. Single value, or l:r:t:b. (" + 
	    leftEdge + ":" + rightEdge + ":" + topEdge + ":" + bottomEdge + ")" + "\n"
	    + "  -minIntT=       Minimum required integration time. " + minIntTime / Unit.SECOND + "\n"
	    + "  -gapOK=         Tolerate timing gaps if found. " + gapOKRule + "\n"
	    + "  -chopped=       Whether to use chopper information. (" + (autoChopRecognition ? "auto" : chopped + "") + ").\n"
	    + "  -smear=         Maximum allowed chopper smear in arcsec. (" + chopTolerance / Unit.ArcSECOND + ")\n"
	    + "  -average=       Downsample data over the number of frames. (" + downSample + ")\n"
	    + "  -flags=         Pixel flag pattern to exclude from reduction. (0x" + Integer.toHexString(excludeFlags) + ")\n"
	    + "  -hipass=        Time scale (s) for the high-pass filter or 'auto'. (" + 
	    (autoHighPass ? "auto" : highPass <= 0.0 ? "off" : pos.format(highPass/Unit.SECOND)) + ")\n"
	    + "  -vclip=         Clip frames if scanning speed is below. (" + 
	    (Double.isNaN(vclip) ? "auto" : (vclip < 0.0 ? "off" : pos.format(vclip/(Unit.ArcSECOND/Unit.SECOND))))  + ")\n"
	    + "  -aclip=         Clip frames if acceleration is above. (" + 
	    (aclip < 0.0 ? "off" : pos.format(aclip/(Unit.ArcSECOND/(Unit.SECOND*Unit.SECOND)))) + ")\n"
	    + "  -scale=         Scaling factor to be applied to scan data. (" + scale + ")\n"
	    + "  -platescale=    The SHARC2 pixel plate scale. (" +
	    (Float.isNaN(pixelXSize) || Float.isNaN(pixelYSize) ? 
	     "auto" : pixelXSize / Unit.ArcSECOND + "x" + pixelYSize / Unit.ArcSECOND) + ")\n" 
	    + "  -pcenter=       Pointing Center from corner in pixels. (" + 
	    (pointingCenter == null ? "auto" : (pointingCenter.x + 1.0) + "," + (pointingCenter.y + 1.0) + "") + ")\n"
	    + "  -rcenter=       Rotation Axis from corner in pixels. (" + 
	    (rotationCenter == null ? "auto" : (rotationCenter.x + 1.0) + "," + (rotationCenter.y + 1.0) + "") + ")\n"	    
	    + "  -rotator=       Obtain/override instrument rotator value. (" + 
	    (Double.isNaN(rotatorAngle) ? "auto" : rotatorAngle + "") + ")\n"
	    + "  -rot0=          Obtain/override the recorded rotator reference value. (" + 
	    (Double.isNaN(rotatorZeroAngle) ? "auto" : rotatorZeroAngle + "") + ")\n"
	    + "  -FAZO=          Adjust the azymuthal pointing. (" + 
	    (Double.isNaN(FAZO) ? "auto" : FAZO + "") + ")\n"
	    + "  -FZAO=          Adjust the zenith angle pointing. (" + 
	    (Double.isNaN(FZAO) ? "auto" : FZAO + "") + ")\n"
	    + "  -excessLoad=    The excess load temperature in Kelvin. (" + f2.format(excessLoadT) + ")\n"
	    + "  -epoch=         Override the recorded coordinate epoch value. (" + 
	    (Float.isNaN(coordinateEpoch) ? "auto" : coordinateEpoch + "") + ")\n";

	

	System.out.println(info);
    }

    // Cassegrain Rotation center from Rick Arendt memo   
    final static Vector2D cassegrainRotationCenter = new Vector2D(17.5, 7.6);
    // Nasmyth rotation center included in pointing model
    final static Vector2D nasmythRotationCenter = new Vector2D(15.5, 5.5);
    static Vector2D rotationCenter;

    // The pointing center is just the array center
    static Vector2D pointingCenter;

    // Pixel scales from A. Kovacs (Jan 2003 & Feb 2003) 
    static float pixelXSize = SharcHeader.pixelXSize;
    static float pixelYSize = SharcHeader.pixelYSize;
    static float pixelSize = (pixelXSize + pixelYSize) / 2.0F;
    static float pixelArea = pixelXSize * pixelYSize;
}
