/************************************************************************* * * * 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.model; import com.act.lcms.XZ; import com.act.lcms.db.analysis.StandardIonAnalysis; import com.act.lcms.db.io.DB; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; import java.io.File; import java.io.IOException; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; public class StandardIonResult extends BaseDBModel<StandardIonResult> { public static final String TABLE_NAME = "standard_ion_result"; protected static final StandardIonResult INSTANCE = new StandardIonResult(); public static StandardIonResult getInstance() { return INSTANCE; } private enum DB_FIELD implements DBFieldEnumeration { ID(1, -1, "id"), CHEMICAL(2, 1, "chemical"), STANDARD_WELL_ID(3, 2, "standard_well_id"), NEGATIVE_WELL_IDS(4, 3, "negative_well_ids"), STANDARD_ION_RESULTS(5, 4, "standard_ion_results"), PLOTTING_RESULT_PATHS(6, 5, "plotting_result_paths"), BEST_METLIN_ION(7, 6, "best_metlin_ion"), MANUAL_OVERRIDE(8, 7, "manual_override"); private final int offset; private final int insertUpdateOffset; private final String fieldName; DB_FIELD(int offset, int insertUpdateOffset, String fieldName) { this.offset = offset; this.insertUpdateOffset = insertUpdateOffset; this.fieldName = fieldName; } @Override public int getOffset() { return offset; } @Override public int getInsertUpdateOffset() { return insertUpdateOffset; } @Override public String getFieldName() { return fieldName; } @Override public String toString() { return this.fieldName; } public static String[] names() { DB_FIELD[] values = DB_FIELD.values(); String[] names = new String[values.length]; for (int i = 0; i < values.length; i++) { names[i] = values[i].getFieldName(); } return names; } } protected static final List<String> ALL_FIELDS = Collections.unmodifiableList(Arrays.asList(DB_FIELD.names())); // id is auto-generated on insertion. protected static final List<String> INSERT_UPDATE_FIELDS = Collections.unmodifiableList(ALL_FIELDS.subList(1, ALL_FIELDS.size())); @Override public String getTableName() { return TABLE_NAME; } @Override public List<String> getAllFields() { return ALL_FIELDS; } @Override public List<String> getInsertUpdateFields() { return INSERT_UPDATE_FIELDS; } protected static final String GET_BY_ID_QUERY = StandardIonResult.getInstance().makeGetByIDQuery(); @Override protected String getGetByIDQuery() { return GET_BY_ID_QUERY; } protected static final String INSERT_QUERY = StandardIonResult.getInstance().makeInsertQuery(); @Override public String getInsertQuery() { return INSERT_QUERY; } protected static final String UPDATE_QUERY = StandardIonResult.getInstance().makeUpdateQuery(); @Override public String getUpdateQuery() { return UPDATE_QUERY; } @Override protected List<StandardIonResult> fromResultSet(ResultSet resultSet) throws SQLException, IOException, ClassNotFoundException { List<StandardIonResult> results = new ArrayList<>(); while (resultSet.next()) { Integer id = resultSet.getInt(DB_FIELD.ID.getOffset()); String chemical = resultSet.getString(DB_FIELD.CHEMICAL.getOffset()); Integer standardWellId = resultSet.getInt(DB_FIELD.STANDARD_WELL_ID.getOffset()); List<Integer> negativeWellIds = StandardIonResult.deserializeNegativeWellIds( resultSet.getString(DB_FIELD.NEGATIVE_WELL_IDS.getOffset())); LinkedHashMap<String, XZ> analysisResults = StandardIonResult.deserializeStandardIonAnalysisResult( resultSet.getString(DB_FIELD.STANDARD_ION_RESULTS.getOffset())); Map<String, String> plottingResultFilePaths = StandardIonResult.deserializePlottingPaths( resultSet.getString(DB_FIELD.PLOTTING_RESULT_PATHS.getOffset())); String bestMetlinIon = resultSet.getString(DB_FIELD.BEST_METLIN_ION.getOffset()); Integer manual_override_id = resultSet.getInt(DB_FIELD.MANUAL_OVERRIDE.getOffset()); if (resultSet.wasNull()) { manual_override_id = null; } results.add( new StandardIonResult(id, chemical, standardWellId, negativeWellIds, analysisResults, plottingResultFilePaths, bestMetlinIon, manual_override_id)); } return results; } private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); protected void bindInsertOrUpdateParameters( PreparedStatement stmt, String chemical, Integer standardWellId, List<Integer> negativeWellIds, LinkedHashMap<String, XZ> analysisResults, Map<String, String> plottingResultFileMapping, String bestMetlinIon, Integer manualOverrideId) throws SQLException, IOException { stmt.setString(DB_FIELD.CHEMICAL.getInsertUpdateOffset(), chemical); stmt.setInt(DB_FIELD.STANDARD_WELL_ID.getInsertUpdateOffset(), standardWellId); stmt.setString(DB_FIELD.NEGATIVE_WELL_IDS.getInsertUpdateOffset(), OBJECT_MAPPER.writeValueAsString(negativeWellIds)); stmt.setString(DB_FIELD.PLOTTING_RESULT_PATHS.getInsertUpdateOffset(), serializePlottingPaths(plottingResultFileMapping)); stmt.setString(DB_FIELD.STANDARD_ION_RESULTS.getInsertUpdateOffset(), serializeStandardIonAnalysisResult(analysisResults)); stmt.setString(DB_FIELD.BEST_METLIN_ION.getInsertUpdateOffset(), bestMetlinIon); if (manualOverrideId == null) { stmt.setNull(DB_FIELD.MANUAL_OVERRIDE.getInsertUpdateOffset(), Types.INTEGER); } else { stmt.setInt(DB_FIELD.MANUAL_OVERRIDE.getInsertUpdateOffset(), manualOverrideId); } } @Override protected void bindInsertOrUpdateParameters(PreparedStatement stmt, StandardIonResult ionResult) throws SQLException, IOException { bindInsertOrUpdateParameters(stmt, ionResult.getChemical(), ionResult.getStandardWellId(), ionResult.getNegativeWellIds(), ionResult.getAnalysisResults(), ionResult.getPlottingResultFilePaths(), ionResult.getBestMetlinIon(), ionResult.getManualOverrideId()); } private static final TypeReference<List<Integer>> typeRefForNegativeWells = new TypeReference<List<Integer>>() {}; private static final TypeReference<Map<String, XZ>> typeRefForStandardIonAnalysis = new TypeReference<Map<String, XZ>>() {}; private static final TypeReference<Map<String, String>> typeRefForPlottingPaths = new TypeReference<Map<String, String>>() {}; private static List<Integer> deserializeNegativeWellIds(String serializedNegativeIds) throws IOException { return OBJECT_MAPPER.readValue(serializedNegativeIds, typeRefForNegativeWells); } private static LinkedHashMap<String, XZ> deserializeStandardIonAnalysisResult(String jsonEntry) throws IOException { // We have to re-sorted the deserialized results so that we meet the contract expected by the caller. Map<String, XZ> deserializedResult = OBJECT_MAPPER.readValue(jsonEntry, typeRefForStandardIonAnalysis); TreeMap<Double, String> sortedIntensityToIon = new TreeMap<>(Collections.reverseOrder()); for (Map.Entry<String, XZ> val : deserializedResult.entrySet()) { sortedIntensityToIon.put(val.getValue().getIntensity(), val.getKey()); } LinkedHashMap<String, XZ> sortedResult = new LinkedHashMap<>(); for (Map.Entry<Double, String> val : sortedIntensityToIon.entrySet()) { String ion = val.getValue(); sortedResult.put(ion, deserializedResult.get(ion)); } return sortedResult; } private static String serializeStandardIonAnalysisResult( LinkedHashMap<String, XZ> analysis) throws IOException { return OBJECT_MAPPER.writeValueAsString(analysis); } private static Map<String, String> deserializePlottingPaths(String jsonEntry) throws IOException { return OBJECT_MAPPER.readValue(jsonEntry, typeRefForPlottingPaths); } private static String serializePlottingPaths( Map<String, String> analysis) throws IOException { return OBJECT_MAPPER.writeValueAsString(analysis); } public static StandardIonResult getForChemicalAndStandardWellAndNegativeWells(File lcmsDir, DB db, String chemical, StandardWell standardWell, List<StandardWell> negativeWells, String plottingDirectory, Map<String, List<Double>> restrictedTimeWindows) throws Exception { return StandardIonResult.getInstance().getByChemicalAndStandardWellAndNegativeWells( lcmsDir, db, chemical, standardWell, negativeWells, plottingDirectory, restrictedTimeWindows); } /** * This function gets a Standard Ion Result based on the input parameters. * @param lcmsDir The directory where the LCMS scans live. * @param db The DB from which to extract plate data. * @param chemical The name of the chemical that is to be analyzed. * @param standardWell The standard well from which the data is extracted. * @param negativeWells The negative wells against which benchmark tests are done. * @param plottingDirectory The dir where the plotted graphs are stored in. * @return A StandardIonResult object that encapsulates the standard ion analysis data. * @throws Exception */ public StandardIonResult getByChemicalAndStandardWellAndNegativeWells(File lcmsDir, DB db, String chemical, StandardWell standardWell, List<StandardWell> negativeWells, String plottingDirectory, Map<String, List<Double>> restrictedTimeWindows) throws Exception { List<Integer> negativeWellIds = new ArrayList<>(negativeWells.size()); for (StandardWell negativeWell : negativeWells) { negativeWellIds.add(negativeWell.getId()); } Collections.sort(negativeWellIds); StandardIonResult cachedResult = this.getByChemicalAndStandardWellAndNegativeWells( db, chemical, standardWell.getId(), negativeWellIds); if (cachedResult == null) { StandardIonResult computedResult = StandardIonAnalysis.getSnrResultsForStandardWellComparedToValidNegativesAndPlotDiagnostics( lcmsDir, db, standardWell, negativeWells, new HashMap<>(), chemical, plottingDirectory, restrictedTimeWindows); if (computedResult == null) { return null; } else { computedResult.setNegativeWellIds(negativeWellIds); computedResult.setManualOverrideId(null); return insert(db, computedResult); } } else { return cachedResult; } } /** * This function takes as input a list of standard ion results and outputs a map of media to list of ion results. * @param db * @param standardIonResults * @return A mapping of media to list of ion results * @throws IOException * @throws SQLException * @throws ClassNotFoundException */ public static Map<String, List<StandardIonResult>> categorizeListOfStandardWellsByMedia( DB db, List<StandardIonResult> standardIonResults) throws IOException, SQLException, ClassNotFoundException { Map<String, List<StandardIonResult>> categories = new HashMap<>(); for (StandardIonResult result : standardIonResults) { if (StandardWell.isMediaMeOH( StandardWell.getInstance().getById(db, result.getStandardWellId()).getMedia())) { List<StandardIonResult> res = categories.get(StandardWell.MEDIA_TYPE.MEOH.name()); if (res == null) { res = new ArrayList<>(); } res.add(result); categories.put(StandardWell.MEDIA_TYPE.MEOH.name(), res); } else if (StandardWell.doesMediaContainYeastExtract( StandardWell.getInstance().getById(db, result.getStandardWellId()).getMedia())) { List<StandardIonResult> res = categories.get(StandardWell.MEDIA_TYPE.YEAST.name()); if (res == null) { res = new ArrayList<>(); } res.add(result); categories.put(StandardWell.MEDIA_TYPE.YEAST.name(), res); } else if (StandardWell.isMediaWater( StandardWell.getInstance().getById(db, result.getStandardWellId()).getMedia())) { List<StandardIonResult> res = categories.get(StandardWell.MEDIA_TYPE.WATER.name()); if (res == null) { res = new ArrayList<>(); } res.add(result); categories.put(StandardWell.MEDIA_TYPE.WATER.name(), res); } } return categories; } // Extra access patterns. public static final String GET_BY_CHEMICAL_AND_STANDARD_WELL_AND_NEGATIVE_WELLS = StringUtils.join(new String[]{ "SELECT", StringUtils.join(StandardIonResult.getInstance().getAllFields(), ','), "from", StandardIonResult.getInstance().getTableName(), "where chemical = ?", " and standard_well_id = ?", " and negative_well_ids = ?", }, " "); private StandardIonResult getByChemicalAndStandardWellAndNegativeWells(DB db, String chemical, Integer standardWellId, List<Integer> negativeWellIds) throws Exception { try (PreparedStatement stmt = db.getConn().prepareStatement(GET_BY_CHEMICAL_AND_STANDARD_WELL_AND_NEGATIVE_WELLS)) { stmt.setString(1, chemical); stmt.setInt(2, standardWellId); stmt.setString(3, OBJECT_MAPPER.writeValueAsString(negativeWellIds)); try (ResultSet resultSet = stmt.executeQuery()) { StandardIonResult result = expectOneResult(resultSet, String.format("chemical = %s, standard_well_id = %d, negative_well_ids = %s", chemical, standardWellId, OBJECT_MAPPER.writeValueAsString(negativeWellIds))); return result; } } } public static List<StandardIonResult> getByChemicalName(DB db, String chemical) throws Exception { return StandardIonResult.getInstance().getForChemicalName(db, chemical); } private List<StandardIonResult> getForChemicalName(DB db, String chemical) throws Exception { try (PreparedStatement stmt = db.getConn().prepareStatement(makeGetQueryForSelectField(DB_FIELD.CHEMICAL.getFieldName()))) { stmt.setString(1, chemical); try (ResultSet resultSet = stmt.executeQuery()) { return fromResultSet(resultSet); } } } private Integer id; private String chemical; private Integer standardWellId; private List<Integer> negativeWellIds; private String bestMetlinIon; private LinkedHashMap<String, XZ> analysisResults; private Map<String, String> plottingResultFilePaths; private Integer manualOverrideId; public StandardIonResult() {} public StandardIonResult(Integer id, String chemical, Integer standardWellId, List<Integer> negativeWellIds, LinkedHashMap<String, XZ> analysisResults, Map<String, String> plottingResultFilePaths, String bestMelinIon, Integer manualOverrideId) { this.id = id; this.chemical = chemical; this.standardWellId = standardWellId; this.negativeWellIds = negativeWellIds; this.plottingResultFilePaths = plottingResultFilePaths; this.analysisResults = analysisResults; this.bestMetlinIon = bestMelinIon; this.manualOverrideId = manualOverrideId; } @Override public Integer getId() { return id; } @Override public void setId(Integer id) { this.id = id; } public String getChemical() { return chemical; } public void setChemical(String chemical) { this.chemical = chemical; } public String getBestMetlinIon() { return bestMetlinIon; } public void setBestMetlinIon(String bestMetlinIon) { this.bestMetlinIon = bestMetlinIon; } public Integer getStandardWellId() { return standardWellId; } public void setStandardWellId(Integer standardWellId) { this.standardWellId = standardWellId; } public List<Integer> getNegativeWellIds() { return negativeWellIds; } public void setNegativeWellIds(List<Integer> negativeWellIds) { this.negativeWellIds = negativeWellIds; } public LinkedHashMap<String, XZ> getAnalysisResults() { return analysisResults; } public void setAnalysisResults(LinkedHashMap<String, XZ> result) { this.analysisResults = result; } public Map<String, String> getPlottingResultFilePaths() { return plottingResultFilePaths; } public void setPlottingResultFilePaths(Map<String, String> plottingResultFilePaths) { this.plottingResultFilePaths = plottingResultFilePaths; } public Integer getManualOverrideId() { return manualOverrideId; } public void setManualOverrideId(Integer manualOverrideId) { this.manualOverrideId = manualOverrideId; } }