...
 
Commits (26)
......@@ -11,7 +11,7 @@ test36:
stage: test
image: python:3.6
script:
- tox -e isort,black,safety # ,flake8
- tox -e isort,black,safety,flake8
- tox -e py36 -- --cov-report=xml --cov-report=term tests
- bash <(curl -s https://codecov.io/bash)
......
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ProblematicWhitespace" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PyMissingOrEmptyDocstringInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PyMissingTypeHintsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
</profile>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/equilibrator-api.iml" filepath="$PROJECT_DIR$/.idea/equilibrator-api.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PySciProjectComponent">
<option name="PY_SCI_VIEW" value="true" />
<option name="PY_SCI_VIEW_SUGGESTED" value="true" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
\ No newline at end of file
......@@ -55,7 +55,7 @@ Import the API and create an instance. Creating the EquilibratorAPI class
instance reads all the data that is used to calculate thermodynamic potentials of reactions.
```python
from equilibrator_api import ComponentContribution, parse_reaction_formula, Q_
from equilibrator_api import ComponentContribution, Q_
eq_api = ComponentContribution(p_h=Q_("6.5"), ionic_strength=Q_('200 mM')) # set pH and I
```
......@@ -63,8 +63,8 @@ You can parse a reaction from a KEGG-style reaction string. The example given
is ATP hydrolysis to ADP and inorganic phosphate.
```python
rxn_str = "KEGG:C00002 + KEGG:C00001 = KEGG:C00008 + KEGG:C00009"
rxn = parse_reaction_formula(rxn_str)
from equilibrator_api import parse_reaction_formula
rxn = parse_reaction_formula("kegg:C00002 + kegg:C00001 = kegg:C00008 + kegg:C00009")
```
We highly recommend that you check that the reaction is atomically balanced
......@@ -81,14 +81,13 @@ Now we know that the reaction is "kosher" and we can safely proceed to
calculate the standard change in Gibbs potential due to this reaction.
```python
# You control the pH and ionic strength!
# ionic strength is in Molar units.
print(f"ΔG0 = {eq_api.standard_dg(rxn)}")
print(f"ΔG'0 = {eq_api.standard_dg_prime(rxn)}")
print(f"ΔG'm = {eq_api.physiological_dg_prime(rxn)}")
```
You can also calculate the [reversibility index](https://doi.org/10.1093/bioinformatics/bts317) for this reaction.
You can also calculate the reversibility index ([Noor et al. 2012](https://doi.org/10.1093/bioinformatics/bts317))
for this reaction.
```python
print(f"ln(Reversibility Index) = {eq_api.ln_reversibility_index(rxn)}")
......@@ -98,7 +97,7 @@ The reversibility index is a measure of the degree of the reversibility of the
reaction that is normalized for stoichiometry. If you are interested in
assigning reversibility to reactions we recommend this measure because 1:2
reactions are much "easier" to reverse than reactions with 1:1 or 2:2 reactions.
You can see the paper linked above for more information.
You can see the [paper](https://doi.org/10.1093/bioinformatics/bts317) linked above for more information.
### Example of pathway analysis using Max-min Driving Force:
Download an example pathway, run Max-min Driving Force analysis and generate two result figures
......@@ -113,6 +112,23 @@ mdf_res.reaction_plot.show()
mdf_res.compound_plot.show()
```
### Further examples for reaction parsing:
We support several compound databases, not just KEGG. One can mix between several sources in the same reaction, e.g.:
```python
rxn = parse_reaction_formula("kegg:C00002 + CHEBI:15377 = metanetx.chemical:MNXM7 + bigg.metabolite:pi")
```
Or, you can use compound names instead of identifiers. However, it is discouraged to use in batch, since we only pick
the closest hit in our database, and that can often be the wrong compound. Always verify that the reaction is balanced,
and preferably also that the InChIKeys are correct:
```python
from equilibrator_api import search_reaction
rxn = search_reaction("beta-D-glucose = glucose")
print(rxn)
```
In this case, the matcher arbitrarily chooses `alpha-D-glucose` as the first hit for the name `glucose`.
Therefore, it is always better to use the most specific synonym to avoid mis-annotations.
## Dependencies:
- python >= 3.6
- equilibrator-cache (latest)
......@@ -122,8 +138,7 @@ mdf_res.compound_plot.show()
- scipy
- optlang
- pandas
- nltk
- pyparsing
- matplotlib
- quilt
- pint
- uncertainties
clone_depth: 2
environment:
matrix:
- PYTHON: "C:\\Miniconda36-x64"
TOXENV: py36
- PYTHON: "C:\\Miniconda37-x64"
TOXENV: py37
matrix:
fast_finish: true
cache:
- '%LOCALAPPDATA%\pip\Cache'
platform: x64
build: off
install:
- set PATH=%PYTHON%;%PYTHON%\Scripts;%PYTHON%\Library\bin;%PATH%
- "ECHO %PYTHON%"
- python --version
- python -m pip install --upgrade --disable-pip-version-check pip setuptools wheel tox
test_script:
# We test isort, black, and flake8 on gitlab-ci so we skip it here.
# There is currently a unicode error with safety, see
# https://github.com/pyupio/safety/issues/119 so we disable it.
# - tox -e safety
- tox
This source diff could not be displayed because it is too large. You can view the blob instead.
[metadata]
name = equilibrator-api
url = https://gitlab.com/elad.noor/equilibrator-api
author = Elad Noor
author_email = noor@imsb.biol.ethz.ch
maintainer = Elad Noor
maintainer_email = noor@imsb.biol.ethz.ch
author =
Elad Noor
Moritz E. Beber
author_email =
noor@imsb.biol.ethz.ch
morbeb@biosustain.dtu.dk
classifiers =
Development Status :: 4 - Beta
Intended Audience :: Science/Research
Topic :: Scientific/Engineering :: Bio-Informatics
Topic :: Scientific/Engineering :: Chemistry
License :: OSI Approved :: MIT License
Natural Language :: English
......@@ -15,12 +18,13 @@ classifiers =
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
license = MIT License
description = Standard reaction Gibbs energy estimation for biochemical reactions.
description = Cache application for compounds, reactions, and enzymes
long_description = file: README.rst
keywords =
component contribution
Gibbs energy
biochemical reaction
equilibrator
eQuilibrator
[options]
zip_safe = True
......@@ -29,12 +33,10 @@ install_requires =
scipy>=1.1.0
optlang>=1.4.3
pandas>=0.23.4
nltk>=3.2.5
pyparsing>=2.2.0
sbtab>=0.9.49
matplotlib>=3.0.0
equilibrator-cache==0.2.4
component-contribution==0.2.5b1
equilibrator-cache==0.2.6b3
component-contribution==0.2.6b3
python_requires = >=3.6
tests_require =
tox
......
......@@ -42,7 +42,14 @@ default_conc_lb = Q_("1e-6 M")
default_conc_ub = Q_("1e-2 M")
default_e_potential = Q_("0 V")
from equilibrator_api.utils import parse_reaction_formula
from equilibrator_api.utils import (
get_compound,
get_compound_by_inchi,
search_compound_by_inchi_key,
search_compound,
parse_reaction_formula,
search_reaction,
)
from equilibrator_api.bounds import Bounds
from equilibrator_api.component_contribution import ComponentContribution
from equilibrator_api.pathway import Pathway
......
......@@ -53,20 +53,18 @@ class BaseBounds(object):
def GetLowerBound(self, compound: Compound):
"""Get the lower bound for this key.
Args:
key: a compound
:param key: a compound
"""
raise NotImplementedError
def GetUpperBound(self, compound: Compound):
"""Get the upper bound for this key.
Args:
key: a compound
:param key: a compound
"""
raise NotImplementedError
def GetLowerBounds(self, compounds: Iterable[Compound]) -> Iterable[float]:
def GetLowerBounds(self, compounds: Iterable[Compound]) -> Iterable[Q_]:
"""Get the bounds for a set of keys in order.
:param compounds: an iterable of Compounds
......@@ -74,7 +72,7 @@ class BaseBounds(object):
"""
return map(self.GetLowerBound, compounds)
def GetUpperBounds(self, compounds: Iterable[Compound]) -> Iterable[float]:
def GetUpperBounds(self, compounds: Iterable[Compound]) -> Iterable[Q_]:
"""Get the bounds for a set of keys in order.
:param compounds: an iterable of Compounds
......@@ -82,7 +80,7 @@ class BaseBounds(object):
"""
return map(self.GetUpperBound, compounds)
def GetBoundTuple(self, compound: Compound) -> Tuple[float, float]:
def GetBoundTuple(self, compound: Compound) -> Tuple[Q_, Q_]:
"""Get both upper and lower bounds for this key.
:param compound: a Compound object
......@@ -92,7 +90,7 @@ class BaseBounds(object):
def GetBounds(
self, compounds: Iterable[Compound]
) -> Tuple[Iterable[float], Iterable[float]]:
) -> Tuple[Iterable[Q_], Iterable[Q_]]:
"""Get the bounds for a set of compounds.
:param compounds: an iterable of Compounds
......@@ -104,13 +102,13 @@ class BaseBounds(object):
@staticmethod
@ureg.check("[concentration]")
def conc2ln_conc(b: float) -> float:
def conc2ln_conc(b: Q_) -> float:
"""Convert a concentration to log-concentration.
:param b: a concentration
:return: the log concentration
"""
return log(b / standard_concentration)
return log(b / standard_concentration).magnitude
def GetLnBounds(
self, compounds: Iterable[Compound]
......@@ -147,14 +145,13 @@ class BaseBounds(object):
return map(self.conc2ln_conc, ubs)
@ureg.check(None, None, "[concentration]", "[concentration]")
def SetBounds(self, compound: Compound, lb: float, ub: float):
def SetBounds(self, compound: Compound, lb: Q_, ub: Q_):
"""Set bounds for a specific key.
Args:
key: a string representation of a KEGG compound ID,
i.e. C00001 for water
lb: the lower bound value
ub: the upper bound value
:param key: a string representation of a KEGG compound ID,
i.e. C00001 for water
:param lb: the lower bound value
:param ub: the upper bound value
"""
assert lb <= ub
self.lower_bounds[compound] = lb
......@@ -164,24 +161,26 @@ class BaseBounds(object):
class Bounds(BaseBounds):
"""Contains upper and lower bounds for various keys.
Allows for defaults
Allows for defaults.
"""
@ureg.check(None, None, None, "[concentration]", "[concentration]")
def __init__(
self,
lower_bounds: Dict[Compound, float] = None,
upper_bounds: Dict[Compound, float] = None,
default_lb: float = default_conc_lb,
default_ub: float = default_conc_ub,
lower_bounds: Dict[Compound, Q_] = None,
upper_bounds: Dict[Compound, Q_] = None,
default_lb: Q_ = default_conc_lb,
default_ub: Q_ = default_conc_ub,
) -> object:
"""Initialize the bounds object.
Args:
lower_bounds: a dictionary mapping keys to float lower bounds
upper_bounds: a dictionary mapping keys to float upper bounds
default_lb: default lower bound to if no specific one is provided
default_lb: default upper bound to if no specific one is provided
:param lower_bounds: a dictionary mapping keys to lower bounds
:param upper_bounds: a dictionary mapping keys to upper bounds
:param default_lb: default lower bound to if no specific one is
provided
:param default_lb: default upper bound to if no specific one is
provided
"""
self.lower_bounds = lower_bounds or dict()
self.upper_bounds = upper_bounds or dict()
......@@ -198,8 +197,8 @@ class Bounds(BaseBounds):
def from_csv(
cls,
f: TextIO,
default_lb: float = default_conc_lb,
default_ub: float = default_conc_ub,
default_lb: Q_ = default_conc_lb,
default_ub: Q_ = default_conc_ub,
) -> object:
"""Read bounds from a .csv file.
......@@ -212,8 +211,8 @@ class Bounds(BaseBounds):
provided
:return: a Bounds object
"""
lbs: Dict[Compound, float] = dict()
ubs: Dict[Compound, float] = dict()
lbs: Dict[Compound, Q_] = dict()
ubs: Dict[Compound, Q_] = dict()
bounds_df = pd.read_csv(f)
for row in bounds_df.itertuples():
......@@ -247,8 +246,8 @@ class Bounds(BaseBounds):
cls,
df: pd.DataFrame,
bounds_unit: str = None,
default_lb: float = default_conc_lb,
default_ub: float = default_conc_ub,
default_lb: Q_ = default_conc_lb,
default_ub: Q_ = default_conc_ub,
) -> Tuple[object, Dict[Compound, str]]:
"""Read bounds from a Pandas DataFrame.
......@@ -329,7 +328,7 @@ class Bounds(BaseBounds):
new_ub = dict(self.upper_bounds.items())
return Bounds(new_lb, new_ub, self.default_lb, self.default_ub)
def GetLowerBound(self, compound: Compound) -> float:
def GetLowerBound(self, compound: Compound) -> Q_:
"""Get the lower bound for this key.
:param compound: a compound
......
......@@ -33,6 +33,7 @@ from component_contribution import GibbsEnergyPredictor
from . import (
FARADAY,
Q_,
R,
ccache,
default_I,
......@@ -55,10 +56,10 @@ class ComponentContribution(object):
@ureg.check(None, None, None, "[concentration]", "[temperature]")
def __init__(
self,
p_h: float = default_pH,
p_mg: float = default_pMg,
ionic_strength: float = default_I,
temperature: float = default_T,
p_h: Q_ = default_pH,
p_mg: Q_ = default_pMg,
ionic_strength: Q_ = default_I,
temperature: Q_ = default_T,
) -> object:
"""Create a ComponentContribution object.
......@@ -73,7 +74,7 @@ class ComponentContribution(object):
self.temperature = temperature
@property
def RT(self) -> float:
def RT(self) -> Q_:
"""Get the value of RT."""
return R * self.temperature
......@@ -293,3 +294,31 @@ class ComponentContribution(object):
return ComponentContribution.predictor.is_using_group_contribution(
reaction
)
@ureg.check(None, None, None, None, "[length]**2 * [mass] / [time]**2")
def multicompartmental_standard_dg_prime(
self,
reaction: PhasedReaction,
nH: int,
charge: int,
delta_p_h: ureg.Measurement,
delta_phi: ureg.Measurement,
) -> ureg.Measurement:
"""Calculate the transformed energies of a multi-compartmental reaction.
Based on the equations from
Harandsdottir et al. 2012 (https://doi.org/10.1016/j.bpj.2012.02.032)
:param reaction:
:param nH: the number of hydrogens being transported
:param charge: the net charge being transported
:param delta_p_h: the difference in pH between the two sides
:param delta_phi: the difference in electro-static potential between
the two sides
:return: the transport reaction Gibbs free energy change
"""
dg = self.standard_dg_prime(reaction)
dg -= nH * self.RT * np.log(10.0) * delta_p_h
dg -= FARADAY * charge * delta_phi
return dg
......@@ -76,14 +76,14 @@ class Pathway(object):
) -> object:
"""Initialize.
Args:
reactions: a list of gibbs.reaction.Reaction objects.
fluxes: numpy.array of relative fluxes in same order as reactions.
standard_dg_primes: reaction energies (in kJ/mol)
dg_sigma: square root of the uncertainty covariance matrix
(in kJ/mol)
bounds: bounds on metabolite concentrations.
Uses default bounds if None provided.
:param reactions: a list of gibbs.reaction.Reaction objects.
:param fluxes: numpy.array of relative fluxes in same order as
reactions.
:param standard_dg_primes: reaction energies (in kJ/mol)
:param dg_sigma: square root of the uncertainty covariance matrix
(in kJ/mol)
:param bounds: bounds on metabolite concentrations. Uses default
bounds if None provided.
"""
self.reactions = reactions
Nr = len(reactions)
......@@ -104,7 +104,7 @@ class Pathway(object):
if standard_dg_primes is None:
assert dg_sigma is None, (
"If standard_dg_primes are not "
"provided, dg_sigme must also be None"
"provided, dg_sigma must also be None"
)
self.set_aqueous_params(
p_h=p_h,
......@@ -174,7 +174,7 @@ class Pathway(object):
if temperature is not None:
self.comp_contrib.temperature = temperature
RT = R * self.comp_contrib.temperature
RT = self.comp_contrib.RT
standard_dg_primes = self.comp_contrib.standard_dg_prime_multi(
self.reactions
......@@ -393,7 +393,7 @@ class Pathway(object):
name_to_compound.get, row.ReactionFormula
)
rxn.rid = row.ID
if not rxn.is_balanced(ignore_atoms=("H")):
if not rxn.is_balanced(ignore_atoms=("H",)):
warnings.warn(f"Reaction {rxn.rid} is not balanced")
reactions.append(rxn)
if row.ID in reaction_ids:
......
......@@ -104,7 +104,7 @@ class Condition(object):
I.e. the phase and the abundance.
"""
def __init__(self, phase: str, abundance: ureg.Quantity = None):
def __init__(self, phase: str, abundance: Q_ = None):
"""Create a new condition object."""
assert phase in PHASE_INFO_DICT, f"Unknown phase: {phase}"
self._phase = phase
......@@ -116,17 +116,17 @@ class Condition(object):
return self._phase
@property
def abundance(self) -> ureg.Quantity:
def abundance(self) -> Q_:
"""Return the abundance."""
return self._abundance
@property
def standard_abundance(self) -> ureg.Quantity:
def standard_abundance(self) -> Q_:
"""Return the standard abundance in this phase."""
return PHASE_INFO_DICT[self._phase].standard_abundance
@property
def physiolical_abundance(self) -> ureg.Quantity:
def physiolical_abundance(self) -> Q_:
"""Return the default physiological abundance in this phase."""
return PHASE_INFO_DICT[self._phase].physiolical_abundance
......@@ -148,7 +148,7 @@ class Condition(object):
if self._abundance is None:
return 0.0 # when the phase does not allow relative abundances
else:
return np.log(float(self._abundance / self.standard_abundance))
return np.log(self._abundance / self.standard_abundance).magnitude
@property
def ln_physiological_abundance(self) -> float:
......@@ -158,12 +158,12 @@ class Condition(object):
if self.physiolical_abundance is None:
return 0.0 # when the phase does not allow relative abundances
else:
return np.log(
float(self.physiolical_abundance / self.standard_abundance)
)
return (
np.log(self.physiolical_abundance / self.standard_abundance)
).magnitude
@abundance.setter
def abundance(self, abundance: ureg.Quantity) -> None:
def abundance(self, abundance: Q_) -> None:
"""Change the phase of a specific compound.
:param abundance: the new abundance in the correct units
......@@ -287,12 +287,12 @@ class PhasedCompound(object):
return NON_AQUEOUS_COMPOUND_DICT.get(self.inchi, (AQUEOUS_PHASE_NAME,))
@property
def abundance(self) -> ureg.Quantity:
def abundance(self) -> Q_:
"""Get the abundance."""
return self.condition.abundance
@abundance.setter
def abundance(self, abundance: ureg.Quantity) -> None:
def abundance(self, abundance: Q_) -> None:
"""Change the phase of a specific compound.
:param abundance: the new abundance in the correct units
......@@ -330,11 +330,8 @@ class PhasedCompound(object):
@ureg.check(None, None, "[concentration]", "[temperature]")
def get_stored_standard_dgf_prime(
self,
p_h: ureg.Quantity,
ionic_strength: ureg.Quantity,
temperature: ureg.Quantity,
) -> ureg.Quantity:
self, p_h: Q_, ionic_strength: Q_, temperature: Q_
) -> Q_:
"""Return the stored formation energy of this phased compound.
Only if it exists, otherwise return None (and we will use
......@@ -365,7 +362,7 @@ class PhasedCompound(object):
# for all other compounds, we will use component_contribution
return None
def get_stored_standard_dgf(self) -> ureg.Quantity:
def get_stored_standard_dgf(self) -> Q_:
"""Return the stored formation energy of this phased compound.
Only if it exists, otherwise return None (and we will use
......
......@@ -330,7 +330,6 @@ class PhasedReaction(Reaction):
"InChI=1S/H3N/h1H3/p+1", # NH4+
]
compounds = list(map(ccache.get_compound_by_inchi, balancing_inchis))
print(compounds)
S = ccache.get_element_data_frame(compounds)
balancing_atoms = S.index
......
......@@ -65,7 +65,12 @@ class PathwayMDFData(object):
:param reaction_prices: shadow prices of reactions
:param compound_prices: shadow prices of compound concentrations
"""
self.mdf = B * R * default_T
if pathway.comp_contrib is not None:
RT = pathway.comp_contrib.RT
else:
RT = R * default_T
self.mdf = B * RT
self.bounds = pathway._bounds.Copy()
concentrations = np.exp(lnC) * standard_concentration
......@@ -78,7 +83,6 @@ class PathwayMDFData(object):
optimized_dg_prime += pathway.dg_sigma @ y
# adjust dG to flux directions and convert to kJ/mol
RT = R * pathway.comp_contrib.temperature
standard_dg_primes = (I_dir @ standard_dg_primes) * RT
physiological_dg_prime = (I_dir @ physiological_dg_prime) * RT
optimized_dg_prime = (I_dir @ optimized_dg_prime) * RT
......@@ -255,7 +259,7 @@ class PathwayMDFData(object):
ax.set_xlabel("Reaction Step")
ax.set_ylabel(r"Cumulative $\Delta_r G^\prime$ (kJ/mol)")
ax.legend(loc="best")
ax.set_title(f"MDF = {self.mdf}")
ax.set_title(f"MDF = {self.mdf:.2f}")
reaction_fig.tight_layout()
return reaction_fig
......
"""A utility module."""
from equilibrator_api import ccache
from typing import List, Union
from equilibrator_api import Compound, ccache
from equilibrator_api.phased_reaction import PhasedReaction
def get_compound(compound_id: str) -> Union[Compound, None]:
"""Get a Compound using the DB namespace and its accession.
:return: a Compound object or None
"""
return ccache.get_compound(compound_id)
def get_compound_by_inchi(inchi: str) -> Union[Compound, None]:
"""Get a Compound using InChI.
:return: a Compound object or None
"""
return ccache.get_compound_by_inchi(inchi)
def search_compound_by_inchi_key(inchi_key: str) -> List[Compound]:
"""Get a Compound using InChI.
:return: a Compound object or None
"""
return ccache.search_compound_by_inchi_key(inchi_key)
def search_compound(query: str) -> Union[None, Compound]:
"""Try to find the compound that matches the name best.
:param query: a compound name
:return: a Compound object of the best match
"""
hits = ccache.search(query)
if not hits:
return None
else:
return hits[0][0]
def parse_reaction_formula(formula: str) -> PhasedReaction:
"""Parse reaction text using exact match.
......@@ -10,3 +49,12 @@ def parse_reaction_formula(formula: str) -> PhasedReaction:
:return: a Reaction object
"""
return PhasedReaction.parse_formula(ccache.get_compound, formula)
def search_reaction(formula: str) -> PhasedReaction:
"""Parse a reaction written using compound names and approximate matching.
:param formula: a string containing the reaction formula
:return: a Reaction object
"""
return PhasedReaction.parse_formula(search_compound, formula)
......@@ -106,6 +106,7 @@ def test_mdf(p_h, ionic_strength, stdev_factor, expected_mdf, toy_pathway):
def test_default_bounds():
"""Test the default bounds file."""
bounds = Bounds.GetDefaultBounds()
bound_df = bounds.to_data_frame()
assert bound_df.shape == (112, 3)
......
......@@ -77,7 +77,7 @@ def test_water_balancing(comp_contribution, missing_h2o):
assert missing_h2o.is_balanced(ignore_atoms=("H", "O", "e-"))
balanced_rxn = missing_h2o.balance_with_compound(
ccache.get_compound("kegg:C00001"), ignore_atoms=("H")
ccache.get_compound("kegg:C00001"), ignore_atoms=("H",)
)
assert balanced_rxn is not None
......
"""unit test for PhaseReaction and ComponentContribution."""
# The MIT License (MIT)
#
# Copyright (c) 2013 Weizmann Institute of Science
# Copyright (c) 2018 Institute for Molecular Systems Biology,
# ETH Zurich
# Copyright (c) 2018 Novo Nordisk Foundation Center for Biosustainability,
# Technical University of Denmark
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import pytest
from equilibrator_api import (
get_compound,
get_compound_by_inchi,
parse_reaction_formula,
search_compound,
search_compound_by_inchi_key,
search_reaction,
)
@pytest.mark.parametrize(
"identifier, inchi, inchi_key",
[
(
"kegg:C00002",
"InChI=1S/C10H16N5O13P3/c11-8-5-9(13-2-12-8)15(3-14-5)10-7(17)6("
"16)4(26-10)1-25-30(21,22)28-31(23,24)27-29(18,19)20/h2-4,6-7,10,"
"16-17H,1H2,(H,21,22)(H,23,24)(H2,11,12,13)(H2,18,19,20)/p-4/t4-,6-"
",7-,10-/m1/s1",
"ZKHQWZAMYRWXGA-KQYNXXCUSA-J",
),
(
"metanetx.chemical:MNXM7",
"InChI=1S/C10H15N5O10P2/c11-8-5-9(13-2-12-8)15(3-14-5)10-7(17)6("
"16)4(24-10)1-23-27(21,22)25-26(18,19)20/h2-4,6-7,10,16-17H,1H2,"
"(H,21,22)(H2,11,12,13)(H2,18,19,20)/p-3/t4-,6-,7-,10-/m1/s1",
"XTWYTFMLZFPYCI-KQYNXXCUSA-K",
),
],
)
def test_get_compound(identifier: str, inchi: str, inchi_key: str):
"""Test get_compound."""
assert get_compound(identifier) == get_compound_by_inchi(inchi)
assert [get_compound(identifier)] == search_compound_by_inchi_key(inchi_key)
@pytest.mark.parametrize(
"name, identifier",
[
("ATP", "kegg:C00002"),
("Adenosine-5'-diphosphate", "bigg.metabolite:adp"),
("water", "CHEBI:15377"),
("H+", "kegg:C00080"),
("ethanol", "kegg:C00469"),
("alpha-D-glucose", "metanetx.chemical:MNXM41"),
("alpha-D-Glucose", "metanetx.chemical:MNXM41"),
("beta-D-glucose", "metanetx.chemical:MNXM105"),
("beta-D-Glucose", "metanetx.chemical:MNXM105"),
],
)
def test_search_compound(name: str, identifier: str):
"""Test search compound (first hit)."""
cpd1 = search_compound(name)
cpd2 = get_compound(identifier)
if cpd1 != cpd2:
raise AssertionError(
f"Search compound result does not match " f"reference: {cpd1}"
)
@pytest.mark.parametrize(
"text_formula, id_formula",
[
(
"ATP + H2O = Adenosine-5'-diphosphate + phosphate",
"kegg:C00002 + kegg:C00001 = kegg:C00008 + kegg:C00009",
),
(
"alpha-D-glucose = 2 ethanol + 2 CO2",
"metanetx.chemical:MNXM41 = 2 metanetx.chemical:MNXM13 + 2 "
"metanetx.chemical:MNXM303",
),
(
"beta-D-glucose = 2 ethanol + 2 CO2",
"metanetx.chemical:MNXM105 = 2 metanetx.chemical:MNXM13 + 2 "
"metanetx.chemical:MNXM303",
),
(
"O2 + NADH = NAD+ + water",
"kegg:C00007 + kegg:C00004 = kegg:C00003 + kegg:C00001",
),
],
)
def test_search_reaction(text_formula: str, id_formula: str):
"""Test parsing of freetext reaction formulas."""
rxn1 = search_reaction(text_formula)
rxn2 = parse_reaction_formula(id_formula)
if rxn1 != rxn2:
raise AssertionError(
f"Search reaction result does not match " f"reference: {rxn1}"
)
......@@ -78,8 +78,6 @@ known_third_party =
unittest
scipy
optlang
nltk
pyparsing
sbtab
matplotlib
equilibrator_cache
......