code: // Copyright (c) 2016 - Patrick Schäfer (patrick.schaefer@hu-berlin.de)
  // Distributed under the GLP 3.0 (See accompanying file LICENSE)
  package dotnet54.classifiers.tschief.splitters.boss;
  
  import java.text.MessageFormat;
  import java.util.*;
  import java.util.Map.Entry;
  import java.util.concurrent.ExecutorService;
  import java.util.concurrent.Executors;
  import java.util.concurrent.atomic.AtomicInteger;
  import com.carrotsearch.hppc.FloatContainer;
  import com.carrotsearch.hppc.IntArrayDeque;
  import com.carrotsearch.hppc.IntArrayList;
  import com.carrotsearch.hppc.cursors.FloatCursor;
  import com.carrotsearch.hppc.cursors.IntCursor;
  import dotnet54.tscore.data.TimeSeries;
  
  public abstract class BossClassifier {
    transient ExecutorService exec;
  
    public static boolean[] NORMALIZATION = new boolean[]{true, false};
  
    public static boolean DEBUG = false;
    public static boolean ENSEMBLE_WEIGHTS = true;
  
    public static int threads = 1;
  
    protected int[][] testIndices;
    protected int[][] trainIndices;
    public static int folds = 10;
  
    // Blocks for parallel execution
    public final static int BLOCKS = 8;
  
    static {
      Runtime runtime = Runtime.getRuntime();
      if (runtime.availableProcessors() <= 4) {
        threads = runtime.availableProcessors() - 1;
      } else {
        threads = runtime.availableProcessors();
      }
  //    threads = 1; // TODO
    }
  
    public BossClassifier() {
      this.exec = Executors.newFixedThreadPool(threads);
    }
  
    /**
     * Invokes {@code shutdown} when this executor is no longer
     * referenced and it has no threads.
     */
    protected void finalize() {
      if (exec != null) {
        exec.shutdown();
      }
    }
  
    /**
     * Build a classifier from the a training set with class labels.
     *
     * @param trainSamples The training set
     * @return The accuracy on the train-samples
     */
    public abstract Score fit(final TimeSeries[] trainSamples);
  
    /**
     * The predicted classes and accuracies of an array of samples.
     *
     * @param testSamples The passed set
     * @return The predictions for each passed sample and the test accuracy.
     */
    public abstract Predictions score(final TimeSeries[] testSamples);
  
  
    /**
     * The predicted classes of an array of samples.
     *
     * @param testSamples The passed set
     * @return The predictions for each passed sample.
     */
    public abstract Double[] predict(final TimeSeries[] testSamples);
  
  
    /**
     * Performs training and testing on a set of train- and test-samples.
     *
     * @param trainSamples The training set
     * @param testSamples  The training set
     * @return The accuracy on the test- and train-samples
     */
    public abstract Score eval(
            final TimeSeries[] trainSamples, final TimeSeries[] testSamples);
  
  
    protected Predictions evalLabels(TimeSeries[] testSamples, Double[] labels) {
      int correct = 0;
      for (int ind = 0; ind < testSamples.length; ind++) {
        correct += compareLabels(labels[ind],(testSamples[ind].label().doubleValue()))? 1 : 0;
      }
      return new Predictions(labels, correct);
    }
  
    protected Predictions evalLabels(double[] sampleLabels, Double[] labels) {
      int correct = 0;
      for (int ind = 0; ind < sampleLabels.length; ind++) {
        correct += compareLabels(labels[ind],(sampleLabels[ind]))? 1 : 0;
      }
      return new Predictions(labels, correct);
    }
  
    public static class Words {
      public static int binlog(int bits) {
        int log = 0;
        if ((bits & 0xffff0000) != 0) {
          bits >>>= 16;
          log = 16;
        }
        if (bits >= 256) {
          bits >>>= 8;
          log += 8;
        }
        if (bits >= 16) {
          bits >>>= 4;
          log += 4;
        }
        if (bits >= 4) {
          bits >>>= 2;
          log += 2;
        }
        return log + (bits >>> 1);
      }
  
      public static long createWord(short[] words, int features, byte usedBits) {
        return fromByteArrayOne(words, features, usedBits);
      }
  
      /**
       * Returns a long containing the values in bytes.
       *
       * @param bytes
       * @param to
       * @param usedBits
       * @return
       */
      public static long fromByteArrayOne(short[] bytes, int to, byte usedBits) {
        int shortsPerLong = 60 / usedBits;
        to = Math.min(bytes.length, to);
  
        long bits = 0;
        int start = 0;
        long shiftOffset = 1;
        for (int i = start, end = Math.min(to, shortsPerLong + start); i < end; i++) {
          for (int j = 0, shift = 1; j < usedBits; j++, shift <<= 1) {
            if ((bytes[i] & shift) != 0) {
              bits |= shiftOffset;
            }
            shiftOffset <<= 1;
          }
        }
  
        return bits;
      }
    }
  
    public static class Model implements Comparable<Model> {
      public String name;
      public int windowLength;
      public boolean normed;
  
      public Score score;
  
      public Model() {
      }
  
      public Model(
              String name,
              int testing,
              int testSize,
              int training,
              int trainSize,
              boolean normed,
              int windowLength
      ) {
        this(name,
                new Score(name, testing, testSize, training, trainSize, windowLength),
                normed,
                windowLength);
      }
  
      public Model(
              String name,
              Score score,
              boolean normed,
              int windowLength
      ) {
        this.name = name;
        this.score = score;
        this.normed = normed;
        this.windowLength = windowLength;
      }
  
      @Override
      public String toString() {
        return score.toString();
      }
  
      public int compareTo(Model bestScore) {
        return this.score.compareTo(bestScore.score);
      }
    }
  
    public static class Score implements Comparable<Score> {
      public String name;
      public int training;
      public int trainSize;
      public int testing;
      public int testSize;
      public int windowLength;
  
      public Double avgOffset; // For earliness
  
      public Score() {
      }
  
      public Score(
              String name,
              int testing,
              int testSize,
              int training,
              int trainSize,
              int windowLength
      ) {
        this.name = name;
        this.training = training;
        this.trainSize = trainSize;
        this.testing = testing;
        this.testSize = testSize;
        this.windowLength = windowLength;
      }
  
      public double getTestingAccuracy() {
        return 1 - formatError(testing, testSize);
      }
  
      public double getTrainingAccuracy() {
        return 1 - formatError(training, trainSize);
      }
  
      public Double getTestEarliness() {
        return this.avgOffset;
      }
  
      public String getEarliness() {
        return String.format(Locale.ENGLISH, "%.2f", this.avgOffset);
      }
  
      @Override
      public String toString() {
        double test = getTestingAccuracy();
        double train = getTrainingAccuracy();
  
        if (avgOffset != null) {
            String earliness = getEarliness();
            return this.name+";"+train+";"+test+ ";" + earliness;
        }
        return this.name + ";" + train + ";" + test;
      }
  
  
      public int compareTo(Score bestScore) {
        if (this.training > bestScore.training
                || this.training == bestScore.training
                && this.windowLength > bestScore.windowLength // on a tie, prefer the one with the larger window-length
                ) {
          return 1;
        }
        return -1;
      }
  
      public void clear() {
        this.testing = 0;
        this.training = 0;
      }
    }
  
    public static class Predictions {
      public Double[] labels;
      public AtomicInteger correct;
  
      double[][] probabilities;
      int[] realLabels;
  
  
      public Predictions(Double[] labels, int bestCorrect) {
        this.labels = labels;
        this.correct = new AtomicInteger(bestCorrect);
      }
  
      public Predictions(
          Double[] predictions,
          double[][] probabilities,
          int[] realLabels) {
        this.labels = predictions;
        this.probabilities = probabilities;
        this.realLabels = realLabels;
      }
    }
  
    public static void outputResult(int correct, long time, int testSize) {
      double error = formatError(correct, testSize);
      //String errorStr = MessageFormat.format("{0,number,#.##%}", error);
      String correctStr = MessageFormat.format("{0,number,#.##%}", 1 - error);
  
      System.out.print("Correct:\t");
      System.out.print("" + correctStr + "");
      System.out.println("\tTime: \t" + (System.currentTimeMillis() - time) / 1000.0 + " s");
    }
  
    public static double formatError(int correct, int testSize) {
      return Math.round(1000 * (testSize - correct) / (double) (testSize)) / 1000.0;
    }
  
  
  
  
    private static void swap(int[] array, int idxA, int idxB) {
      int temp = array[idxA];
      array[idxA] = array[idxB];
      array[idxB] = temp;
    }
  
    // Stratified cross validation
  
  
    public static class Pair<E, T> {
      public E key;
      public T value;
  
      public Pair(E e, T t) {
        this.key = e;
        this.value = t;
      }
  
      public static <E, T> Pair<E, T> create(E e, T t) {
        return new Pair<>(e, t);
      }
  
      @Override
      public int hashCode() {
        return this.key.hashCode();
      }
  
      @SuppressWarnings("unchecked")
      @Override
      public boolean equals(Object obj) {
        return this.key.equals(((Pair<E, T>) obj).key);
      }
    }
  
  
    protected boolean compareLabels(Double label1, Double label2) {
      // compare 1.0000 to 1.0 in String returns false, hence the conversion to double
      return label1 != null && label2 != null && label1.equals(label2);
    }
  
    protected <E extends Model> BossEnsemble<E> filterByFactor(
            List<E> results,
            int correctTraining,
            double factor) {
  
      // sort descending
      Collections.sort(results, Collections.reverseOrder());
  
      // only keep best scores
      List<E> model = new ArrayList<>();
      for (E score : results) {
        if (score.score.training >= correctTraining * factor) { // all with same score
          model.add(score);
        }
      }
  
      return new BossEnsemble<>(model);
    }
  
    protected Double[] score(
            final String name,
            final TimeSeries[] samples,
            final List<Pair<Double, Integer>>[] labels,
            final List<Integer> currentWindowLengths) {
  
      Double[] predictedLabels = new Double[samples.length];
      //long[] maxCounts = new long[samples.length];
  
      //int correctTesting = 0;
      for (int i = 0; i < labels.length; i++) {
        Map<Double, Long> counts = new HashMap<>();
  
        for (Pair<Double, Integer> k : labels[i]) {
          if (k != null && k.key != null) {
            Double label = k.key;
            Long count = counts.get(label);
            long increment = ENSEMBLE_WEIGHTS ? k.value : 1;
            count = (count == null) ? increment : count + increment;
            counts.put(label, count);
          }
        }
  
        long maxCount = -1;
        for (Entry<Double, Long> e : counts.entrySet()) {
          if (predictedLabels[i] == null
                  || maxCount < e.getValue()
                  || maxCount == e.getValue()  // break ties
                  && predictedLabels[i] <= e.getKey()
                  ) {
            maxCount = e.getValue();
            // maxCounts[i] = maxCount;
            predictedLabels[i] = e.getKey();
          }
        }
      }
  
      //System.out.println(Arrays.toString(predictedLabels));
      //System.out.println(Arrays.toString(maxCounts));
  
      if (DEBUG) {
        System.out.print(name + " Testing with " + currentWindowLengths.size() + " models:\t");
        System.out.println(currentWindowLengths.toString() + "\n");
      }
  
      return predictedLabels;
    }
  
  
    protected Integer[] getWindowsBetween(int minWindowLength, int maxWindowLength) {
      List<Integer> windows = new ArrayList<>();
      for (int windowLength = maxWindowLength; windowLength >= minWindowLength; windowLength--) {
        windows.add(windowLength);
      }
      return windows.toArray(new Integer[]{});
    }
  
    protected int getMax(TimeSeries[] samples, int maxWindowSize) {
      int max = 0;
      for (TimeSeries ts : samples) {
        max = Math.max(ts.length(), max);
      }
      return Math.min(maxWindowSize,max);
    }
  
    protected static Set<Double> uniqueClassLabels(TimeSeries[] ts) {
      Set<Double> labels = new HashSet<>();
      for (TimeSeries t : ts) {
        labels.add(t.label().doubleValue());
      }
      return labels;
    }
  
    protected static double magnitude(FloatContainer values) {
      double mag = 0.0D;
      for (FloatCursor value : values) {
        mag = mag + value.value * value.value;
      }
      return Math.sqrt(mag);
    }
  
    protected static int[] createIndices(int length) {
      int[] indices = new int[length];
      for (int i = 0; i < length; i++) {
        indices[i] = i;
      }
      return indices;
    }
  
    protected void generateIndices(TimeSeries[] samples) {
      IntArrayList[] sets = getStratifiedTrainTestSplitIndices(samples, folds);
      this.testIndices = new int[folds][];
      this.trainIndices = new int[folds][];
      for (int s = 0; s < folds; s++) {
        this.testIndices[s] = convertToInt(sets[s]);
        this.trainIndices[s] = convertToInt(sets, s);
      }
    }
  
    protected IntArrayList[] getStratifiedTrainTestSplitIndices(
            TimeSeries[] samples,
            int splits) {
  
      Map<Double, IntArrayDeque> elements = new HashMap<>();
  
      for (int i = 0; i < samples.length; i++) {
        Double label = samples[i].label().doubleValue();
        IntArrayDeque sameLabel = elements.get(label);
        if (sameLabel == null) {
          sameLabel = new IntArrayDeque();
          elements.put(label, sameLabel);
        }
        sameLabel.addLast(i);
      }
  
      // pick samples
      IntArrayList[] sets = new IntArrayList[splits];
      for (int i = 0; i < splits; i++) {
        sets[i] = new IntArrayList();
      }
  
      // all but one
      for (Entry<Double, IntArrayDeque> data : elements.entrySet()) {
        IntArrayDeque d = data.getValue();
        separate:
        while (true) {
          for (int s = 0; s < splits; s++) {
            if (!d.isEmpty()) {
              int dd = d.removeFirst();
              sets[s].add(dd);
            } else {
              break separate;
            }
          }
        }
      }
  
      return sets;
    }
  
    protected static int[] convertToInt(IntArrayList trainSet) {
      int[] train = new int[trainSet.size()];
      int a = 0;
      for (IntCursor i : trainSet) {
        train[a++] = i.value;
      }
      return train;
    }
  
    protected static int[] convertToInt(IntArrayList[] setToSplit, int exclude) {
      int count = 0;
  
      for (int i = 0; i < setToSplit.length; i++) {
        if (i != exclude) {
          count += setToSplit[i].size();
        }
      }
  
      int[] setData = new int[count];
      int a = 0;
      for (int i = 0; i < setToSplit.length; i++) {
        if (i != exclude) {
          for (IntCursor d : setToSplit[i]) {
            setData[a++] = d.value;
          }
        }
      }
  
      return setData;
    }
  
  
  }
  