docs: private double nextIteration(double correctionConstant) {
          // compute contribution of p(a|b_i) for each feature and the new
          // correction parameter
          double loglikelihood = 0.0;
          int numEvents = 0;
          int numCorrect = 0;
  
          int numberOfThreads = modelExpects.length;
          ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
          int taskSize = numUniqueEvents / numberOfThreads;
          int leftOver = numUniqueEvents % numberOfThreads;
  
          List<Future<?>> futures = new ArrayList<>();
  
          for (int i = 0; i < numberOfThreads; i++) {
              int len = i == numberOfThreads - 1 ? taskSize + leftOver : taskSize;
              futures.add(executor.submit(new ModelExpactationComputeTask(i, i * taskSize, len)));
          }
  
          for (Future<?> future : futures) {
              ModelExpactationComputeTask finishedTask;
              try {
                  finishedTask = (ModelExpactationComputeTask) future.get();
              } catch (InterruptedException e) {
                  // TODO: We got interrupted, but that is currently not really supported!
                  // For now we just print the exception and fail hard. We hopefully soon
                  // handle this case properly!
                  e.printStackTrace();
                  throw new IllegalStateException("Interruption is not supported!", e);
              } catch (ExecutionException e) {
                  // Only runtime exception can be thrown during training, if one was thrown
                  // it should be re-thrown. That could for example be a NullPointerException
                  // which is caused through a bug in our implementation.
                  throw new RuntimeException("Exception during training: " + e.getMessage(), e);
              }
  
              // When they are done, retrieve the results ...
              numEvents += finishedTask.getNumEvents();
              numCorrect += finishedTask.getNumCorrect();
              loglikelihood += finishedTask.getLoglikelihood();
          }
  
          executor.shutdown();
  
          // merge the results of the two computations
          for (int pi = 0; pi < featNames.length; pi++) {
              int[] activeOutcomes = params[pi].getOutcomes();
  
              for (int aoi = 0; aoi < activeOutcomes.length; aoi++) {
                  for (int i = 1; i < modelExpects.length; i++) {
                      modelExpects[0][pi].updateParameter(aoi, modelExpects[i][pi].getParameters()[aoi]);
                  }
              }
          }
  
          // compute the new parameter values
          for (int pi = 0; pi < featNames.length; pi++) {
              double[] observed = observedExpects[pi].getParameters();
              double[] model = modelExpects[0][pi].getParameters();
              int[] activeOutcomes = params[pi].getOutcomes();
              for (int aoi = 0; aoi < activeOutcomes.length; aoi++) {
                  if (useGaussianSmoothing) {
                      params[pi].updateParameter(aoi, gaussianUpdate(pi, aoi, correctionConstant));
                  } else {
                      if (model[aoi] == 0) {
                          LOG.error("Model expects == 0 for " + featNames[pi] + " " + labels[aoi]);
                      }
                      params[pi].updateParameter(aoi, ((Math.log(observed[aoi]) - Math.log(model[aoi])) / correctionConstant));
                  }
  
                  for (MutableContext[] modelExpect : modelExpects) {
                      modelExpect[pi].setParameter(aoi, 0.0); // re-initialize to 0.0's
                  }
  
              }
          }
  
          LOG.debug("loglikelihood = " + loglikelihood + "\taccuracy: " + ((double) numCorrect / numEvents) * 100);
  
          return loglikelihood;
      }