/* Copyright 2009-2016 David Hadka
*
* This file is part of the MOEA Framework.
*
* The MOEA Framework is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* The MOEA Framework is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the MOEA Framework. If not, see <http://www.gnu.org/licenses/>.
*/
package org.moeaframework.util.progress;
import org.apache.commons.lang3.event.EventListenerSupport;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import org.moeaframework.Executor;
/**
* Helper for notifying {@link ProgressListener}s when the evaluation progress
* of an {@link Executor} changes. This class reports the current progress,
* percent complete, elapsed time, and remaining time. Be sure to first call
* {@link #start(int, int)} to set the total number of seeds and NFE prior to
* invoking any other method.
*/
public class ProgressHelper {
/**
* The listeners to receive progress reports.
*/
private final EventListenerSupport<ProgressListener> listeners;
/**
* Calculates the moving average processing speed for estimating remaining
* time.
*/
private final DescriptiveStatistics statistics;
/**
* The executor using this progress helper.
*/
private final Executor executor;
/**
* The current seed being evaluated, starting at 1.
*/
private int currentSeed;
/**
* The total number of seeds to be evaluated.
*/
private int totalSeeds;
/**
* The current number of objective function evaluations for the current
* seed.
*/
private int currentNFE;
/**
* The maximum number of objective function evaluations per seed.
*/
private int maxNFE;
/**
* The time the {@link #start(int, int)} method was invoked.
*/
private long startTime;
/**
* The last time that the {@link #updateStatistics()} method was invoked,
* used for estimating the remaining time.
*/
private long lastTime;
/**
* The seed that was evaluated when {@link #updateStatistics()} was last
* invoked, used for estimating the remaining time.
*/
private int lastSeed;
/**
* The NFE when {@link #updateStatistics()} was last invoked, used for
* estimating the remaining time.
*/
private int lastNFE;
/**
* Constructs a new progress helper for generating progress reports for
* the given executor.
*
* @param executor the executor that will be reporting progress
*/
public ProgressHelper(Executor executor) {
super();
this.executor = executor;
statistics = new DescriptiveStatistics(25);
listeners = EventListenerSupport.create(ProgressListener.class);
}
/**
* Adds the given listener to receive progress reports.
*
* @param listener the listener to receive progress reports
*/
public void addProgressListener(ProgressListener listener) {
listeners.addListener(listener);
}
/**
* Removes the given listener so it no longer receives progress reports.
*
* @param listener the listener to no longer receive progress reports
*/
public void removeProgressListener(ProgressListener listener) {
listeners.removeListener(listener);
}
/**
* Updates the moving average statistics used for estimating the remaining
* time. The processing speed is measured by the NFE completed since this
* method was last invoked.
*/
private void updateStatistics() {
long currentTime = System.currentTimeMillis();
// calculate the change since the last call
double diffTime = currentTime - lastTime;
double diffSeed = currentSeed - lastSeed;
double diffNFE = currentNFE - lastNFE;
double percentChange = (diffSeed + (diffNFE / maxNFE)) / totalSeeds;
// only update if the change was significant
if ((diffTime > 0.0) && (percentChange > 0.0001)) {
statistics.addValue(diffTime / percentChange);
// update the last values
lastTime = currentTime;
lastSeed = currentSeed;
lastNFE = currentNFE;
}
}
/**
* Sends a progress report to all registered listeners. Use the argument
* {@code isSeedFinished} to indicate that a seed has completed and
* results can be accessed from the executor.
*
* @param isSeedFinished {@code true} if this report is being sent as a
* result of a completed seed; {@code false} otherwise
*/
private void sendProgressEvent(boolean isSeedFinished) {
long currentTime = System.currentTimeMillis();
double remainingSeeds = totalSeeds - currentSeed;
double remainingNFE = maxNFE - currentNFE;
double percentRemaining = (remainingSeeds + (remainingNFE / maxNFE)) /
totalSeeds;
ProgressEvent event = new ProgressEvent(
executor,
currentSeed,
totalSeeds,
isSeedFinished,
currentNFE,
maxNFE,
Math.max(1.0 - percentRemaining, 0.0),
(currentTime - startTime) / 1000.0,
(statistics.getMean() * percentRemaining) / 1000.0);
listeners.fire().progressUpdate(event);
}
/**
* Sets the current number of objective function evaluations. This method
* will generate a progress report.
*
* @param currentNFE the current number of objective function evaluations
*/
public void setCurrentNFE(int currentNFE) {
this.currentNFE = currentNFE;
updateStatistics();
sendProgressEvent(false);
}
/**
* Sets the current seed. This call will have no affect if the current
* seed is unchanged. This method will generate a progress report if the
* current seed changes.
* <p>
* <b>It is strongly recommended that {@link #nextSeed()} is used instead
* of this method, as {@code nextSeed()} sets the current NFE to 0.</b>
*
* @param currentSeed the current seed being processed, starting at
* {@code 1}
*/
public void setCurrentSeed(int currentSeed) {
if (this.currentSeed != currentSeed) {
this.currentSeed = currentSeed;
updateStatistics();
sendProgressEvent(true);
}
}
/**
* Increments the current seed and sets NFE to 0. This method will
* generate a progress report. This method should be invoked after every
* seed completes in order to notify listeners that the seed completed.
*/
public void nextSeed() {
currentSeed++;
currentNFE = 0;
updateStatistics();
sendProgressEvent(true);
}
/**
* Prepares this progress helper for use. This method must be invoked
* prior to calling all other methods. The internal state of the progress
* helper is reset, allowing a single progress helper to be reused across
* many sequential runs.
*
* @param totalSeeds the total number of seeds to be evaluated
* @param maxNFE the maximum number of objective function evaluations per
* seed
*/
public void start(int totalSeeds, int maxNFE) {
this.totalSeeds = totalSeeds;
this.maxNFE = maxNFE;
// reset all internal parameters
lastSeed = 1;
lastNFE = 0;
currentSeed = 1;
currentNFE = 0;
statistics.clear();
startTime = System.currentTimeMillis();
lastTime = startTime;
}
/**
* Stops this progress helper. No other methods should be invoked after
* calling this method. However, {@link #start(int, int)} can be called
* to reset and restart this progress helper.
*/
public void stop() {
// this currently does nothing, but may be used in the future if we
// do anything to reduce the number of reports (i.e., send updates
// at most once every second)
}
}