/************************************************************************* * * * 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.analysis.similarity; import act.server.DBIterator; import act.server.MongoDB; import act.shared.Chemical; import chemaxon.formats.MolFormatException; import chemaxon.formats.MolImporter; import chemaxon.license.LicenseManager; import chemaxon.sss.SearchConstants; import chemaxon.sss.search.MolSearch; import chemaxon.sss.search.MolSearchOptions; import chemaxon.sss.search.SearchException; import chemaxon.struc.Molecule; import chemaxon.util.MolHandler; import com.act.utils.TSVParser; import com.act.utils.TSVWriter; 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.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.function.Consumer; /** * This class is based on Chris's substructure search from the biointerpretation branch. */ public class SubstructureSearch { private static final Logger LOGGER = LogManager.getFormatterLogger(SubstructureSearch.class); // TODO: are these options sufficient? Are there others we might want to use? /* Chemaxon exposes a very non-uniform means of configuring substructure search. Hence the mess of lambdas below. * Consumer solves the Function<T, void> problem. */ private static final Map<String, Consumer<MolSearchOptions>> SEARCH_OPTION_ENABLERS = Collections.unmodifiableMap(new HashMap<String, Consumer<MolSearchOptions>>() {{ put("CHARGE_MATCHING_EXACT", (so -> so.setChargeMatching(SearchConstants.CHARGE_MATCHING_EXACT))); put("CHARGE_MATCHING_IGNORE", (so -> so.setChargeMatching(SearchConstants.CHARGE_MATCHING_IGNORE))); put("IMPLICIT_H_MATCHING_ENABLED", (so -> so.setImplicitHMatching(SearchConstants.IMPLICIT_H_MATCHING_ENABLED))); put("IMPLICIT_H_MATCHING_DISABLED", (so -> so.setImplicitHMatching(SearchConstants.IMPLICIT_H_MATCHING_DISABLED))); put("IMPLICIT_H_MATCHING_IGNORE", (so -> so.setImplicitHMatching(SearchConstants.IMPLICIT_H_MATCHING_IGNORE))); put("STEREO_EXACT", (so -> so.setStereoSearchType(SearchConstants.STEREO_EXACT))); put("STEREO_IGNORE", (so -> so.setStereoSearchType(SearchConstants.STEREO_IGNORE))); put("STEREO_MODEL_COMPREHENSIVE", (so -> so.setStereoModel(SearchConstants.STEREO_MODEL_COMPREHENSIVE))); put("STEREO_MODEL_GLOBAL", (so -> so.setStereoModel(SearchConstants.STEREO_MODEL_GLOBAL))); put("STEREO_MODEL_LOCAL", (so -> so.setStereoModel(SearchConstants.STEREO_MODEL_LOCAL))); put("TAUTOMER_SEARCH_ON", (so -> so.setTautomerSearch(SearchConstants.TAUTOMER_SEARCH_ON))); put("TAUTOMER_SEARCH_OFF", (so -> so.setTautomerSearch(SearchConstants.TAUTOMER_SEARCH_OFF))); put("TAUTOMER_SEARCH_ON_IGNORE_TAUTOMERSTEREO", (so -> so.setTautomerSearch(SearchConstants.TAUTOMER_SEARCH_ON_IGNORE_TAUTOMERSTEREO))); put("VAGUE_BOND_OFF", (so -> so.setVagueBondLevel(SearchConstants.VAGUE_BOND_OFF))); put("VAGUE_BOND_LEVEL_HALF", (so -> so.setVagueBondLevel(SearchConstants.VAGUE_BOND_LEVEL_HALF))); put("VAGUE_BOND_LEVEL1", (so -> so.setVagueBondLevel(SearchConstants.VAGUE_BOND_LEVEL1))); put("VAGUE_BOND_LEVEL2", (so -> so.setVagueBondLevel(SearchConstants.VAGUE_BOND_LEVEL2))); put("VAGUE_BOND_LEVEL3", (so -> so.setVagueBondLevel(SearchConstants.VAGUE_BOND_LEVEL3))); put("VAGUE_BOND_LEVEL4", (so -> so.setVagueBondLevel(SearchConstants.VAGUE_BOND_LEVEL4))); }}); public static final List<String> VALID_SEARCH_OPTION_SORTED; static { List<String> keys = new ArrayList<>(SEARCH_OPTION_ENABLERS.keySet()); Collections.sort(keys); VALID_SEARCH_OPTION_SORTED = Collections.unmodifiableList(keys); } public static final String OPTION_INPUT_FILE = "f"; public static final String OPTION_INPUT_DB = "d"; public static final String OPTION_INPUT_DB_HOST = "s"; public static final String OPTION_INPUT_DB_PORT = "p"; public static final String OPTION_OUTPUT_FILE = "o"; public static final String OPTION_QUERY = "q"; public static final String OPTION_LICENSE_FILE = "l"; public static final String OPTION_SEARCH_OPTIONS = "x"; public static final String FIELD_INCHI = "inchi"; public static final String FIELD_ID = "id"; public static final String DEFAULT_HOST = "localhost"; public static final String DEFAULT_PORT = "27017"; public static final String HELP_MESSAGE = StringUtils.join(new String[] { "This class does substructure matching against an installer DB or a TSV file using a single SMILES query. ", "All matching chemicals are outputted; non-matches are ignored." }, ""); 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_INPUT_FILE) .argName("tsv file") .desc(String.format("The name of the input TSV file to read (must contain an '%s' field)", FIELD_INCHI)) .hasArg() .longOpt("tsv") ); add(Option.builder(OPTION_INPUT_DB) .argName("db name") .desc("The name of the database to read") .hasArg() .longOpt("db") ); add(Option.builder(OPTION_INPUT_DB_HOST) .argName("db host") .desc("The host to which connect when reading from a DB") .hasArg() .longOpt("host") ); add(Option.builder(OPTION_INPUT_DB_PORT) .argName("db port") .desc("The port to which connect when reading from a DB") .hasArg() .longOpt("port") ); add(Option.builder(OPTION_OUTPUT_FILE) .argName("file name") .desc("The name of the output tsv to write") .hasArg().required() .longOpt("out") ); add(Option.builder(OPTION_QUERY) .argName("query string") .desc("The SMILES query string for which to search") .hasArg().required() .longOpt("query") ); add(Option.builder(OPTION_LICENSE_FILE) .argName("license file") .desc("A chemaxon license file to use") .hasArg() .longOpt("license") ); add(Option.builder(OPTION_SEARCH_OPTIONS) .argName("search options") .desc(String.format("Options to supply to the substructure search. See " + "https://www.chemaxon.com/jchem/doc/dev/java/api/chemaxon/sss/SearchConstants.html " + "for explanations. Valid options are: %s", StringUtils.join(VALID_SEARCH_OPTION_SORTED, ", "))). hasArgs().valueSeparator(','). longOpt("search-opts") ); // Everybody needs a little help from their friends. add(Option.builder("h") .argName("help") .desc("Prints this help message") .longOpt("help") ); }}; MolSearch ms = new MolSearch(); // From https://docs.chemaxon.com/display/jchembase/Bond+specific+search+options. private final MolSearchOptions searchOptions = new MolSearchOptions(SearchConstants.SUBSTRUCTURE); public void init(String smilesQuery, List<String> extraOpts) throws IllegalArgumentException, IOException, MolFormatException { // Apply all the specified extra search options using the key -> function mapping above. for (String opt : extraOpts) { if (!SEARCH_OPTION_ENABLERS.containsKey(opt)) { throw new IllegalArgumentException(String.format("Unrecognized search option: %s", opt)); } SEARCH_OPTION_ENABLERS.get(opt).accept(searchOptions); } ms.setSearchOptions(searchOptions); ms.setQuery(new MolHandler(smilesQuery, true).getMolecule()); } public boolean matchSubstructure(Molecule target) throws SearchException { ms.setTarget(target); /* hits are arrays of atom ids in the target that matched the query. If multiple sites in the target matched, * then there should be multiple arrays of atom ids (but we don't care since we're just looking for any match). */ int[][] hits = ms.findAll(); if (hits != null) { for (int i = 0; i < hits.length; i++) { if (hits[i].length > 0) { return true; } } } return false; } 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(SubstructureSearch.class.getCanonicalName(), HELP_MESSAGE, opts, null, true); System.exit(1); } if (cl.hasOption("help")) { HELP_FORMATTER.printHelp(SubstructureSearch.class.getCanonicalName(), HELP_MESSAGE, opts, null, true); return; } if (cl.hasOption(OPTION_LICENSE_FILE)) { LicenseManager.setLicenseFile(cl.getOptionValue(OPTION_LICENSE_FILE)); } List<String> searchOpts = Collections.emptyList(); if (cl.hasOption(OPTION_SEARCH_OPTIONS)) { searchOpts = Arrays.asList(cl.getOptionValues(OPTION_SEARCH_OPTIONS)); } // Make sure we can initialize correctly before opening any file handles for writing. SubstructureSearch matcher = new SubstructureSearch(); try { matcher.init(cl.getOptionValue(OPTION_QUERY), searchOpts); } catch (IllegalArgumentException e) { System.err.format("Unable to initialize substructure search. %s\n", e.getMessage()); HELP_FORMATTER.printHelp(SubstructureSearch.class.getCanonicalName(), HELP_MESSAGE, opts, null, true); System.exit(1); } catch (MolFormatException e) { System.err.format("Invalid SMILES structure query. %s\n", e.getMessage()); HELP_FORMATTER.printHelp(SubstructureSearch.class.getCanonicalName(), HELP_MESSAGE, opts, null, true); System.exit(1); } Pair<List<String>, Iterator<Map<String, String>>> iterPair = null; if (cl.hasOption(OPTION_INPUT_FILE)) { File inFile = new File(cl.getOptionValue(OPTION_INPUT_FILE)); if (!inFile.exists()) { System.err.format("File at %s does not exist", inFile.getAbsolutePath()); HELP_FORMATTER.printHelp(SubstructureSearch.class.getCanonicalName(), HELP_MESSAGE, opts, null, true); System.exit(1); } iterPair = iterateOverTSV(inFile); } else if (cl.hasOption(OPTION_INPUT_DB)) { iterPair = iterateOverDB( cl.getOptionValue(OPTION_INPUT_DB_HOST, DEFAULT_HOST), Integer.parseInt(cl.getOptionValue(OPTION_INPUT_DB_HOST, DEFAULT_PORT)), cl.getOptionValue(OPTION_INPUT_DB) ); } else { System.err.format("Must specify either input TSV file or input DB from which to read.\n"); HELP_FORMATTER.printHelp(SubstructureSearch.class.getCanonicalName(), HELP_MESSAGE, opts, null, true); System.exit(1); } TSVWriter<String, String> writer = new TSVWriter<>(iterPair.getLeft()); writer.open(new File(cl.getOptionValue(OPTION_OUTPUT_FILE))); LOGGER.info("Seaching for substructure '%s'", cl.getOptionValue(OPTION_QUERY)); try { int rowNum = 0; while (iterPair.getRight().hasNext()) { Map<String, String> row = iterPair.getRight().next(); rowNum++; try { String inchi = row.get(FIELD_INCHI); Molecule target = null; try { target = MolImporter.importMol(inchi); } catch (Exception e) { LOGGER.warn("Skipping molecule %d due to exception: %s\n", rowNum, e.getMessage()); continue; } if (matcher.matchSubstructure(target)) { writer.append(row); writer.flush(); } else { // Don't output if not a match. LOGGER.debug("Found non-matching molecule: %s", inchi); } } catch (SearchException e) { LOGGER.error("Exception on input line %d: %s\n", rowNum, e.getMessage()); throw e; } } } finally { writer.close(); } LOGGER.info("Done with substructure search"); } public static Pair<List<String>, Iterator<Map<String, String>>> iterateOverTSV(File inputFile) throws Exception { TSVParser parser = new TSVParser(); parser.parse(inputFile); List<String> header = parser.getHeader(); Iterator<Map<String, String>> chemsIter = parser.getResults().iterator(); return Pair.of(header, chemsIter); } public static Pair<List<String>, Iterator<Map<String, String>>> iterateOverDB( String host, Integer port, String dbName) throws Exception { MongoDB db = new MongoDB(host, port, dbName); final DBIterator iter = db.getIteratorOverChemicals(); Iterator<Map<String, String>> chemsIter = new Iterator<Map<String, String>>() { @Override public boolean hasNext() { return iter.hasNext(); } @Override public Map<String, String> next() { Chemical c = db.getNextChemical(iter); return new HashMap<String, String>() {{ put(FIELD_ID, c.getUuid().toString()); put(FIELD_INCHI, c.getInChI()); }}; } }; return Pair.of(Arrays.asList(FIELD_ID, FIELD_INCHI), chemsIter); } }