package net.sf.openrocket.gui.dialogs.optimization;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import javax.swing.SwingUtilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.sf.openrocket.document.Simulation;
import net.sf.openrocket.optimization.general.FunctionOptimizer;
import net.sf.openrocket.optimization.general.OptimizationController;
import net.sf.openrocket.optimization.general.OptimizationException;
import net.sf.openrocket.optimization.general.ParallelExecutorCache;
import net.sf.openrocket.optimization.general.ParallelFunctionCache;
import net.sf.openrocket.optimization.general.Point;
import net.sf.openrocket.optimization.general.multidim.MultidirectionalSearchOptimizer;
import net.sf.openrocket.optimization.general.onedim.GoldenSectionSearchOptimizer;
import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter;
import net.sf.openrocket.optimization.rocketoptimization.OptimizationGoal;
import net.sf.openrocket.optimization.rocketoptimization.RocketOptimizationFunction;
import net.sf.openrocket.optimization.rocketoptimization.RocketOptimizationListener;
import net.sf.openrocket.optimization.rocketoptimization.SimulationDomain;
import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier;
import net.sf.openrocket.unit.Value;
import net.sf.openrocket.util.BugException;
/**
* A background worker that runs the optimization in the background. It supports providing
* evaluation and step counter information via listeners that are executed on the EDT.
*
* @author Sampo Niskanen <sampo.niskanen@iki.fi>
*/
public abstract class OptimizationWorker extends Thread implements OptimizationController, RocketOptimizationListener {
/*
* Note: This is implemented as a separate Thread object instead of a SwingWorker because
* the SwingWorker cannot be interrupted in any way except by canceling the task, which
* makes it impossible to wait for its exiting (SwingWorker.get() throws a CancellationException
* if cancel() has been called).
*
* SwingWorker also seems to miss some chunks that have been provided to process() when the
* thread ends.
*
* Nothing of this is documented, of course...
*/
private static final Logger log = LoggerFactory.getLogger(OptimizationWorker.class);
/** Notify listeners every this many milliseconds */
private static final long PURGE_TIMEOUT = 500;
/** End optimization when step size is below this threshold */
private static final double STEP_SIZE_LIMIT = 0.005;
private final FunctionOptimizer optimizer;
private final RocketOptimizationFunction function;
private final Simulation simulation;
private final SimulationModifier[] modifiers;
private final ParallelFunctionCache cache;
private final LinkedBlockingQueue<FunctionEvaluationData> evaluationQueue =
new LinkedBlockingQueue<FunctionEvaluationData>();
private final LinkedBlockingQueue<OptimizationStepData> stepQueue =
new LinkedBlockingQueue<OptimizationStepData>();
private volatile long lastPurge = 0;
private OptimizationException optimizationException = null;
/**
* Sole constructor
* @param simulation the simulation
* @param parameter the optimization parameter
* @param goal the optimization goal
* @param domain the optimization domain
* @param modifiers the simulation modifiers
*/
public OptimizationWorker(Simulation simulation, OptimizableParameter parameter,
OptimizationGoal goal, SimulationDomain domain, SimulationModifier... modifiers) {
this.simulation = simulation;
this.modifiers = modifiers.clone();
function = new RocketOptimizationFunction(simulation, parameter, goal, domain, modifiers);
function.addRocketOptimizationListener(this);
cache = new ParallelExecutorCache(1);
cache.setFunction(function);
if (modifiers.length == 1) {
optimizer = new GoldenSectionSearchOptimizer(cache);
} else {
optimizer = new MultidirectionalSearchOptimizer(cache);
}
}
@Override
public void run() {
try {
double[] current = new double[modifiers.length];
for (int i = 0; i < modifiers.length; i++) {
current[i] = modifiers[i].getCurrentScaledValue(simulation);
}
Point initial = new Point(current);
optimizer.optimize(initial, this);
} catch (OptimizationException e) {
this.optimizationException = e;
} finally {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
lastPurge = System.currentTimeMillis() + 24L * 3600L * 1000L;
processQueue();
done(optimizationException);
}
});
}
}
/**
* This method is called after the optimization has ended, either normally, when interrupted
* or by throwing an exception. This method is called on the EDT, like the done() method of SwingWorker.
* <p>
* All data chunks to the listeners will be guaranteed to have been processed before calling done().
*
* @param exception a possible optimization exception that occurred, or <code>null</code> for normal exit.
*/
protected abstract void done(OptimizationException exception);
/**
* This method is called for each function evaluation that has taken place.
* This method is called on the EDT.
*
* @param data the data accumulated since the last call
*/
protected abstract void functionEvaluated(List<FunctionEvaluationData> data);
/**
* This method is called after each step taken by the optimization algorithm.
* This method is called on the EDT.
*
* @param data the data accumulated since the last call
*/
protected abstract void optimizationStepTaken(List<OptimizationStepData> data);
/**
* Publishes data to the listeners. The queue is purged every PURGE_TIMEOUT milliseconds.
*
* @param data the data to publish to the listeners
*/
private synchronized void publish(FunctionEvaluationData evaluation, OptimizationStepData step) {
if (evaluation != null) {
evaluationQueue.add(evaluation);
}
if (step != null) {
stepQueue.add(step);
}
// Add a method to the EDT to process the queue data
long now = System.currentTimeMillis();
if (lastPurge + PURGE_TIMEOUT <= now) {
lastPurge = now;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
processQueue();
}
});
}
}
/**
* Process the queue and call the listeners. This method must always be called from the EDT.
*/
private void processQueue() {
if (!SwingUtilities.isEventDispatchThread()) {
throw new BugException("processQueue called from non-EDT");
}
List<FunctionEvaluationData> evaluations = new ArrayList<FunctionEvaluationData>();
evaluationQueue.drainTo(evaluations);
if (!evaluations.isEmpty()) {
functionEvaluated(evaluations);
}
List<OptimizationStepData> steps = new ArrayList<OptimizationStepData>();
stepQueue.drainTo(steps);
if (!steps.isEmpty()) {
optimizationStepTaken(steps);
}
}
/*
* NOTE: The stepTaken and evaluated methods may be called from other
* threads than the EDT or the SwingWorker thread!
*/
@Override
public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) {
publish(null, new OptimizationStepData(oldPoint, oldValue, newPoint, newValue, stepSize));
if (stepSize < STEP_SIZE_LIMIT) {
log.info("stepSize=" + stepSize + " is below limit, ending optimization");
return false;
} else {
return true;
}
}
@Override
public void evaluated(Point point, Value[] state, Value domainReference, Value parameterValue, double goalValue) {
publish(new FunctionEvaluationData(point, state, domainReference, parameterValue, goalValue), null);
}
}