/*************************************************************************
* *
* 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.
}
}
}
}
}
}
}