package gdsc.smlm.engine; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import gdsc.core.logging.FileLogger; import gdsc.core.logging.Logger; import gdsc.smlm.filters.MaximaSpotFilter; import gdsc.smlm.results.PeakResults; /** * Fits local maxima using a 2D Gaussian. * <p> * Multi-threaded for speed. Uses a BlockingQueue to hold the ImageProcessor work which is then processed sequentially * by worker threads. The queue behaviour when the size is much greater than the number of worker threads can be * configured. */ public class FitEngine { private final BlockingQueue<FitJob> jobs; private final List<FitWorker> workers; private final List<Thread> threads; private long time; private final FitQueue queueType; private final PeakResults results; private boolean isAlive = true; private FitJob sum = null; // Used by the FitWorkers private int fitting; private MaximaSpotFilter spotFilter; private Logger logger = null; private FileLogger fileLogger = null; private FitTypeCounter counter = null; /** * Return the fitting window size calculated using the fitting parameter and the configured peak * widths. The actual window is 2n+1 around the local maxima. * * @return The size of the fitting window */ public int getFitting() { return fitting; } /** * @return The filter used for identifying candidate local maxima */ public MaximaSpotFilter getSpotFilter() { return spotFilter.clone(); } /** * Constructor * * @param config * The fit configuration * @param results * Output results * @param threads * The number of threads to use (set to 1 if less than 1) * @param queueType * Specify the queue behaviour */ public FitEngine(FitEngineConfiguration config, PeakResults results, int threads, FitQueue queueType) { this(config, results, threads, queueType, 3 * threads); } /** * Constructor * * @param config * The fit configuration * @param results * Output results * @param threads * The number of threads to use (set to 1 if less than 1) * @param queueType * Specify the queue behaviour * @param queueSize * The size of the queue ({@link #queueType} */ public FitEngine(FitEngineConfiguration config, PeakResults results, int threads, FitQueue queueType, int queueSize) { if (threads < 1) { threads = 1; queueSize = 3; } workers = new ArrayList<FitWorker>(threads); this.threads = new ArrayList<Thread>(threads); this.queueType = queueType; switch (queueType) { case BLOCKING: default: this.jobs = new ArrayBlockingQueue<FitJob>(queueSize); break; case NON_BLOCKING: case IGNORE: this.jobs = new LinkedBlockingQueue<FitJob>(); break; } this.results = results; fitting = config.getRelativeFitting(); spotFilter = config.createSpotFilter(true); logger = config.getFitConfiguration().getLog(); //// Allow debugging the fit process //try //{ // fileLogger = new FileLogger( // String.format("/tmp/%s%s.log", config.getFitConfiguration().getFitSolver().getShortName(), // (config.getFitConfiguration().isModelCamera()) ? "C" : "")); //} //catch (java.io.FileNotFoundException e) //{ //} // Allow logging the type of fit if (logger != null) counter = new FitTypeCounter(); // Create the workers for (int i = 0; i < threads; i++) { // Note - Clone the configuration and spot filter for each worker FitWorker worker = new FitWorker(config.clone(), results, jobs); worker.setSearchParameters(getSpotFilter(), fitting); worker.setLogger2(fileLogger); worker.setCounter(counter); Thread t = new Thread(worker); workers.add(worker); this.threads.add(t); t.start(); } } /** * Locate all the peaks in the given processor. Adds the work to the current queue. * * @param job * The job */ public void run(FitJob job) { if (!isAlive || job == null || job.getData() == null) return; // Check the output is still OK. If no output then there is no point running any calculations. if (results.isActive()) { // Allow the jobs to create a small backlog since some frames may process faster if (queueType == FitQueue.IGNORE && jobs.size() > threads.size() * 1.5) return; put(job); } else { isAlive = false; } } /** * Adds the work to the current queue. * * @param job * The job */ private void put(FitJob job) { try { jobs.put(job); } catch (InterruptedException e) { // TODO - Handle thread errors throw new RuntimeException("Unexpected interruption", e); } } /** * Signal that no more fitting work will be added to the queue. * <p> * Ask all threads to end and wait. Returns when all threads have stopped running. * * @param now * Stop the work immediately, otherwise finish all work in the queue */ public synchronized void end(boolean now) { if (threads.isEmpty()) return; if (sum != null) put(sum); // Final frame time = 0; if (now) { // Request worker shutdown for (FitWorker worker : workers) worker.finish(); // Workers may be waiting for a job. // Add null jobs if the queue is not at capacity so they can be collected by alive workers. // If there are already jobs then the worker will stop due to the finish() signal. for (int i = 0; i < threads.size(); i++) { jobs.offer(new FitJob()); // non-blocking add to queue } } else { // Finish all the worker threads by passing in a null job for (int i = 0; i < threads.size(); i++) { put(new FitJob()); // blocking add to queue } } // Collect all the threads for (int i = 0; i < threads.size(); i++) { try { threads.get(i).join(); time += workers.get(i).getTime(); } catch (InterruptedException e) { // TODO - Handle thread errors e.printStackTrace(); } } if (fileLogger != null) fileLogger.close(); // Output this to the log if (counter != null) { // Get the stats we want... //System.out.println(results.getName()); // Dataset name logger.info("Fitting paths..."); final int total = counter.getTotal(); final int single = counter.getUnset(FitType.MULTI); report("Single", single, total); report("Multi", total - single, total); final int ok = counter.getSet(FitType.OK); report("OK", ok, total); report("Fail", total - ok, total); final int multi = total - single; report("FailSingle", counter.getUnset(FitType.OK | FitType.MULTI), single); report("FailMulti", counter.get(FitType.MULTI, FitType.OK), multi); report("FitSingle", counter.get(FitType.OK, FitType.MULTI), ok); report("FitSingleSingle", counter.get(FitType.OK, FitType.MULTI | FitType.DOUBLET_OK), ok); report("FitSingleDoublet", counter.get(FitType.DOUBLET_OK, FitType.MULTI), ok); report("FitMulti", counter.getSet(FitType.OK | FitType.MULTI), ok); report("FitMultiSingle", counter.getSet(FitType.MULTI_OK), ok); report("FitMultiDoublet", counter.getSet(FitType.MULTI_DOUBLET_OK), ok); report("FailMultiFitSingle", counter.get(FitType.OK | FitType.MULTI, FitType.MULTI_OK | FitType.MULTI_DOUBLET_OK | FitType.DOUBLET_OK), ok); report("FailMultiFitDoublet", counter.get(FitType.OK | FitType.MULTI | FitType.DOUBLET_OK, FitType.MULTI_OK | FitType.MULTI_DOUBLET_OK), ok); } threads.clear(); } private void report(String name, int count, int total) { logger.info("%s %d / %d = %.2f", name, count, total, (100.00 * count) / total); //System.out.printf("%s %d / %d = %.2f\n", name, count, total, (100.00 * count) / total); } /** * @return the total fitting time */ public long getTime() { return time; } /** * If false then the engine can be shutdown by using {@link #end(boolean)} * * @return True if there are no worker threads */ public boolean isThreadsEmpty() { return threads.isEmpty(); } /** * @return True if there are no jobs queued */ public boolean isQueueEmpty() { return jobs.isEmpty(); } }