"""Define lower and upper bounds on compounds."""
# The MIT License (MIT)
#
# Copyright (c) 2013 Weizmann Institute of Science
# Copyright (c) 2018-2020 Institute for Molecular Systems Biology,
# ETH Zurich
# Copyright (c) 2018-2020 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.
from importlib import resources
from typing import Dict, Iterable, TextIO, Tuple, Union
import numpy as np
import pandas as pd
from equilibrator_cache import Compound
from path import Path
from .. import (
Q_,
ComponentContribution,
data,
default_conc_lb,
default_conc_ub,
standard_concentration,
ureg,
)
[docs]
class BaseBounds(object):
"""A base class for declaring bounds on things."""
[docs]
def copy(self):
"""Return a (deep) copy of self."""
raise NotImplementedError
[docs]
def get_lower_bound(self, compound: Union[str, Compound]):
"""Get the lower bound for this key.
:param key: a compound
"""
raise NotImplementedError
[docs]
def get_upper_bound(self, compound: Union[str, Compound]):
"""Get the upper bound for this key.
:param key: a compound
"""
raise NotImplementedError
[docs]
def get_lower_bounds(
self, compounds: Iterable[Union[str, Compound]]
) -> Iterable[Q_]:
"""Get the bounds for a set of keys in order.
:param compounds: an iterable of Compounds or strings
:return: an iterable of the lower bounds
"""
return map(self.get_lower_bound, compounds)
[docs]
def get_upper_bounds(
self, compounds: Iterable[Union[str, Compound]]
) -> Iterable[Q_]:
"""Get the bounds for a set of keys in order.
:param compounds: an iterable of Compounds or strings
:return: an iterable of the upper bounds
"""
return map(self.get_upper_bound, compounds)
[docs]
def get_bound_tuple(self, compound: Union[str, Compound]) -> Tuple[Q_, Q_]:
"""Get both upper and lower bounds for this key.
:param compound: a Compound object or string
:return: a 2-tuple (lower bound, upper bound)
"""
return self.get_lower_bound(compound), self.get_upper_bound(compound)
[docs]
def get_bounds(
self, compounds: Iterable[Union[str, Compound]]
) -> Tuple[Iterable[Q_], Iterable[Q_]]:
"""Get the bounds for a set of compounds.
:param compounds: an iterable of Compounds
:return: a 2-tuple (lower bounds, upper bounds)
"""
bounds = map(self.get_bound_tuple, compounds)
lbs, ubs = zip(*bounds)
return lbs, ubs
@staticmethod
@ureg.check("[concentration]")
[docs]
def conc2ln_conc(b: Q_) -> float:
"""Convert a concentration to log-concentration.
:param b: a concentration
:return: the log concentration
"""
return np.log((b / standard_concentration).m_as(""))
[docs]
def get_ln_bounds(
self, compounds: Iterable[Union[str, Compound]]
) -> Tuple[Iterable[float], Iterable[float]]:
"""Get the log-bounds for a set of compounds.
:param compounds: an iterable of Compounds or strings
:return: a 2-tuple (log lower bounds, log upper bounds)
"""
lbs, ubs = self.get_bounds(compounds)
return map(self.conc2ln_conc, lbs), map(self.conc2ln_conc, ubs)
[docs]
def get_ln_lower_bounds(
self, compounds: Iterable[Union[str, Compound]]
) -> Iterable[float]:
"""Get the log lower bounds for a set of compounds.
:param compounds: an iterable of Compounds or strings
:return: an iterable of log lower bounds
"""
lbs = self.get_lower_bounds(compounds)
return map(self.conc2ln_conc, lbs)
[docs]
def get_ln_upper_bounds(
self, compounds: Iterable[Union[str, Compound]]
) -> Iterable[float]:
"""Get the log upper bounds for a set of compounds.
:param compounds: an iterable of Compounds or strings
:return: an iterable of log upper bounds
"""
ubs = self.get_upper_bounds(compounds)
return map(self.conc2ln_conc, ubs)
@ureg.check(None, None, "[concentration]", "[concentration]")
[docs]
def set_bounds(self, compound: Union[str, Compound], lb: Q_, ub: Q_) -> None:
"""Set bounds for a specific key.
:param key: a Compounds or string
:param lb: the lower bound value
:param ub: the upper bound value
"""
assert lb <= ub
self.lower_bounds[compound] = lb
self.upper_bounds[compound] = ub
[docs]
class Bounds(BaseBounds):
"""Contains upper and lower bounds for various keys.
Allows for defaults.
"""
@ureg.check(None, None, None, "[concentration]", "[concentration]")
def __init__(
self,
lower_bounds: Dict[Union[str, Compound], Q_] = None,
upper_bounds: Dict[Union[str, Compound], Q_] = None,
default_lb: Q_ = default_conc_lb,
default_ub: Q_ = default_conc_ub,
) -> None:
"""Initialize the bounds object.
: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()
for b in self.lower_bounds.values():
assert b.check("[concentration]")
for b in self.upper_bounds.values():
assert b.check("[concentration]")
self.default_lb = default_lb
self.default_ub = default_ub
@classmethod
@ureg.check(None, None, None, "[concentration]", "[concentration]")
[docs]
def from_csv(
cls,
f: Union[TextIO, Path],
comp_contrib: ComponentContribution,
default_lb: Q_ = default_conc_lb,
default_ub: Q_ = default_conc_ub,
) -> "Bounds":
"""Read Bounds from a CSV file.
Parameters
----------
f : File
an open .csv file stream
comp_contrib : ComponentContribution
used for parsing compound accessions
default_lb : Q_
the default lower bound
default_ub : Q_
the default upper bound
"""
lbs: Dict[Union[str, Compound], Q_] = dict()
ubs: Dict[Union[str, Compound], Q_] = dict()
bounds_df = pd.read_csv(f)
for row in bounds_df.itertuples():
compound = comp_contrib.get_compound(row.compound_id)
if compound is None:
raise ValueError(
f"Cannot find this compound accession: " f"{row.compound_id}"
)
lbs[compound] = row.lb * ureg.molar
ubs[compound] = row.ub * ureg.molar
bounds = Bounds(lbs, ubs, default_lb, default_ub)
bounds.check_bounds()
return bounds
[docs]
def to_data_frame(self) -> pd.DataFrame:
"""Convert the list of bounds to a Pandas DataFrame."""
data = [
(c, self.get_lower_bound(c), self.get_upper_bound(c))
for c in self.lower_bounds.keys()
]
return pd.DataFrame(data=data, columns=["compound", "lb", "ub"])
[docs]
def check_bounds(self) -> None:
"""Assert the bounds are valid (i.e. that lb <= ub)."""
assert self.default_lb <= self.default_ub, (
f"default lower bound ({self.default_lb}) is higher than the "
f"default upper bound ({self.default_ub})"
)
for compound in self.upper_bounds:
lb = self.get_lower_bound(compound)
ub = self.get_upper_bound(compound)
assert lb <= ub, (
f"lower bound ({lb}) for {compound} is higher "
f"than the upper bound ({ub})"
)
[docs]
def copy(self) -> "Bounds":
"""Return a deep copy of self."""
return Bounds(
self.lower_bounds.copy(),
self.upper_bounds.copy(),
self.default_lb,
self.default_ub,
)
[docs]
def get_lower_bound(self, compound: Union[str, Compound]) -> Q_:
"""Get the lower bound for this compound."""
return self.lower_bounds.get(compound, self.default_lb)
[docs]
def get_upper_bound(self, compound: Union[str, Compound]) -> Q_:
"""Get the upper bound for this compound."""
return self.upper_bounds.get(compound, self.default_ub)
# the default is generated only at the first call of GetDefaultBounds()
# the reason to do this, is because looking up all the co-factor compounds
# in the cache takes about 20 seconds
@staticmethod
[docs]
def get_default_bounds(comp_contrib: ComponentContribution) -> "Bounds":
"""Return the default lower and upper bounds for a pre-determined list.
Parameters
----------
comp_contrib : ComponentContribution
Returns
-------
a Bounds object with the default values
"""
if Bounds.DEFAULT_BOUNDS is None:
with resources.as_file(
resources.files(data).joinpath("cofactors.csv")
) as fp:
Bounds.DEFAULT_BOUNDS = Bounds.from_csv(fp, comp_contrib)
return Bounds.DEFAULT_BOUNDS