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