/*************************************************************************
* *
* 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 act.shared;
import act.shared.helpers.P;
import org.biopax.paxtools.model.level3.CatalysisDirectionType;
import org.biopax.paxtools.model.level3.ConversionDirectionType;
import org.biopax.paxtools.model.level3.StepDirection;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Reaction implements Serializable {
private static final long serialVersionUID = 42L;
Reaction() { /* default constructor for serialization */ }
public enum RxnDataSource { BRENDA, KEGG, METACYC, MERGED }; // Note: MERGED should be last.
public enum RefDataSource { PMID, BRENDA, KEGG, METACYC };
public enum RxnDetailType { CONCRETE, ABSTRACT };
private int uuid;
private RxnDataSource dataSource;
protected Long[] substrates, products;
protected Long[] substrateCofactors, productCofactors;
protected Long[] coenzymes;
protected Map<Long, Integer> substrateCoefficients, productCoefficients;
private Double estimatedEnergy;
private String ecnum, rxnName;
private RxnDetailType type = RxnDetailType.CONCRETE;
private Set<P<RefDataSource, String>> references;
private Set<JSONObject> proteinData;
private Set<String> keywords;
private Set<String> caseInsensitiveKeywords;
private ConversionDirectionType conversionDirection;
private StepDirection pathwayStepDirection;
private JSONObject mechanisticValidatorResult;
private int sourceReactionUuid; // The ID of the reaction from this object was derived, presumably via reversal.
public Reaction(long uuid,
Long[] substrates, Long[] products,
Long[] substrateCofactors, Long[] productCofactors,
Long[] coenzymes,
String ecnum,
ConversionDirectionType conversionDirection, StepDirection pathwayStepDirection,
String reaction_name_field, RxnDetailType type) {
this.type = type;
this.uuid = Long.valueOf(uuid).intValue();
this.substrates = substrates;
this.products = products;
this.substrateCofactors = substrateCofactors;
this.productCofactors = productCofactors;
this.coenzymes = coenzymes;
this.ecnum = ecnum;
this.rxnName = reaction_name_field;
this.conversionDirection = conversionDirection;
this.pathwayStepDirection = pathwayStepDirection;
this.mechanisticValidatorResult = null;
this.substrateCoefficients = new HashMap<Long, Integer>();
this.productCoefficients = new HashMap<Long, Integer>();
this.references = new HashSet<P<RefDataSource, String>>();
this.proteinData = new HashSet<JSONObject>();
this.keywords = new HashSet<String>();
this.caseInsensitiveKeywords = new HashSet<String>();
}
private Reaction(int sourceReactionUuid, int uuid,
Long[] substrates, Long[] products,
Long[] substrateCofactors, Long[] productCofactors,
Long[] coenzymes,
String ecnum,
ConversionDirectionType conversionDirection, StepDirection pathwayStepDirection,
String reaction_name_field, RxnDetailType type) {
this(uuid, substrates, products, substrateCofactors, productCofactors, coenzymes, ecnum,
conversionDirection, pathwayStepDirection, reaction_name_field, type);
this.sourceReactionUuid = sourceReactionUuid;
}
public RxnDataSource getDataSource() {
return this.dataSource;
}
public JSONObject getMechanisticValidatorResult() {
return mechanisticValidatorResult;
}
public void setMechanisticValidatorResult(JSONObject mechanisticValidatorResult) {
this.mechanisticValidatorResult = mechanisticValidatorResult;
}
public void setDataSource(RxnDataSource src) {
this.dataSource = src;
}
public Double getEstimatedEnergy() {
return estimatedEnergy;
}
public void setEstimatedEnergy(Double energy) {
estimatedEnergy = energy;
}
public Set<String> getKeywords() { return this.keywords; }
public void addKeyword(String k) { this.keywords.add(k); }
public Set<String> getCaseInsensitiveKeywords() { return this.caseInsensitiveKeywords; }
public void addCaseInsensitiveKeyword(String k) { this.caseInsensitiveKeywords.add(k); }
public static int reverseID(int id) {
return -(id + 1);
}
public static long reverseID(long id) {
return -(id + 1);
}
public static long reverseNegativeId(long id) { return (-1 * id) - 1; }
public static Set<Long> reverseAllIDs(Set<Long> ids) {
Set<Long> result = new HashSet<Long>();
for (Long id : ids) {
result.add(reverseID(id));
}
return result;
}
public Reaction makeReversedReaction() {
ConversionDirectionType reversedDirection = null;
ConversionDirectionType conversionDirection = this.getConversionDirection();
if (conversionDirection == null) {
// Assume reactions are left-to-right by default.
reversedDirection = ConversionDirectionType.RIGHT_TO_LEFT;
} else {
switch (this.getConversionDirection()) {
case LEFT_TO_RIGHT:
reversedDirection = ConversionDirectionType.RIGHT_TO_LEFT;
break;
case RIGHT_TO_LEFT:
reversedDirection = ConversionDirectionType.LEFT_TO_RIGHT;
break;
case REVERSIBLE:
reversedDirection = ConversionDirectionType.REVERSIBLE;
break;
default:
// Assume reactions are left-to-right by default.
reversedDirection = ConversionDirectionType.RIGHT_TO_LEFT;
break;
}
}
StepDirection reversedPathwayDirection = null;
StepDirection pathwayDirection = this.getPathwayStepDirection();
if (pathwayDirection != null) {
switch (pathwayDirection) {
case LEFT_TO_RIGHT:
reversedPathwayDirection = StepDirection.RIGHT_TO_LEFT;
break;
case RIGHT_TO_LEFT:
reversedPathwayDirection = StepDirection.LEFT_TO_RIGHT;
break;
default:
// Do nothing if we don't recognize the pathway step direction.
break;
}
}
// TODO: should we copy the arrays? That might eat a lot of unnecessary memory.
// TODO: we don't want to use reverseID, but how else we will we guarantee no collisions?
Reaction r =
new Reaction(this.uuid, reverseID(this.getUUID()),
this.getProducts(), this.getSubstrates(),
this.getProductCofactors(), this.getSubstrateCofactors(),
this.getCoenzymes(),
this.getECNum(),
reversedDirection, reversedPathwayDirection, this.getReactionName(), this.getRxnDetailType());
r.setDataSource(this.getDataSource());
return r;
}
public Set<Reaction> correctForReactionDirection() {
Set<Reaction> reactions = new HashSet<>(1); // Only expect one reaction in most cases.
boolean addRightToLeft = false;
boolean addLeftToRight = false;
boolean foundConversionOrCatalysisDirection = false;
ConversionDirectionType cd = this.getConversionDirection();
if (cd != null) {
foundConversionOrCatalysisDirection = true;
switch (this.getConversionDirection()) {
case LEFT_TO_RIGHT:
addLeftToRight = true;
break;
case RIGHT_TO_LEFT:
addRightToLeft = true;
break;
case REVERSIBLE:
addLeftToRight = true;
addRightToLeft = true;
break;
default:
// Assume reactions are left-to-right by default.
addLeftToRight = true;
break;
}
}
// TODO: partition proteins by direction and split them into respective reactions.
// Note that currently each reaction has exactly one protein, so this TODO is not urgent.
for (JSONObject protein : this.getProteinData()) {
if (protein.has("catalysis_direction")) {
String cds = protein.getString("catalysis_direction");
if (cds != null) {
switch (CatalysisDirectionType.valueOf(cds)) {
case LEFT_TO_RIGHT:
foundConversionOrCatalysisDirection = true;
addLeftToRight = true;
break;
case RIGHT_TO_LEFT:
foundConversionOrCatalysisDirection = true;
addRightToLeft = true;
break;
default: // No other catalysis direction value adds evidence.
break;
}
}
}
}
// Fall back to pathway step direction if no conversion or catalysis directions were found.
if (!foundConversionOrCatalysisDirection && this.getPathwayStepDirection() != null) {
switch (this.getPathwayStepDirection()) {
case LEFT_TO_RIGHT:
addLeftToRight = true;
break;
case RIGHT_TO_LEFT:
addRightToLeft = true;
break;
default: // No other pathway step direction value adds evidence.
break;
}
}
// Assume reaction is left-to-right if no evidence has been found to indicate a direction.
if (!addLeftToRight && !addRightToLeft) {
addLeftToRight = true;
}
if (addLeftToRight) {
reactions.add(this);
}
if (addRightToLeft) {
reactions.add(this.makeReversedReaction());
}
if (reactions.size() == 0) {
// We never expect an empty result set here.
throw new RuntimeException(
String.format("ERROR: Unexpected empty direction-corrected reaction set for %d\n", this.getUUID()));
}
return reactions;
}
public void addReference(RefDataSource src, String ref) {
this.references.add(new P<RefDataSource, String>(src, ref));
}
public Set<P<RefDataSource, String>> getReferences() {
return this.references;
}
public Set<String> getReferences(Reaction.RefDataSource type) {
Set<String> filtered = new HashSet<String>();
for (P<Reaction.RefDataSource, String> ref : this.references)
if (ref.fst() == type)
filtered.add(ref.snd());
return filtered;
}
public void removeAllProteinData() {
this.proteinData.clear();
}
public void addProteinData(JSONObject proteinData) {
this.proteinData.add(proteinData);
}
public Set<JSONObject> getProteinData() {
return this.proteinData;
}
public void setProteinData(Set<JSONObject> proteinData) { this.proteinData = proteinData; }
public boolean hasProteinSeq() {
boolean hasSeq = false;
for (JSONObject protein : this.proteinData) {
boolean has = proteinDataHasSeq(protein);
hasSeq |= has;
if (has) break;
}
return hasSeq;
}
private boolean proteinDataHasSeq(JSONObject prt) {
switch (this.dataSource) {
case METACYC:
return metacycProteinDataHasSeq(prt);
case BRENDA:
return brendaProteinDataHasSeq(prt);
case KEGG:
return false; // kegg entries dont map to sequences, AFAIK
default:
return false; // no seq
}
}
private boolean metacycProteinDataHasSeq(JSONObject prt) {
// Example of a protein field entry for a METACYC rxn:
// *****************************************************
// {
// "datasource" : "METACYC",
// "organisms" : [
// NumberLong(198094)
// ],
// "sequences" : [
// NumberLong(8033)
// ]
// }
// *****************************************************
if (!prt.has("sequences"))
return false;
JSONArray seqs = prt.getJSONArray("sequences");
for (int i = 0; i < seqs.length(); i++) {
Long s = seqs.getLong(i);
if (s != null)
return true;
}
return false;
}
private boolean brendaProteinDataHasSeq(JSONObject prt) {
// Example of a protein field entry for a BRENDA rxn:
// *****************************************************
// {
// "localization" : [ ],
// "km" : [ { "val" : 0.01, "comment" : "in 200 mM bicine, pH 6.0, at 60°C" }, ],
// "expression" : [ ],
// "organism" : NumberLong("4000006340"),
// "cofactor" : [ { "val" : "NAD+", "comment" : "dependent on" } ],
// "sequences" : [
// NumberLong(0)
// ],
// "kcat/km" : [ ],
// "subunits" : [ ],
// "recommended_name" : { "recommended_name" : "alcohol dehydrogenase", "go_num" : "GO:0004025" },
// ...
// }
// *****************************************************
if (!prt.has("sequences"))
return false;
JSONArray seqs = prt.getJSONArray("sequences");
return seqs != null && seqs.length() > 0;
}
public int getUUID() { return this.uuid; }
public void clearUUID() { this.uuid = -1; }
public Long[] getSubstrates() { return substrates; }
public Long[] getProducts() { return products; }
public Long[] getSubstrateCofactors() { return substrateCofactors; }
public Long[] getProductCofactors() { return productCofactors; }
public Long[] getCoenzymes() { return coenzymes; }
public void setSubstrates(Long[] sUp) { this.substrates = sUp; }
public void setProducts(Long[] pUp) { this.products = pUp; }
public void setSubstrateCofactors(Long[] sUp) { this.substrateCofactors = sUp; }
public void setProductCofactors(Long[] pUp) { this.productCofactors = pUp; }
public void setCoenzymes(Long[] coenzymes) { this.coenzymes = coenzymes; }
public Set<Long> getSubstrateIdsOfSubstrateCoefficients() { return substrateCoefficients.keySet(); }
public Set<Long> getProductIdsOfProductCoefficients() { return productCoefficients.keySet(); }
public Integer getSubstrateCoefficient(Long s) { return substrateCoefficients.get(s); }
public Integer getProductCoefficient(Long p) { return productCoefficients.get(p); }
public void setSubstrateCoefficient(Long s, Integer c) { substrateCoefficients.put(s, c); }
public void setProductCoefficient(Long p, Integer c) { productCoefficients.put(p, c); }
public void setAllProductCoefficients(Map<Long, Integer> inputIdToCoefficient) {
productCoefficients = inputIdToCoefficient;
}
public void setAllSubstrateCoefficients(Map<Long, Integer> inputIdToCoefficient) {
substrateCoefficients = inputIdToCoefficient;
}
public String getECNum() { return ecnum; }
public String getReactionName() { return rxnName; }
public RxnDetailType getRxnDetailType() { return type; }
public ConversionDirectionType getConversionDirection() { return this.conversionDirection; }
public StepDirection getPathwayStepDirection() { return this.pathwayStepDirection; }
public int getSourceReactionUUID() { return this.sourceReactionUuid; }
@Override
public String toString() {
return "uuid: " + uuid +
"\n ec: " + ecnum +
" \n rxnName: " + rxnName +
" \n substrates: " + Arrays.toString(substrates) +
" \n products: " + Arrays.toString(products);
}
public String toStringDetail() {
return "uuid: " + uuid +
"\n ec: " + ecnum +
" \n rxnName: " + rxnName +
" \n refs: " + references +
" \n substrates: " + Arrays.toString(substrates) +
" \n products: " + Arrays.toString(products);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Reaction r = (Reaction)o;
if (this.uuid != r.uuid || !this.ecnum.equals(r.ecnum) || !this.rxnName.equals(r.rxnName))
return false;
if (this.substrates.length != r.substrates.length || this.products.length != r.products.length)
return false;
List<Long> ss = new ArrayList<Long>();
List<Long> pp = new ArrayList<Long>();
for (Long s : r.substrates) ss.add(s);
for (Long p : r.products) pp.add(p);
for (int i = 0 ;i <substrates.length; i++)
if (!ss.contains(substrates[i]))
return false;
for (int i = 0 ;i <products.length; i++)
if (!pp.contains(products[i]))
return false;
return true;
}
}