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.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import java.util.concurrent.atomic.AtomicInteger;
  import com.carrotsearch.hppc.cursors.IntIntCursor;
  import dotnet54.tscore.data.TimeSeries;
  import dotnet54.tscore.threading.ParallelFor;
  
  
  /**
   * The Bag-of-SFA-Symbols Ensemble Classifier as published in
   * <p>
   * Schäfer, P.: The boss is concerned with time series classification
   * in the presence of noise. DMKD (2015)
   */
  public class BOSSEnsembleClassifier extends BossClassifier {
    // default training parameters
    public static double factor = 0.92;
  
    public static int maxF = 16;
    public static int minF = 6;
    public static int maxS = 4;
  
    public static int MAX_WINDOW_LENGTH = 250;
  
    // the trained weasel
    public BossEnsemble<BOSSModel> model;
  
    public BOSSEnsembleClassifier() {
      super();
    }
  
    public static class BOSSModel extends Model {
  
      public BOSSModel(){}
  
      public BOSSModel(
          boolean normed,
          int windowLength) {
        super("BOSS", -1, 1, -1, 1, normed, windowLength);
      }
  
      // the BOSS representation for each train sample
      public BOSS.BagOfPattern[] bag;
  
      // the trained BOSS transformation
      public BOSS boss;
  
      // the best number of Fourier values to be used
      public int features;
    }
  
    @Override
    public Score eval(
            final TimeSeries[] trainSamples, final TimeSeries[] testSamples) {
      long startTime = System.currentTimeMillis();
  
      Score score = fit(trainSamples);
  
      if (DEBUG) {
        System.out.println(score.toString());
        outputResult(score.training, startTime, testSamples.length);
        System.out.println("");
      }
  
      // Classify: testing score
      int correctTesting = score(testSamples).correct.get();
  
      return new Score(
          "BOSS Ensemble",
          correctTesting, testSamples.length,
          score.training, trainSamples.length,
          score.windowLength);
    }
  
    @Override
    public Score fit(final TimeSeries[] trainSamples) {
      // generate test train/split for cross-validation
      generateIndices(trainSamples);
  
      Score bestScore = null;
      int bestCorrectTraining = 0;
  
      int minWindowLength = 10;
      int maxWindowLength = getMax(trainSamples, MAX_WINDOW_LENGTH);
      Integer[] windows = getWindowsBetween(minWindowLength, maxWindowLength);
  
      for (boolean normMean : NORMALIZATION) {
        // train the shotgun models for different window lengths
        BossEnsemble<BOSSModel> model = fitEnsemble(windows, normMean, trainSamples);
        Double[] labels = predict(model, trainSamples);
        Predictions pred = evalLabels(trainSamples, labels);
  
        if (model == null || bestCorrectTraining < pred.correct.get()) {
          bestCorrectTraining = pred.correct.get();
          bestScore = model.getHighestScoringModel().score;
          bestScore.training = pred.correct.get();
          this.model = model;
        }
      }
  
      // return score
      return bestScore;
    }
  
    @Override
    public Predictions score(final TimeSeries[] testSamples) {
      Double[] labels = predict(testSamples);
      return evalLabels(testSamples, labels);
    }
  
  
    @Override
    public Double[] predict(final TimeSeries[] testSamples) {
      return predict(this.model, testSamples);
    }
  
  
    protected BossEnsemble<BOSSModel> fitEnsemble(Integer[] windows,
                                                  boolean normMean,
                                                  TimeSeries[] samples) {
  
      final ArrayList<BOSSModel> results = new ArrayList<>(windows.length);
      final AtomicInteger correctTraining = new AtomicInteger(0);
  
      ParallelFor.withIndex(exec, threads, new ParallelFor.Each() {
        @Override
        public void run(int id, AtomicInteger processed) {
          for (int i = 0; i < windows.length; i++) {
            if (i % threads == id) {
              BOSSModel model = new BOSSModel(normMean, windows[i]);
              try {
                BOSS boss = new BOSS(maxF, maxS, windows[i], model.normed);
                int[][] words = boss.createWords(samples);
  
                for (int f = minF; f <= maxF; f += 2) {
  
                  BOSS.BagOfPattern[] bag = boss.createBagOfPattern(words, samples, f);
  
                  Predictions p = predict(bag, bag);
  //                System.out.println("window:  " + windows[i].intValue() +  " wordlen: " + f + " " + p.correct + " was correct");
  
                  if (p.correct.get() > model.score.training) {
                    model.score.training = p.correct.get();
                    model.features = f;
                    model.boss = boss;
                    model.bag = bag;
  
                    if (p.correct.get() == samples.length) {
                      break;
                    }
                  }
                }
              } catch (Exception e) {
                e.printStackTrace();
              }
  
              // keep best scores
              synchronized (correctTraining) {
                if (model.score.training > correctTraining.get()) {
                  correctTraining.set(model.score.training);
                }
  
                // add to ensemble if train-score is within factor to the best score
                if (model.score.training >= correctTraining.get() * factor) {
                  results.add(model);
                }
              }
            }
          }
        }
      });
  
      // returns the ensemble based on the best window-lengths within factor
      return filterByFactor(results, correctTraining.get(), factor);
    }
  
  
    protected Predictions predict(
        final BOSS.BagOfPattern[] bagOfPatternsTestSamples,
        final BOSS.BagOfPattern[] bagOfPatternsTrainSamples) {
  
      Predictions p = new Predictions(new Double[bagOfPatternsTestSamples.length], 0);
  
      ParallelFor.withIndex(BLOCKS, new ParallelFor.Each() {
        @Override
        public void run(int id, AtomicInteger processed) {
          // iterate each sample to classify
          for (int i = 0; i < bagOfPatternsTestSamples.length; i++) {
            if (i % BLOCKS == id) {
              long minDistance = Integer.MAX_VALUE;
  
              // Distance if there is no matching word
              double noMatchDistance = 0.0;
              for (IntIntCursor key : bagOfPatternsTestSamples[i].bag) {
                noMatchDistance += key.value * key.value;
              }
  
              nnSearch:
              for (int j = 0; j < bagOfPatternsTrainSamples.length; j++) {
                if (bagOfPatternsTestSamples[i] != bagOfPatternsTrainSamples[j]) {
                  // determine distance
                  long distance = 0;
                  for (IntIntCursor key : bagOfPatternsTestSamples[i].bag) {
                    long buf = key.value - bagOfPatternsTrainSamples[j].bag.get(key.key);
                    distance += buf * buf;
  
                    if (distance >= minDistance) {
                      continue nnSearch;
                    }
                  }
  
                  // update nearest neighbor
                  if (distance != noMatchDistance && distance < minDistance) {
                    minDistance = distance;
                    p.labels[i] = bagOfPatternsTrainSamples[j].getLabel().doubleValue();
                  }
                }
              }
  
              // check if the prediction is correct
              if (compareLabels(bagOfPatternsTestSamples[i].getLabel().doubleValue(), p.labels[i])) {
                p.correct.incrementAndGet();
              }
            }
          }
        }
      });
  
      return p;
    }
  
    protected Double[] predict(final BossEnsemble<BOSSModel> model, final TimeSeries[] testSamples) {
      @SuppressWarnings("unchecked")
      final List<Pair<Double, Integer>>[] testLabels = new List[testSamples.length];
      for (int i = 0; i < testLabels.length; i++) {
        testLabels[i] = new ArrayList<>();
      }
  
      final List<Integer> usedLengths = Collections.synchronizedList(new ArrayList<>(model.size()));
  
      // parallel execution
      ParallelFor.withIndex(exec, threads, new ParallelFor.Each() {
        @Override
        public void run(int id, AtomicInteger processed) {
          // iterate each sample to classify
          for (int i = 0; i < model.size(); i++) {
            if (i % threads == id) {
              final BOSSModel score = model.get(i);
              usedLengths.add(score.windowLength);
  
              BOSS model = score.boss;
  
              // create words and BOSS boss for test samples
              int[][] wordsTest = model.createWords(testSamples);
              BOSS.BagOfPattern[] bagTest = model.createBagOfPattern(wordsTest, testSamples, score.features);
  
              Predictions p = predict(bagTest, score.bag);
  
              for (int j = 0; j < p.labels.length; j++) {
                synchronized (testLabels[j]) {
                  if (p.labels[j] != null) {
                    testLabels[j].add(new Pair<>(p.labels[j], score.score.training));
                  }
                }
              }
            }
          }
        }
      });
  
      return score("BOSS", testSamples, testLabels, usedLengths);
    }
  
  }