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