FinalJudge.java

package de.uka.ipd.sdq.beagle.core.judge;

import de.uka.ipd.sdq.beagle.core.Blackboard;
import de.uka.ipd.sdq.beagle.core.BlackboardStorer;
import de.uka.ipd.sdq.beagle.core.ExternalCallParameter;
import de.uka.ipd.sdq.beagle.core.MeasurableSeffElement;
import de.uka.ipd.sdq.beagle.core.ResourceDemandingInternalAction;
import de.uka.ipd.sdq.beagle.core.SeffBranch;
import de.uka.ipd.sdq.beagle.core.SeffLoop;
import de.uka.ipd.sdq.beagle.core.analysis.ProposedExpressionAnalyserBlackboardView;
import de.uka.ipd.sdq.beagle.core.evaluableexpressions.EvaluableExpression;
import de.uka.ipd.sdq.beagle.core.timeout.Timeout;

import org.apache.commons.lang3.Validate;

import java.util.Set;

/**
 * Implements the break condition for evolution of evaluable expressions and decides which
 * proposed evaluable expression describes the measured result best and will be annotated
 * in the PCM.
 *
 * @author Christoph Michelbach
 */
public class FinalJudge implements BlackboardStorer<FinalJudgeData> {

	/**
	 * Fitness values bigger than this number will be set to this number and therefore
	 * will not be considered as the values they truly are. This includes infinity.
	 */
	public static final double MAX_CONSIDERED_FITNESS_VALUE = 1E250d;

	/**
	 * If a proposed expression is fitter than this value, it will be considered
	 * “perfect”, e.g. no further evolution of the element the expression was proposed for
	 * is needed.
	 */
	private static final double PERFECT_FITNESS = 0.5E-10d;

	/**
	 * If more generations than this have less than {@link #SIGNIFICANT_IMPROVEMENT}
	 * relative improvement to the previously best value, evolution of evaluable
	 * expressions will be stopped.
	 */
	private static final int MAX_NUMBER_OF_GENERATIONS_WITHOUT_SIGNIFICANT_IMPROVEMENT = 12;

	/**
	 * If more generations than
	 * {@link #MAX_NUMBER_OF_GENERATIONS_WITHOUT_SIGNIFICANT_IMPROVEMENT} have less than
	 * this much relative improvement to the previously best value, evolution of evaluable
	 * expressions will be stopped.
	 */
	private static final double SIGNIFICANT_IMPROVEMENT = 0.06;

	/**
	 * Stores all status of this {@link FinalJudge} object.
	 */
	private FinalJudgeData data;

	/**
	 * Initialises the {@link FinalJudge} object. Call this method before starting
	 * evolution of evaluable expressions to start counting the total time the entire
	 * evolution of evaluable expressions takes.
	 *
	 * @param blackboard The {@link Blackboard} to store the data of this
	 *            {@link FinalJudge} on. Must not be {@code null}.
	 *
	 */
	public void init(final Blackboard blackboard) {
		Validate.notNull(blackboard);

		blackboard.writeFor(FinalJudge.class, new FinalJudgeData());

		this.loadData(blackboard);

		final Timeout timeout = blackboard.getProjectInformation().getTimeout();
		timeout.init();
	}

	/**
	 * Implements the break condition for evolution of evaluable expressions. Decides
	 * whether evolution will be continued or stopped depending on the fitness of the
	 * fittest evaluable expression, the number of generations passed, the time passed,
	 * and the relative improvement of a generation.
	 *
	 * @param blackboard The {@link Blackboard} this {@link FinalJudge} operates on. Must
	 *            not be {@code null}.
	 * @return {@code true} to indicate that evolution of evaluable expressions will be
	 *         stopped; {@code false} otherwise.
	 */
	public boolean judge(final Blackboard blackboard) {
		Validate.notNull(blackboard);
		this.loadData(blackboard);
		this.data.newGeneration();

		// Determine the criteria which aren't CPU-intensive first.
		if (this.timePassedTooHigh(blackboard)) {
			// We are now destined to return {@code true}.
			this.data.setWillReturnTrue();
		}

		// Take the measurements.
		this.measureFitnessOfAllSeffElementsAndAddResultsToBlackboard(blackboard);

		if (this.allElementsArePerfect()) {
			// We are now destined to return {@code true}.
			this.data.setWillReturnTrue();
		} else if (!this.sufficientRelativeImprovement()) {
			// We are now destined to return {@code true}.
			this.data.setWillReturnTrue();
		}

		return this.data.isWillReturnTrue();
	}

	/**
	 * Loads the data stored for this object from the {@link Blackboard}.
	 *
	 * @param blackboard The {@link Blackboard} to use. Must not be {@code null}.
	 * @throws IllegalStateException Thrown if this method is called before
	 *             {@link #init()}.
	 */
	private void loadData(final Blackboard blackboard) throws IllegalStateException {
		Validate.notNull(blackboard);

		this.data = blackboard.readFor(FinalJudge.class);

		if (this.data == null) {
			throw new IllegalStateException("loadData(Blackboard) cannot be called on FinalJudge before init().");
		}
	}

	/**
	 * Measures the fitness of all seff branches, seff loops, rdias, and external call
	 * parameters on the {@link Blackboard} and for each seff element adds the expression
	 * describing it best to the blackboard.
	 *
	 * @param blackboard The {@link Blackboard} to use.
	 */
	private void measureFitnessOfAllSeffElementsAndAddResultsToBlackboard(final Blackboard blackboard) {
		final EvaluableExpressionFitnessFunction fitnessFunction = blackboard.getFitnessFunction();

		final Set<SeffBranch> seffBranches = blackboard.getAllSeffBranches();
		final Set<SeffLoop> seffLoops = blackboard.getAllSeffLoops();
		final Set<ResourceDemandingInternalAction> rdias = blackboard.getAllRdias();
		final Set<ExternalCallParameter> extCallParameters = blackboard.getAllExternalCallParameters();

		this.measureFitnessAndAddResultsToBlackboard(seffBranches, blackboard, fitnessFunction::gradeFor);
		this.measureFitnessAndAddResultsToBlackboard(seffLoops, blackboard, fitnessFunction::gradeFor);
		this.measureFitnessAndAddResultsToBlackboard(rdias, blackboard, fitnessFunction::gradeFor);
		this.measureFitnessAndAddResultsToBlackboard(extCallParameters, blackboard, fitnessFunction::gradeFor);
	}

	/**
	 * Measures the fitness of all {@linkplain MeasurableSeffElement measurable SEFF
	 * elements} and for each seff element adds the expression describing it best to the
	 * blackboard.
	 *
	 * <p/> CAUTION: All elements of {@code measurableSeffElements} have to be of type
	 * {@code SEFF_ELEMENT_TYPE}.
	 *
	 * @param <SEFF_ELEMENT_TYPE> The type of which all {@linkplain MeasurableSeffElement
	 *            MeasurableSeffElements} of the set {@code measurableSeffElements} are.
	 *            Must not be {@code null}.
	 *
	 * @param measurableSeffElements The set of {@linkplain MeasurableSeffElement
	 *            measurable SEFF elements} to operate on. Must not be {@code null}.
	 * @param blackboard The {@link Blackboard} to operate on. Must not be {@code null}.
	 * @param fitnessFunction The fitness function to use. Must not be {@code null}.
	 * @return {@code true} if {@code measurableSeffElements} contains an element with
	 *         sufficient fitness to stop evolution of evaluable expressions;
	 *         {@code false} otherwise.
	 */
	private <SEFF_ELEMENT_TYPE extends MeasurableSeffElement> boolean measureFitnessAndAddResultsToBlackboard(
		final Set<SEFF_ELEMENT_TYPE> measurableSeffElements, final Blackboard blackboard,
		final TypedFitnessFunction<SEFF_ELEMENT_TYPE> fitnessFunction) {

		final EvaluableExpressionFitnessFunctionBlackboardView fitnessFunctionView =
			new ProposedExpressionAnalyserBlackboardView(blackboard);

		for (final SEFF_ELEMENT_TYPE seffElement : measurableSeffElements) {
			double fittest = MAX_CONSIDERED_FITNESS_VALUE;
			EvaluableExpression bestExpression = null;
			// Then let it attain a better fitness if possible.
			for (final EvaluableExpression proposedExpression : blackboard.getProposedExpressionFor(seffElement)) {
				final double fitnessValue =
					fitnessFunction.gradeFor(seffElement, proposedExpression, fitnessFunctionView);
				// This has to be smaller-than (not smaller) because {@code fittest}
				// starts out as {@code MAX_CONSIDERED_FITNESS_VALUE}, not higher.
				if (fitnessValue <= fittest) {
					fittest = fitnessValue;
					bestExpression = proposedExpression;
				}

			}

			this.data.addFittestValue(fittest);

			// Add the best expression to the blackboard.
			blackboard.setFinalExpressionFor(seffElement, bestExpression);
		}

		return true;
	}

	/**
	 * Determines whether all seff elements on the Blackboard have a perfect expression
	 * proposed. This means that one of the element’s proposed expressions has a fitness
	 * value that’s below or equal to {@link #PERFECT_FITNESS}.
	 *
	 * @return {@code true} if all {@code measurableSeffElements} have an expression
	 *         proposed, such that its fitness is below or equal {@link #PERFECT_FITNESS}.
	 */
	private boolean allElementsArePerfect() {
		return this.data.getFittestValues().allMatch(fitnessValue -> fitnessValue <= PERFECT_FITNESS);
	}

	/**
	 * Determines whether the amount of time passed is too high.
	 *
	 * @param blackboard The {@link Blackboard}.
	 *
	 * @return {@code true} if and only if the timeout is reached.
	 */
	private boolean timePassedTooHigh(final Blackboard blackboard) {
		final Timeout timeout = blackboard.getProjectInformation().getTimeout();

		return timeout.isReached();
	}

	/**
	 * Evaluates the relative improvement to the latest
	 * {@linkplain FinalJudgeData#getFitnessBaseline() fitness baseline}.
	 *
	 * @return {@code true} if the relative improvement to the latest baseline is
	 *         sufficient to continue the analysis or there still can be tries without
	 *         significant improvement; {@code false} otherwise.
	 */
	private boolean sufficientRelativeImprovement() {
		final double fitnessBaselineValue = this.data.getFitnessBaselineValue();
		final double overallFitniss = this.data.getFittestValues().average().orElse(0);

		// This never is a division by zero because if it was, we’d already exited the
		// final judge before entering this method.
		final double relativeImprovement = 1 - overallFitniss / fitnessBaselineValue;

		if (relativeImprovement > SIGNIFICANT_IMPROVEMENT) {
			// There was significant improvement so note the new value and set the number
			// of generations without significant improvement passed back to 0.
			this.data.setFitnessBaselineValue(overallFitniss);
			this.data.setNumberOfGenerationsWithoutSignificantImprovementPassed(0);
			return true;
		} else {
			// Without significant improvement, first note this fact and then check
			// whether this was the last try without success.
			this.data.setNumberOfGenerationsWithoutSignificantImprovementPassed(
				this.data.getNumberOfGenerationsWithoutSignificantImprovementPassed() + 1);
			// @formatter:off
			return this.data.getNumberOfGenerationsWithoutSignificantImprovementPassed()
				<= MAX_NUMBER_OF_GENERATIONS_WITHOUT_SIGNIFICANT_IMPROVEMENT;
			// @formatter:on
		}
	}

	/**
	 * Provides the method {@code EvaluableExpressionFitnessFunction#gradeFor} for a
	 * specified {@code SEFF_ELEMENT_TYPE}.
	 *
	 * @author Christoph Michelbach
	 * @param <SEFF_ELEMENT_TYPE> The type of which all {@linkplain MeasurableSeffElement
	 *            MeasurableSeffElements} of the set {@code measurableSeffElements} are.
	 */
	private interface TypedFitnessFunction<SEFF_ELEMENT_TYPE extends MeasurableSeffElement> {

		/**
		 * Provides the method {@code EvaluableExpressionFitnessFunction#gradeFor} for a
		 * specified {@code SEFF_ELEMENT_TYPE}.
		 *
		 * @param seffElement A SEFF element. Must not be {@code null}.
		 * @param expression An expression proposed to describe {@code seffElement}’s
		 *            measurement results. Must not be {@code null}.
		 * @param blackboard Beagle’s blackboard instance. Must not be {@code null}.
		 * @return A value judging how well {@code expression} fits to describe the
		 *         corresponding measurement results. Will be a value between 0 and
		 *         {@link Double#MAX_VALUE}. The lower the value, the better the fitness.
		 */
		double gradeFor(SEFF_ELEMENT_TYPE seffElement, EvaluableExpression expression,
			EvaluableExpressionFitnessFunctionBlackboardView blackboard);
	}
}