/************************************************************************* * * * 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.MongoDB; import act.shared.Chemical; import act.shared.Reaction; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInput; import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; public class Network implements Serializable { private static final long serialVersionUID = 4643733150478812924L; String name; HashSet<Node> nodes; HashMap<Node, Node> nodeMapping; HashSet<Edge> edges; HashMap<Long, Node> idToNode; HashMap<Pair<Node, Node>, Edge> edgeHash; HashMap<Node, Long> nids; // it goes from Node -> Id coz sometimes same ids might be prefixed with r_ or c_ to distinguish categories of nodeMapping HashMap<Long, Edge> toParentEdge; // indexed by nodeid HashMap<Long, Long> parents; // indexed by nodeid HashMap<Long, Integer> tree_depth; HashMap<Node, Set<Edge>> edgesGoingToNode; HashMap<Long, Set<Edge>> edgesGoingToId; Network(String name) { this.name = name; this.nodes = new HashSet<Node>(); this.nodeMapping = new HashMap<Node, Node>(); this.edges = new HashSet<Edge>(); this.nids = new HashMap<Node, Long>(); this.tree_depth = new HashMap<Long, Integer>(); this.edgesGoingToNode = new HashMap<>(); this.edgesGoingToId = new HashMap<>(); this.edgeHash = new HashMap<>(); this.idToNode = new HashMap<>(); this.selectedNodes = new HashSet<Node>(); this.parents = new HashMap<>(); this.toParentEdge = new HashMap<>(); } public Map<Node, Long> nodesAndIds() { return this.nids; } public JSONArray disjointGraphs(MongoDB db) throws JSONException { return JSONDisjointGraphs.get(db, new HashSet(this.nodeMapping.values()), this.edges); } public JSONObject disjointTrees(MongoDB db) throws JSONException { return JSONDisjointTrees.get(db, new HashSet(this.nodeMapping.values()), this.edges, this.parents, this.toParentEdge); } void addNode(Node n, Long nid) { if (this.nodeMapping.containsKey(n)) { if (!Boolean.valueOf((String)Node.getAttribute(n.id, "isrxn"))) { return; } Node currentNode = this.nodeMapping.get(n); if (Node.getAttribute(nid, "reaction_ids") != null) { HashSet s = ((HashSet) Node.getAttribute(currentNode.id, "reaction_ids")); s.addAll((HashSet) Node.getAttribute(nid, "reaction_ids")); Node.setAttribute(currentNode.id, "reaction_ids", s); Node.setAttribute(currentNode.id, "reaction_count", s.size()); } if (Node.getAttribute(nid, "organisms") != null) { HashSet orgs = ((HashSet) Node.getAttribute(currentNode.id, "organisms")); orgs.addAll((HashSet) Node.getAttribute(nid, "organisms")); Node.setAttribute(currentNode.id, "organisms", orgs); } } else { this.idToNode.put(nid, n); this.nodeMapping.put(n, n); this.nids.put(n, nid); } } public Node getNodeById(Long id){ return this.idToNode.get(id); } void addEdge(Edge e) { this.edges.add(e); if (this.edgesGoingToNode.containsKey(e.getDst())) { this.edgesGoingToNode.get(e.getDst()).add(e); this.edgesGoingToId.get(e.getDst().id).add(e); } else { Set<Edge> newEdgeList = new HashSet<>(); newEdgeList.add(e); this.edgesGoingToNode.put(e.getDst(), newEdgeList); this.edgesGoingToId.put(e.getDst().id, newEdgeList); } } public Edge getEdge(Node src, Node dst) { return this.edgeHash.get(Pair.of(src, dst)); } public Set<Edge> getEdgesGoingInto(Node n) { return this.edgesGoingToNode.get(n); } public Set<Edge> getEdgesGoingInto(Long id) { return this.edgesGoingToId.get(id); } void mergeInto(Network that) { // this is only written to work for graphs, not // specifically trees, expect undefined behaviour // if you are keeping track of trees that.nodeMapping.values().forEach(n -> addNode(n, n.id)); that.edges.stream().forEach(this::addEdge); this.nids.putAll(that.nids); } public void serialize(String toFile) { try { OutputStream file = new FileOutputStream(toFile); OutputStream buffer = new BufferedOutputStream(file); ObjectOutput output = new ObjectOutputStream(buffer); try { output.writeObject(this); } finally { output.close(); } } catch(IOException ex) { throw new RuntimeException("Network serialize failed: " + ex); } } public static Network deserialize(String fromFile) { try { InputStream file = new FileInputStream(fromFile); InputStream buffer = new BufferedInputStream(file); ObjectInput input = new ObjectInputStream(buffer); try { return (Network) input.readObject(); } finally { input.close(); } } catch(ClassNotFoundException ex) { throw new RuntimeException("Network deserialize failed: Class not found: " + ex); } catch(IOException ex) { throw new RuntimeException("Network deserialize failed: IO problem: " + ex); } } public String toDOT() { List<String> lines = new ArrayList<String>(); lines.add("digraph " + this.name + " {"); for (Node n : new ArrayList<Node>(this.nodeMapping.values())) { String id; String label; String tooltip; String url; String color = Cascade.quote("black"); if (Boolean.valueOf((String)Node.getAttribute(n.id, "isrxn"))) { id = String.valueOf(n.getIdentifier()); int reactionCount = (int) Node.getAttribute(n.id, "reaction_count"); Set<String> rawLabel = (HashSet) Node.getAttribute(n.id, "label_string"); List<String> filteredRawLabel = rawLabel.stream().filter(x -> !x.equals("")).collect(Collectors.toList()); Long labelId = n.getIdentifier() - Cascade.rxnIdShift(); if (labelId < 0){ labelId = Reaction.reverseNegativeId(labelId); } HashSet<String> organisms = (HashSet<String>) Node.getAttribute(n.id, "organisms"); String fullLabel; if (filteredRawLabel.isEmpty()) { if ((boolean) Node.getAttribute(n.id, "isSpontaneous")){ fullLabel = "Spontaneous"; } else { fullLabel = "Not Available"; } } else { fullLabel = filteredRawLabel.get(0); if (filteredRawLabel.size() > 1) { fullLabel += " and " + String.valueOf(filteredRawLabel.size() - 1) + " more"; } } label = Cascade.quote(fullLabel); tooltip = Cascade.quote((String)Node.getAttribute(n.id, "tooltip_string")); if ((boolean) Node.getAttribute(n.id, "hasSequence")) { String forestGreen = "#228B22"; color = Cascade.quote(forestGreen); } else if ((boolean) Node.getAttribute(n.id, "isSpontaneous")) { String goldenrodYellow = "#E8BD2B"; color = Cascade.quote(goldenrodYellow); } else { String crimsonRed = "#DC143C"; color = Cascade.quote(crimsonRed); } url = Cascade.quote((String)Node.getAttribute(n.id, "url_string")); } else { id = String.valueOf(n.getIdentifier()); label = (String)Node.getAttribute(n.id, "label_string"); if (label == null) { label = n.id >= Cascade.rxnIdShift() ? "Reaction_" + n.id.toString() : "Chemical_" + n.id.toString(); } tooltip = (String)Node.getAttribute(n.id, "tooltip_string"); url = (String)Node.getAttribute(n.id, "url_string"); } String node_line = id + " [shape=box," + " label=" + label + "," + " tooltip=" + tooltip + "," + " URL=" + url + "," + " color=" + color + "," + "];"; lines.add(node_line); } for (Edge e : new ArrayList<Edge>(this.edges)) { // create a line for nodeMapping like so: // id -> id; Long src_id = e.getSrc().getIdentifier(); Long dst_id = e.getDst().getIdentifier(); String edge_line; if (e.getAttribute("color") != null) { edge_line = src_id + " -> " + dst_id + " [color=" + e.getAttribute("color") + "]" + ";"; } else { edge_line = src_id + " -> " + dst_id + ";"; } lines.add(edge_line); Edge.setAttribute(e, "color", null); } lines.add("}"); return StringUtils.join(lines.toArray(new String[0]), "\n"); } void addNodeTreeSpecific(Node n, Long nid, Integer atDepth, Long parentid) { this.nodeMapping.put(n, n); this.nids.put(n, nid); this.parents.put(n.id, parentid); this.tree_depth.put(nid, atDepth); } public HashMap<Long, Integer> nodeDepths() { return this.tree_depth; } void addEdgeTreeSpecific(Edge e, Long childnodeid) { this.edges.add(e); this.toParentEdge.put(childnodeid, e); } Long get_parent(Long n) { // all addNode's are called with Node's whose id is (id: Long).toString() return this.parents.get(n); } HashSet<Node> selectedNodes; void unselectAllNodes() { this.selectedNodes.clear(); } void setSelectedNodeState(Set<Node> nodes, boolean flag) { this.selectedNodes.addAll(nodes); } } class JSONDisjointTrees { public static JSONObject get(MongoDB db, Set<Node> nodes, Set<Edge> edges, HashMap<Long, Long> parentIds, HashMap<Long, Edge> toParentEdges) throws JSONException { // init the json object with structure: // { // "name": "nodeid" // "children": [ // { "name": "childnodeid", toparentedge: {}, nodedata:.. }, ... // ] // } HashMap<Long, Node> nodeById = new HashMap<>(); for (Node n : nodes) nodeById.put(n.id, n); HashMap<Long, JSONObject> nodeObjs = new HashMap<>(); // un-deconstruct tree... for (Long nid : parentIds.keySet()) { JSONObject nObj = JSONHelper.nodeObj(db, nodeById.get(nid)); nObj.put("name", nid); if (toParentEdges.get(nid) != null) { JSONObject eObj = JSONHelper.edgeObj(toParentEdges.get(nid), null /* no ordering reqd for referencing nodeMapping */); nObj.put("edge_up", eObj); } else { } nodeObjs.put(nid, nObj); } // now that we know that each node has an associated obj // link the objects together into the tree structure // put each object inside its parent HashSet<Long> unAssignedToParent = new HashSet<>(parentIds.keySet()); for (Long nid : parentIds.keySet()) { JSONObject child = nodeObjs.get(nid); // append child to "children" key within parent JSONObject parent = nodeObjs.get(parentIds.get(nid)); if (parent != null) { parent.append("children", child); unAssignedToParent.remove(nid); } else { } } // outputting a single tree makes front end processing easier // we can always remove the root in the front end and get the forest again // if many trees remain, assuming they indicate a disjoint forest, // add then as child to a proxy root. // if only one tree then return it JSONObject json; if (unAssignedToParent.size() == 0) { json = null; throw new RuntimeException("All nodeMapping have parents! Where is the root? Abort."); } else if (unAssignedToParent.size() == 1) { json = unAssignedToParent.toArray(new JSONObject[0])[0]; // return the only element in the set } else { json = new JSONObject(); for (Long cid : unAssignedToParent) { json.put("name" , "root"); json.append("children", nodeObjs.get(cid)); } } return json; } /* Note: there is a streaming variant of JSON tree serialization in the commit history if it is ever needed. * See f45bc81818322b8054cf88ae1c22ba5ad654a5d1 for details. */ } class JSONHelper { public static JSONObject nodeObj(MongoDB db, Node n) throws JSONException { Chemical thisChemical = db.getChemicalFromChemicalUUID(n.id); JSONObject no = thisChemical == null ? new JSONObject() : new JSONObject(ComputeReachablesTree.getExtendedChemicalInformationJSON(thisChemical)); no.put("id", n.id); HashMap<String, Serializable> attr = n.getAttr(); for (String k : attr.keySet()) { // only output the fields relevants to the reachables tree structure if (k.equals("NameOfLen20") || k.equals("ReadableName") || k.equals("Synonyms") || k.equals("InChI") || k.equals("InChiKEY") || k.equals("parent") || k.equals("under_root") || k.equals("num_children") || k.equals("subtreeVendorsSz") || k.equals("subtreeSz") || k.equals("SMILES")) no.put(k, attr.get(k).toString()); if (k.equals("has")) no.put(k, attr.get(k)); } // Object v; // String label = "" + ((v = n.getAttribute("canonical")) != null ? v : n.id ); // no.put("name", label ); // required // String layer = "" + ((v = n.getAttribute("globalLayer")) != null ? v : 1); // no.put("group", layer ); // required: node color by group return no; } public static JSONObject edgeObj(Edge e, HashMap<Node, Integer> order) throws JSONException { JSONObject eo = new JSONObject(); if (order != null) { // 1. when printing a graph (and not a tree), the source and target nodeMapping are identified // by the array index they appear in the nodeMapping JSONArray. Those indices are contained in the order-map. // 2. such an ordering is not required when we are working with trees, so these fields not output there. eo.put("source", order.get(e.src)); // required, and have to lookup its order in the node spec eo.put("target", order.get(e.dst)); // required, and have to lookup its order in the node spec } // eo.put("source_id", e.src.id); // only informational // eo.put("target_id", e.dst.id); // only informational // eo.put("value", 1); // weight of edge: not really needed HashMap<String, Serializable> attr = e.getAttr(); for (String k : attr.keySet()) { // only output the fields relevant to the reachables tree structures if (k.equals("under_root") || k.equals("functionalCategory") || k.equals("importantAncestor")) eo.put(k, attr.get(k).toString()); } return eo; } } class JSONDisjointGraphs { public static JSONArray get(MongoDB db, Set<Node> nodes, Set<Edge> edges) throws JSONException { // init the json object with structure: // { // "nodeMapping":[ // { "name":"Myriel", "group":1 }, ... // ], // "links":[ // { "source":1, "target":0, "value":1 }, ... // ] // } // nodeMapping.group specifies the node color // links.value specifies the edge weight JSONArray json = new JSONArray(); HashMap<Long, Set<Node>> treenodes = new HashMap<Long, Set<Node>>(); HashMap<Long, Set<Edge>> treeedges = new HashMap<Long, Set<Edge>>(); for (Node n : nodes) { Long k = (Long)n.getAttribute("under_root"); if (!treenodes.containsKey(k)) { treenodes.put(k, new HashSet<Node>()); treeedges.put(k, new HashSet<Edge>()); } treenodes.get(k).add(n); } for (Edge e : edges) { Long k = (Long)e.getAttribute("under_root"); if (!treeedges.containsKey(k)) { throw new RuntimeException("Fatal: Edge found rooted under a tree (under_root) that has no node!"); } treeedges.get(k).add(e); } for (Long root : treenodes.keySet()) { JSONObject tree = new JSONObject(); HashMap<Node, Integer> nodeOrder = new HashMap<Node, Integer>(); tree.put("nodeMapping", nodeListObj(db, treenodes.get(root), nodeOrder /*inits this ordering*/)); tree.put("links", edgeListObj(treeedges.get(root), nodeOrder /* uses the ordering */)); json.put(tree); } return json; } private static JSONArray nodeListObj(MongoDB db, Set<Node> treenodes, HashMap<Node, Integer> nodeOrder) throws JSONException { JSONArray a = new JSONArray(); Node[] nodesAr = treenodes.toArray(new Node[0]); for (int i = 0; i < nodesAr.length; i++) { Node n = nodesAr[i]; a.put(i, JSONHelper.nodeObj(db, n)); // put the object at index i in the array nodeOrder.put(n, i); } return a; } private static JSONArray edgeListObj(Set<Edge> treeedges, HashMap<Node, Integer> order) throws JSONException { JSONArray a = new JSONArray(); for (Edge e : treeedges) a.put(JSONHelper.edgeObj(e, order)); return a; } }