ProfileScheduleSubparticle(PowerProfileInfo ppi) {
    phases = ppi.getEnergyPhases();

    PowerProfileTimeConstraints pptc = ppi.getTimeConstraints();
    long startTime = Math.max(pptc.getStartAfterConstraint(), System.currentTimeMillis());
    profileEarliestStart = CalendarUtil.getSlotOf(startTime);

    profileSlackInterval = CalendarUtil.slotsFromMillis(pptc.getStopBeforeConstraint() - startTime);

    for (int i = phases.length; --i >= 0; profileSlackInterval -= phases[i].getSlotDuration()) ;

    if (profileSlackInterval <= 0)
      throw new IllegalArgumentException("Inconsistent time constraints with profile duration.");

    phasesBestPositions = new float[phases.length];
    phasesCurrentPositions = new float[phases.length];

    randomizePositions();
  }
final class ProfileScheduleSubparticle {
  public static final int PHASE_DELAY_THRESHOLD = CalendarUtil.slotsFromMinutes(4);
  public static final int EXCEEDING_TIME_ALLOCATION_PENALTY = 1000 * 1000;

  public static final double HALF_PI = 0.5 * Math.PI;
  public static final double LEVY_ALFA = 1.4;

  public static double CAUCHY_CONSTRICTION = 0.35;
  public static double LEVY_CONSTRICTION = 0.75;
  public static double EXPONENTIAL_CONSTRICTION = 1.15;

  private static final Random rand = new Random();

  // the general Levy distribution is rather computing demanding
  public static final double nextLevy() {
    double v = Math.PI * (rand.nextDouble() - 0.5);
    double k = LEVY_ALFA * v;
    double w = -Math.log(rand.nextDouble()) / Math.cos(v - k);
    double h = w * Math.cos(v);
    return LEVY_CONSTRICTION * Math.abs(w * Math.sin(k) * Math.exp(-Math.log(h) / LEVY_ALFA));
  }

  public static final double nextCauchy() {
    // return CAUCHY_CONSTRICTION * Math.tan(Math.PI * (rand.nextDouble() - 0.5));
    return CAUCHY_CONSTRICTION * Math.tan(HALF_PI * rand.nextDouble());
  }

  public static final double nextExponential() {
    return EXPONENTIAL_CONSTRICTION * -Math.log(rand.nextDouble());
  }

  /*
  public static final double nextRandom(int randomDistribution) {
  	double r;
  	switch (randomDistribution) {
  	case GAUSSIAN_STEP:
  		return GAUSSIAN_CONSTRICTION * rand.nextGaussian();

  	case ABS_GAUSSIAN_STEP:
  		return GAUSSIAN_CONSTRICTION * Math.abs(rand.nextGaussian());

  	case CAUCHY_STEP:
  		return CAUCHY_CONSTRICTION * Math.tan(Math.PI * (rand.nextDouble() - 0.5));

  	case ABS_CAUCHY_STEP:
  		return CAUCHY_CONSTRICTION * Math.tan(HALF_PI * rand.nextDouble());

  	case EXPONENTIAL_STEP:
  		r = Math.log(rand.nextDouble());
  		return EXPONENTIAL_CONSTRICTION * (rand.nextDouble() < 0.5 ? -r : r);

  	case ABS_EXPONENTIAL_STEP:
  		return EXPONENTIAL_CONSTRICTION * -Math.log(rand.nextDouble());

  	case LEVY_STEP:
  		r = nextLevy();
  		return LEVY_CONSTRICTION * (rand.nextDouble() < 0.5 ? -r : r);

  	case ABS_LEVY_STEP:
  		return LEVY_CONSTRICTION * nextLevy();

  	default:
  		return rand.nextDouble();
  	}
  }
  */

  private EnergyPhaseInfo[] phases = new EnergyPhaseInfo[0];
  private int profileEarliestStart, profileSlackInterval;
  private float tardiness;
  private float[] phasesBestPositions, phasesCurrentPositions;

  ProfileScheduleSubparticle(PowerProfileInfo ppi) {
    phases = ppi.getEnergyPhases();

    PowerProfileTimeConstraints pptc = ppi.getTimeConstraints();
    long startTime = Math.max(pptc.getStartAfterConstraint(), System.currentTimeMillis());
    profileEarliestStart = CalendarUtil.getSlotOf(startTime);

    profileSlackInterval = CalendarUtil.slotsFromMillis(pptc.getStopBeforeConstraint() - startTime);

    for (int i = phases.length; --i >= 0; profileSlackInterval -= phases[i].getSlotDuration()) ;

    if (profileSlackInterval <= 0)
      throw new IllegalArgumentException("Inconsistent time constraints with profile duration.");

    phasesBestPositions = new float[phases.length];
    phasesCurrentPositions = new float[phases.length];

    randomizePositions();
  }

  ProfileScheduleSubparticle(ProfileScheduleSubparticle sampleSchedule) {
    phases = sampleSchedule.phases;
    profileEarliestStart = sampleSchedule.profileEarliestStart;
    profileSlackInterval = sampleSchedule.profileSlackInterval;

    phasesBestPositions = new float[phases.length];
    phasesCurrentPositions = new float[phases.length];

    randomizePositions();
  }

  private void randomizePositions() {
    // initialize profile position using uniform distribution
    phasesCurrentPositions[0] = phasesBestPositions[0] = rand.nextFloat() * profileSlackInterval;
    tardiness = phasesCurrentPositions[0];

    for (int i = 1; i < phases.length; ++i) {
      float maxDelay = Math.min(profileSlackInterval - tardiness, phases[i].getSlotMaxDelay());
      phasesCurrentPositions[i] = phasesBestPositions[i] = maxDelay * rand.nextFloat();
      tardiness += phasesCurrentPositions[i];
    }
  }

  // tardiness is the difference between the current profile end and the minimum theoretical
  // termination
  float getTardiness() {
    return tardiness;
  }

  /*
   * Implements a Quantum inspired PSO with Cauchy (Levy) flights
   */
  void nextRandomFlight(ProfileScheduleSubparticle bestParticle) {
    // Set globalBestPositions Array to the best Particle's best Positions Array
    float[] globalBestPositions = bestParticle.phasesBestPositions;

    float currentMaxSlack = profileSlackInterval;
    float phaseMaxDelay = profileSlackInterval;

    // For each i iterating over all Phases of the Profile
    for (int i = 0; i < phases.length; i++) {
      // If i is greater than 0 (i.e. for all Pahses other than the firts one)
      if (i > 0) {
        // Set phaseMaxDelay to the minimum between currentMaxSlack, and Phase(i)'s MaxAlowedDelay
        phaseMaxDelay = Math.min(currentMaxSlack, phases[i].getSlotMaxDelay());
        if (phaseMaxDelay < PHASE_DELAY_THRESHOLD) {
          phasesCurrentPositions[i] = 0;
          continue;
        }
      }

      // Initialise r to a random real number uniform in [0,1]
      double r = rand.nextDouble();
      // Initialise attractor to r multiplied by Phase(i)'s currentBestPositions(i) plus ( 1 minus r
      // ) multiplied by Phases(i)'s globalBestPositions(i)
      double attractor = r * phasesBestPositions[i] + (1 - r) * globalBestPositions[i];
      // Initialise c to a random real number with Cauchy distribution
      double c = nextCauchy();
      // Initialise step to c multiplied by (attractor minus Phase(i)'s currentPositions(i))
      double step = c * (attractor - phasesCurrentPositions[i]);
      // Set Phase(i)'s currentPosition to attractor plus step
      phasesCurrentPositions[i] = (float) (attractor + step);

      // If Phase(i)'s currentPosition is less than 0 or greater than phaseMaxDelay
      if (phasesCurrentPositions[i] < 0 || phasesCurrentPositions[i] > phaseMaxDelay)
        // Set Phase(i)'s currentPosition to 0
        phasesCurrentPositions[i] = 0;

      // Subtract to currentMaxSlack the new updated Phase(i)'s currentPosition
      currentMaxSlack -= phasesCurrentPositions[i];
    }

    tardiness = profileSlackInterval - currentMaxSlack;
  }

  void allocateBiasedPeakEnergy(float[] energyAllocation) {
    int start = profileEarliestStart;
    for (int i = 0; i < phases.length; ++i) {
      start += (int) Math.round(phasesCurrentPositions[i]);
      int duration = phases[i].getSlotDuration();
      allocateEnergyPhase(
          start, duration, phases[i].getOneSlotBiasedPeakEnergy(), energyAllocation);
      start += duration;
    }
  }

  void allocateMeanEnergy(float[] energyAllocation) {
    int start = profileEarliestStart;
    for (int i = 0; i < phases.length; ++i) {
      start += (int) Math.round(phasesCurrentPositions[i]);
      int duration = phases[i].getSlotDuration();
      allocateEnergyPhase(
          start, phases[i].getSlotDuration(), phases[i].getOneSlotMeanEnergy(), energyAllocation);
      start += duration;
    }
  }

  private void allocateEnergyPhase(
      int start, int duration, float oneSlotEnergy, float[] energyAllocation) {
    while (start + duration > energyAllocation.length) {
      start -= CalendarUtil.SLOTS_IN_ONE_DAY;
      oneSlotEnergy += EXCEEDING_TIME_ALLOCATION_PENALTY;
    }
    for (int i = duration; --i >= 0; energyAllocation[start + i] += oneSlotEnergy) ;
  }

  void allocateEnergy(float[] energyAllocation, boolean isOverloadCalculation) {
    int start = profileEarliestStart;
    for (int i = 0; i < phases.length; ++i) {
      start += (int) Math.round(phasesCurrentPositions[i]);
      int duration = phases[i].getSlotDuration();
      float oneSlotEnergy =
          isOverloadCalculation
              ? phases[i].getOneSlotBiasedPeakEnergy()
              : phases[i].getOneSlotMeanEnergy();

      int offset = start;
      while (offset + duration > energyAllocation.length) {
        offset -= CalendarUtil.SLOTS_IN_ONE_DAY;
        oneSlotEnergy += EXCEEDING_TIME_ALLOCATION_PENALTY;
      }
      for (int j = duration; --j >= 0; energyAllocation[offset + j] += oneSlotEnergy) ;
      start += duration;
    }
  }

  // copy current position and velocity as local best
  void setCurrentAsBest() {
    System.arraycopy(
        phasesCurrentPositions, 0, phasesBestPositions, 0, phasesCurrentPositions.length);
  }

  void setEnergyPhasesBestSchedule() {
    // 1st phase schedule is set as absolute, while all other phases are relative delays
    phases[0].setScheduledSlot(profileEarliestStart + (int) Math.round(phasesBestPositions[0]));
    for (int i = 1; i < phases.length; ++i) {
      phases[i].setScheduledSlot((int) Math.round(phasesBestPositions[i]));
    }
  }
}