public class Bijection implements Serializable {
	private static final long serialVersionUID = 6526310928919842268L;
	// hy: name, posterior index
	final private HashMap<String, Integer> keys;
	// hy: posterior index, name
	final private Vector<String> values;
	private int size;

	public int getSize() {
		return size;
	}

	public boolean contains(String key) {
		return keys.containsKey(key);
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder("{");
		Iterator<String> v = values.iterator();
		int i = 0;
		while (v.hasNext()) {
			String s = v.next();
			sb.append(s);
			sb.append("=");
			sb.append(i++);
			if (v.hasNext())
				sb.append(", ");
		}
		sb.append("}");
		return sb.toString();
	}

	// FIXME: should return an inmutable collection
	public Collection<Integer> values() {
		return keys.values(); // return posterior index
	}

	// FIXME: should return an inmutable collection
	public Collection<String> keys() {
		return keys.keySet();
	}

	public Bijection() {
		keys = new HashMap<String, Integer>();
		values = new Vector<String>();
		size = 0;
	}

	// hy:
	public Bijection(Bijection b) {
		size = 0;
		keys = new HashMap<String, Integer>();
		values = new Vector<String>();
		for (int i = 0; i < b.getSize(); i++) {
			this.put(b.get(i));
		}
	}

	public Bijection(String[] keys) {
		this();
		for (String k : keys)
			this.put(k);

	}

	public Integer get(String key) {
		return keys.get(key);
	}

	public String get(Integer value) {
		return values.get(value);
	}

	public Integer put(String key) {
		Integer value = keys.get(key);
		if (value == null) {
			value = size++;
			keys.put(key, value);
			values.add(value, key);
		}
		return value;
	}

	static final Integer max(Collection<Integer> coll) {
		if (coll.isEmpty())
			return new Integer(-1);
		else
			return Collections.max(coll);
	}

}
 public class Functions {

	public static double logistic(double value){
		return (1.0 / (1.0 + Math.exp(-1.0 * value)));
	}
	
	public static Double logistic(ArrayList<Double> coefficients, ArrayList<Double> values){
		if (coefficients == null || values == null)
			return null;
		if (coefficients.size() != values.size())
			return null;
		double logit = 0;
		for (int i = 0; i < coefficients.size(); i++)
			logit += coefficients.get(i) * values.get(i);
		return (1.0 / (1.0 + Math.exp(-1.0 * logit)));//1.0 / (1.0 + Math.exp((-1.0) * logit)
	}
	
	public static Double euclidean_distance(ArrayList<Double> vector1, ArrayList<Double> vector2){
		if (vector1 == null || vector2 == null)
			return null;
		if (vector1.size() != vector2.size())
			return null;
		double distance = 0;
		for (int i = 0; i < vector1.size(); i++)
			distance += Math.pow((vector1.get(i) - vector2.get(i)), 2);
		return (Math.sqrt(distance));
	}

} public final class Stats
{
	
	public static class ValueIndexSummary{
		public ArrayList<Double> values;
		public ArrayList<Integer> indexes;
		
		public ValueIndexSummary(ArrayList<Double> values, ArrayList<Integer> indexes){
			this.values = values;
			this.indexes = indexes;
		}
		
		public String toString(){//NumberFormat nf) {
			String str = "";
			String delimiter = ",";
			if (values != null && indexes != null){
				for (int i = 0; i < values.size(); i++) {
					double value = values.get(i);
					int index = indexes.get(i);
					str += value +  "(" + index + ")" + delimiter;
				}
				str = str.substring(0, str.length() - delimiter.length());
			}
			return str;
		}
	}

	public static Double min (ArrayList<Double> a)
	{	
		if (a == null || a.size() == 0)
			return null;
	  Double min = null;
		if (a.size() > 0) 
	        for (int i = 0; i < a.size(); i++) {
	        	Double value = a.get(i);
	        	if (value == null)
	        		continue;
	        	if (i == 0 || min == null)
	        		min = value;
	        	else
	            min = (value < min)? value: min;
	        }
	    return min;
	}
	
	public static Double max (ArrayList<Double> a)
	{	
		if (a == null || a.size() == 0)
			return null;
	  Double max = null;
		if (a.size() > 0) 
	        for (int i = 0; i < a.size(); i++) {
	        	Double value = a.get(i);
	        	if (value == null)
	        		continue;
	        	if (i == 0 || max == null)
	        		max = value;
	        	else{
	            max = (value > max)? value: max;
	        	}
	        }
	    return max;
	}
	
	public static ValueIndexSummary max_with_index (ArrayList<Double> a)
	{	
		if (a == null || a.size() == 0)
			return null;
	   double max = 0.0;
	   int index = 0;
	   ArrayList<Double> maxes = new ArrayList<Double>();
	   ArrayList<Integer> indexes = new ArrayList<Integer>();
		if (a.size() > 0) 
	        for (int i = 0; i < a.size(); i++) {
	        	Double value = a.get(i);
	        	if (value == null)
	        		continue;
	        	if (i == 0){
	        		max = value;
	        		index = i;
        			maxes.add(max);
        			indexes.add(index);
	        	}
	        	else if (value > max){
	        			max = value;
	        			index = i;
	        			maxes = new ArrayList<Double>();
	        			indexes = new ArrayList<Integer>();
	        			maxes.add(max);
	        			indexes.add(index);
	        	}
	        	else if (value == max){
	        		maxes.add(value);
	        		indexes.add(i);
	        	}
	        }
		ValueIndexSummary maxObj = new ValueIndexSummary(maxes,indexes);
		return maxObj;
	}
	
	public static ValueIndexSummary min_with_index (ArrayList<Double> a)
	{	
		if (a == null || a.size() == 0)
			return null;
	   double min = 0.0;
	   int index = 0;
	   ArrayList<Double> mins = new ArrayList<Double>();
	   ArrayList<Integer> indexes = new ArrayList<Integer>();
		if (a.size() > 0) 
	        for (int i = 0; i < a.size(); i++) {
	        	Double value = a.get(i);
	        	if (value == null)
	        		continue;
	        	if (i == 0){
	        		min = value;
	        		index = i;
	        		mins.add(min);
        			indexes.add(index);
	        	}
	        	else if (value < min){
	        			min = value;
	        			index = i;
	        			mins = new ArrayList<Double>();
	        			indexes = new ArrayList<Integer>();
	        			mins.add(min);
	        			indexes.add(index);
	        	}
	        	else if (value == min){
	        		mins.add(value);
	        		indexes.add(i);
	        	}
	        }
		ValueIndexSummary minObj = new ValueIndexSummary(mins,indexes);
		return minObj;
	}
	
	public static Integer countLessThan (ArrayList<Double> a, int lessThanValue)
	{	
		if (a == null || a.size() == 0)
			return null;
	  Integer nb = null;
    for (int i = 0; i < a.size(); i++) {
    	Double value = a.get(i);
    	if (value == null)
    		continue;
    	if (value < lessThanValue){
    		if (nb == null)
    			nb = 0;
    		nb++;
    	}
    }
	   return nb;
	}
	
	public static Double sum (ArrayList<Double> a)
	{	
		if (a == null || a.size() < 1) 
			return null;
	  Double sum = null;
    for (Double i : a) 
    	 if (i != null){
    		 if (sum == null)
    			 sum = 0.0;
         sum += i;
    	 }
    return sum;
	}

	public static Double mean (ArrayList<Double> a)
	{
			if (a == null || a.size() < 1)
				return null;
	    Double sum = null;//sum(accuracy);
	    Double mean = null;
	    int nb = 0;
	    for (Double i : a) {
	    	 if (i != null && !Double.isNaN(i)){
	    		 if (sum == null)
	    			 sum = 0.0;
	         sum += i;
	         nb++;
	    	 }
	    }
	    if (sum != null && nb != 0)
	    	mean = sum / (nb * 1.0);
	    return mean;
	}

	/**
	* This formula is the corrected sample standard deviation, which is generally known simply as the "sample standard deviation", which is less biased than the uncorrected sample standard deviation (standard formula of variance taking square root)
	* sample sd: When only a sample of data from a population is available, the term standard deviation of the sample or sample standard deviation can refer to either the above-mentioned quantity as applied to those data or to a modified quantity that is a better estimate of the population standard deviation (the standard deviation of the entire population).
	* not se:The standard error of the mean (SEM) is the standard deviation of the sample-mean's estimate of a population mean. (It can also be viewed as the standard deviation of the error in the sample mean with respect to the true mean, since the sample mean is an unbiased estimator.) 
	*/
	public static Double sd (ArrayList<Double> a)
	{
		if (a == null || a.size() < 2)
			return null;
		Double sd = null;
    Double sum = null;
    Double mean = null;
    int nb = 0;
    for (Double i : a) {
   	 	if (i != null && !Double.isNaN(i)){
    		 if (sum == null)
    			 sum = 0.0;
         sum += i;
         nb++;
    	 }
    }
    if (sum != null && nb != 0)
    	mean = sum / (nb * 1.0);
    
    if (mean == null)
    	return null;
    
    sum = null;
    for (Double i : a)
   	 	if (i != null && !Double.isNaN(i)){
    		if (sum == null)
    			sum = 0.0;
        sum += Math.pow((i - mean), 2);
    	}
    if (sum != null && nb > 1)
    	sd = Math.sqrt( sum / ( nb - 1.0 ) ); // sample
    
	  return sd;
	}

}
 public class Utility {
	
	public static void swap(ArrayList<String> dataStrs, int i, int j) {
		String temp = dataStrs.get(i);
		dataStrs.set(i, dataStrs.get(j));
		dataStrs.set(j, temp);
	}
	

	public static void printArray(double[] array, String info) {
		System.out.println(info);
		String outStr = "";
		for (int i = 0; i < array.length; i++)
			outStr += array[i] + "\t";
		System.out.println(outStr);
	}


	public static double[] normalizedBySum(double[] values) {
		double[] normedValues = new double[values.length];
		double sum = 0.0;
		for (int k = 0; k < values.length; k++) {
			sum += values[k];
		}
		for (int k = 0; k < values.length; k++) {
			normedValues[k] = values[k] / sum;
		}
		return normedValues;
	}
	
	public static double[] uniformRandomArray(int dim, double lower,
			double upper, Random rand) {
		double range = upper - lower;
		double[] weights = new double[dim];
		for (int i = 0; i < dim; ++i) {
			double randVal = rand.nextDouble();
			weights[i] = lower + (range * randVal);
		}
		return weights;
	}
	
	public static double[] uniformRandomArraySumToOne(int dim, double lower,
			double upper, Random rand) {
		double range = upper - lower;
		double[] weights = new double[dim];
		double sum = 0.0;
		int i = 0;
		for (; i < dim-1; ++i) {
			double randVal = rand.nextDouble();
			weights[i] = lower + (range * randVal);
			sum += weights[i];
		}
		weights[i] = 1 - sum;
		return weights;
	}
	
	public static double[] intToDoubleArray(int[] labels) {
		double[] targets = new double[labels.length];
		for (int i = 0; i < labels.length; i++) {
			targets[i] = labels[i];
		}
		return targets;
	}
	
	public void printArray(double[] oneArray) {
		for (int i = 0; i < oneArray.length; i++)
			System.out.print(oneArray[i] + "\t");
		System.out.println();
	}
	
	public static String doubleArrayListToString(ArrayList<Double> oneList, DecimalFormat formatter, String delimiter){
		String str = "";
		for (double value : oneList)
			str += getValidString(value, formatter) + delimiter;
		return str;
	}
	
	public static void arrayListToArray(ArrayList<Double> aList, double[] a) {
		for (int i = 0; i < aList.size(); i++)
			a[i] = aList.get(i);
	}
	
	public static String[] linkedHashMapToStrings(LinkedHashMap<String, Double> aMap, String delimiter){
		String[] strs = {"", ""};
		for (Map.Entry<String, Double> entry : aMap.entrySet()) {
	    String key = entry.getKey();
	    Double value = entry.getValue();
	    strs[0] += key + delimiter;
	    strs[1] += value + delimiter;
		}
		return strs;
	}
	
	public static String getValidString(Double value, DecimalFormat formatter){
		String str = "";
		if (value == null || Double.isNaN(value))
			str = "NaN";
		else
			str = formatter.format(value) + "";
		return str;
	}


} public class DataPointList extends LinkedList<DataPoint> {// loading per HMM

	//private Options opts;h
	private static final long serialVersionUID = 4848754572225564741L;

	private final boolean parameterizing, parameterizedInit, parameterizedTran, parameterizedEmit;
	private final boolean forceUsingAllInputFeatures;
	//private final boolean differentBias;
	private final boolean bias;
	private final int nbHiddenStates;

	/* this is for all features including null for one hmm */
	private Bijection inputFeatures;
	/* this is for one hmm exclusing null, which should be the same as overall */
	private Bijection inputFeaturesNonNull;
	/* this is for one hmm all features excluding null; */
	private Bijection finalFeatures; // nonNull, can extend(add prefix) inputFeatures by init, tran and emit depending on configuration
	private Bijection finalInitFeatures;// nonNull for one HMM
	private Bijection finalTranFeatures;// nonNull for one HMM
	private Bijection finalEmitFeatures;// nonNull for one HMM
	private Bijection students, problems;
	private Bijection steps, outcomes, skills;
	private HashMap<Integer, Set<Integer>> cognitiveModel; // aProb/aStep to a set of skills
	/* Feature column index to feature name. Ordering is important.*/
	private TreeMap<Integer, String> featureColumnToName;
	
	private int student = -1, problemName = -1, stepName = -1, outcome = -1, fold = -1, skill = -1;
	private int feature = -1;
	private boolean generateStudentDummy = false, generateItemDummy = false;
	private static String delimiterStr = "\\s*[,\t]+\\s*";
	private boolean verbose;
	
	public DataPointList(ArrayList<String> instances, 
			boolean parameterizing, boolean parameterizedInit, boolean parameterizedTran, boolean parameterizedEmit, boolean forceUsingInputFeatures, 
			boolean bias, int nbHiddenStates){
		this(instances, parameterizing, parameterizedInit, parameterizedTran, parameterizedEmit,
				 forceUsingInputFeatures, bias, nbHiddenStates, null, null);
	}
	
	public DataPointList(ArrayList<String> instances, 
			boolean parameterizing, boolean parameterizedInit, boolean parameterizedTran, boolean parameterizedEmit, boolean forceUsingInputFeatures, 
			boolean bias, int nbHiddenStates, Bijection students, Bijection items){//, boolean generateStudentDummy, boolean generateItemDummy){
		this.parameterizing = parameterizing;
		this.parameterizedInit = parameterizedInit;
		this.parameterizedTran = parameterizedTran;
		this.parameterizedEmit = parameterizedEmit;
		this.forceUsingAllInputFeatures = forceUsingInputFeatures;
		//this.differentBias = differentBias;
		this.bias = bias;
		this.nbHiddenStates = nbHiddenStates;
//		this.generateStudentDummy = generateStudentDummy;
//		this.generateItemDummy = generateItemDummy;
		if (students != null && students.getSize() > 0)
			generateStudentDummy = true;
		else{
			generateStudentDummy = false;
			students = new Bijection();
		}
		if (items != null && items.getSize() > 0)
			generateItemDummy = true;
		else{
			generateItemDummy = false;
			items = new Bijection();
		}
		this.students = students; // Dont'save the index of students
		this.problems = items;
		
		// TODO: in the future, if trained by stu, may need to change
		cognitiveModel = new HashMap<Integer, Set<Integer>>();
		skills = new Bijection();
		steps = new Bijection();
		outcomes = new Bijection();
		outcomes.put("incorrect");
		outcomes.put("correct");
		// if (!inputProvideFeatureColumns && !nowInTrain) {
		// finalAllFeatures = trainFeatures;
		// }

		inputFeaturesNonNull = new Bijection();
		finalFeatures = new Bijection(); // nonNull, can extend(add prefix) inputFeatures by init, tran and emit depending on configuration
		finalInitFeatures = new Bijection();// nonNull for one HMM
		finalTranFeatures = new Bijection();// nonNull for one HMM
		finalEmitFeatures = new Bijection();
			
		inputFeatures = new Bijection();
		featureColumnToName = new TreeMap<Integer, String>();
		
		int lineNumber = 0;
		for (int ins = 0; ins < instances.size(); ins++) {
			// System.out.println("ins=" + ins);
			String columns[] = instances.get(ins).split(delimiterStr);
			if (lineNumber == 0) {
//				if (!(inputFeatures != null && inputFeatures.getSize() > 0 && featureColumnToName != null && featureColumnToName.size() > 0))
//					parseColumns(columns, inputFeatures, featureColumnToName);
					parseColumns(columns, inputFeatures, featureColumnToName);
					lineNumber++;
					continue;	
			}

			/* use integer to save memory. Need the Bijection to recover the original string */

			int aStudent = students.put(columns[student]);
			Integer aOutcome = outcomes.get(columns[outcome]);
			if (aOutcome == null){
				System.out.println("ERROR: the code only support two observed states: correct or incorrect.");
				System.exit(-1);
			}
			int aFold, aProb, aStep, aSkill = -1;

			if (fold == -1)
				aFold = 1;
			else
				aFold = Integer.parseInt(columns[fold]);
			if (problemName == -1)
				aProb = problems.put("NULL");
			else
				aProb = problems.put(columns[problemName]);
			if (stepName == -1)
				aStep = steps.put("NULL");
			else
				aStep = steps.put(columns[stepName]);
			if (skill == -1) {
				if (stepName == -1)
					cognitiveModel.put(aProb, new HashSet<Integer>());
				else
					cognitiveModel.put(aStep, new HashSet<Integer>());
				aSkill = skills.put("");
			}
			else {
				HashSet<Integer> s = new HashSet<Integer>();
				//if (oneKcInKcColumn) {
				aSkill = skills.put(columns[skill]);
				s.add(aSkill);
				//}
//				else { // TODO: now only treat the kcs in kc_column as one kc; Now its
//								// not correct to put aSkill = skills.put... directly
//					for (String a : columns[skill].split("-")) {
//						aSkill = skills.put(a.trim());
//						s.add(aSkill);
//					}
				//}
				if (stepName == -1)
					cognitiveModel.put(aProb, s);
				else
					cognitiveModel.put(aStep, s);
			}
			//System.out.println(aSkill);

			// if (oneBiasFeature) {
			// double[][][] finalFeatureValues = new double[nbHiddenStates][3][];
			// finalFeatureValues[0] = 1.0;
			// finalAllFeatures.put("bias");
			// this.add(new DataPoint(aStudent, aProb, aStep, aFold, aOutcome,
			// finalFeatureValues));
			// }
			// else
			if (!parameterizing)
				this.add(new DataPoint(aStudent, aSkill, aProb, aStep, aFold, aOutcome));
			else {
				if ((featureColumnToName == null || featureColumnToName.size() <= 0) && !generateStudentDummy && !generateItemDummy)
					throw new RuntimeException("ERROR: I can't read any features. Please provide correct feature column names!");
				
				/* inputFeaturesNonNull: nonNull; no bias features; it can be "features_", "init_features_", "tran_features", "emit_features_" */
				ArrayList<Double> inputFeatureValuesNonNull = new ArrayList<Double>();
				// int nbFeatures = 0;
				if (featureColumnToName != null){
					for (Map.Entry<Integer, String> iter : featureColumnToName.entrySet()) {
						// System.out.println("nbFeatures=" + nbFeatures++);
						int column = iter.getKey();
						String featureValue = columns[column];
						if (!featureValue.matches("(?i)null") && !featureValue.matches("(?i)nan") && !featureValue.equals("")) {//columns[i].matches("(?i).*feature.*")
							String featureName = iter.getValue();
							// double value = 0.0;
							// if (!(featureName.contains("stufield")
							// ||featureName.contains("quefield") ))
							// value = Double.parseDouble(featureValue);
							inputFeatureValuesNonNull.add(Double.parseDouble(featureValue));
							inputFeaturesNonNull.put(featureName);
						}
						//else
						//	outputFeatureCoefToEval = false;
					}
				}
				if (generateStudentDummy){
					for (int stuId = 0; stuId < students.getSize(); stuId++){
						Double featureValue = 0.0;
						String studentStr = students.get(stuId);
						if (studentStr.equals(columns[student]))
							featureValue = 1.0;
						inputFeatureValuesNonNull.add(featureValue);
						//TODO: differentiate init, tran, emit
						inputFeaturesNonNull.put("*feature_student_" + studentStr);
					}
				}
				if (generateItemDummy){
					for (int itemId = 0; itemId < problems.getSize(); itemId++){
						Double featureValue = 0.0;
						String itemStr = problems.get(itemId);
						if (itemStr.equals(columns[problemName]))
							featureValue = 1.0;
						inputFeatureValuesNonNull.add(featureValue);
						//TODO: differentiate init, tran, emit
						inputFeaturesNonNull.put("*feature_item_" + itemStr);
					}
				}
				
				/* Current line #non-null features should = all the lines #non-null features */
				if (inputFeaturesNonNull.getSize() != inputFeatureValuesNonNull.size())
					throw new RuntimeException("ERROR: inputFeaturesNonNull.getSize() != inputFeatureValuesNonNull.size(). Please make sure that the datapoints for the same HMM(KC) have the same NULL(NAN/empty) columns");
			
				// 1st: hiddenStates; 2nd: 0-init,1-tran,2-emit; 3rd:featureValues
				// corresponding to current hiddenState;
				double[][][] finalFeatureValues = new double[nbHiddenStates][3][];
				finalFeatureValues = getExpandedFeatureVector(inputFeatureValuesNonNull, inputFeaturesNonNull, finalFeatures, finalInitFeatures, finalTranFeatures, finalEmitFeatures);
				this.add(new DataPoint(aStudent, aSkill, aProb, aStep, aFold, aOutcome, finalFeatureValues));
			}
		}
	}

	/**
	 * inputFeatureValuesNonNull(originalFeatureVector): nonNull; no bias features; it can be "(*)features_", "(*)features_init_", "(*)features_trans_", "(*)features_emit_"
	 * Return: 
	 *     finalXXXFeatures: final feature names after expansion and adding bias
	 *     newFeatureVector: 1st: hiddenStates; 2nd: 0-init,1-tran,2-emit; 3rd:featureValues corresponding to current hiddenState;
	 */
	public double[][][] getExpandedFeatureVector(ArrayList<Double> inputFeatureValuesNonNull, Bijection inputFeaturesNonNull, 
						Bijection finalAllFeatures, Bijection finalInitFeatures, Bijection finalTranFeatures, Bijection finalEmitFeatures) {

		// if (!oneLogisticRegression || oneBiasFeature) {
		// System.out
		// .println("ERROR: !oneLogisticRegression || oneBiasFeature");
		// System.exit(1);
		// }

		double[][][] newFeatureVector = new double[nbHiddenStates][3][];// hiddenStates;

		for (int hiddenStateIndex = 0; hiddenStateIndex < nbHiddenStates; hiddenStateIndex++) {
			ArrayList<Double> initFeatureVector = new ArrayList<Double>();
			ArrayList<Double> tranFeatureVector = new ArrayList<Double>();
			ArrayList<Double> emitFeatureVector = new ArrayList<Double>();
			for (int i = 0; i < inputFeatureValuesNonNull.size(); i++) {
				String featureName = inputFeaturesNonNull.get(i);
				double featureValue = inputFeatureValuesNonNull.get(i);
				/* For init features, it doesn't differentiate hidden states. */
				if ((featureName.startsWith("init_") || forceUsingAllInputFeatures) && parameterizedInit)//both hidden state has the same feature vector (same length same values)
					getFeatures(featureName, featureValue, initFeatureVector, finalInitFeatures, finalAllFeatures);
				if ((featureName.startsWith("tran_") || forceUsingAllInputFeatures) && parameterizedTran){
					getFeatures(hiddenStateIndex, featureName, featureValue, tranFeatureVector, finalTranFeatures, finalAllFeatures);
				}
				if ((featureName.startsWith("emit_") || forceUsingAllInputFeatures) && parameterizedEmit)//(!featureName.startsWith("init_") && !featureName.startsWith("tran_")
					getFeatures(hiddenStateIndex, featureName, featureValue, emitFeatureVector, finalEmitFeatures, finalAllFeatures);
			}
			if ((parameterizedInit && initFeatureVector.size() == 0) || (parameterizedTran && tranFeatureVector.size() == 0) || (parameterizedEmit && emitFeatureVector.size() == 0)) {
				if (parameterizedInit && initFeatureVector.size() == 0)
					System.out.println("ERROR: (parameterizedInit && initFeatures.size() == 0)");
				if (parameterizedTran && tranFeatureVector.size() == 0)
					System.out.println("ERROR: (parameterizedTran && tranFeatures.size() == 0)");
				if (parameterizedEmit && emitFeatureVector.size() == 0)
					System.out.println("ERROR: (parameterizedEmit && emitFeatures.size() == 0)");
				System.out.println("\tPlease recheck input format or configurtion! (e.g. If parameterizing initial, transition or emission probabilities, you should have feature column names with prefix 'init_', 'tran_' or 'emit_' such as 'tran_feature_XXX'; if the feature columns are named as 'feature_XXX', then please specify forceUsingInputFeature=true\n" 
													+ "\t#initFeatures=" + initFeatureVector.size() + ", #tranFeatures=" + tranFeatureVector.size() + ", #emitFeatures=" + emitFeatureVector.size());
				System.exit(1);
			}

			if (initFeatureVector.size() > 0) {
				if (bias)
					addBiasFeature(initFeatureVector, finalInitFeatures, finalAllFeatures);
				newFeatureVector[hiddenStateIndex][0] = new double[initFeatureVector.size()];//0: type="init"
				Utility.arrayListToArray(initFeatureVector, newFeatureVector[hiddenStateIndex][0]);
			}
			if (tranFeatureVector.size() > 0) {
				if (bias)
					addBiasFeature(hiddenStateIndex, tranFeatureVector, finalTranFeatures, finalAllFeatures);
				newFeatureVector[hiddenStateIndex][1] = new double[tranFeatureVector.size()];//1: type="tran"
				Utility.arrayListToArray(tranFeatureVector, newFeatureVector[hiddenStateIndex][1]);
			}
			if (emitFeatureVector.size() > 0) {
				if (bias)
					addBiasFeature(hiddenStateIndex, emitFeatureVector, finalEmitFeatures, finalAllFeatures);
				newFeatureVector[hiddenStateIndex][2] = new double[emitFeatureVector.size()];//2: type="emit"
				Utility.arrayListToArray(emitFeatureVector, newFeatureVector[hiddenStateIndex][2]);
			}
			
			if (parameterizedInit && (initFeatureVector.size() != finalInitFeatures.getSize())){
				System.out.println("ERROR: (parameterizedInit && initFeatureVectorSize(" +initFeatureVector.size() + ") != finalInitFeaturesSize(" + finalInitFeatures.getSize() + ").");
				System.exit(1);
			}
			if (parameterizedTran && (tranFeatureVector.size() != finalTranFeatures.getSize())){
				System.out.println("ERROR: (parameterizedTran && initFeatureVectorSize(" +tranFeatureVector.size() + ") != finalInitFeaturesSize(" + finalTranFeatures.getSize() + ").");
				System.exit(1);
			}
			if (parameterizedEmit && (emitFeatureVector.size() != finalEmitFeatures.getSize())){
				System.out.println("ERROR: (parameterizedEmit && emitFeatureVector(" +emitFeatureVector.size() + ") != finalInitFeaturesSize(" + finalEmitFeatures.getSize() + ").");
				System.exit(1);
			}
		}

		return newFeatureVector;
	}


	public void getFeatures(int hiddenStateIndex, String featureName, double featureValue, ArrayList<Double> featureVector, 
											Bijection aTypeFeatures, Bijection allFeatures) {

		if (featureName.contains("*feature")) {
//			System.out.println("WARNING: * in feature name is not completely ready yet!");
//			System.exit(-1);
			featureVector.add(featureValue);
			aTypeFeatures.put(featureName);
			allFeatures.put(featureName);
		}
		else {
			if (hiddenStateIndex == 0) {
				featureVector.add(featureValue);
				featureVector.add(0.0);
			}
			else {
				featureVector.add(0.0);
				featureVector.add(featureValue);
			}
			aTypeFeatures.put(featureName);
			aTypeFeatures.put(featureName + "_hidden1");
			allFeatures.put(featureName);
			allFeatures.put(featureName + "_hidden1");
		}
	}

	public void addBiasFeature(int hiddenStateIndex, ArrayList<Double> featureVector, Bijection aTypeFeatures, Bijection allFeatures) {

//		if (!differentBias) {
//			featureVector.add(1.0);
//			aTypeFeatures.put("bias");
//			allFeatures.put("bias");
//		}
//		else {
		if (hiddenStateIndex == 0) {
			featureVector.add(1.0);
			featureVector.add(0.0);
		}
		else {
			featureVector.add(0.0);
			featureVector.add(1.0);
		}
		aTypeFeatures.put("bias");
		aTypeFeatures.put("bias_hidden1");
		allFeatures.put("bias");
		allFeatures.put("bias_hidden1");
		//}
	}

	public void addBiasFeature(ArrayList<Double> featureVector, Bijection aTypeFeatures, Bijection allFeatures) {
		featureVector.add(1.0);
		aTypeFeatures.put("bias");
		allFeatures.put("bias");
	}

	// here is for getting features without differentiating hiddenstates (for
	// init)
	public void getFeatures(String featureName, double featureValue, ArrayList<Double> featureVector, 
													Bijection aTypeFeatures, Bijection allFeatures) {
		featureVector.add(featureValue);
		aTypeFeatures.put(featureName);
		allFeatures.put(featureName);
	}
	
	public static int[] parseColumns(String[] columns){
		//String columns[] = header.split("\\s*[,\t]+\\s*");
		int[] indexes = {-1, -1};
		for (int i = 0; i < columns.length; i++) {
			if (indexes[0] != -1 && indexes[1] != -1)
				break;
			if (columns[i].matches("(?i)student.*"))// (?i): ignore case sensitive
				indexes[0] = i;
			else if (columns[i].matches("(?i)problem.*"))
				indexes[1] = i;
		}
		return indexes;
	}

	public void parseColumns(String[] columns, Bijection inputFeatures, TreeMap<Integer, String> featureColumnToName) {
		//inputFeatures = new Bijection();
		String ignoredColumns = "";
		ArrayList<Integer> ignoredColumnList = new ArrayList<Integer>();
		//featureColumnToName = null;

		// Case-insensitive matching can also be enabled via the embedded flag
		// expression (?i).
		for (int i = 0; i < columns.length; i++) {
			if (columns[i].matches("(?i)student.*"))// (?i): ignore case sensitive
				student = i;
			else if (columns[i].matches("(?i)problem.*"))
				problemName = i;
			else if (columns[i].matches("(?i)step.*"))
				stepName = i;
			else if (columns[i].matches("(?i)outcome.*"))
				outcome = i;
			else if (columns[i].matches("(?i)fold.*"))
				fold = i;
			else if (columns[i].matches("(?i)kc.*") || columns[i].matches("(?i)skill.*"))// (?i): ignore case sensitive
				skill = i;
			else if (columns[i].matches("(?i).*feature.*") && parameterizing) {
				// && !oneBiasFeature) {
				// [TODO]currently, use specific name "feature_" for geting all
				// features, didn't differentiate features for emit or transition
				feature = i;// starting index
				String featureName = columns[i];
				if (featureColumnToName == null)
					throw new RuntimeException("ERROR: featureColumnToName==null!");
					//featureColumnToName = new TreeMap<Integer, String>();
				featureColumnToName.put(i, featureName);
				// String featureName = columns[i].replace("features_", "");
				// featureName = columns[i].replace("feature_", "");
				if (inputFeatures == null)
					throw new RuntimeException("ERROR: inputFeatures==null!");
					//inputFeatures = new Bijection();
				inputFeatures.put(featureName);
			}
			else {
				ignoredColumns += " " + columns[i];
				ignoredColumnList.add(i);
			}

			// if (parameterizedEmit && (bias > 0 || oneBiasFeature)) {
			// String featureName = "bias";
			// if (features == null)
			// features = new Bijection();
			// features.put(featureName);
			// }
		}

		if (ignoredColumns != "" && verbose) {
			// logger.info("Ignored Column(s):\t" + ignoredColumns);
			System.out.println("Ignored Column(s):\t" + ignoredColumns);
			// logger.info("#Ignored Column(s):\t" + ignoredColumnList.size());
			System.out.println("#Ignored Column(s):\t" + ignoredColumnList.size());
		}

		if (student == -1 || outcome == -1 || skill == -1 || 
				(parameterizing && !generateStudentDummy && !generateItemDummy && (feature == -1 || featureColumnToName == null))) {
			String str = "ERROR: Missing column in input data. Cannot find column ";
			if (student == -1)
				str += "'student' ";
			if (outcome == -1)
				str += "'outcome' ";
			if (skill == -1)
				str += "'KC' ";
			if (parameterizing && !generateStudentDummy && !generateItemDummy && (feature == -1 || featureColumnToName == null))
				str += "for features ";
			str += "!";
			//System.out.println(str);
			throw new RuntimeException(str);
		}
		//return inputFeatures;
	}

	public Bijection getStudents() {
		return students;
	}

	public Bijection getProblems() {
		return problems;
	}

	public Bijection getSteps() {
		return steps;
	}

	public Bijection getOutcomes() {
		return outcomes;
	}

//	public String getFilename() {
//		return filename;
//	}

	public Bijection getSkills() {
		return skills;
	}

	public Bijection getAllFeatures() {
		return finalFeatures;
	}

	public Bijection getInitFeatures() {
		return finalInitFeatures;
	}

	public Bijection getTranFeatures() {
		return finalTranFeatures;
	}

	public Bijection getEmitFeatures() {
		return finalEmitFeatures;
	}

	public HashMap<Integer, Set<Integer>> getCognitiveModel() {
		return cognitiveModel;
	}
	
	public static void readStudentsOrItems(ArrayList<String> instances, Bijection students, Bijection items){
		boolean generateStudentDummy = false, generateItemDummy = false;
		if (students != null)
			generateStudentDummy = true;
		if (items != null)
			generateItemDummy = true;
		int lineNumber = 0;
		int[] indexes = {-1, -1}; //student, item
			for (int ins = 0; ins < instances.size(); ins++) {
				String columns[] = instances.get(ins).split(delimiterStr);
				if (lineNumber == 0) {
					indexes = parseColumns(columns);
					lineNumber++;
					continue;
				}
				int studentColumn = indexes[0];
				int itemColumn = indexes[1];
				if (generateStudentDummy){
					if (studentColumn == -1)
						throw new RuntimeException("ERROR: please provide \"student\" column so that I could generate student dummies for you.");
					else
						students.put(columns[studentColumn]);
				}
				if (generateItemDummy){
					if (itemColumn == -1)
						throw new RuntimeException("ERROR: please provide \"problem\" column so that I could generate item dummies for you.");
					else
						items.put(columns[itemColumn]);
				}
			}
	}
} public class StudentList extends LinkedList<CVStudent> {

	private static final long serialVersionUID = 730176924591642451L;

	/* students vs. finalStudents: Datapoints' student interger always matches to students, so you should always look up students to get original student name; However, finalStudents keep track of the actual number of final students if you configured to remove sequencelength=1 students; */
	private final Bijection students, problems, steps, outcomes, skills;
	private Bijection finalStudents;
	private Bijection allFeatures, initFeatures, tranFeatures, emitFeatures; // allFeatures includes init, tran, emit (all)
	private int nbDataPoints = 0;
	//private final HashMap<Integer, Set<Integer>> cognitiveModel;
	
	public StudentList(ArrayList<String> training, boolean parameterizing, boolean parameterizedInit, boolean parameterizedTran, boolean parameterizedEmit,
			boolean forceUsingInputFeatures, boolean bias, int nbHiddenStates, Bijection students, Bijection items) {//boolean differentBias, 
		this(new DataPointList(training, parameterizing, parameterizedInit, parameterizedTran, parameterizedEmit,
				 forceUsingInputFeatures, bias, nbHiddenStates, students, items));//differentBias, 
	}
	
	public StudentList(DataPointList data) {
		super();
		//this.filename = data.getFilename();
		this.finalStudents = new Bijection();
		this.students = data.getStudents();
		this.problems = data.getProblems();
		this.steps = data.getSteps();
		this.outcomes = data.getOutcomes();
		this.skills = data.getSkills();
		//this.cognitiveModel = data.getCognitiveModel();
		this.allFeatures = data.getAllFeatures();// get finalFeatures()
		this.initFeatures = data.getInitFeatures();
		this.tranFeatures = data.getTranFeatures();
		this.emitFeatures = data.getEmitFeatures();
		//this.numberOfObservations = data.size();
		Iterator<DataPoint> iDatum = data.iterator();
		int previousStudent = -1, previousProblem = -1, previousStep = -1;
		// String previousStudentStr = "";
		// double[][] previousFeatures_ = null;
		// double[] previousFeatures = null;
		int notPutStudent = -1;
		ArrayList<String> seqLength1Stu = new ArrayList<String>();
		// int nbLines = 1;
		nbDataPoints = 0;
		
		while (iDatum.hasNext()) {
			// System.out.println("nbLines=" + nbLines);
			DataPoint datum = iDatum.next();
			nbDataPoints++;
			int aStudent = datum.getStudent();
			int aProblem = datum.getProblem();
			int aStep = datum.getStep();

			// hy: "this" keep track of the LinkedList
			CVStudent student;
			if (previousStudent != aStudent) {
				// hy: remove the student's sequence which has just length 1
				if (previousStudent != -1) {
					CVStudent preStudent = this.getLast();
					if (preStudent.size() == 1) {
						String stu = students.get(previousStudent);// students.get(preStudent.get(0).getStudent());
//						if (opts.removeSeqLength1InTrain && opts.nowInTrain || (opts.removeSeqLength1InTest && !opts.nowInTrain)) {
//							System.out.println("Removing seq length=1 for student:" + stu);
//							// }
//							notPutStudent = previousStudent;
//							this.removeLast();
//						}
//						else 
						finalStudents.put(previousStudent + "");
						seqLength1Stu.add(stu);
					}
					else if (preStudent.size() == 2) {
						int fold = preStudent.get(0).getFold();
						if (fold == -1) {
							String stu = students.get(preStudent.get(0).getStudent());
//							if (opts.removeSeqLength1InTrain && opts.nowInTrain || (opts.removeSeqLength1InTest && !opts.nowInTrain)) {
//								// if (opts.verbose) {
//								// logger.error("Removing seq length=1 for student:" + stu);
//								//System.out.println("Removing seq length=1 for student:" + stu);
//								// }
//								notPutStudent = previousStudent;
//								this.removeLast();
//							}
//							else
							finalStudents.put(previousStudent + "");
							seqLength1Stu.add(stu);
						}
					}
					if (previousStudent != notPutStudent)
						finalStudents.put(previousStudent + "");
				}
				// finalStudents.put(previousStudent + "");
				student = new CVStudent(datum.getFold());
				this.add(student);
			}
			else {
				student = this.getLast();
			}
			student.add(datum);

			if (// reportError&&
			(previousStudent == aStudent) && (previousProblem == aProblem)
					&& (previousStep == aStep)) {
				System.out.println("WARNING: Repeated datum (same as the one before) for kc=" + data.getSkills().get(datum.getSkill()) 
						+ ", student= " + data.getStudents().get(aStudent) 
						+ " problem=" + data.getProblems().get(aProblem) 
						+ " step=" + data.getSteps().get(aStep) + ". Yet I still keep it in the dataset.");// + "or features are repeated!");
			}
			
			previousStudent = aStudent;
			previousProblem = aProblem;
			previousStep = aStep;
		}
		
		if (previousStudent != -1) {
			CVStudent preStudent = this.getLast();
			if (preStudent.size() == 1) {
				String stu = students.get(previousStudent);// students.get(preStudent.get(0).getStudent());
//				if (opts.removeSeqLength1InTrain && opts.nowInTrain || (opts.removeSeqLength1InTest && !opts.nowInTrain)) {
//					notPutStudent = previousStudent;
//					this.removeLast();
//				}
//				else
				finalStudents.put(previousStudent + "");
				seqLength1Stu.add(stu);
			}
			else if (preStudent.size() == 2) {
				int fold = preStudent.get(0).getFold();
				if (fold == -1) {
					String stu = students.get(preStudent.get(0).getStudent());
//					if (opts.removeSeqLength1InTrain && opts.nowInTrain
//							|| (opts.removeSeqLength1InTest && !opts.nowInTrain)) {
//						// if (opts.verbose) {
//						// logger.error("Removing seq length=1 for student:" + stu);
//						//System.out.println("Removing seq length=1 for student:" + stu);
//						// }
//						notPutStudent = previousStudent;
//						this.removeLast();
//					}
//					else 
					finalStudents.put(previousStudent + "");
					seqLength1Stu.add(stu);
				}
			}
			if (previousStudent != notPutStudent)
				finalStudents.put(previousStudent + "");
		}
		seqLength1Stu = new ArrayList<String>();
	}

	public Bijection getFinalStudents() {
		return finalStudents;// students;
	}

	// here is the id matches the datapoing student ids.
	public Bijection getOriStudents() {
		return students;// students;
	}

	public Bijection getProblems() {
		return problems;
	}

	public Bijection getSteps() {
		return steps;
	}

	public Bijection getOutcomes() {
		return outcomes;
	}

//	public String getFilename() {
//		return filename;
//	}

	public Bijection getExpertSkills() {
		return skills;
	}

//	public HashMap<Integer, Set<Integer>> getExpertCM() {
//		return cm;
//	}

	public void shuffleTime() {
		for (CVStudent student : this)
			Collections.shuffle(student);
	}

	public Bijection getAllFeatures() {
		return allFeatures;
	}

	public Bijection getInitFeatures() {
		return initFeatures;
	}

	public Bijection getTranFeatures() {
		return tranFeatures;
	}

	public Bijection getEmitFeatures() {
		return emitFeatures;
	}
	
	public int getNbDataPoints(){
		return nbDataPoints;
	}

//	public static StudentList loadData(String file) {// boolean
//		// parameterizedEmit,
//		// int obsClass1, String obsClass1Name) {
//		String problems = "problem.*";// "problem.name";
//		String steps = "";
//		if (inputHasStepColumn)
//			steps = "step.*";// "step.name"
//		Bijection bProblems, bSteps, bOutcome, bTrainFeatures;
//		bProblems = new Bijection();
//		bSteps = new Bijection();
//		bOutcome = new Bijection();
//		bTrainFeatures = new Bijection();
//
//		ArrayList<String> instances = new ArrayList<String>();
//		try {
//			BufferedReader br = new BufferedReader(new FileReader(file));
//			String line = "";
//			// logger.debug("Loading data file: " + file);
//			System.out.println("Loading data file: " + file);
//			while ((line = br.readLine()) != null) {
//				instances.add(line);
//			}
//			br.close();
//		}
//		catch (IOException e) {
//			// logger.error("Error reading file");
//			System.out.println("Error reading file");
//			e.printStackTrace();
//		}
//
//		// hy* return new StudentList(data, problems, steps, bProblems, bSteps,
//		// bOutcome);
//		return new StudentList(instances, problems, steps, bProblems, bSteps,
//				bOutcome, bTrainFeatures);// parameterizedEmit, obsClass1,
//																				// obsClass1Name);
//	}

	// received data from one hmm
//	public static StudentList loadData(ArrayList<String> data) {
//		String problems = "problem.*";// "problem.name";
//		String steps = "";
//		if (inputHasStepColumn)
//			steps = "step.*";// "step.name";
//
//		Bijection bProblems, bSteps, bOutcome, bTrainFeatures;
//		bProblems = new Bijection();
//		bSteps = new Bijection();
//		bOutcome = new Bijection();
//		bTrainFeatures = new Bijection();
//
//		return new StudentList(data, problems, steps, bProblems, bSteps, bOutcome,
//				bTrainFeatures);
//	}

//	public static StudentList loadData(ArrayList<String> data,
//			Bijection trainFeatures) {
//		String problems = "problem.*";// "problem.name";
//		String steps = "";
//		if (opts.inputHasStepColumn)
//			steps = "step.*";// "step.name";
//
//		Bijection bProblems, bSteps, bOutcome, bTrainFeatures;
//		bProblems = new Bijection();
//		bSteps = new Bijection();
//		bOutcome = new Bijection();
//		bTrainFeatures = trainFeatures;
//
//		// hy* return new StudentList(data, problems, steps, bProblems, bSteps,
//		// bOutcome);
//		return new StudentList(data, problems, steps, bProblems, bSteps, bOutcome,
//				bTrainFeatures);// parameterizedEmit, obsClass1, obsClass1Name);
//	}
	
} 	public class OneFileLogger{//all Hmms
		private int fileId;
		public Bijection trainAllHMMs = new Bijection();
		public HashSet<String> trainedHMMs = new HashSet<String>();
//		public double trainLL = 0.0;
//		public int trainNbLLError = 0;
//		public double trainMaxLLDecrease;
//		public double trainMaxLLDecreaseRatio;
//		public int trainNbStopByEMIteration = 0;
		//public HashSet<String> trainParameterizingFailed = new HashSet<String>();
		/* the index is corresponding to the original hmmId (even there are hmmIDs that are skipped, the code still add "fake" content) */
		private ArrayList<LinkedHashMap<String, Double>> trainedParameters = new ArrayList<LinkedHashMap<String, Double>>(); //per HMM
		//public Bijection outputParameterNamesForStats = new Bijection(); // in order to specify order of the features
		//public HashMap<String, ArrayList<Double>> featureToAllKCValues = new HashMap<String, ArrayList<Double>>();// feature to values (ordered by kc)
		//public HashMap<String, ArrayList<Double>> metricToAllKCMeanAcrossRestarts = new HashMap<String, ArrayList<Double>>();
		//public HashMap<String, ArrayList<Double>> metricToAllKCSdAcrossRestarts = new HashMap<String, ArrayList<Double>>();
		private ArrayList<Integer> testActualLabels = new ArrayList<Integer>();
		private ArrayList<Integer> testPredLabels = new ArrayList<Integer>();
		private ArrayList<Double> testPredProbs = new ArrayList<Double>();
		private ArrayList<Metrics> testEvals = new ArrayList<Metrics>(); //per HMM
		
		public OneFileLogger(int fileId){
			this.fileId = fileId;
		}
		
		public void add(int hmmId, OneRestartLogger curHmmLogger){
			trainedHMMs.add(curHmmLogger.hmmName);
//			trainLL += curHmmLogger.trainSummary.trainLL;
//			trainNbLLError += curHmmLogger.trainSummary.nbLLError;
//			trainMaxLLDecrease = (curHmmLogger.trainSummary.maxLLDecrease > trainMaxLLDecrease) ? curHmmLogger.trainSummary.maxLLDecrease : trainMaxLLDecrease;
//			trainMaxLLDecreaseRatio = (curHmmLogger.trainSummary.maxLLDecreaseRatio > trainMaxLLDecreaseRatio) ? curHmmLogger.trainSummary.maxLLDecreaseRatio: trainMaxLLDecreaseRatio;
//			
//			if (curHmmLogger.trainSummary.nbParameterizingFailed == 1)
//				trainParameterizingFailed.add(curHmmLogger.hmmName);
			
			if (trainedParameters.size() == hmmId)
				trainedParameters.add(curHmmLogger.trainSummary.parameters);
			else
				throw new RuntimeException("ERROR: trainedParameters.size() != hmmId");
			
			if (testEvals.size() == hmmId){
				testEvals.add(curHmmLogger.testSummary.eval);
				//testEvals.get(testEvals.size() - 1).
			}
			else
				throw new RuntimeException("ERROR: testEvals.size() == hmmId");	
			testActualLabels.addAll(curHmmLogger.testSummary.actualLabels);
			testPredLabels.addAll(curHmmLogger.testSummary.predLabels);
			testPredProbs.addAll(curHmmLogger.testSummary.predProbs);
			
		}
		
		public int getFileId(){
			return fileId;
		}
		
		
		public ArrayList<Metrics> getTestEval(){
			return testEvals;
		}
		
		public ArrayList<LinkedHashMap<String, Double>> getTrainedParameters(){
			return trainedParameters;
		}
		
		public ArrayList<Integer> getActualLabels(){
			return testActualLabels;
		}
	
		public ArrayList<Integer> getPredLabels(){
			return testPredLabels;
		}
	
		public ArrayList<Double> getPredProbs(){
			return testPredProbs;
		}
		
		
//		public void printTrainSummary(String delimiter) throws IOException{
//			String str =  ("trainLL" + delimiter + "nBstopByEMIteration" + delimiter + "nbLLError" + delimiter + "maxLLDecrease" + delimiter + "maxLLDecreaseRatio" 
//					+ delimiter +  "nbParameterizingFailed") + "\n";
//			str += (formatter.format(trainLL) + delimiter + formatter.format(trainNbStopByEMIteration) + delimiter + formatter.format(trainNbLLError)
//					+ delimiter + formatter.format(trainMaxLLDecrease) + delimiter + formatter.format(trainMaxLLDecreaseRatio)
//					+ delimiter + trainParameterizingFailed.size());
//			if (trainParameterizingFailed.size() > 0){
//				str += "\nParameterizingFailed: ";
//				for (String f : trainParameterizingFailed)
//					str += f + delimiter;
//			}
//			printAndLog(str + "\n");
//		}
	} 	public class OneRestartLogger{
		private String hmmName = "";
		private int restartId = 0;
		public TrainSummary trainSummary = new TrainSummary();
		public TestSummary testSummary = new TestSummary(); //Also contrain some train information
		
//		public ArrayList<Integer> testActualLabels = new ArrayList<Integer>();
//		public ArrayList<Integer> testPredLabels = new ArrayList<Integer>();
//		public ArrayList<Double> testPredProbs = new ArrayList<Double>();
//		//public Bijection outputParameterNames = new Bijection(); // in order to specify order of the features
//		public Mastery mastery = new Mastery();
//		public Degeneracy degeneracy = new Degeneracy();
		//public double trainTestTime;

		public OneRestartLogger(int restartId, String hmmName){
			this.restartId = restartId;
			this.hmmName = hmmName;
		}
		
		public String getHMMName(){
			return hmmName;
		}

		public void getTrainsetDegeneray(FeatureHMM hmm, StudentList curKcTrainStuList, boolean allowForget, boolean useEmissionToJudgeHiddenStates,
				String degeneracyJudgementInequality){
			testSummary.degeneracy.degeneracyJudgementInequality = degeneracyJudgementInequality;
			int knownState = FeatureHMM.getKnownState(hmm, curKcTrainStuList.get(0).get(0), useEmissionToJudgeHiddenStates, allowForget);
			int nbDp = 0;//, current_knownState = 0;
			double[] degenerateStatitics = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
			for (int i = 0; i < curKcTrainStuList.size(); i++) {//per student
			for (int j = 0; j < curKcTrainStuList.get(i).size(); j++) { //per datapoint
				DataPoint dp = curKcTrainStuList.get(i).get(j);
				double sumGuessSlip = FeatureHMM.checkDegeneracy(hmm, dp, knownState);
				if ((sumGuessSlip >= 1 && degeneracyJudgementInequality.equals("be")) || (sumGuessSlip > 1 && degeneracyJudgementInequality.equals("b")))
					degenerateStatitics[0] += 1;
				degenerateStatitics[4] += sumGuessSlip;
				if (sumGuessSlip < degenerateStatitics[8])
					degenerateStatitics[8] = sumGuessSlip;
				nbDp++;
			}
			degenerateStatitics[1] = nbDp;
			testSummary.degeneracy.degeneracyJudgementsAcrossDataPoints = degenerateStatitics;
			}	
		}
		
		//TODO: refine and combine
//		public void printTrainSummary() throws IOException{
//			String llErrorStr = "current KC:" + "\t#LLError:\t" + curRestart.trainSummary.nbLLError;
//			String maxllDecreasePerHmmStr = "current KC:" + "\tmaxLLDecreaseValuePerIter:\t" + formatter.format(curRestart.trainSummary.maxLLDecrease);
//			String maxllDecreaseRatioPerHmmStr = "current KC:" + "\tmaxLLDecreaseRatioValuePerIter:\t" + formatter.format(curRestart.trainSummary.maxLLDecreaseRatio);
//			printAndLog(llErrorStr);
//			printAndLog(maxllDecreasePerHmmStr);
//			printAndLog(maxllDecreaseRatioPerHmmStr);
//		}
		
	} public class FeatureHMM implements Serializable, Cloneable {
	private static final long serialVersionUID = 2L;

	private final boolean parameterizedInit,  parameterizedTran,  parameterizedEmit;
	private final boolean allowForget;
  private final int nbHiddenStates;
	private final ArrayList<PdfFeatureAwareLogisticRegression> initialPdfs;// = new ArrayList<PdfFeatureAwareLogisticRegression>();
	private final ArrayList<PdfFeatureAwareLogisticRegression> transitionPdfs;// = new ArrayList<PdfFeatureAwareLogisticRegression>();
	private final ArrayList<PdfFeatureAwareLogisticRegression> emissionPdfs;// = new ArrayList<PdfFeatureAwareLogisticRegression>();
//	private final PdfFeatureAwareLogisticRegression initialPdf;// = new ArrayList<PdfFeatureAwareLogisticRegression>();
//	private final PdfFeatureAwareLogisticRegression transitionPdf;// = new ArrayList<PdfFeatureAwareLogisticRegression>();
//	private final PdfFeatureAwareLogisticRegression emissionPdf;// = new ArrayList<PdfFeatureAwareLogisticRegression>();
	

	public FeatureHMM(ArrayList<PdfFeatureAwareLogisticRegression> initPdfs,
											ArrayList<PdfFeatureAwareLogisticRegression> transPdfs,
											ArrayList<PdfFeatureAwareLogisticRegression> emitPdfs//, 
											//boolean parameterizing, boolean parameterizedInit, boolean parameterizedTran, boolean parameterizedEmit, 
											//boolean allowForget,
											){
			//int restartId, String kcName, String modelName) {
		if (initPdfs.size() == 0 || initPdfs.size() != transPdfs.size() || emitPdfs.size() != transPdfs.size())
						throw new IllegalArgumentException("ERROR: Wrong initial parameters for HMM (constructor)!");
		this.initialPdfs = initPdfs;
		this.transitionPdfs = transPdfs;
		this.emissionPdfs = emitPdfs;
		this.parameterizedInit = initPdfs.get(0).getParameterizedInit();
		this.parameterizedTran = transPdfs.get(0).getParameterizedTran();
		this.parameterizedEmit = emitPdfs.get(0).getParameterizedEmit();
//		if (this.parameterizedInit || this.parameterizedTran || this.parameterizedEmit)
//			this.parameterizing = true;
//		else
//			this.parameterizing = false;
		this.allowForget = transPdfs.get(0).getAllowForget();
		this.nbHiddenStates = initPdfs.size();
//		this.restartId = restartId;
//		this.kcName = kcName;
//		this.modelName = modelName;
	}

	/* The same for i=0 or i=1, because the code expanded features for both hiddenStates to train one logistic regression. */
	public PdfFeatureAwareLogisticRegression getInitialPdf(int i) {
		return initialPdfs.get(i);
	}

	public PdfFeatureAwareLogisticRegression getTransitionPdf(int i) {
		return transitionPdfs.get(i);
	}

	public PdfFeatureAwareLogisticRegression getEmissionPdf(int i) {
		return emissionPdfs.get(i);
	}

	public double getInitiali(int i, double[] featureValues) {
		if (!parameterizedInit)
			return initialPdfs.get(i).probability(null, i, "init");
		else
			return initialPdfs.get(i).probability(featureValues, i, "init");

	}

	/** The probability associated to the transition going from <code>i</code> to / state <code>j</code>. */
	public double getTransitionij(int i, int j, double[] featureValues) {
		if (!parameterizedTran)
			return transitionPdfs.get(i).probability(null, j, "trans");
		else{
			if (!allowForget && i == 1){
				return (j == 0 ? 0.0:1.0);
			}
			else
				return transitionPdfs.get(i).probability(featureValues, j, "trans");
		}
	}

	public double getEmissionjk(int j, int k, double[] featureValues) {
		if (!parameterizedEmit)
			return emissionPdfs.get(j).probability(null, k, "emit");
		else{
			double prob = emissionPdfs.get(j).probability(featureValues, k, "emit");
			return prob;
		}
	}

	public void setInit(PdfFeatureAwareLogisticRegression pdf, int hiddenStateIndex) {
		initialPdfs.set(hiddenStateIndex, pdf);
	}

	public void setTransition(PdfFeatureAwareLogisticRegression pdf, int hiddenStateIndex) {
		transitionPdfs.set(hiddenStateIndex, pdf);
	}

	public void setEmission(PdfFeatureAwareLogisticRegression pdf, int hiddenStateIndex) {
		emissionPdfs.set(hiddenStateIndex, pdf);
	}

//	public String toString(NumberFormat nf) {
//		String s = "Hmm with " + nbHiddenStates + " state(s)\n";
//
//		for (int i = 0; i < nbHiddenStates; i++) {
//			s += "\nState " + i + "\n";
//			s += "  Initial:\n"
//					+ getHmmString(initialPdfs.get(i), (parameterizedInit ? true
//							: false)) + "\n";
//			s += "  Transition:\n"
//					+ getHmmString(transitionPdfs.get(i), (parameterizedTran ? true
//							: false)) + "\n";
//			s += "  Emission:\n"
//					+ getHmmString(emissionPdfs.get(i), (parameterizedEmit ? true
//							: false)) + "\n";
//		}
//		return s;
//	}

//	public String getHmmString(PdfFeatureAwareLogisticRegression pdf,
//			boolean parameterized) {
//		String s = "";
//		//TODO: differentiate between allowing forget vs not
//		if (parameterized) {
//			s += "\tparameterized:";
//			double[] w = pdf.featureWeights;
//			if (w.length == 1) {
//				s += "\tprobabilities:\t" + 1 / (1 + Math.exp(-w[0]));
//			}
//			s += "\tweights:";
//			for (int k = 0; k < w.length; k++)
//				s += "\t" + w[k];
//		}
//		else {
//			double[] p = pdf.probabilities;
//			if (p != null) {
//				for (int k = 0; k < p.length; k++)
//					s += "\t" + pdf.classMapping.get(k) + "\t" + p[k];
//			}
//			else
//				s += "\t" + pdf.probability;
//
//		}
//		return s;
//	}
//
//	public String toString() {
//		return toString(NumberFormat.getInstance());
//	}

	public FeatureHMM clone() throws CloneNotSupportedException {
		FeatureHMM Hmm = new FeatureHMM(initialPdfs, transitionPdfs, emissionPdfs);
		for (int i = 0; i < Hmm.nbHiddenStates; i++) {
			Hmm.initialPdfs.set(i, initialPdfs.get(i).clone()); //Can be shallow copy
			Hmm.transitionPdfs.set(i, transitionPdfs.get(i).clone());
			Hmm.emissionPdfs.set(i, emissionPdfs.get(i).clone());
		}
		return Hmm;
	}

	public int getNbHiddenStates() {
		// return pi.length;
		return nbHiddenStates;//initialPdfs.size();
	}
	
	public static int getKnownState(FeatureHMM hmm, DataPoint dp, boolean useEmissionToJudgeHiddenStates, boolean allowForget) {
		if (!allowForget)
			return 1;
		int knownState = -1;
		if (useEmissionToJudgeHiddenStates){//this requires guess+slip<1; if not, this way of judgement is questionable
			double hidden0obs0 = hmm.getEmissionjk(0, 0, dp.getFeatures(0, 2));
			double hidden1obs0 = hmm.getEmissionjk(1, 0, dp.getFeatures(1, 2));
			if (hidden0obs0 > hidden1obs0) {
				knownState = 1;//known
			}
			else {
				knownState = 0;
			}
		}
		else{//this requires guess+slip<1; if not, this way of judgement is questionable
			double a01 = hmm.getTransitionij(0, 1, dp.getFeatures(0, 1));
			double a10 = hmm.getTransitionij(1, 0, dp.getFeatures(1, 1));
			if (a01 < a10) {
				knownState = 0;// know
			}
			else {
				knownState= 1;// know
			}
		}
		return knownState;
	}
	
	public static double checkDegeneracy(FeatureHMM hmm, DataPoint dp, int knownState){
		double guess = hmm.getEmissionjk(1 - knownState, 1, dp.getFeatures(1 - knownState, 2));
		double slip =  hmm.getEmissionjk(knownState, 0, dp.getFeatures(knownState, 2));
		return (guess + slip);
	}
} public class Predictor {

	private final boolean useEmissionToJudgeHiddenStates;
	private final boolean allowForget;
	
	private Mastery masteryObj = null;
	private Degeneracy degeneracyObj = null;
	
	private double cutoff = 0.5; // >=cutoff -> class1
	private boolean verbose = false;
	
	//public HashMap<String, int[]> knowStateJudgements = null;
	private ArrayList<Integer> actualLabels = new ArrayList<Integer>();
	private ArrayList<Integer> predLabels  = new ArrayList<Integer>();
	private ArrayList<Double> predProbs  = new ArrayList<Double>();
	private ArrayList<Double> priorProbKnowns  = new ArrayList<Double>();
	private ArrayList<Double> posteriorProbKnowns  = new ArrayList<Double>();


	public Predictor(boolean useEmissionToJudgeHiddenStates, boolean allowForget){
		this(useEmissionToJudgeHiddenStates, allowForget,  null, null);
	}
	
	//TODO: simplify the parameters; consider knowStateJudgements
	public Predictor(boolean useEmissionToJudgeHiddenStates, boolean allowForget, Degeneracy degeneracyObj, Mastery masteryObj) {
		this.useEmissionToJudgeHiddenStates = useEmissionToJudgeHiddenStates;
		this.allowForget = allowForget;
		this.masteryObj = masteryObj;
		this.degeneracyObj = degeneracyObj;
	}

	
	public void test(FeatureHMM hmm, StudentList testSequences) throws IOException {
		actualLabels = new ArrayList<Integer>();
		predLabels  = new ArrayList<Integer>();
		predProbs  = new ArrayList<Double>();
		priorProbKnowns  = new ArrayList<Double>();
		posteriorProbKnowns  = new ArrayList<Double>();

		
		Bijection skills = testSequences.getExpertSkills();
//		Bijection stus = testSequences.getOriStudents();
//		Bijection problems = testSequences.getProblems();
//		Bijection steps = testSequences.getSteps();
		String firstDpKC = "";

		int unknownState = 0; // before judging
		int knownState = 1;// before judging
		boolean hiddenStateChecked = false;
		double priorProbState1_n = 0.0;
		double probClass1_n_1 = 0.0;
		int nbTrainInTest = 0;
		int nbTest = 0;
		int nbDecreasePKnonw = 0;
		int nbDecreasePCorrect = 0;
		
		for (int stuId = 0; stuId < testSequences.size(); stuId++) {// per student
			priorProbState1_n = hmm.getInitiali(knownState, testSequences.get(stuId).get(0).getFeatures(knownState, 0));
			for (int dpId = 0; dpId < testSequences.get(stuId).size(); dpId++) { // per datapoint
				DataPoint dp = testSequences.get(stuId).get(dpId);
				String kc = skills.get(dp.getSkill());
				if (firstDpKC.length() == 0)
					firstDpKC = kc;
				else if (!firstDpKC.equals(kc)) {
					System.out.println("ERROR: this datapoint should belong to " + firstDpKC);
					System.exit(-1);
				}
				//String stu = stus.get(dp.getStudent());
				int fold = dp.getFold();
				int outcome = dp.getOutcome();
				//String problem = problems.get(dp.getProblem());
				//String step = steps.get(dp.getStep());
				if (fold == -1)//need to consider in order to update till the test set datapoint
					nbTrainInTest++;
				else
					nbTest++;
				if (!hiddenStateChecked) {
					knownState = FeatureHMM.getKnownState(hmm, dp, useEmissionToJudgeHiddenStates, allowForget);
					unknownState = 1 - knownState;
					hiddenStateChecked = true;
				}
				
				if (hiddenStateChecked && dpId == 0)
					priorProbState1_n = hmm.getInitiali(knownState, testSequences.get(stuId).get(0).getFeatures(knownState, 0));// assuming it's P(K)
				
				// INFERENCE
				double probState1Class0 = Math.min(hmm.getEmissionjk(knownState, 0, dp.getFeatures(knownState, 2)), 1.0);
				double probState0Class1 = Math.min(hmm.getEmissionjk(unknownState, 1, dp.getFeatures(unknownState, 2)), 1.0);
				double probClass1_n = Math.min(priorProbState1_n * (1 - probState1Class0) + (1 - priorProbState1_n) * probState0Class1, 1.0);// should be P(C)

				int predLabel = (probClass1_n > cutoff) ? 1 : 0;

				// UPDATE
				/* This is the posterior knowledge probability. */
				double posteriorProbState1_n;
				if (outcome == 1)
					posteriorProbState1_n = Math.min(priorProbState1_n * (1 - probState1Class0) / (probClass1_n == 0.0 ? Double.MIN_VALUE : probClass1_n), 1.0);
				else
					posteriorProbState1_n = Math.min(priorProbState1_n * probState1Class0 / ((1 - probClass1_n) == 0.0 ? Double.MIN_VALUE : (1 - probClass1_n)), 1.0);
				double probState1State0 = Math.min(hmm.getTransitionij(knownState, unknownState, dp.getFeatures(knownState, 1)), 1.0);
				double probState0State1 = Math.min(hmm.getTransitionij(unknownState, knownState, dp.getFeatures(unknownState, 1)), 1.0);
				/* Correspond to original paper Formula (1). This is the prior for next datapoint */
				double priorProbState1_n1 = Math.min(posteriorProbState1_n * (1 - probState1State0) + (1 - posteriorProbState1_n) * probState0State1, 1.0); 

				if (verbose) 
					System.out.println("\toutcome:\t" + dp.getOutcome() + "\tP(Correct):\t" + probClass1_n + "\tPprior('K'):\t" + priorProbState1_n + "\tP('slip'):\t" + probState1Class0 + "\tP('guess'):\t" + probState0Class1 + "\tP('learn'):\t" + probState0State1 + "\tP('forget'):\t" + probState1State0 + "\tP(Correct):\t" + probClass1_n + "\tPpost('K'):\t" + posteriorProbState1_n + "\tPprior('K')_next:\t" + priorProbState1_n1);
				if (probClass1_n == 0.0 || probClass1_n == 1.0)
					System.out.println("WARNING: probClass1_n=" + probClass1_n);
				
				if (fold != -1){
					actualLabels.add(outcome);
					predLabels.add(predLabel);
					predProbs.add(probClass1_n);
					priorProbKnowns.add(priorProbState1_n);
					posteriorProbKnowns.add(posteriorProbState1_n);
//					
//					if (predWriter != null){
//						//before 20150616, it outputs priorProbState1_n and priorProbState1_n1
//						String s = outcome + "," + predLabel + "," + (probClass1_n) + "," + (priorProbState1_n) + "," + posteriorProbState1_n + "," + stu + "," + kc + "," + problem + "," + step;
//						predWriter.write(s + "\n");
//						predWriter.flush();
//					}
					if (degeneracyObj != null) {
						double[] degenerate_statitics = degeneracyObj.degeneracyJudgementsAcrossDataPoints;
						double sumGuessSlip = FeatureHMM.checkDegeneracy(hmm, dp, knownState);
						if ((sumGuessSlip >= 1 && degeneracyObj.degeneracyJudgementInequality.equals("be")) || (sumGuessSlip > 1 && degeneracyObj.degeneracyJudgementInequality.equals("b")))
							degenerate_statitics[2] += 1;
						degenerate_statitics[5] += sumGuessSlip;
						if (sumGuessSlip < degenerate_statitics[9])
							degenerate_statitics[9] = sumGuessSlip;
					}
					if (masteryObj != null){
						if (!masteryObj.studentsReachedMastery.contains(stuId + "")) {
							if (priorProbState1_n >= masteryObj.MASTERY_THRESHOLD) {
								masteryObj.studentsReachedMastery.put(stuId + "");
								masteryObj.nbPracToReachMastery.add(dpId * 1.0);
							}
							if (priorProbState1_n1 >= this.masteryObj.MASTERY_THRESHOLD) {
								masteryObj.studentsReachedMastery.put(stuId + "");
								masteryObj.nbPracToReachMastery.add(dpId + 1.0);
							}
						}
					}
					if (degeneracyObj != null) {
						if (priorProbState1_n1 < priorProbState1_n && dpId > 0)
							nbDecreasePKnonw++;
						if (probClass1_n < probClass1_n_1 && dpId > 0)
							nbDecreasePCorrect++;
					}
				}//fold = -1
				// for checking whether use first dp to judge is suitable
//			if (knowStateJudgements != null && allowForget && hiddenStateChecked ) {
//				int[] statitics = knowStateJudgements.get(kc);
//				statitics[3] = knownState;
//				int current_knownState = FeatureHMM.judgeHiddenStates(hmm, dp, this.useEmissionToJudgeHiddenStates);
//				if (knownState != current_knownState)
//					statitics[4] += 1;
//			}
				priorProbState1_n = priorProbState1_n1;
				probClass1_n_1 = probClass1_n;

			}// per student sequence
			if (verbose)
				System.out.println("");
		}
		if (verbose)
			System.out.println("nbTrainDataPointsInTest=" + nbTrainInTest);
//		if (predWriter != null)
//			predWriter.close();
		if (degeneracyObj != null) {
			degeneracyObj.degeneracyJudgementsAcrossDataPoints[3] = nbTest;
			degeneracyObj.degeneracyJudgementsAcrossDataPoints[6] = nbDecreasePKnonw / (1.0 * (nbTest - testSequences.size()));
			degeneracyObj.degeneracyJudgementsAcrossDataPoints[7] = nbDecreasePCorrect / (1.0 * (nbTest - testSequences.size()));
		}
		// for checking knownState judgement
		//		if (knowStateJudgements != null && allowForget) {
		//			int[] statitics = knowStateJudgements.get(aHmmKC);
		//			statitics[5] = nb_dp;
		//			knowStateJudgements.put(aHmmKC, statitics);
		//		}
	}
	
	public static void writeHMMPrediction(String predictionFile, StudentList testSequences, TestSummary testSummary,
																boolean append, boolean header) throws IOException{
		Bijection skills = testSequences.getExpertSkills();
		Bijection stus = testSequences.getOriStudents();
		Bijection problems = testSequences.getProblems();
		Bijection steps = testSequences.getSteps();
		int id = 0;
		
		BufferedWriter predWriter = null;
		if (append)
			 predWriter = new BufferedWriter(new FileWriter(predictionFile, true));
		else
			 predWriter = new BufferedWriter(new FileWriter(predictionFile));
		for (int stuId = 0; stuId < testSequences.size(); stuId++) {// per student
			for (int dpId = 0; dpId < testSequences.get(stuId).size(); dpId++) {
				
				DataPoint dp = testSequences.get(stuId).get(dpId);
				int fold = dp.getFold();
				if (fold == -1)
					continue;
				int outcome = dp.getOutcome();
				int actualLabel = testSummary.actualLabels.get(id);
				if (outcome != actualLabel){
					System.out.println("ERROR: outcome != actualLabel");
					System.exit(-1);
				}
				int predLabel = testSummary.predLabels.get(id);
				double predProb = testSummary.predProbs.get(id);
				double priorProbKnown = testSummary.priorProbKnowns.get(id);
				double posteriorProbKnown = testSummary.posteriorProbKnowns.get(id);
				String kc = skills.get(dp.getSkill());
				String problem = problems.get(dp.getProblem());
				String step = steps.get(dp.getStep());
				String stu = stus.get(dp.getStudent());
				
				//before 20150616, it outputs priorProbState1_n and priorProbState1_n1
				if (header){
					predWriter.write("actualLabel,predLabel,predProbCorrect,priorProbKnown,posteriorProbKnown,student,kc,problem,step\n");
					header = false;
				}
				String s = outcome + "," + predLabel + "," + predProb + "," + priorProbKnown + "," + posteriorProbKnown + "," + stu + "," + kc + "," + problem + "," + step;
				predWriter.write(s + "\n");
				predWriter.flush();
				
				id++;
			}
		}
		predWriter.close();
	}
	
	public ArrayList<Integer> getActualLabels(){
		return actualLabels;
	}
	
	public ArrayList<Integer> getPredLabels(){
		return predLabels;
	}
	
	public ArrayList<Double> getPredProbs(){
		return predProbs;
	}
	
	
	public ArrayList<Double> getPriorProbKnowns(){
		return priorProbKnowns;
	}
	
	public ArrayList<Double> getPosteriorProbKnowns(){
		return posteriorProbKnowns;
	}
	
//	public TestSummary getTestSummary(){
//		TestSummary testSummary = new TestSummary();
//		testSummary.update(actualLabels, predictLabels, predictProbs, priorProbKnowns, posteriorProbKnowns);
//		return testSummary;
//	}
} public class Learner {

	private final int nbHiddenStates, nbObsStates;
	private final boolean parameterizing, parameterizedInit,  parameterizedTran,  parameterizedEmit;
	private final boolean allowForget;//, specify_initial_values;
	private final double INIT_K0, INIT_L, INIT_G, INIT_S;
	private final double PROBABILITY_MIN_VALUE;
	private final double LBFGS_REGULARIZATION_WEIGHT, LBFGS_REGULARIZATION_BIAS;
	private final Random featureWeightsRand, nonFeatureParasRands;
	private final boolean useBaumWelchScaledLearner;
	private final double EM_TOLERANCE, EPS, ACCETABLE_LL_DECREASE;
	private final int EM_MAX_ITERS;
	private final double LBFGS_TOLERANCE;
	private final int LBFGS_MAX_ITERS;
	private final double initialWeightsBounds;

	private double[] regularizationWeightsForLBFGS, regularizationBiasesForLBFGS;
	private BaumWelchLearner learner; 
	
	//TODO: simplify the parameters
	public Learner(int nbHiddenState, int nbObsStates,
									boolean parameterizing, boolean parameterizedInit, boolean parameterizedTran, boolean parameterizedEmit,
									boolean allowForget, 
									double PROBABILITY_MIN_VALUE, double LBFGS_REGULARIZATION_WEIGHT, double LBFGS_REGULARIZATION_BIAS,
									double INIT_K0, double INIT_L, double INIT_G, double INIT_S,
									Random featureWeightsRand, double initialWeightsBounds, Random nonFeatureParasRands,
									boolean useBaumWelchScaledLearner,
									double EM_TOLERANCE, double EPS, double ACCETABLE_LL_DECREASE, int EM_MAX_ITER,
									double LBFGS_TOLERANCE, int LBFGS_MAX_ITERS) throws IOException {
				this.nbHiddenStates = nbHiddenState;
				this.nbObsStates = nbObsStates;
				this.parameterizing = parameterizing;
				this.parameterizedInit = parameterizedInit;
				this.parameterizedTran = parameterizedTran;
				this.parameterizedEmit = parameterizedEmit;
				this.allowForget = allowForget;
				//this.specify_initial_values = specify_initial_values;
				this.PROBABILITY_MIN_VALUE = PROBABILITY_MIN_VALUE;
				this.LBFGS_REGULARIZATION_WEIGHT = LBFGS_REGULARIZATION_WEIGHT;
				this.LBFGS_REGULARIZATION_BIAS = LBFGS_REGULARIZATION_BIAS;
				this.INIT_K0 = INIT_K0;
				this.INIT_L = INIT_L;
				this.INIT_G = INIT_G;
				this.INIT_S = INIT_S;
				this.featureWeightsRand = featureWeightsRand;
				this.initialWeightsBounds = initialWeightsBounds;
				this.nonFeatureParasRands = nonFeatureParasRands;
				this.useBaumWelchScaledLearner = useBaumWelchScaledLearner;
				this.EM_TOLERANCE = EM_TOLERANCE;
				this.EPS = EPS;
				this.ACCETABLE_LL_DECREASE = ACCETABLE_LL_DECREASE;
				this.EM_MAX_ITERS = EM_MAX_ITER;
				this.LBFGS_TOLERANCE = LBFGS_TOLERANCE;
				this.LBFGS_MAX_ITERS = LBFGS_MAX_ITERS;
	}

	
		public FeatureHMM train(StudentList curKcStuList) throws IOException{
//		opts.regularizationWeightsForLBFGS = initializer.regularizationWeightsForLBFGS;
//		opts.regularizationBiasesForLBFGS = initializer.regularizationBiasesForLBFGS;

			Bijection initFeatures = curKcStuList.getInitFeatures();
			Bijection tranFeatures = curKcStuList.getTranFeatures();
			Bijection emitFeatures = curKcStuList.getEmitFeatures();
			
			FeatureHMM hmm = getRandomFeatureHMM(initFeatures.getSize(), tranFeatures.getSize(), emitFeatures.getSize(), //Bijection outcomes, 
																					initialWeightsBounds, featureWeightsRand, nonFeatureParasRands);
			if (curKcStuList.size() == 0)
				return hmm;
			BaumWelchLearner learner; 
			
			if (useBaumWelchScaledLearner) 
				learner = new BaumWelchScaledLearner(parameterizing, parameterizedInit, parameterizedTran, parameterizedEmit, nbHiddenStates, EM_TOLERANCE, EPS, ACCETABLE_LL_DECREASE, EM_MAX_ITERS);
			else 
				learner = new BaumWelchLearner(parameterizing, parameterizedInit, parameterizedTran, parameterizedEmit, nbHiddenStates, EM_TOLERANCE, EPS, ACCETABLE_LL_DECREASE, EM_MAX_ITERS);
			//learner.setNbIterations(EM_MAX_ITERS);
			hmm = learner.learn(hmm, curKcStuList);
			this.learner = learner;
			return hmm;
	}

	public FeatureHMM getRandomFeatureHMM(int nbInitFeatures, int nbTranFeatures, int nbEmitFeatures, //Bijection outcomes, 
																double initialWeightsBounds, Random featureWeightsRand, Random nonFeatureParasRands) throws IOException {
	
//		Bijection hiddenStates = new Bijection();
//		if (nbHiddenStates == 2){
//			hiddenStates.put("hidden0");
//			hiddenStates.put("hidden1");
//		}
//		else{
//			for (int i = 0; i < nbHiddenStates; i++)
//				hiddenStates.put("hidden" + i);
//		}
//		int nbClasses = hiddenStates.getSize();
	
		ArrayList<PdfFeatureAwareLogisticRegression> initials = new ArrayList<PdfFeatureAwareLogisticRegression>();
		ArrayList<PdfFeatureAwareLogisticRegression> transitions = new ArrayList<PdfFeatureAwareLogisticRegression>();
		ArrayList<PdfFeatureAwareLogisticRegression> emissions = new ArrayList<PdfFeatureAwareLogisticRegression>();
		
		if (parameterizing && parameterizedInit)
			initializeParameterizedProb(initials, nbHiddenStates, nbHiddenStates, nbInitFeatures, initialWeightsBounds, featureWeightsRand);
		else
			initializeNonParameterizedProb(initials, nbHiddenStates, nbHiddenStates, "init", nonFeatureParasRands);
	
		if (parameterizing && parameterizedTran)
			initializeParameterizedProb(transitions, nbHiddenStates, nbHiddenStates, nbTranFeatures, initialWeightsBounds, featureWeightsRand);
		else
			initializeNonParameterizedProb(transitions, nbHiddenStates, nbHiddenStates, "trans", nonFeatureParasRands);
	
		if (parameterizing && parameterizedEmit)
			initializeParameterizedProb(emissions,  nbHiddenStates, nbObsStates, nbEmitFeatures, initialWeightsBounds, featureWeightsRand);
		else
			initializeNonParameterizedProb(emissions, nbHiddenStates, nbObsStates, "emit", nonFeatureParasRands);

		
		FeatureHMM hmm = new FeatureHMM(initials, transitions, emissions); // (opts.limit_fast_guess_slip?opts.FAST_GUESS_SLIP_LIMIT: null),
	
		return hmm;
	}

	/** classes can be outcomes or hiddenStates */
	public void initializeNonParameterizedProb( ArrayList<PdfFeatureAwareLogisticRegression> pdfs,
			int nb1stStates, int nbClasses,
			String type, Random nonFeatureParasRand) {//Bijection classes, 
		int nb2ndStates = nbClasses;
		if (nb2ndStates == 0)
			nb2ndStates = nb1stStates;
		double[][] params = new double[nb1stStates][nb2ndStates];
		for (int i = 0; i < nb1stStates; i++)
			params[i] = Utility.uniformRandomArraySumToOne(nb2ndStates, PROBABILITY_MIN_VALUE, 1, nonFeatureParasRand);
		
	//	if (specify_initial_values){
		if (allowForget){
//		params[1][0] = opts.INIT_FORGET;
//		params[0][0] = 1 - opts.INIT_FORGET;
			System.out.println("WARNING: the code doesn't support allowForget=true yet!");
			System.exit(-1);
		}
		if (type.contains("tran") && INIT_L >= 0) {
			params[0][1] = INIT_L;
			params[0][0] = 1 - INIT_L;
//			params[1][0] = 0.0;
//			params[1][1] = 1.0;
		}	
		else if (type.contains("emi")){
			if (INIT_G >= 0){
				params[0][1] = INIT_G;
				params[0][0] = 1 - INIT_G;
			}
			if (INIT_S >= 0){
				params[1][0] = INIT_S;
				params[1][1] = 1 - INIT_S;
			}
		}
	  else if (type.contains("init") && INIT_K0 >= 0) {//only the second dimension is used
			params[0][1] = INIT_K0;
			params[0][0] = 1 - INIT_K0;
		}
		
		if (type.contains("tran") && !allowForget){
			params[1][0] = 0.0;
			params[1][1] = 1.0;
		}
		
		for (int i = 0; i < nb1stStates; i++) {
			PdfFeatureAwareLogisticRegression pdf = new PdfFeatureAwareLogisticRegression(parameterizing, 
																											parameterizedInit, parameterizedTran, parameterizedEmit,
																											allowForget,
																											PROBABILITY_MIN_VALUE, nbHiddenStates, nbObsStates,
																											regularizationWeightsForLBFGS, regularizationBiasesForLBFGS,
																											LBFGS_TOLERANCE, LBFGS_MAX_ITERS);
			if (type.contains("tran") || type.contains("emi")){
				pdf.initialize(params[i]);//classes
			}
			else if (type.contains("init")) {
				pdf.initialize(params[0][i]);//ensure sum up to 1; nb1stStates=nb2ndStates
			}
			pdfs.add(pdf);
		}
	}

	/** 
	 * classes can be outcome or hiddenstates.
	 * TODO: even if we don't allow forget, here still initialize the feature weights. so we always need to use "allowForget" to judge when computing transition probabilities.
	 */
	public void initializeParameterizedProb(ArrayList<PdfFeatureAwareLogisticRegression> pdfs, 
																				int nbHiddenStates, int nbClasses, int nbFinalFeatures, //Bijection finalFeatures, Bijection classes,
													 							double initialWeightsBounds, Random featureWeightsRand) throws IOException {
		int featureDimension = -1;
		featureDimension = nbFinalFeatures;
		double[] initialFeatureWeights = new double[featureDimension];
		initialFeatureWeights = Utility.uniformRandomArray(featureDimension, initialWeightsBounds * (-1.0), initialWeightsBounds, featureWeightsRand);
	
		regularizationWeightsForLBFGS = new double[nbFinalFeatures];
		regularizationBiasesForLBFGS = new double[nbFinalFeatures];
		for (int f = 0; f < nbFinalFeatures; f++) {
			regularizationWeightsForLBFGS[f] = LBFGS_REGULARIZATION_WEIGHT;
			regularizationBiasesForLBFGS[f] = LBFGS_REGULARIZATION_BIAS;
		}
		
		// TODO: model forget..
		PdfFeatureAwareLogisticRegression pdf = new PdfFeatureAwareLogisticRegression(
																								parameterizing, parameterizedInit, parameterizedTran, parameterizedEmit,
																								allowForget,
																								PROBABILITY_MIN_VALUE, nbHiddenStates, nbObsStates,
																								regularizationWeightsForLBFGS, regularizationBiasesForLBFGS,
																								LBFGS_TOLERANCE, LBFGS_MAX_ITERS);
		pdf.initialize(initialFeatureWeights, nbClasses);//, finalFeatures);//classes, 
	
		// CAUTION: even put the same transition/emission object, later when initializing hmm, actually it turns into two objects
		// TODO: later when fitting the feature weights, both objects are assigned the same addresses' same weights; try to improve this design!
		for (int i = 0; i < nbHiddenStates; i++) 
			pdfs.add(pdf);
	}
	
//	public TrainSummary getTrainSummary(){
//	   return learner.getTrainSummary();
//	}
	public double getTrainLL(){
		return learner.getTrainLL();
	}
	
	public int getNbLLError(){
		return learner.getNbLLError();
	}
	
	public double getMaxLLDecrease(){
		return learner.getMaxLLDecrease();
	}
	
	public double getMaxLLDecreaseRatio(){
		return learner.getMaxLLDecreaseRatio();
	}
	
	public int getNbParameterizingFailed(){
		return learner.getNbParameterizingFailed();
	}
	
	public int getNbStopByEMIteration(){
		return learner.getNbStopByEMIteration();
	}

} public class Execution {
	@Option(gloss = "Whether to create a directory for this run; if not, don't generate output files")
	public static boolean create = false;
	@Option(gloss = "Whether to create a thread to monitor the status.")
	public static boolean monitor = false;

	// How to create the execution directory
	@Option(gloss = "Directory to put all output files; if blank, use execPoolDir.")
	public static String execDir;
	@Option(gloss = "Directory which contains all the executions (or symlinks).")
	public static String execPoolDir;
	@Option(gloss = "Directory which actually holds the executions.")
	public static String actualExecPoolDir;
	@Option(gloss = "Overwrite the contents of the execDir if it doesn't exist (e.g., when running a thunk).")
	public static boolean overwriteExecDir;
	@Option(gloss = "Assume in the run directory, automatically set execPoolDir and actualExecPoolDir")
	public static boolean useStandardExecPoolDirStrategy = false;

	@Option(gloss = "Simply print options and exit.")
	public static boolean printOptionsAndExit = false;
	@Option(gloss = "Miscellaneous options (written to options.map and output.map, displayed in servlet); example: a=3 b=4")
	public static ArrayList<String> miscOptions = new ArrayList();

	@Option(gloss = "Name of the view to add this execution to in the servlet")
	public static ArrayList<String> addToView = new ArrayList<String>();
	@Option(gloss = "Record file to write to")
	public static String recordPath;

	@Option(gloss = "Character encoding")
	public static String charEncoding;
	@Option(gloss = "Name of jar files to load prior to execution")
	public static ArrayList<String> jarFiles = new ArrayList<String>();

	@Option(gloss = "Skip initialization of jars")
	public static boolean dontInitializeJars = false;

	@Option(gloss = "Initialize from jars after copying them to a newly created execDir")
	public static boolean initializeJarsAfterDirCreation = false;

	// Thunk
	@Option(gloss = "Make a thunk (a delayed computation).")
	public static boolean makeThunk;
	@Option(gloss = "A note to the servlet to automatically run the thunk when it sees it")
	public static boolean thunkAutoQueue;
	@Option(gloss = "Priority of the thunk.")
	public static int thunkPriority;
	@Option(gloss = "Launch this class")
	public static String thunkMainClassName;
	@Option(gloss = "Java options to pass to Java when later running the thunk")
	public static String thunkJavaOpts;
	@Option(gloss = "Use Scala to run rather than Java")
	public static boolean thunkUseScala = false;
	@Option(gloss = "Use Scala to run rather than Java (in MB)")
	public static int thunkReqMemory = 1024;

	// Exception handling
	@Option(gloss = "Whether to catch exceptions (ignored when making a thunk)")
	public static boolean dontCatchExceptions;

	// Whether to print out start a main() track (LogInfo)
	public static boolean startMainTrack = true;

	// Execution directory that we write to (execDir is just a suggestion)
	// Could be a symlink to a directory in actualExecPoolDir
	private static String virtualExecDir;

	// Passed to the options parser
	public static boolean ignoreUnknownOpts = false;

	static OrderedStringMap inputMap = new OrderedStringMap(); // Accessed by
																															// monitor thread
	private static OrderedStringMap outputMap = new OrderedStringMap();
	private static OptionsParser parser;
	private static MonitorThread monitorThread; // Thread for monitoring
	static int exitCode = 0;

	static boolean shouldBail = false; // Set by monitor thread

	public static boolean shouldBail() {
		return shouldBail;
	}

	private static void mkdirHard(File f) {
		if (!f.mkdir()) {
			stderr.println("Cannot create directory: " + f);
			System.exit(1);
		}
	}

	public static String getVirtualExecDir() {
		return virtualExecDir;
	}

	/**
	 * Return an unused directory in the execution pool directory. Set
	 * virtualExecDir
	 */
	public static String createVirtualExecDir() {
		if (useStandardExecPoolDirStrategy) {
			// Assume we are in the run directory, so set the standard paths
			execPoolDir = new File(SysInfoUtils.getcwd(), "state/execs").toString();
			actualExecPoolDir = new File(SysInfoUtils.getcwd(), "state/hosts/"
					+ SysInfoUtils.getShortHostName()).toString();
			if (!new File(actualExecPoolDir).isDirectory())
				actualExecPoolDir = null;
		}
		if (!StrUtils.isEmpty(execPoolDir) && !new File(execPoolDir).isDirectory())
			throw Exceptions.bad("Execution pool directory '" + execPoolDir
					+ "' doesn't exist");
		if (!StrUtils.isEmpty(actualExecPoolDir)
				&& !new File(actualExecPoolDir).isDirectory())
			throw Exceptions.bad("Actual execution pool directory '"
					+ actualExecPoolDir + "' doesn't exist");

		if (!StrUtils.isEmpty(execDir)) { // Use specified execDir
			boolean exists = new File(execDir).isDirectory();
			if (exists && !overwriteExecDir)
				throw Exceptions
						.bad("Directory already exists and overwrite flag is false");
			if (!exists)
				mkdirHard(new File(execDir));
			else {
				// This part looks at actualExecPoolDir
				// This case is overwriting an existing execution directory, which
				// happens when we are executing a thunk. We have to be careful here
				// because the actual symlinked directory that was created when thunking
				// might be using a different actualPoolDir. If this happens, we need
				// to move the actual thunked symlinked directory into the actual
				// execution pool directory requested. In fact, we always do this for
				// simplicity.
				String oldActualExecDir = Utils.systemGetStringOutputEasy("readlink "
						+ execDir);
				if (oldActualExecDir == null) { // Not symlink
					if (!StrUtils.isEmpty(actualExecPoolDir))
						throw Exceptions
								.bad("The old execution directory was not created with actualExecPoolDir but now we want an actualExecPoolDir");
					// Do nothing, just use the directory as is
				}
				else { // Symlink
					oldActualExecDir = oldActualExecDir.trim();
					if (StrUtils.isEmpty(actualExecPoolDir))
						throw Exceptions
								.bad("The old execution directory was created with actualExecPoolDir but now we don't want an actualExecPoolDir");
					// Note that now the execution numbers might not correspond between
					// the
					// actual and virtual execution pool directories.
					File newActualExecDir = null;
					for (int i = 0;; i++) {
						newActualExecDir = new File(actualExecPoolDir, i + "a.exec");
						if (!newActualExecDir.exists())
							break;
					}
					// Move the old directory to the new directory
					Utils.systemHard(String.format("mv %s %s", oldActualExecDir,
							newActualExecDir));
					// Update the symlink (execDir -> newActualExecDir)
					Utils.systemHard(String.format("ln -sf %s %s",
							newActualExecDir.getAbsolutePath(), execDir));
				}
			}
			return virtualExecDir = execDir;
		}

		// execDir hasn't been specified, so we need to pick one from a pool
		// directory
		// execPoolDir must exist; actualExecPoolDir is optional

		// Get a list of files that already exists
		Set<String> files = new HashSet<String>();
		for (String f : new File(execPoolDir).list())
			files.add(f);

		// Go through and pick out a file that doesn't exist
		int numFailures = 0;
		for (int i = 0; numFailures < 3; i++) {
			// Either the virtual file (a link) or the actual file
			File f = new File(execPoolDir, i + ".exec");
			// Actual file
			File g = StrUtils.isEmpty(actualExecPoolDir) ? null : new File(
					actualExecPoolDir, i + ".exec");

			if (!files.contains(i + ".exec") && (g == null || !g.exists())) {
				if (g == null || g.equals(f)) {
					mkdirHard(f);
					return virtualExecDir = f.toString();
				}
				// Create symlink before mkdir to try to reserve the name and avoid race
				// conditions
				if (Utils.createSymLink(g.getAbsolutePath(), f.getAbsolutePath())) {
					mkdirHard(g);
					return virtualExecDir = f.toString();
				}

				// Probably because someone else already linked to it
				// in the race condition: so try again
				stderr.println("Cannot create symlink from " + f + " to " + g);
				numFailures++;
			}
		}
		throw Exceptions.bad("Failed many times to create execution directory");
	}

	// Get the path of the file (in the execution directory)
	public static String getFile(String file) {
		if (StrUtils.isEmpty(virtualExecDir))
			return null;
		if (StrUtils.isEmpty(file))
			return null;
		return new File(virtualExecDir, file).toString();
	}

	public static void linkFileToExec(String realFileName, String file) {
		if (StrUtils.isEmpty(realFileName) || StrUtils.isEmpty(file))
			return;
		File f = new File(realFileName);
		Utils.createSymLink(f.getAbsolutePath(), getFile(file));
	}

	public static void linkFileFromExec(String file, String realFileName) {
		if (StrUtils.isEmpty(realFileName) || StrUtils.isEmpty(file))
			return;
		File f = new File(realFileName);
		Utils.createSymLink(getFile(file), f.getAbsolutePath());
	}

	// Getting input and writing output
	public static boolean getBooleanInput(String s) {
		String t = inputMap.get(s, "0");
		return t.equals("true") || t.equals("1");
	}

	public static String getInput(String s) {
		return inputMap.get(s);
	}

	public synchronized static void putOutput(String s, Object t) {
		outputMap.put(s, StrUtils.toString(t));
	}

	public synchronized static void printOutputMapToStderr() {
		outputMap.print(stderr);
	}

	public synchronized static void printOutputMap(String path) {
		if (StrUtils.isEmpty(path))
			return;
		// First write to a temporary directory and then rename the file
		String tmpPath = path + ".tmp";
		if (outputMap.printEasy(tmpPath))
			new File(tmpPath).renameTo(new File(path));
	}

	public static void setExecStatus(String newStatus, boolean override) {
		String oldStatus = outputMap.get("exec.status");
		if (oldStatus == null || oldStatus.equals("running"))
			override = true;
		if (override)
			putOutput("exec.status", newStatus);
	}

	public static void putLogRec(String key, Object value) {
		logss("%s = %s", key, value);
		Record.add(key, value);
		putOutput(key, value);
	}

	static OrderedStringMap getInfo() {
		OrderedStringMap map = new OrderedStringMap();
		map.put("Date", SysInfoUtils.getCurrentDateStr());
		map.put("Host", SysInfoUtils.getHostName());
		map.put("CPU speed", SysInfoUtils.getCPUSpeedStr());
		map.put("Max memory", SysInfoUtils.getMaxMemoryStr());
		map.put("Num CPUs", SysInfoUtils.getNumCPUs());
		return map;
	}

	public static void init(String[] args, Object... objects) {
		// // Parse options
		// If one of the objects is an options parser, use that; otherwise, create a
		// new one
		for (int i = 0; i < objects.length; i++) {
			if (objects[i] instanceof OptionsParser) {
				parser = (OptionsParser) objects[i];
				objects[i] = null;
			}
		}
		if (parser == null)
			parser = new OptionsParser();
		parser.doRegister("log", LogInfo.class);
		parser.doRegister("exec", Execution.class);
		parser.doRegisterAll(objects);
		// These options are specific to the execution, so we don't want to
		// overwrite them
		// with a previous execution's.
		parser.setDefaultDirFileName("options.map");
		parser.setIgnoreOptsFromFileName("options.map", ListUtils.newList(
				"log.file", "exec.execDir", "exec.execPoolDir", "exec.actualPoolDir",
				"exec.makeThunk"));
		if (ignoreUnknownOpts)
			parser.ignoreUnknownOpts();
		if (!parser.doParse(args))
			System.exit(1);

		// Load classes
		if (!dontInitializeJars && !initializeJarsAfterDirCreation) {
			initializeJars(false);
		}
		// Set character encoding
		if (charEncoding != null)
			CharEncUtils.setCharEncoding(charEncoding);

		if (printOptionsAndExit) { // Just print options and exit
			parser.doGetOptionPairs().print(stdout);
			System.exit(0);
		}

		// Create a new directory
		if (create) {
			createVirtualExecDir();
			// stderr.println(virtualExecDir);
			if (!makeThunk)
				LogInfo.file = getFile("log");

			// Copy the Jar files for reference
			if (!makeThunk) {
				for (String jarFile : jarFiles)
					Utils.systemHard(String.format("cp %s %s", jarFile, virtualExecDir));
				if (initializeJarsAfterDirCreation) {
					initializeJars(true);
				}
			}
		}
		else {
			LogInfo.file = "";
		}

		// Handle miscOptions
		for (String opt : miscOptions) {
			String[] tokens = opt.split("=");
			if (tokens.length == 2)
				putOutput(tokens[0], tokens[1]);
		}

		if (!makeThunk) {
			LogInfo.init();
			if (startMainTrack)
				track("main()", true);
		}

		// Output options
		if (!makeThunk && virtualExecDir != null)
			logs("Execution directory: " + virtualExecDir);
		if (!makeThunk)
			getInfo().printEasy(getFile("info.map"));
		printOptions();
		if (create && addToView.size() > 0)
			IOUtils.printLinesHard(Execution.getFile("addToView"), addToView);

		// Start monitoring
		if (!makeThunk && monitor) {
			monitorThread = new MonitorThread();
			monitorThread.start();
		}

		if (!makeThunk)
			Record.init(!StrUtils.isEmpty(recordPath) ? recordPath : Execution
					.getFile("record"));
	}

	/**
 * 
 */
	private static void initializeJars(boolean inVirtualExecDir) {
		if (jarFiles.size() > 0) {
			List<String> names = new ArrayList();
			for (String jarFile : jarFiles)
				names.add(new File(jarFile).getName());
			stderr.println("Loading JAR files: " + StrUtils.join(names));
			for (String jarFile : jarFiles) {
				// Load classes
				String jarPath = inVirtualExecDir ? new File(virtualExecDir, new File(
						jarFile).getName()).getPath() : jarFile;
				ClassInitializer.initializeJar(jarPath);
			}
		}
	}

	// Might want to call this again after some command-line options were changed.
	public static void printOptions() {
		boolean saveMakeThunk = makeThunk;
		makeThunk = false;
		parser.doGetOptionPairs().printEasy(getFile("options.map"));
		parser.doGetOptionStrings().printEasy(getFile("options.help"));
		makeThunk = saveMakeThunk;
	}

	public static void raiseException(Throwable t) {
		error(t + ":\n" + StrUtils.join(t.getStackTrace(), "\n"));
		t = t.getCause();
		if (t != null)
			error("Caused by " + t + ":\n" + StrUtils.join(t.getStackTrace(), "\n"));
		putOutput("exec.status", "exception");
		exitCode = 1;
	}

	public static void finish() {
		if (!makeThunk) {
			Record.finish();

			if (monitor)
				monitorThread.finish();
			setExecStatus(shouldBail ? "bailed" : "done", false);
			outputMap.printEasy(getFile("output.map"));
			StopWatchSet.getStats().printEasy(getFile("time.map"));
			if (!makeThunk && virtualExecDir != null)
				logs("Execution directory: " + virtualExecDir);
			if (makeThunk && virtualExecDir != null)
				stderr.println(virtualExecDir);
			if (LogInfo.getNumErrors() > 0 || LogInfo.getNumWarnings() > 0)
				stderr.printf("%d errors, %d warnings\n", LogInfo.getNumErrors(),
						LogInfo.getNumWarnings());
			if (startMainTrack)
				end_track();
		}

		System.exit(exitCode);
	}

	// This should be all we need to put in a main function.
	// args are the commandline arguments
	// First object is the Runnable object to call run on.
	// All of them are objects whose options args is to supposed to populate.
	public static void run(String[] args, Object... objects) {
		runWithObjArray(args, objects);
	}

	public static void runWithObjArray(String[] args, Object[] objects) {
		init(args, objects);

		Object mainObj;
		if (objects[0] instanceof String)
			mainObj = objects[1];
		else
			mainObj = objects[0];

		if (makeThunk) {
			setExecStatus("thunk", true);
			printOutputMap(Execution.getFile("output.map"));
			List<String> cmd = new ArrayList();
			if (thunkUseScala) { // Run as Scala
				cmd.add("env");
				if (thunkJavaOpts != null)
					cmd.add("'JAVA_OPTS=" + thunkJavaOpts + "'");
				cmd.add("scala");
			}
			else { // Run of Java
				cmd.add("java");
				if (thunkJavaOpts != null)
					cmd.add(thunkJavaOpts);
			}
			// Set classpath to make sure we have the exact same environment when we
			// run the thunk
			cmd.add("-cp " + StrUtils.join(jarFiles, ":") + ":"
					+ System.getenv("CLASSPATH"));
			// java.class.path doesn't pick up $CLASSPATH for scala programs
			// cmd.add("-cp " + StrUtils.join(jarFiles,
			// ":")+":"+System.getProperty("java.class.path"));
			cmd.addAll(ListUtils.newList(thunkMainClassName == null ? mainObj
					.getClass().getName() : thunkMainClassName, "++" + virtualExecDir
					+ "/options.map", // Load these options
					// Next time when we run, just run in the same path that we used to
					// create the thunk
					"-execDir", virtualExecDir, "-overwriteExecDir"));
			IOUtils.printLinesHard(
					Execution.getFile("job.map"),
					ListUtils.newList("workingDir\t" + SysInfoUtils.getcwd(), // Run from
																																		// current
																																		// directory
							"command\t" + StrUtils.join(cmd, "\t"), "reqMemory\t"
									+ thunkReqMemory, "priority\t" + thunkPriority));
			System.out.println(virtualExecDir);
		}
		else {
			if (dontCatchExceptions) {
				((Runnable) mainObj).run();
			}
			else {
				try {
					((Runnable) mainObj).run();
				}
				catch (Throwable t) {
					raiseException(t);
				}
			}
		}
		finish();
	}
}
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import fast.common.Bijection;
import fast.common.Functions;
import fast.common.Stats;
import fast.common.Utility;
import fast.data.DataPointList;
import fast.data.StudentList;
import fast.evaluation.*;
import fast.experimenter.Logger.OneFileLogger;
import fast.experimenter.Logger.OneRestartLogger;
import fast.featurehmm.FeatureHMM;
import fast.featurehmm.Predictor;
import fast.featurehmm.Learner;
import fig.exec.Execution;
/**
 * FAST v1.0       08/12/2014
 * 
 * This code is only for research purpose not commercial purpose.
 * It is originally developed for research purpose and is still under improvement. 
 * Please email to us if you want to keep in touch with the latest release.
	 We sincerely welcome you to contact Yun Huang (huangyun.ai@gmail.com), or Jose P.Gonzalez-Brenes (josepablog@gmail.com) for problems in the code or cooperation.
 * We thank Taylor Berg-Kirkpatrick (tberg@cs.berkeley.edu) and Jean-Marc Francois (jahmm) for part of their codes that FAST is developed based on.
 *
 */

/**
 * Important Code Reminder:
 *   -- State index: When fitting HMM, index 0(1) is always corresponding to 0(1). 
 *   		-- For observed states, 1 always corresponds to "correct", because when reading input in DataPointList.java, the Bijection outcomes put "incorrect" first. Since the direct feature weights (without taking negative) in LR(LBFGS) predict Prob(Class=1), so using these feature weights we can get Prob("correct"). 
 *   		-- For hidden states, if allowForget=false, then 1 alway correpsond to "known". Similarly, direclty using feature weights in LR(LBFGS) predict Prob("known").
 *   -- Feature values and weights: In expanded feature names, "_hidden1" correspond to index 1 state.
 *   		-- For transition and emission features, the code expand feature by "feature_A", "feature_B",..., "feature_A_hidden1", "feature_B_hidden1",... 
 *   		-- For initial features, it doesn't differentiate hidden states. Both hidden states have the same feature vector length and values ("feature_A", "feature_B"...)
 *    	-- Currently in Pdf, only the hidden state=0 (i=0) maintains feature weights, because the code expanded features for both hiddenStates to train one logistic regression.
 *   		-- In output parameter files, "slip_XXX" means if you use this coefficient direclty into logistic function (1/(1+exp(-x)), you can get slip probability (without any negative tranformation). The same applies for other prefix.
 *   -- InstanceWeights:
 *   		-- For transition parameterization instanceWeights, the code expand the dataset by 4 times with 1st and 2nd -> prior time step hiddenState0, 3rd and 4th ->  prior time step hiddenState1;
 *   		-- For initial and emission parameterization instanceWeights, the code doubles the dataset with the first part corresponding to hiddenState0 and second part corresponding to hiddenState1.
 */


package fast.experimenter;

//import java.io.File;
//import java.io.FileReader;
//import java.text.DateFormat;
//import java.text.SimpleDateFormat;
//import java.util.Map;
//import java.util.HashMap;
//import fast.common.Functions;
//import fast.data.DataPoint;
//import fast.common.Stats.ValueIndexSummary;
//import fast.data.DataPoint;
//import fast.experimenter.Logger.MultiFiles;
//import fast.experimenter.Logger.OneRestartLogger;
//import fast.experimenter.Logger.*;
//import fast.evaluation.TestInfo;
//import fast.experiment.Log;

public class Runner implements Runnable {

	private Options opts;
	private Logger logger;
	
	public Runner() {
		opts = new Options();
		logger = new Logger(opts.formatter);
	}

	public static void main(String[] args) throws IOException {
		Runner runner = new Runner();
		// Opts opts = new Opts();
		if (args != null && args.length > 0) {
			if (args.length == 1)
				Execution.run(args, runner, runner.opts);
			else {
				System.out.println("ERROR: AUGUMENTS: ++input/your_configuration_file");
				System.exit(1);
			}
		}
		else if (args == null || args.length == 0) {
			runner.run();
		}
		else {
			System.out.println("ERROR: AUGUMENTS: ++input/your_configuration_file");
			System.exit(1);
		}
	}

	// This is the necessary function to call to start the whole program.
	public void run() {
		try {
			start();
			logger.curMultiFiles = logger.new MultiFilesLogger();
			for (int fileId = 0; fileId < opts.nbFiles; fileId++){
//      if (!opts.readOneHmmOneTime)
				runOneFileByReadingAllHMM(fileId); 
//			else
//				runOnebyOne(foldID, runID);
			}
			if (opts.nbFiles > 1){
				logger.curMultiFiles.update(logger.curFile.trainAllHMMs);
				writeAllFilesSummary();
			}
			close();
		}
		catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * By default, use runBatch to start the actual process
	 */
	public void runOneFileByReadingAllHMM(int fileId) throws IOException { //int foldId, int runId
		opts.configure(fileId, 0, "");
		logger.curFile =  logger.new OneFileLogger(fileId);

		Bijection oriTrainKcs = new Bijection();
		ArrayList<ArrayList<String>> oriTrainAllKcArrayList = splitHMMs(opts.trainFile, oriTrainKcs);//per kc, then per instance
		Bijection oriTestKcs = new Bijection();
		ArrayList<ArrayList<String>> oriTestAllKcArrayList = splitHMMs(opts.testFile, oriTestKcs);
		if (oriTestAllKcArrayList.size() > oriTrainAllKcArrayList.size() || oriTestKcs.getSize() > oriTestKcs.getSize())
			throw new RuntimeException("ERROR: The # of hmms(KCs) in testset is bigger than that in trainset! Please remove new KCs on testset!");
		Bijection trainKcs = new Bijection();
		Bijection testKcs = new Bijection(); 
		ArrayList<ArrayList<String>> trainAllKcArrayList = new ArrayList<ArrayList<String>>();
		ArrayList<ArrayList<String>> testAllKcArrayList = new ArrayList<ArrayList<String>>();
		
		String fileProgress = "(" + Integer.toString(fileId + 1) + "/" + Integer.toString(opts.nbFiles) + ")";
		String str = "\n\n\n***** Starting new file: fileId=" + fileId + fileProgress 
							+ " *****\n#HMMs(train)=" + oriTrainKcs.getSize() 
							+ "\n#HMMs(test)=" + oriTestKcs.getSize() +"\n";
		logger.printAndLog(str);
		
		/* remove those KCs on train but not in test */
		for (int hmmId = 0; hmmId < oriTrainKcs.getSize(); hmmId++){
			String hmmName = oriTrainKcs.get(hmmId);
			if (!oriTestKcs.contains(hmmName))
				System.out.println("WARNING: Testset doesn't conatin kc=" + hmmName + "! I am skipping it.");
			else{
				int hmmIdOnTest = oriTestKcs.get(hmmName);
				trainKcs.put(hmmName);
				testKcs.put(hmmName);
				trainAllKcArrayList.add(oriTrainAllKcArrayList.get(hmmId));
				testAllKcArrayList.add(oriTestAllKcArrayList.get(hmmIdOnTest));
			}
		}
		System.out.println("");
		logger.curFile.trainAllHMMs = trainKcs;
		opts.resetRandom(fileId, opts.nbRandomRestart, trainKcs.getSize());
		
		for (int hmmId = 0; hmmId < trainKcs.getSize(); hmmId++) {
			String hmmName = trainKcs.get(hmmId);
//			if (!testKcs.contains(hmmName)){
//				System.out.println("WARNING: Testset doesn't conatin kc=" + hmmName + "! I am skipping it.");
//			}
//			else{
			int testHmmId = testKcs.get(hmmName);
//			StudentList curKcTrainStuList = trainKcsStuLists.get(hmmId);
//			StudentList curKcTestStuList = testKcsStuLists.get(curKcTestId);
			ArrayList<String> curKcTrainArrayList = trainAllKcArrayList.get(hmmId);
			ArrayList<String> curKcTestArrayList = testAllKcArrayList.get(testHmmId);
			
			Bijection students = null;
			Bijection items = null;
			if (opts.parameterizing){
				students = opts.generateStudentDummy ? (new Bijection()):null;
				items = opts.generateItemDummy ? (new Bijection()):null;
				if (opts.generateStudentDummy || opts.generateItemDummy){
					DataPointList.readStudentsOrItems(curKcTrainArrayList, students, items);
					DataPointList.readStudentsOrItems(curKcTestArrayList, students, items);
				}
			}
			StudentList curKcTrainStuList  = getFormattedInputData(curKcTrainArrayList, students, items);
			StudentList curKcTestStuList = getFormattedInputData(curKcTestArrayList, students, items);
		
			checkTrainTestFeatures(curKcTrainStuList.getAllFeatures(), curKcTestStuList.getAllFeatures());
			checkTrainTestFeatures(curKcTrainStuList.getInitFeatures(), curKcTestStuList.getInitFeatures());
			checkTrainTestFeatures(curKcTrainStuList.getTranFeatures(), curKcTestStuList.getTranFeatures());
			checkTrainTestFeatures(curKcTrainStuList.getEmitFeatures(), curKcTestStuList.getEmitFeatures());
			
			OneRestartLogger bestRestartLogger = null;
			if (opts.nbRandomRestart <= 1)
				bestRestartLogger = runOneHMMOneRestart(fileId, 0, hmmId, hmmName, curKcTrainStuList, curKcTestStuList, trainKcs);
			else
				bestRestartLogger = runOneHMMMultiRestarts(fileId, hmmId, hmmName, curKcTrainStuList, curKcTestStuList, trainKcs, opts.nbRandomRestart);
			
			logger.curFile.trainedHMMs.add(hmmName);
			int nbTrainedHmms = logger.curFile.trainedHMMs.size();
			writeOneHMMOneRestartSummary(curKcTestStuList, bestRestartLogger.testSummary, nbTrainedHmms);
			
			logger.curFile.add(hmmId, bestRestartLogger);
			//closeAndClearAllRestarts();
		}
		writeOneFileAllHMMSummary();//opts.allHmmsSummaryDir, opts.modelName);
		//TODO: release the memory space for logger.curFileLogger;
		//closeOneFileLogger();
	}


	// TODO: The design of fileId is weird
	public OneRestartLogger runOneHMMMultiRestarts(int fileId, int hmmId, String hmmName, 
															StudentList curKcTrainStuList, StudentList curKcTestStuList, Bijection trainHMMs,
															int nbRandomRestart) throws IOException{
		  logger.curMultiRestarts = logger.new MultiRestartsLogger(hmmName);
			for (int restartId = 0; restartId < nbRandomRestart; restartId++){
				runOneHMMOneRestart(fileId, restartId, hmmId, hmmName, curKcTrainStuList, curKcTestStuList, trainHMMs);
				logger.curMultiRestarts.add(logger.curRestart);
				//logger.printAndLog("\n\nWARNING: The code currently doesn't output multiple restart results. We will implement later!");
			}
			int bestRestartId = logger.curMultiRestarts.getBestRestartId();
			logger.printAndLog("\n\n***** Choose restartId=" + bestRestartId + " *****\n\n");
			OneRestartLogger oneRestartLogger = logger.curMultiRestarts.getLogger(bestRestartId);
			return oneRestartLogger;
	}
	

	/** Actual function to do one restart train and test per KC */
	public OneRestartLogger runOneHMMOneRestart(int fileId, int restartId, int hmmId, String hmmName, 
										StudentList curKcTrainStuList, StudentList curKcTestStuList, Bijection trainHMMs) throws IOException {

		opts.configure(fileId, restartId, hmmName);
		logger.curRestart = logger.new OneRestartLogger(restartId, hmmName);
		logger.curRestart.trainSummary.allFeatures = curKcTrainStuList.getAllFeatures();
		logger.curRestart.trainSummary.initFeatures = curKcTrainStuList.getInitFeatures();
		logger.curRestart.trainSummary.tranFeatures = curKcTrainStuList.getTranFeatures();
		logger.curRestart.trainSummary.emitFeatures = curKcTrainStuList.getEmitFeatures();
		
		String fileProgress = "fileId=" + fileId + "(" + (fileId + 1) + "/" + opts.nbFiles + ")";
		String restartProgress = (opts.nbRandomRestart > 1) ? (", restartId=" + restartId + "(" + (restartId + 1) + "/" + opts.nbRandomRestart + ")"):"";
		String hmmProgress = ", kc=" + hmmName + "(" + (logger.curFile.trainedHMMs.size() + 1) + "/" + trainHMMs.getSize() + ")";
//		for (String kc : logger.curFileLogger.trainedHmms)
//			System.out.println(kc);
		String progress = fileProgress + hmmProgress + restartProgress;
		
		String str = "\n\n***** Training: " + progress + " *****";
		logger.printAndLog(str);
		FeatureHMM trainHmm = trainOneHMMOneRestart(restartId, hmmName, hmmId, curKcTrainStuList);

		str = "\n***** Testing: " + progress + " *****";
		logger.printAndLog(str);
		testOneHMMOneRestart(hmmName, trainHmm, curKcTestStuList); //TestSummary testSummary = 

		str = "\n***** Evaluating: " + progress + " *****";
		logger.printAndLog(str);
		evaluateOneHMMOneRestart(trainHmm, curKcTrainStuList);
	
		return logger.curRestart;
	}
	
	public void writeOneHMMOneRestartSummary(StudentList curKcTestStuList, TestSummary testSummary, int nbHMMTrained) throws IOException{
		//int nbHMMTrained = logger.curFile.trainedHmms.size();
		boolean append = (nbHMMTrained <= 1) ? false : true;
		boolean printHeader = (nbHMMTrained <= 1) ? true : false;
		Predictor.writeHMMPrediction(opts.predictionFile, curKcTestStuList, testSummary, append, printHeader);