/************************************************************************* * * * 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.reachables; import act.server.DBIterator; import act.server.MongoDB; import act.shared.Chemical; import act.shared.Chemical.REFS; import act.shared.Reaction; import com.mongodb.BasicDBObject; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.tuple.Pair; import org.json.JSONArray; import org.json.JSONObject; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; public class LoadAct extends SteppedTask { // Constants private static String _fileloc = "com.act.reachables.LoadAct"; private static final boolean SET_METADATA_ON_NW_NODES = false; private static final String DEFAULT_DB_HOST = "localhost"; private static final int DEFAULT_PORT = 27017; // Fields private MongoDB db; private Set<String> optional_universal_inchis; private Set<String> optional_cofactor_inchis; private int loaded, total; private List<String> fieldSetForChemicals; private LoadAct(String dbToUse, Set<String> optional_universal_inchis, Set<String> optional_cofactor_inchis) { this.optional_universal_inchis = optional_universal_inchis; this.optional_cofactor_inchis = optional_cofactor_inchis; this.fieldSetForChemicals = new ArrayList<>(); this.db = new MongoDB(DEFAULT_DB_HOST, DEFAULT_PORT, dbToUse); ActData.instance().Act = new Network("Act"); ActData.instance().ActTree = new Network("Act Tree"); // the following take time, so will be done in init(); ActData.instance().allrxnids = null; ActData.instance().cofactors = null; ActData.instance().natives = null; ActData.instance().chemInchis = null; ActData.instance().chemId2Inchis = null; ActData.instance().chemId2ReadableName = null; ActData.instance().chemToxicity = null; ActData.instance().chemsReferencedInRxns = null; GlobalParams._hostOrganismIDs = new Long[GlobalParams._hostOrganisms.length]; for (int i = 0; i<GlobalParams._hostOrganisms.length; i++) { GlobalParams._hostOrganismIDs[i] = this.db.getOrganismId(GlobalParams._hostOrganisms[i]); } total = -1; loaded = -1; } public static Network getReachablesTree(String dbToUse, Set<String> natives, Set<String> cofactors, boolean restrictToSeq, String[] extra_chem_fields) { GlobalParams._actTreeOnlyIncludeRxnsWithSequences = restrictToSeq; // init loader LoadAct act = new LoadAct(dbToUse, natives, cofactors); // set fields to include in the tree even if they are not reachables if (extra_chem_fields != null) for (String f : extra_chem_fields) act.setFieldForExtraChemicals(f); // load data from db; and compute reachables tree act.run(); return ActData.instance().ActTree; } public static Network getReachablesTree(String dbToUse, Set<String> natives, Set<String> cofactors, boolean restrictToSeq) { return getReachablesTree(dbToUse, natives, cofactors, restrictToSeq, null); } public static Network getReachablesTree(String dbToUse, Set<String> natives, Set<String> cofactors) { return getReachablesTree(dbToUse, natives, cofactors, true); } public static String toInChI(Long id) { return ActData.instance().chemId2Inchis.get(id); } private static void addToNw(Reaction rxn) { /** * This class takes in a reaction and adds it to ActData. * * Special handling is afforded to abstract reactions and reactions w/ only cofactors as substrates. */ Long rxnid = (long) rxn.getUUID(); // Filter out abstract substrates. These reactions are invalid Long[] inputChemicalsArray = ArrayUtils.addAll(rxn.getSubstrates(), rxn.getSubstrateCofactors()); // Some reactions have coenzymes, so don't forget about those. inputChemicalsArray = ArrayUtils.addAll(inputChemicalsArray, rxn.getCoenzymes()); if (Arrays.stream(inputChemicalsArray).anyMatch(x -> ActData.instance().metaCycBigMolsOrRgrp.contains(x))){ logProgress(String.format("Skipping reaction %d as it contains an abstract substrate.", rxnid)); return; } // Remove cofactors from the needed substrates. List<Long> inputChemicals = Arrays.asList(inputChemicalsArray).stream() .filter(c -> !ActData.instance().cofactors.contains(c)) .collect(Collectors.toList()); Long[] outputChemicalsArray = ArrayUtils.addAll(rxn.getProducts(), rxn.getProductCofactors()); // Output chemicals, only real ones. Also remove all of the cofactors as we don't care if those are produced. List<Long> outputChemicals = Arrays.asList(outputChemicalsArray).stream() .filter(p -> !ActData.instance().metaCycBigMolsOrRgrp.contains(p) && !ActData.instance().cofactors.contains(p)) .collect(Collectors.toList()); // Don't bother if we don't get any new chemicals from this. if (outputChemicals.isEmpty()){ logProgress(String.format("Skipping reaction %d as it has no non-abstract products.", rxnid)); return; } if (inputChemicals.isEmpty()) { logProgress(String.format("Undoing filtering on reaction %d substrates as it has only cofactor substrates.", rxnid)); // These will be substrate only inputChemicals = Arrays.asList(inputChemicalsArray); } // Load all the nodeMapping. inputChemicals.stream().distinct().forEach(s -> { Node sub = Node.get(s, true); ActData.instance().chemsInAct.put(s, sub); ActData.instance().Act.addNode(sub, s); outputChemicals.stream().distinct().forEach(p -> { Node prd = Node.get(p, true); ActData.instance().Act.addNode(prd, p); ActData.instance().chemsInAct.put(p, prd); Edge r = Edge.get(sub, prd, true); ActData.instance().Act.addEdge(r); }); }); Set<Long> inputSet = new HashSet<>(inputChemicals); Set<Long> outputSet = new HashSet<>(outputChemicals); /* --------------- Update ActData ------------------ */ ActData.instance().rxnSubstrates.put(rxnid, inputSet); ActData.instance().rxnProducts.put(rxnid, outputSet); inputSet.stream().forEach(ActData.instance().chemsReferencedInRxns::add); outputSet.stream().forEach(ActData.instance().chemsReferencedInRxns::add); inputSet.stream().forEach(s -> { Set<Long> pSet = ActData.instance().rxnsThatConsumeChem.getOrDefault(s, new HashSet<>()); pSet.add(rxnid); ActData.instance().rxnsThatConsumeChem.put(s, pSet); }); outputSet.stream().forEach(p -> { Set<Long> pSet = ActData.instance().rxnsThatProduceChem.getOrDefault(p, new HashSet<>()); pSet.add(rxnid); ActData.instance().rxnsThatProduceChem.put(p, pSet); }); // add to internal copy of network ActData.instance().rxnHasSeq.put(rxnid, rxn.hasProteinSeq()); /* -------- Reaction Classes ------- */ Pair<Set<Long>, Set<Long>> rxnClass = Pair.of(inputSet, outputSet); if (!ActData.instance().rxnClasses.contains(rxnClass)) { // the first reaction that shows up in this class, get to // represent the entire class. So we install it in the // datasets mirroring the non-class structures... ActData.instance().rxnClassesSubstrates.put(rxnid, inputSet); ActData.instance().rxnClassesProducts.put(rxnid, outputSet); ActData.instance().rxnClasses.add(rxnClass); } } public static void annotateRxnEdges(Reaction rxn, HashSet<Edge> rxn_edges) { for (Edge e : rxn_edges) { Edge.setAttribute(e, "isRxn", true); Edge.setAttribute(e, "datasource", rxn.getDataSource()); Edge.setAttribute(e, "srcRxnID", rxn.getUUID()); Edge.setAttribute(e, "srcRxn", rxn.getReactionName()); if (rxn.getECNum() != null) Edge.setAttribute(e, "srcRxnEC", rxn.getECNum()); } } public static boolean isCofactor(long m) { return ActData.instance().cofactors.contains(m); } private static void logProgress(String format, Object... args) { if (!GlobalParams.LOG_PROGRESS) return; System.err.format(_fileloc + ": " + format, args); } private static void logProgress(String msg) { if (!GlobalParams.LOG_PROGRESS) return; System.err.println(_fileloc + ": " + msg); } private List<Long> getAllIDsSorted() { List<Long> allids = this.db.getAllReactionUUIDs(); Collections.sort(allids); return allids; } private Set<Long> getNatives() { Set<Long> natives_ids = new HashSet<>(); if (this.optional_universal_inchis == null) { // pull whatever is in the DB List<Chemical> cs = this.db.getNativeMetaboliteChems(); for (Chemical c : cs) natives_ids.add(c.getUuid()); } else { // use the inchis provided to the constructor for (String inchi : this.optional_universal_inchis) { Chemical c = this.db.getChemicalFromInChI(inchi); if (c == null) { logProgress("LoadAct: WARNING: Starting native not in db."); logProgress("LoadAct: : InChI = " + inchi); continue; } natives_ids.add(c.getUuid()); } } logProgress(String.format("%d natives loaded into Act",natives_ids.size())); return natives_ids; } private Set<Long> getCofactors() { Set<Long> cofactor_ids = new HashSet<>(); if (this.optional_cofactor_inchis == null) { // pull whatever is in the DB List<Chemical> cs = this.db.getCofactorChemicals(); // TODO Add filter for chemicals w/o inchis for (Chemical c : cs) if (c.getInChI() != null) { cofactor_ids.add(c.getUuid()); } } else { // use the inchis provided to the constructor for (String inchi : this.optional_cofactor_inchis) { Chemical c = this.db.getChemicalFromInChI(inchi); if (c == null) { logProgress("LoadAct: SEVERE WARNING: Starting cofactor not in db."); logProgress("LoadAct: : InChI = " + inchi); continue; } cofactor_ids.add(c.getUuid()); } } return cofactor_ids; } public void setFieldForExtraChemicals(String f) { this.fieldSetForChemicals.add(f); } private HashMap<String, List<Long>> getChemicalWithUserSpecFields() { HashMap<String, List<Long>> specials = new HashMap<>(); for (String f : this.fieldSetForChemicals) { List<Chemical> cs = this.db.getChemicalsThatHaveField(f); specials.put(f, extractChemicalIDs(cs)); } return specials; } private Set<Long> getMetaCycBigMolsOrRgrp() { HashSet<Long> ids = new HashSet<>(); DBIterator chemCursor = this.db.getIdCursorForFakeChemicals(); while (chemCursor.hasNext()) { ids.add((Long) chemCursor.next().get("_id")); } chemCursor.close(); return ids; } private List<Long> extractChemicalIDs(List<Chemical> cs) { List<Long> cids = new ArrayList<Long>(); for (Chemical c : cs) cids.add(c.getUuid()); return cids; } private void addReactionsToNetwork() { // Ignore reactions with "protein" in their description. This is only necessary because there are some problems in // data parsing where we don't always put the proteins into the data, so sometimes fake data looks real. BasicDBObject noFakeReactions = new BasicDBObject("easy_desc", new BasicDBObject("$regex", "^((?!protein).)*$")); DBIterator iterator = this.db.getIteratorOverReactions(noFakeReactions, null); Reaction r; Map<Reaction.RxnDataSource, Integer> counts = new HashMap<>(); for (Reaction.RxnDataSource src : Reaction.RxnDataSource.values()) counts.put(src, 0); // since we are iterating until the end, // the getNextReaction call will close the DB cursor... while ((r = this.db.getNextReaction(iterator)) != null) { // this rxn comes from a datasource, METACYC, BRENDA or KEGG. // ensure the configuration tells us to include this datasource... Reaction.RxnDataSource src = r.getDataSource(); Set<Reaction> reactionsWithAccurateDirections = r.correctForReactionDirection(); counts.put(src, counts.get(src) + reactionsWithAccurateDirections.size()); // Correct for right-to-left and reversible actions, adding all appropriate directions to the graph. for (Reaction directedRxn : reactionsWithAccurateDirections) { addToNw(directedRxn); } } logProgress(""); logProgress("Rxn aggregate into %d classes.\n", ActData.instance().rxnClasses.size()); } @Override public double percentDone() { return 100.0 * ((double) this.loaded / this.total); } @Override public void doMoreWork() { logProgress("Pulling %d reactions from MongoDB:\n", this.total); addReactionsToNetwork(); this.loaded = this.total; } @Override public void init() { ActData.instance().allrxnids = getAllIDsSorted(); total = ActData.instance().allrxnids.size(); loaded = 0; ActData.instance().cofactors = getCofactors(); ActData.instance().natives = getNatives(); ActData.instance().chemicalsWithUserField = getChemicalWithUserSpecFields(); ActData.instance().chemicalsWithUserField_treeOrganic = new HashSet<>(); ActData.instance().chemicalsWithUserField_treeArtificial = new HashSet<>(); ActData.instance().metaCycBigMolsOrRgrp = getMetaCycBigMolsOrRgrp(); ActData.instance().chemsReferencedInRxns = new HashSet<>(); ActData.instance().chemsInAct = new HashMap<>(); ActData.instance().rxnsInAct = new HashMap<>(); ActData.instance().rxnSubstrates = new HashMap<>(); ActData.instance().rxnsThatConsumeChem = new HashMap<>(); ActData.instance().rxnsThatProduceChem = new HashMap<>(); ActData.instance().rxnProducts = new HashMap<>(); ActData.instance().rxnOrganisms = new HashMap<>(); ActData.instance().rxnSubstratesCofactors = new HashMap<>(); ActData.instance().rxnProductsCofactors = new HashMap<>(); ActData.instance().rxnHasSeq = new HashMap<>(); ActData.instance().rxnClassesSubstrates = new HashMap<>(); ActData.instance().rxnClassesProducts = new HashMap<>(); ActData.instance().rxnClasses = new HashSet<>(); ActData.instance().rxnClassesThatConsumeChem = new HashMap<>(); ActData.instance().rxnClassesThatProduceChem = new HashMap<>(); ActData.instance().noSubstrateRxnsToProducts = new HashMap<>(); logProgress("Initialization complete."); } @Override public void finalize(TaskMonitor tm) { logProgress("ComputeReachablesTree.. starting"); ActData.instance().chemToxicity = new HashMap<Long, Set<Integer>>(); ActData.instance().chemInchis = new HashMap<String, Long>(); ActData.instance().chemId2Inchis = new HashMap<Long, String>(); ActData.instance().chemId2ReadableName = new HashMap<Long, String>(); ActData.instance().chemIdIsAbstraction = new HashMap<Long, Boolean>(); pullChemicalsReferencedInRxns(); ActData.instance().chemsReferencedInRxns.addAll(ActData.instance().cofactors); ActData.instance().chemsReferencedInRxns.addAll(ActData.instance().natives); for (String f : ActData.instance().chemicalsWithUserField.keySet()) ActData.instance().chemsReferencedInRxns.addAll(ActData.instance().chemicalsWithUserField.get(f)); // computes reachables tree and writes it into ActData.instance().ActTree new ComputeReachablesTree(this.db); } private void pullChemicalsReferencedInRxns() { int N = ActData.instance().chemsReferencedInRxns.size(); int count = 0; logProgress("Extracting metadata from chemicals."); for (Long id : ActData.instance().chemsReferencedInRxns) { logProgress("\t pullChemicalsReferencedInRxns: %d\r", count++); Chemical c = this.db.getChemicalFromChemicalUUID(id); ActData.instance().chemInchis.put(c.getInChI(), id); ActData.instance().chemId2Inchis.put(id, c.getInChI()); ActData.instance().chemIdIsAbstraction.put(id, isAbstractInChI(c.getInChI())); String name = c.getShortestBRENDAName(); if (name == null) { // see if there is a metacyc name: Object meta = c.getRef(Chemical.REFS.METACYC, new String[] { "meta" }); if (meta != null) { // if we are here, the entry was referenced in metacyc, so must // have some name association there. see if we can pull that out. // the xref.METACYC.meta field should *always* be a JSONArray // (even if its an array with a single JSONObject within it) // sanity check that, and abort if the type does not match if (!(meta instanceof JSONArray)) { throw new RuntimeException("Expect only Arrays in db.chemicals.{xref.METACYC.meta}, but found: " + meta.getClass()); } name = ((JSONObject) ((JSONArray)meta).get(0) ).getString("sname"); // if failed to pull out a name from metacyc, report it if (name == null) System.out.println("ERROR: Looks like a metacyc entry chemical, but no metacyc name: " + id); } } if (name == null) { // Try to use wikipedia Object meta = c.getRef(Chemical.REFS.WIKIPEDIA, new String[] { "metadata" }); if (meta != null) { if (!(meta instanceof JSONObject)) { throw new RuntimeException("Unable to parse Wikipedia metadata."); } name = (String) ((JSONObject) meta).get("article"); } } if (name == null) { // stuff the inchi into the name name = c.getInChI(); } ActData.instance().chemId2ReadableName.put(id, name); if (SET_METADATA_ON_NW_NODES) { String[] xpath = { "metadata", "toxicity" }; Object o = c.getRef(REFS.DRUGBANK, xpath); if (o == null || !(o instanceof String)) continue; Set<Integer> ld50s = extractLD50vals((String)o); ActData.instance().chemToxicity.put(id, ld50s); // set chemical attributes String txt = null; // D MongoDB.chemicalAsString(c, id); Set<Integer> tox = ActData.instance().chemToxicity.get(id); Long n1 = ActData.instance().chemsInAct.get(id).getIdentifier(); int fanout = ActData.instance().rxnsThatConsumeChem.containsKey(id) ? ActData.instance().rxnsThatConsumeChem.get(id).size() : -1; int fanin = ActData.instance().rxnsThatProduceChem.containsKey(id) ? ActData.instance().rxnsThatProduceChem.get(id).size() : -1; setMetadata(n1, tox, c, txt, fanout, fanin); } } logProgress(""); } private boolean isAbstractInChI(String inchi) { // Create a Pattern object Pattern r = Pattern.compile("^InChI=1S\\/[A-Z0-9]*R"); return r.matcher(inchi).find(); } private Set<Integer> extractLD50vals(String annotation) { // an example of what we want to process is: "Oral, mouse: LD50 = 338 mg/kg; Oral, rat: LD50 = 1944 mg/kg" // a second example of what to process is: "Acute oral toxicity (LD<sub>50</sub>) in rats is 264 mg/kg." // a third example of what to process is : "Oral mouse and rat LD<sub>50</sub> are 338 mg/kg and 425 mg/kg respectively" // a fourth example of what to process is: "oral LD<sub>50</sub>s were 1,100-1,550 mg/kg; 1,450 mg/kg; and 1,490 mg/kg; respectively" // most of time it is specified as mg/kg but rarely we also have g/kg: "Oral LD50 in rat: >5 g/kg" Set<Integer> ld50s = new HashSet<Integer>(); int idx = 0; int len = annotation.length(); String[] locator = { "LD50", "LD<sub>50</sub>" }; int[] locator_len = { locator[0].length(), locator[1].length() }; int delta = 60; // 60 chars +- the locator for (int l = 0; l<locator.length; l++) { while ((idx = annotation.indexOf(locator[l], idx)) != -1) { // look around a few tokens to find numbers that we can use... int low = idx - delta < 0 ? 0 : idx - delta; int high = idx + delta > len ? len : idx + delta; String sub = annotation.substring(low, idx) + annotation.substring(idx + locator_len[l], high); Scanner scan = new Scanner(sub).useDelimiter("[^0-9,]+"); while (scan.hasNext()) { String scanned = scan.next().replaceAll(",", ""); try { ld50s.add(Integer.parseInt(scanned)); } catch (NumberFormatException e) { } } idx++; // so that we skip the occurrence that we just added } } return ld50s; } private void setMetadata(Long n, Set<Integer> tox, Chemical c, String fulltxt, int fanout, int fanin) { Node.setAttribute(n, "isNative", c.isNative()); Node.setAttribute(n, "fanout", fanout); Node.setAttribute(n, "fanin", fanin); if (c.getCanon() != null) Node.setAttribute(n, "canonical", c.getCanon()); if (c.getInChI() != null) Node.setAttribute(n, "InChI", c.getInChI()); if (c.getSmiles() != null) Node.setAttribute(n, "SMILES", c.getSmiles()); if (c.getShortestName() != null) Node.setAttribute(n, "Name", c.getShortestName()); if (c.getBrendaNames() != null && c.getSynonyms() != null) Node.setAttribute(n, "Synonyms", c.getBrendaNames().toString() + c.getSynonyms().toString()); setXrefs(n, c); if (fulltxt != null) Node.setAttribute(n, "fulltxt", fulltxt); if (tox != null && tox.size() > 0) { Node.setAttribute(n, "toxicity_all", tox.toString()); int max = Integer.MIN_VALUE, min = Integer.MAX_VALUE; for (int i : tox) { max = max < i ? i : max; min = min > i ? i : min; } Node.setAttribute(n, "toxicity_min", min); Node.setAttribute(n, "toxicity_max", max); } } private void setXrefs(Long node, Chemical c) { for (REFS typ : Chemical.REFS.values()) { if (c.getRef(typ) != null) { Double valuation = c.getRefMetric(typ); Node.setAttribute(node, typ.name(), c.getRef(typ).toString()); Node.setAttribute(node, "metric" + typ.name(), valuation == null ? -999999999.0 : valuation); Node.setAttribute(node, "log10metric" + typ.name(), valuation == null ? -99.0 : Math.log10(valuation)); Node.setAttribute(node, "has" + typ.name(), true); } else { Node.setAttribute(node, "has" + typ.name(), false); } } } }