docs: public GISModel trainModel(int iterations, DataIndexer di, Prior modelPrior, int cutoff, int threads) {
          if (threads <= 0) {
              threads = 1;
          }
  
          modelExpects = new MutableContext[threads][];
  
          /************** Incorporate all of the needed info ******************/
          LOG.debug("Incorporating indexed data for training...");
          trainingDataFeatNameIndices = di.getContexts();
          trainingDataFeatValues = di.getValues();
          this.cutoff = cutoff;
          predicateCounts = di.getPredCounts();
          numTimesEventsSeen = di.getNumTimesEventsSeen();
          numUniqueEvents = trainingDataFeatNameIndices.length;
          this.prior = modelPrior;
  
          labels = di.getOutcomeLabels();
          outcomeList = di.getOutcomeList();
  
          featNames = di.getPredLabels();
          prior.setLabels(labels, featNames);
  
          // determine the correction constant and its inverse
          double correctionConstant = 0;
          for (int ci = 0; ci < trainingDataFeatNameIndices.length; ci++) {
              if (trainingDataFeatValues == null || trainingDataFeatValues[ci] == null) {
                  if (trainingDataFeatNameIndices[ci].length > correctionConstant) {
                      correctionConstant = trainingDataFeatNameIndices[ci].length;
                  }
              } else {
                  float cl = trainingDataFeatValues[ci][0];
                  for (int vi = 1; vi < trainingDataFeatValues[ci].length; vi++) {
                      cl += trainingDataFeatValues[ci][vi];
                  }
  
                  if (cl > correctionConstant) {
                      correctionConstant = cl;
                  }
              }
          }
  
          LOG.debug("Number of Event Tokens: " + numUniqueEvents);
          LOG.debug("Number of Outcomes: " + labels.length);
          LOG.debug("Number of Predicates: " + featNames.length);
  
          // set up feature arrays
          float[][] featCount = new float[featNames.length][labels.length];
          for (int ti = 0; ti < numUniqueEvents; ti++) {
              for (int j = 0; j < trainingDataFeatNameIndices[ti].length; j++) {
                  if (trainingDataFeatValues != null && trainingDataFeatValues[ti] != null) {
                      featCount[trainingDataFeatNameIndices[ti][j]][outcomeList[ti]] += numTimesEventsSeen[ti] * trainingDataFeatValues[ti][j];
                  } else {
                      featCount[trainingDataFeatNameIndices[ti][j]][outcomeList[ti]] += numTimesEventsSeen[ti];
                  }
              }
          }
  
          // A fake "observation" to cover features which are not detected in
          // the data.  The default is to assume that we observed "1/10th" of a
          // feature during training.
          final double smoothingObservation = this.smoothingObservation;
  
          // Get the observed expectations of the features. Strictly speaking,
          // we should divide the counts by the number of Tokens, but because of
          // the way the model's expectations are approximated in the
          // implementation, this is cancelled out when we compute the next
          // iteration of a parameter, making the extra divisions wasteful.
          params = new MutableContext[featNames.length];
          for (int i = 0; i < modelExpects.length; i++) {
              modelExpects[i] = new MutableContext[featNames.length];
          }
          observedExpects = new MutableContext[featNames.length];
  
          // The model does need the correction constant and the correction feature. The correction constant
          // is only needed during training, and the correction feature is not necessary.
          // For compatibility reasons the model contains form now on a correction constant of 1,
          // and a correction param 0.
          evalParams = new EvalParameters(params, 0, 1, labels.length);
          int[] activeOutcomes = new int[labels.length];
          int[] labelPattern = new int[labels.length];
          int[] outcomePattern;
  
          for (int oi = 0; oi < labels.length; oi++) {
              labelPattern[oi] = oi;
          }
  
          for (int pi = 0; pi < featNames.length; pi++) {
              int numActiveOutcomes = 0;
              if (useSimpleSmoothing) {
                  numActiveOutcomes = labels.length;
                  outcomePattern = labelPattern;
              } else { //determine active outcomes
                  for (int oi = 0; oi < labels.length; oi++) {
                      if (featCount[pi][oi] > 0 && predicateCounts[pi] >= cutoff) {
                          activeOutcomes[numActiveOutcomes] = oi;
                          numActiveOutcomes++;
                      }
                  }
                  if (numActiveOutcomes == labels.length) {
                      outcomePattern = labelPattern;
                  } else {
                      outcomePattern = new int[numActiveOutcomes];
                      for (int aoi = 0; aoi < numActiveOutcomes; aoi++) {
                          outcomePattern[aoi] = activeOutcomes[aoi];
                      }
                  }
              }
              params[pi] = new MutableContext(outcomePattern, new double[numActiveOutcomes]);
              for (int i = 0; i < modelExpects.length; i++)
                  modelExpects[i][pi] = new MutableContext(outcomePattern, new double[numActiveOutcomes]);
              observedExpects[pi] = new MutableContext(outcomePattern, new double[numActiveOutcomes]);
              for (int aoi = 0; aoi < numActiveOutcomes; aoi++) {
                  int oi = outcomePattern[aoi];
                  params[pi].setParameter(aoi, 0.0);
                  for (MutableContext[] modelExpect : modelExpects) {
                      modelExpect[pi].setParameter(aoi, 0.0);
                  }
                  if (featCount[pi][oi] > 0) {
                      observedExpects[pi].setParameter(aoi, featCount[pi][oi]);
                  } else if (useSimpleSmoothing) {
                      observedExpects[pi].setParameter(aoi, smoothingObservation);
                  }
              }
          }
  
          /***************** Find the parameters ************************/
          LOG.debug("Computing model parameters in " + threads + " threads...");
          findParameters(iterations, correctionConstant);
  
          /*************** Create and return the model ******************/
          // To be compatible with old models the correction constant is always 1
          return new GISModel(params, featNames, labels, 1, evalParams.getCorrectionParam());
  
      }