com.act.lcms.db.analysis.StandardIonAnalysis.java Source code

Java tutorial

Introduction

Here is the source code for com.act.lcms.db.analysis.StandardIonAnalysis.java

Source

/*************************************************************************
*                                                                        *
*  This file is part of the 20n/act project.                             *
*  20n/act enables DNA prediction for synthetic biology/bioengineering.  *
*  Copyright (C) 2017 20n Labs, Inc.                                     *
*                                                                        *
*  Please direct all queries to act@20n.com.                             *
*                                                                        *
*  This program is free software: you can redistribute it and/or modify  *
*  it under the terms of the GNU General Public License as published by  *
*  the Free Software Foundation, either version 3 of the License, or     *
*  (at your option) any later version.                                   *
*                                                                        *
*  This program 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 General Public License for more details.                          *
*                                                                        *
*  You should have received a copy of the GNU General Public License     *
*  along with this program.  If not, see <http://www.gnu.org/licenses/>. *
*                                                                        *
*************************************************************************/

package com.act.lcms.db.analysis;

import com.act.lcms.XZ;
import com.act.lcms.db.io.DB;
import com.act.lcms.db.io.LoadPlateCompositionIntoDB;
import com.act.lcms.db.model.ChemicalAssociatedWithPathway;
import com.act.lcms.db.model.ConstructEntry;
import com.act.lcms.db.model.Plate;
import com.act.lcms.db.model.ScanFile;
import com.act.lcms.db.model.StandardIonResult;
import com.act.lcms.db.model.StandardWell;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;

import java.io.FileWriter;
import java.io.IOException;
import java.io.File;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;

public class StandardIonAnalysis {
    private static final boolean USE_SNR_FOR_LCMS_ANALYSIS = true;
    public static final String CSV_FORMAT = "csv";
    public static final String OPTION_DIRECTORY = "d";
    public static final String OPTION_CONSTRUCT = "c";
    public static final String OPTION_STANDARD_PLATE_BARCODE = "sp";
    public static final String OPTION_STANDARD_CHEMICAL = "sc";
    public static final String OPTION_OUTPUT_PREFIX = "o";
    public static final String OPTION_MEDIUM = "m";
    public static final String OPTION_PLOTTING_DIR = "p";
    public static final String OPTION_OVERRIDE_NO_SCAN_FILE_FOUND = "s";

    public static final String HELP_MESSAGE = StringUtils.join(new String[] { "TODO: write a help message." }, "");
    public static final HelpFormatter HELP_FORMATTER = new HelpFormatter();

    static {
        HELP_FORMATTER.setWidth(100);
    }

    public static final List<Option.Builder> OPTION_BUILDERS = new ArrayList<Option.Builder>() {
        {
            add(Option.builder(OPTION_DIRECTORY).argName("directory")
                    .desc("The directory where LCMS analysis results live").hasArg().required()
                    .longOpt("data-dir"));
            add(Option.builder(OPTION_CONSTRUCT).argName("construct")
                    .desc("The construct whose pathway chemicals should be analyzed").hasArg()
                    .longOpt("construct"));
            add(Option.builder(OPTION_STANDARD_PLATE_BARCODE).argName("standard plate barcode")
                    .desc("The plate barcode to use when searching for a compatible standard").hasArg()
                    .longOpt("standard-plate"));
            add(Option.builder(OPTION_STANDARD_CHEMICAL).argName("standard chemical")
                    .desc("The standard chemical to analyze").hasArg().longOpt("standard-chemical"));
            add(Option.builder(OPTION_OUTPUT_PREFIX).argName("output prefix")
                    .desc("A prefix for the output data/pdf files").hasArg().required().longOpt("output-prefix"));
            add(Option.builder(OPTION_MEDIUM).argName("medium").desc("A name of the medium to search wells by.")
                    .hasArg().longOpt("medium"));
            add(Option.builder(OPTION_PLOTTING_DIR).argName("plotting directory")
                    .desc("The absolute path of the plotting directory").hasArg().required()
                    .longOpt("plotting-dir"));
            add(Option.builder(OPTION_OVERRIDE_NO_SCAN_FILE_FOUND).argName("override option")
                    .desc("Do not fail when the scan file cannot be found").longOpt("override-option"));
        }
    };
    static {
        // Add DB connection options.
        OPTION_BUILDERS.addAll(DB.DB_OPTION_BUILDERS);
    }

    /**
     * This function gets all the best time windows from spectra in water and meoh media, so that they can analyzed
     * by the yeast media samples for snr analysis.
     * @param waterAndMeohSpectra A list of ions to best XZ value.
     * @return A map of ion to list of restricted time windows.
     */
    public static Map<String, List<Double>> getRestrictedTimeWindowsForIonsFromWaterAndMeOHMedia(
            List<LinkedHashMap<String, XZ>> waterAndMeohSpectra) {

        Map<String, List<Double>> ionToRestrictedTimeWindows = new HashMap<>();

        for (LinkedHashMap<String, XZ> entry : waterAndMeohSpectra) {
            for (String ion : entry.keySet()) {
                List<Double> restrictedTimes = ionToRestrictedTimeWindows.get(ion);
                if (restrictedTimes == null) {
                    restrictedTimes = new ArrayList<>();
                    ionToRestrictedTimeWindows.put(ion, restrictedTimes);
                }
                Double timeValue = entry.get(ion).getTime();
                restrictedTimes.add(timeValue);
            }
        }

        return ionToRestrictedTimeWindows;
    }

    /**
     * Given a construct id (like "pa1"), return the associated ConstructEntry object and a list of the chemical
     * products/byproducts associated with that pathway (including all intermediate and side-reaction products).
     * @param db The DB connection to query.
     * @param constructId The identifier for the constructs whose products should be queried (like "pa1").
     * @return A pair of the ConstructEntry for the specified construct id and a list of chemical products associated
     *         with that pathway.
     * @throws SQLException
     */
    public Pair<ConstructEntry, List<ChemicalAssociatedWithPathway>> getChemicalsForConstruct(DB db,
            String constructId) throws SQLException {
        ConstructEntry construct = ConstructEntry.getInstance().getCompositionMapEntryByCompositionId(db,
                constructId);
        if (construct == null) {
            throw new RuntimeException(String.format("Unable to find construct '%s'", constructId));
        }

        List<ChemicalAssociatedWithPathway> products = ChemicalAssociatedWithPathway.getInstance()
                .getChemicalsAssociatedWithPathwayByConstructId(db, constructId);

        return Pair.of(construct, products);
    }

    /**
     * Find all standard wells containing a specified chemical.
     * @param db The DB connection to query.
     * @param pathwayChem The chemical for which to find standard wells.
     * @return A list of standard wells (in any plate) containing the specified chemical.
     * @throws SQLException
     */
    public static List<StandardWell> getStandardWellsForChemical(DB db, String pathwayChem) throws SQLException {
        return StandardWell.getInstance().getStandardWellsByChemical(db, pathwayChem);
    }

    /**
     * Find all standard wells containing a specified chemical.
     * @param db The DB connection to query.
     * @param chemical The chemical for which to find standard wells.
     * @param plateId The plateId to filter by.
     * @return A list of standard wells (in any plate) containing the specified chemical.
     * @throws SQLException
     */
    public List<StandardWell> getStandardWellsForChemicalInSpecificPlate(DB db, String chemical, Integer plateId)
            throws SQLException {
        return StandardWell.getInstance().getStandardWellsByChemicalAndPlateId(db, chemical, plateId);
    }

    /**
     * Find all standard wells containing a specified chemical.
     * @param db The DB connection to query.
     * @param chemical The chemical for which to find standard wells.
     * @param plateId The plateId to filter by.
     * @param medium The medium of the plate to filter by.
     * @return A list of standard wells (in any plate) containing the specified chemical.
     * @throws SQLException
     */
    public List<StandardWell> getStandardWellsForChemicalInSpecificPlateAndMedium(DB db, String chemical,
            Integer plateId, String medium) throws SQLException {
        return StandardWell.getInstance().getStandardWellsByChemicalAndPlateIdAndMedium(db, chemical, plateId,
                medium);
    }

    public static List<StandardWell> getViableNegativeControlsForStandardWell(DB db, StandardWell baseStandard)
            throws SQLException, IOException, ClassNotFoundException {
        List<StandardWell> wellsFromSamePlate = StandardWell.getInstance().getByPlateId(db,
                baseStandard.getPlateId());

        // TODO: take availability of scan files into account here?
        List<StandardWell> candidates = new ArrayList<>();
        for (StandardWell well : wellsFromSamePlate) {
            if (well.getChemical().equals(baseStandard.getChemical())) {
                continue; // Skip wells with the same chemical.
            }

            if (baseStandard.getConcentration() != null && well.getConcentration() != null
                    && !baseStandard.getConcentration().equals(well.getConcentration())) {
                continue; // Skip non-matching concentrations if both wells define concentration.
            }

            if (baseStandard.getMedia() != null && well.getMedia() != null
                    && !baseStandard.getMedia().equals(well.getMedia())) {
                continue; // Skip non-matching media if both wells define media type.
            }
            candidates.add(well);
        }

        return candidates;
    }

    /**
     * Given a standard well and viable negative control candidates, returns a map of mapping of all specified standard
     * wells to scan files sharing the ion modes available for the specified standard well.  For example, if the specified
     * standard well has only positive ion mode scan files available, the map will contain only positive ion mode scan
     * files for that well and all specified negativeCandidate wells.  If both positive and negative ion mode scan files
     * are available for the specified well, then, both positive and negative mode scan files will be included in the map.
     * @param db The DB connection to query.
     * @param primaryStandard The primary standard well being analysed.
     * @param negativeCandidates A list of standard wells that could be used as negative controls in the analysis.
     * @return A map from all specified standard wells (primary and negative controls) to a list of scan files.
     * @throws SQLException
     */
    public Map<StandardWell, List<ScanFile>> getViableScanFilesForStandardWells(DB db, StandardWell primaryStandard,
            List<StandardWell> negativeCandidates) throws SQLException {
        Map<StandardWell, List<ScanFile>> wellToFilesMap = new HashMap<>();
        List<ScanFile> posScanFiles = ScanFile.getScanFileByPlateIDRowAndColumn(db, primaryStandard.getPlateId(),
                primaryStandard.getPlateRow(), primaryStandard.getPlateColumn());
        wellToFilesMap.put(primaryStandard, posScanFiles);

        Set<ScanFile.SCAN_MODE> viableScanModes = new HashSet<>();
        for (ScanFile file : posScanFiles) {
            viableScanModes.add(file.getMode());
        }

        for (StandardWell well : negativeCandidates) {
            List<ScanFile> allScanFiles = ScanFile.getScanFileByPlateIDRowAndColumn(db, well.getPlateId(),
                    well.getPlateRow(), well.getPlateColumn());
            List<ScanFile> viableScanFiles = new ArrayList<>();
            for (ScanFile file : allScanFiles) {
                if (viableScanModes.contains(file.getMode())) {
                    viableScanFiles.add(file);
                }
            }
            wellToFilesMap.put(well, viableScanFiles);
        }

        return wellToFilesMap;
    }

    /**
     * This function returns the best SNR values and their times for each metlin ion based on the StandardIonResult
     * datastructure and plots diagnostics.
     * @param lcmsDir The directory where the LCMS scan data can be found.
     * @param db The DB connection to query.
     * @param positiveStandardWell This is the positive standard well against which the snr comparison is done.
     * @param negativeStandardWells These are the negative standard wells which are used for benchmarking.
     * @param plateCache A hash of Plates already accessed from the DB.
     * @param chemical This is chemical of interest we are running ion analysis against.
     * @param plottingDir This is the directory where the plotting diagnostics will live.
     * @param restrictedTimeWindows This map of ion to list of doubles represent time windows over which the analysis has
     *                              to be done.
     * @return The StandardIonResult datastructure which contains the standard ion analysis results.
     * @throws Exception
     */
    public static StandardIonResult getSnrResultsForStandardWellComparedToValidNegativesAndPlotDiagnostics(
            File lcmsDir, DB db, StandardWell positiveStandardWell, List<StandardWell> negativeStandardWells,
            HashMap<Integer, Plate> plateCache, String chemical, String plottingDir,
            Map<String, List<Double>> restrictedTimeWindows) throws Exception {
        Plate plate = plateCache.get(positiveStandardWell.getPlateId());

        if (plate == null) {
            plate = Plate.getPlateById(db, positiveStandardWell.getPlateId());
            plateCache.put(plate.getId(), plate);
        }

        List<Pair<String, Double>> searchMZs;
        Pair<String, Double> searchMZ = Utils.extractMassFromString(db, chemical);
        if (searchMZ != null) {
            searchMZs = Collections.singletonList(searchMZ);
        } else {
            throw new RuntimeException("Could not find Mass Charge value for " + chemical);
        }

        List<StandardWell> allWells = new ArrayList<>();
        allWells.add(positiveStandardWell);
        allWells.addAll(negativeStandardWells);

        ChemicalToMapOfMetlinIonsToIntensityTimeValues peakData = AnalysisHelper.readStandardWellScanData(db,
                lcmsDir, searchMZs, ScanData.KIND.STANDARD, plateCache, allWells, false, null, null,
                USE_SNR_FOR_LCMS_ANALYSIS, positiveStandardWell.getChemical());

        if (peakData == null || peakData.getIonList().size() == 0) {
            return null;
        }

        LinkedHashMap<String, XZ> snrResults = WaveformAnalysis
                .performSNRAnalysisAndReturnMetlinIonsRankOrderedBySNR(peakData, chemical, restrictedTimeWindows);

        String bestMetlinIon = AnalysisHelper.getBestMetlinIonFromPossibleMappings(snrResults);

        Map<String, String> plottingFileMappings = peakData
                .plotPositiveAndNegativeControlsForEachMetlinIon(searchMZ, plottingDir, chemical, allWells);

        StandardIonResult result = new StandardIonResult();
        result.setChemical(chemical);
        result.setAnalysisResults(snrResults);
        result.setStandardWellId(positiveStandardWell.getId());
        result.setPlottingResultFilePaths(plottingFileMappings);
        result.setBestMetlinIon(bestMetlinIon);
        return result;
    }

    /**
     * This function returns the best metlion ions of the SNR analysis for each well.
     * @param chemical - This is chemical of interest we are running ion analysis against.
     * @param lcmsDir - The directory where the LCMS scan data can be found.
     * @param db
     * @param standardWells - The standard wells over which the analysis is done. This list is sorted in the following
     *                      order: wells in water or meoh are processed first, followed by everything else. This ordering
     *                      is required since the analysis on yeast media depends on the analysis of results in water/meoh.
     * @param plottingDir - This is the directory where the plotting diagnostics will live.
     * @return A mapping of the well that was analyzed to the standard ion result.
     * @throws Exception
     */
    public static Map<StandardWell, StandardIonResult> getBestMetlinIonsForChemical(String chemical, File lcmsDir,
            DB db, List<StandardWell> standardWells, String plottingDir) throws Exception {

        Map<StandardWell, StandardIonResult> result = new HashMap<>();
        List<LinkedHashMap<String, XZ>> waterAndMeohSpectra = new ArrayList<>();

        for (StandardWell wellToAnalyze : standardWells) {
            List<StandardWell> negativeControls = StandardIonAnalysis.getViableNegativeControlsForStandardWell(db,
                    wellToAnalyze);

            StandardIonResult cachingResult;
            if (StandardWell.doesMediaContainYeastExtract(wellToAnalyze.getMedia())) {
                // Since the standard wells are sorted in a way that the Water and MeOH media wells are analyzed first, we are
                // guaranteed to get the restricted time windows from these wells for the yeast media analysis.
                // TODO: Find a better way of doing this. There is a dependency on other ion analysis from other media and the way to
                // achieve this is by caching those ion runs first before the yeast media analysis. However, this algorithm is
                // dependant on which sequence gets analyzed first, which is brittle.
                Map<String, List<Double>> restrictedTimeWindows = getRestrictedTimeWindowsForIonsFromWaterAndMeOHMedia(
                        waterAndMeohSpectra);

                cachingResult = StandardIonResult.getForChemicalAndStandardWellAndNegativeWells(lcmsDir, db,
                        chemical, wellToAnalyze, negativeControls, plottingDir, restrictedTimeWindows);
            } else if (StandardWell.isMediaMeOH(wellToAnalyze.getMedia())
                    || StandardWell.isMediaWater(wellToAnalyze.getMedia())) {
                // If the media is not yeast, there is no time window restrictions, hence the last argument is null.
                cachingResult = StandardIonResult.getForChemicalAndStandardWellAndNegativeWells(lcmsDir, db,
                        chemical, wellToAnalyze, negativeControls, plottingDir, null);

                if (cachingResult != null) {
                    waterAndMeohSpectra.add(cachingResult.getAnalysisResults());
                }
            } else {
                // This case is redundant for now but in the future, more media might be added, in which case, this conditional
                // will handle it.
                cachingResult = StandardIonResult.getForChemicalAndStandardWellAndNegativeWells(lcmsDir, db,
                        chemical, wellToAnalyze, negativeControls, plottingDir, null);
            }

            if (cachingResult != null) {
                result.put(wellToAnalyze, cachingResult);
            }
        }

        return result;
    }

    public static void main(String[] args) throws Exception {
        Options opts = new Options();
        for (Option.Builder b : OPTION_BUILDERS) {
            opts.addOption(b.build());
        }

        CommandLine cl = null;
        try {
            CommandLineParser parser = new DefaultParser();
            cl = parser.parse(opts, args);
        } catch (ParseException e) {
            System.err.format("Argument parsing failed: %s\n", e.getMessage());
            HELP_FORMATTER.printHelp(LoadPlateCompositionIntoDB.class.getCanonicalName(), HELP_MESSAGE, opts, null,
                    true);
            System.exit(1);
        }

        if (cl.hasOption("help")) {
            HELP_FORMATTER.printHelp(LoadPlateCompositionIntoDB.class.getCanonicalName(), HELP_MESSAGE, opts, null,
                    true);
            return;
        }

        File lcmsDir = new File(cl.getOptionValue(OPTION_DIRECTORY));
        if (!lcmsDir.isDirectory()) {
            System.err.format("File at %s is not a directory\n", lcmsDir.getAbsolutePath());
            HELP_FORMATTER.printHelp(LoadPlateCompositionIntoDB.class.getCanonicalName(), HELP_MESSAGE, opts, null,
                    true);
            System.exit(1);
        }

        try (DB db = DB.openDBFromCLI(cl)) {
            ScanFile.insertOrUpdateScanFilesInDirectory(db, lcmsDir);
            StandardIonAnalysis analysis = new StandardIonAnalysis();
            HashMap<Integer, Plate> plateCache = new HashMap<>();

            String plateBarcode = cl.getOptionValue(OPTION_STANDARD_PLATE_BARCODE);
            String inputChemicals = cl.getOptionValue(OPTION_STANDARD_CHEMICAL);
            String medium = cl.getOptionValue(OPTION_MEDIUM);

            // If standard chemical is specified, do standard LCMS ion selection analysis
            if (inputChemicals != null && !inputChemicals.equals("")) {
                String[] chemicals;
                if (!inputChemicals.contains(",")) {
                    chemicals = new String[1];
                    chemicals[0] = inputChemicals;
                } else {
                    chemicals = inputChemicals.split(",");
                }

                String outAnalysis = cl.getOptionValue(OPTION_OUTPUT_PREFIX) + "." + CSV_FORMAT;
                String plottingDirectory = cl.getOptionValue(OPTION_PLOTTING_DIR);
                String[] headerStrings = { "Molecule", "Plate Bar Code", "LCMS Detection Results" };
                CSVPrinter printer = new CSVPrinter(new FileWriter(outAnalysis),
                        CSVFormat.DEFAULT.withHeader(headerStrings));

                for (String inputChemical : chemicals) {
                    List<StandardWell> standardWells;

                    Plate queryPlate = Plate.getPlateByBarcode(db,
                            cl.getOptionValue(OPTION_STANDARD_PLATE_BARCODE));
                    if (plateBarcode != null && medium != null) {
                        standardWells = analysis.getStandardWellsForChemicalInSpecificPlateAndMedium(db,
                                inputChemical, queryPlate.getId(), medium);
                    } else if (plateBarcode != null) {
                        standardWells = analysis.getStandardWellsForChemicalInSpecificPlate(db, inputChemical,
                                queryPlate.getId());
                    } else {
                        standardWells = analysis.getStandardWellsForChemical(db, inputChemical);
                    }

                    if (standardWells.size() == 0) {
                        throw new RuntimeException("Found no LCMS wells for " + inputChemical);
                    }

                    // Sort in descending order of media where MeOH and Water related media are promoted to the top and
                    // anything derived from yeast media are demoted.
                    Collections.sort(standardWells, new Comparator<StandardWell>() {
                        @Override
                        public int compare(StandardWell o1, StandardWell o2) {
                            if (StandardWell.doesMediaContainYeastExtract(o1.getMedia())
                                    && !StandardWell.doesMediaContainYeastExtract(o2.getMedia())) {
                                return 1;
                            } else {
                                return 0;
                            }
                        }
                    });

                    Map<StandardWell, StandardIonResult> wellToIonRanking = StandardIonAnalysis
                            .getBestMetlinIonsForChemical(inputChemical, lcmsDir, db, standardWells,
                                    plottingDirectory);

                    if (wellToIonRanking.size() != standardWells.size()
                            && !cl.hasOption(OPTION_OVERRIDE_NO_SCAN_FILE_FOUND)) {
                        throw new Exception("Could not find a scan file associated with one of the standard wells");
                    }

                    for (StandardWell well : wellToIonRanking.keySet()) {
                        LinkedHashMap<String, XZ> snrResults = wellToIonRanking.get(well).getAnalysisResults();

                        String snrRankingResults = "";
                        int numResultsToShow = 0;

                        Plate plateForWellToAnalyze = Plate.getPlateById(db, well.getPlateId());

                        for (Map.Entry<String, XZ> ionToSnrAndTime : snrResults.entrySet()) {
                            if (numResultsToShow > 3) {
                                break;
                            }

                            String ion = ionToSnrAndTime.getKey();
                            XZ snrAndTime = ionToSnrAndTime.getValue();

                            snrRankingResults += String.format(ion + " (%.2f SNR at %.2fs); ",
                                    snrAndTime.getIntensity(), snrAndTime.getTime());
                            numResultsToShow++;
                        }

                        String[] resultSet = { inputChemical,
                                plateForWellToAnalyze.getBarcode() + " " + well.getCoordinatesString() + " "
                                        + well.getMedia() + " " + well.getConcentration(),
                                snrRankingResults };

                        printer.printRecord(resultSet);
                    }
                }

                try {
                    printer.flush();
                    printer.close();
                } catch (IOException e) {
                    System.err.println("Error while flushing/closing csv writer.");
                    e.printStackTrace();
                }
            } else {
                // Get the set of chemicals that includes the construct and all it's intermediates
                Pair<ConstructEntry, List<ChemicalAssociatedWithPathway>> constructAndPathwayChems = analysis
                        .getChemicalsForConstruct(db, cl.getOptionValue(OPTION_CONSTRUCT));
                System.out.format("Construct: %s\n", constructAndPathwayChems.getLeft().getCompositionId());

                for (ChemicalAssociatedWithPathway pathwayChem : constructAndPathwayChems.getRight()) {
                    System.out.format("  Pathway chem %s\n", pathwayChem.getChemical());

                    // Get all the standard wells for the pathway chemicals. These wells contain only the
                    // the chemical added with controlled solutions (ie no organism or other chemicals in the
                    // solution)

                    List<StandardWell> standardWells;

                    if (plateBarcode != null) {
                        Plate queryPlate = Plate.getPlateByBarcode(db,
                                cl.getOptionValue(OPTION_STANDARD_PLATE_BARCODE));
                        standardWells = analysis.getStandardWellsForChemicalInSpecificPlate(db,
                                pathwayChem.getChemical(), queryPlate.getId());
                    } else {
                        standardWells = analysis.getStandardWellsForChemical(db, pathwayChem.getChemical());
                    }

                    for (StandardWell wellToAnalyze : standardWells) {
                        List<StandardWell> negativeControls = analysis.getViableNegativeControlsForStandardWell(db,
                                wellToAnalyze);
                        Map<StandardWell, List<ScanFile>> allViableScanFiles = analysis
                                .getViableScanFilesForStandardWells(db, wellToAnalyze, negativeControls);

                        List<String> primaryStandardScanFileNames = new ArrayList<>();
                        for (ScanFile scanFile : allViableScanFiles.get(wellToAnalyze)) {
                            primaryStandardScanFileNames.add(scanFile.getFilename());
                        }
                        Plate plate = plateCache.get(wellToAnalyze.getPlateId());
                        if (plate == null) {
                            plate = Plate.getPlateById(db, wellToAnalyze.getPlateId());
                            plateCache.put(plate.getId(), plate);
                        }

                        System.out.format("    Standard well: %s @ %s, '%s'%s%s\n", plate.getBarcode(),
                                wellToAnalyze.getCoordinatesString(), wellToAnalyze.getChemical(),
                                wellToAnalyze.getMedia() == null ? ""
                                        : String.format(" in %s", wellToAnalyze.getMedia()),
                                wellToAnalyze.getConcentration() == null ? ""
                                        : String.format(" @ %s", wellToAnalyze.getConcentration()));
                        System.out.format("      Scan files: %s\n",
                                StringUtils.join(primaryStandardScanFileNames, ", "));

                        for (StandardWell negCtrlWell : negativeControls) {
                            plate = plateCache.get(negCtrlWell.getPlateId());
                            if (plate == null) {
                                plate = Plate.getPlateById(db, negCtrlWell.getPlateId());
                                plateCache.put(plate.getId(), plate);
                            }
                            List<String> negativeControlScanFileNames = new ArrayList<>();
                            for (ScanFile scanFile : allViableScanFiles.get(negCtrlWell)) {
                                negativeControlScanFileNames.add(scanFile.getFilename());
                            }

                            System.out.format("      Viable negative: %s @ %s, '%s'%s%s\n", plate.getBarcode(),
                                    negCtrlWell.getCoordinatesString(), negCtrlWell.getChemical(),
                                    negCtrlWell.getMedia() == null ? ""
                                            : String.format(" in %s", negCtrlWell.getMedia()),
                                    negCtrlWell.getConcentration() == null ? ""
                                            : String.format(" @ %s", negCtrlWell.getConcentration()));
                            System.out.format("        Scan files: %s\n",
                                    StringUtils.join(negativeControlScanFileNames, ", "));
                            // TODO: do something useful with the standard wells and their scan files, and then stop all the printing.
                        }
                    }
                }
            }
        }
    }
}