RelativeAmplitudeAndPhaseCreator.java

/**
 * VStar: a statistical analysis tool for variable star data.
 * Copyright (C) 2010  AAVSO (http://www.aavso.org/)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 */
package org.aavso.tools.vstar.util.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.aavso.tools.vstar.util.locale.NumberParser;

/**
 * This class computes relative amplitudes and phases from a set of multi-period
 * fit model coefficients in the presence of harmonics, grouped by fundamental
 * frequency, and makes them available in different string formats.
 */
public class RelativeAmplitudeAndPhaseCreator {

	private List<PeriodFitParameters> paramsSeq;
	private Map<Double, List<PeriodFitParameters>> harmonicMap;

	/**
	 * Constructor.
	 * 
	 * @param paramsList
	 *            A list of period fit parameters containing information about
	 *            coefficients (sine and cosine) and the frequencies (and
	 *            harmonics) that were inputs to the model.
	 */
	public RelativeAmplitudeAndPhaseCreator(
			List<PeriodFitParameters> paramsList) {
		paramsSeq = new ArrayList<PeriodFitParameters>(paramsList);
		Collections.sort(paramsSeq);

		createHarmonicMap();
	}

	/**
	 * 
	 * @return The mapping from fundamental frequencies to sorted period fit
	 *         parameter list.
	 */
	public Map<Double, List<PeriodFitParameters>> getHarmonicMap() {
		return harmonicMap;
	}

	/**
	 * Returns a list of known (mapped) fundamental frequencies.
	 * 
	 * @return The list of fundamental frequencies.
	 */
	public Set<Double> getFundamentals() {
		return harmonicMap.keySet();
	}

	/**
	 * Do any of the fundamental frequencies map to lists of harmonics, i.e.
	 * lists of period fit parameters of length > 1 (since first is always the
	 * fundamental)?
	 * 
	 * @return Whether or not any fundamentals have harmonics.
	 */
	public boolean hasHarmonics() {
		boolean result = false;

		for (Double freq : harmonicMap.keySet()) {
			if (harmonicMap.get(freq).size() > 1) {
				result = true;
				break;
			}
		}

		return result;
	}

	/**
	 * Returns a string representing the sequence of relative amplitude-phase
	 * pairs for a given fundamental frequency, at a specified precision, with
	 * the option of whether the relative phase values should be in radians or
	 * cycles.
	 * 
	 * @param fundamental
	 *            The fundamental frequency whose relative values we seek.
	 * @param precision
	 *            The required precision (decimal places).
	 * @param cycles
	 *            Whether cycles (true) or radians (false) should be returned in
	 *            the string.
	 * @return The string of space-delimited relative amplitude-phase pairs.
	 */
	public String getRelativeSequenceString(double fundamental, int precision,
			boolean cycles) {
		String str = "";

		if (harmonicMap.containsKey(fundamental)) {
			List<PeriodFitParameters> paramsList = harmonicMap.get(fundamental);

			String fmt = "%1." + precision + "f %1." + precision + "f ";

			double firstAmplitude = paramsList.get(0).getAmplitude();
			double firstPhase = paramsList.get(0).getPhase();

			for (int i = 1; i < paramsList.size(); i++) {
				PeriodFitParameters params = paramsList.get(i);

				double relativeAmplitude = params
						.getRelativeAmplitude(firstAmplitude);

				double relativePhase = Double.NaN;

				if (cycles) {
					relativePhase = params.getRelativePhaseInCycles(firstPhase);
				} else {
					relativePhase = params.getRelativePhase(firstPhase);
				}

				str += String.format(fmt, relativeAmplitude, relativePhase);
			}
		} else {
			String msg = String.format("Fundamental frequency '" + fundamental
					+ "' unknown.", fundamental);
			throw new IllegalArgumentException(msg);
		}

		return str;
	}

	/**
	 * Returns a list relative amplitudes for a given fundamental frequency.
	 * 
	 * @param fundamental
	 *            The fundamental frequency whose relative values we seek.
	 * @return The list of relative amplitudes.
	 */
	public List<Double> getRelativeAmplitudes(double fundamental) {
		List<Double> relativeAmplitudes = new ArrayList<Double>();

		if (harmonicMap.containsKey(fundamental)) {
			List<PeriodFitParameters> paramsList = harmonicMap.get(fundamental);

			double firstAmplitude = paramsList.get(0).getAmplitude();

			for (int i = 1; i < paramsList.size(); i++) {
				PeriodFitParameters params = paramsList.get(i);

				double relativeAmplitude = params
						.getRelativeAmplitude(firstAmplitude);

				relativeAmplitudes.add(relativeAmplitude);
			}
		} else {
			String msg = String.format("Fundamental frequency '" + fundamental
					+ "' unknown.", fundamental);
			throw new IllegalArgumentException(msg);
		}

		return relativeAmplitudes;
	}

	/**
	 * Returns a list relative phases for a given fundamental frequency, with
	 * the option of whether the relative phase values should be in radians or
	 * cycles.
	 * 
	 * @param fundamental
	 *            The fundamental frequency whose relative values we seek.
	 * @param cycles
	 *            Whether cycles (true) or radians (false) should be returned.
	 * @return The list of relative phases.
	 */
	public List<Double> getRelativePhases(double fundamental, boolean cycles) {
		List<Double> relativePhases = new ArrayList<Double>();

		if (harmonicMap.containsKey(fundamental)) {
			List<PeriodFitParameters> paramsList = harmonicMap.get(fundamental);

			double firstPhase = paramsList.get(0).getPhase();

			for (int i = 1; i < paramsList.size(); i++) {
				PeriodFitParameters params = paramsList.get(i);

				double relativePhase = Double.NaN;

				if (cycles) {
					relativePhase = params.getRelativePhaseInCycles(firstPhase);
				} else {
					relativePhase = params.getRelativePhase(firstPhase);
				}

				relativePhases.add(relativePhase);
			}
		} else {
			String msg = String.format("Fundamental frequency '" + fundamental
					+ "' unknown.", fundamental);
			throw new IllegalArgumentException(msg);
		}

		return relativePhases;
	}

	// Create a mapping from fundamental frequency to period fit parameters
	// ordered by harmonic number (multiple of fundamental frequency).
	private void createHarmonicMap() {
		harmonicMap = new TreeMap<Double, List<PeriodFitParameters>>();

		// Find the fundamentals, setting up the map's keys and empty lists.
		for (PeriodFitParameters params : paramsSeq) {
			if (params.getHarmonic().isFundamental()) {
				// Truncate the string to ensure we don't get rounding errors
				// when looking for harmonics.
				double fund = round(params.getHarmonic().getFrequency(), 4);
				harmonicMap.put(fund, new ArrayList<PeriodFitParameters>());
			}
		}

		// Now iterate over all parameters again, looking for frequencies
		// that have a particular fundamental, and adding each to the
		// corresponding mapped list.
		//
		// The post-condition is that all parameters whose frequencies have a
		// particular fundamental (including the fundamental itself) will be
		// grouped into a list mapped from a key which is the fundamental
		// frequency. List elements will be ordered by harmonic number iff
		// the parameters list was so ordered to begin with.
		for (PeriodFitParameters params : paramsSeq) {
			double fund = round(params.getHarmonic().getFundamentalFrequency(), 4);
			harmonicMap.get(fund).add(params);
		}
	}

	// Return the number rounded to the specified precision.
	private double round(double n, int precision) {
		String fmt = "%1." + precision + "f";
		String nStr = String.format(fmt, n);
		return NumberParser.parseDouble(nStr);
	}
}