From b11268c0ee80e5bc8ab12d972c71038049389cf2 Mon Sep 17 00:00:00 2001 From: "uri.akavia" Date: Fri, 25 Mar 2022 18:31:37 -0400 Subject: [PATCH 1/4] initial attempt at compartments --- src/cobra/core/compartment.py | 209 ++++++++++++++++++++++++++++++++++ src/cobra/core/metabolite.py | 5 +- src/cobra/core/reaction.py | 11 +- src/cobra/core/species.py | 1 + 4 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 src/cobra/core/compartment.py diff --git a/src/cobra/core/compartment.py b/src/cobra/core/compartment.py new file mode 100644 index 000000000..f1d192f55 --- /dev/null +++ b/src/cobra/core/compartment.py @@ -0,0 +1,209 @@ +"""Define the group class.""" + +from typing import Iterable, Optional, Union, List, FrozenSet +from warnings import warn + +from .dictlist import DictList +from .object import Object +from .. import Metabolite, Gene, Reaction, Model + + +class Compartment(Object): + """ + Manage groups via this implementation of the SBML group specification. + + `Compartment` is a class for holding information regarding a bounded space in + which species are located. You can think of it as a location in a model, + usually representing organelles like the Endoplasmic Reticulum (ER). + + Parameters + ---------- + id : str + The identifier to associate with this group + name : str, optional + A human readable name for the group + members : iterable, optional + A DictList containing references to cobra.Model-associated objects + that belong to the group. Members should be metabolites or genes, where + reactions are inferred from metabolites. + compartment_type : str, optional + SBML Level 2 Versions 2–4 provide the compartment type as a grouping construct + that can be used to establish a relationship between multiple Compartment + objects. A CompartmentType object only has an identity, and this identity can + only be used to indicate that particular Compartment objects in the model + belong to this type. This may be useful for conveying a modeling intention, + such as when a model contains many similar compartments, either by their + biological function or the reactions they carry. Without a compartment type + construct, it would be impossible within SBML itself to indicate that all of + the compartments share an underlying conceptual relationship because each + SBML compartment must be given a unique and separate identity. A + CompartmentType has no mathematical meaning in SBML—it has no effect on + a model's mathematical interpretation. Simulators and other numerical analysis + software may ignore CompartmentType definitions and references in a model. + dimensions: float, optional + Compartments can have dimensions defined, if they are volume (3 dimensions) or + 2 (a two-dimensional compartment, a surface, like a membrane). In theory, this + can be 1 or 0 dimensions, and even partial dimensions, but that will needlessly + complicate the math. The number of dimensions influences the size and units + used. + """ + def __init__( + self, + id: str, + name: str = "", + members: Optional[Iterable] = None, + compartment_type: Optional[str] = None, + dimensions: Optional[float] = None, + ): + """Initialize the group object. + + id : str + The identifier to associate with this group + name : str, optional + A human readable name for the group + members : iterable, optional + A DictList containing references to cobra.Model-associated objects + that belong to the group. + kind : {"collection", "classification", "partonomy"}, optional + The kind of group, as specified for the Groups feature in the SBML + level 3 package specification. + """ + Object.__init__(self, id, name) + + self._members = DictList() if members is None else DictList(members) + self._compartment_type = None + self.compartment_type = "" if compartment_type is None else compartment_type + # self.model is None or refers to the cobra.Model that + # contains self + self._dimensions = dimensions + self._model = None + + def __len__(self) -> int: + """Get length of group. + + Returns + ------- + int + An int with the length of the group. + + """ + return len(self._members) + + # read-only + @property + def members(self) -> DictList: + """Get members of the group. + + Returns + ------- + DictList + A dictlist containing the members of the group. + """ + return self._members + + @property + def compartment_type(self) -> str: + """Return the compartment type. + + Returns + ------- + str + The compartment type. + + """ + return self._compartment_type + + @compartment_type.setter + def compartment_type(self, compartment_type: str) -> None: + """Set the compartment type. + + Parameters + ---------- + compartment_type: str + + """ + self._compartment_type = compartment_type + + def add_members(self, new_members: Union[str, Metabolite, Reaction, + List[Union[Reaction, Metabolite]]]) -> None: + """Add objects to the group. + + Parameters + ---------- + new_members : list or str or Metabolite or Reaction + A list of cobrapy Metabolites or Genes to add to the group. If it isn't a + list a warning will be raised. + If it isn't a metaoblite or a gene, an error will be raised. + + Raises + ------ + TypeError - if given any object other than Metaoblite or Gene + """ + if isinstance(new_members, str) or hasattr(new_members, "id"): + warn("need to pass in a list") + new_members = [new_members] + + #TODO - add some filtering on type. What happens if given a string? Check + # DictList and groups. + + self._members.union(new_members) + for _member in new_members: + _member._compartment = self + + def remove_members(self, to_remove: list) -> None: + """Remove objects from the group. + + Parameters + ---------- + to_remove : list + A list of cobra objects to remove from the group + """ + if isinstance(to_remove, str) or hasattr(to_remove, "id"): + warn("need to pass in a list") + to_remove = [to_remove] + + for member_to_remove in to_remove: + self._members.remove(member_to_remove) + member_to_remove._compartment = None + + @property + def metabolites(self) -> DictList[Metabolite]: + """Return the metaoblites in the compartment, if any. + + Returns + ------- + DictList: + DictList of metaoblites if any are present in the compartment. If there are + no metaoblties, will return an empty DictList. + + """ + return self._members.query(lambda x: isinstance(x, Metabolite)) + + @property + def reactions(self) -> Optional[FrozenSet[Reaction]]: + """Return the reactions whose metabolites are in the compartment. + + This is returned as a FrozenSet of reactions for each metabolite in the + compartment, if any. + + Returns + ------- + FrozenSet of cobra.Reactions + Reactions that have metabolites that belong to this compartment. + """ + direct_set = set(self._members.query(lambda x: isinstance(x, Reaction))) + rxn_set = set() + for met in self.metabolites: + rxn_set.update(met._reactions) + return frozenset(rxn_set.union(direct_set)) + + def __contains__(self, member: Union[Metabolite, Gene]): + return member.compartment is self + + def merge(self, other): + warn("Not implemented yet") + return + + def copy(self, new_id: str, new_model: Model, new_name: Optional[str] = None): + warn("Not implemented yet") + return diff --git a/src/cobra/core/metabolite.py b/src/cobra/core/metabolite.py index 2376f6945..fe5a0805a 100644 --- a/src/cobra/core/metabolite.py +++ b/src/cobra/core/metabolite.py @@ -51,7 +51,7 @@ def __init__( formula: Optional[str] = None, name: Optional[str] = "", charge: Optional[float] = None, - compartment: Optional[str] = None, + compartment: Optional["Compartment"] = None, ) -> None: """Initialize Metaboblite cobra Species. @@ -71,7 +71,8 @@ def __init__( super().__init__(id=id, name=name) self.formula = formula # because in a Model a metabolite may participate in multiple Reactions - self.compartment = compartment + self._compartment = compartment + self.compartment = self._compartment.id self.charge = charge self._bound = 0.0 diff --git a/src/cobra/core/reaction.py b/src/cobra/core/reaction.py index 4f78cbf14..52e2d723a 100644 --- a/src/cobra/core/reaction.py +++ b/src/cobra/core/reaction.py @@ -111,6 +111,8 @@ def __init__( # contains self self._model = None + self._compartment = None + # from cameo ... self._lower_bound = ( lower_bound if lower_bound is not None else config.lower_bound @@ -1417,9 +1419,12 @@ def compartments(self) -> Set: set A set of compartments the metabolites are in. """ - return { - met.compartment for met in self._metabolites if met.compartment is not None - } + if self._compartment: + return set(self._compartment.id) + else: + return { + met.compartment for met in self._metabolites if met.compartment is not None + } def get_compartments(self) -> list: """List compartments the metabolites are in. diff --git a/src/cobra/core/species.py b/src/cobra/core/species.py index 68b095d9c..4ff106123 100644 --- a/src/cobra/core/species.py +++ b/src/cobra/core/species.py @@ -45,6 +45,7 @@ def __init__( self._model = None # references to reactions that operate on this species self._reaction = set() + self._comparment = None @property def reactions(self) -> FrozenSet: From 9809fc66987b431c0a3553476fed94215bd4158b Mon Sep 17 00:00:00 2001 From: "uri.akavia" Date: Mon, 28 Mar 2022 15:13:28 -0400 Subject: [PATCH 2/4] revised to be based on group --- src/cobra/core/compartment.py | 150 +++++++++++++++------------------- 1 file changed, 66 insertions(+), 84 deletions(-) diff --git a/src/cobra/core/compartment.py b/src/cobra/core/compartment.py index f1d192f55..67e09ed10 100644 --- a/src/cobra/core/compartment.py +++ b/src/cobra/core/compartment.py @@ -4,11 +4,11 @@ from warnings import warn from .dictlist import DictList -from .object import Object -from .. import Metabolite, Gene, Reaction, Model +from .group import Group +from .. import Metabolite, Reaction, Model -class Compartment(Object): +class Compartment(Group): """ Manage groups via this implementation of the SBML group specification. @@ -26,20 +26,6 @@ class Compartment(Object): A DictList containing references to cobra.Model-associated objects that belong to the group. Members should be metabolites or genes, where reactions are inferred from metabolites. - compartment_type : str, optional - SBML Level 2 Versions 2–4 provide the compartment type as a grouping construct - that can be used to establish a relationship between multiple Compartment - objects. A CompartmentType object only has an identity, and this identity can - only be used to indicate that particular Compartment objects in the model - belong to this type. This may be useful for conveying a modeling intention, - such as when a model contains many similar compartments, either by their - biological function or the reactions they carry. Without a compartment type - construct, it would be impossible within SBML itself to indicate that all of - the compartments share an underlying conceptual relationship because each - SBML compartment must be given a unique and separate identity. A - CompartmentType has no mathematical meaning in SBML—it has no effect on - a model's mathematical interpretation. Simulators and other numerical analysis - software may ignore CompartmentType definitions and references in a model. dimensions: float, optional Compartments can have dimensions defined, if they are volume (3 dimensions) or 2 (a two-dimensional compartment, a surface, like a membrane). In theory, this @@ -47,14 +33,8 @@ class Compartment(Object): complicate the math. The number of dimensions influences the size and units used. """ - def __init__( - self, - id: str, - name: str = "", - members: Optional[Iterable] = None, - compartment_type: Optional[str] = None, - dimensions: Optional[float] = None, - ): + def __init__(self, id: str, name: str = "", members: Optional[Iterable] = None, + dimensions: Optional[float] = None): """Initialize the group object. id : str @@ -64,69 +44,32 @@ def __init__( members : iterable, optional A DictList containing references to cobra.Model-associated objects that belong to the group. - kind : {"collection", "classification", "partonomy"}, optional - The kind of group, as specified for the Groups feature in the SBML - level 3 package specification. - """ - Object.__init__(self, id, name) + dimensions: float, optional + Compartments can have dimensions defined, if they are volume (3 dimensions) or + 2 (a two-dimensional compartment, a surface, like a membrane). In theory, this + can be 1 or 0 dimensions, and even partial dimensions. The number of + dimensions influences the size and units used. + Raises + ------ + TypeError if given anything other than metaoblites or reactions. + """ + super().__init__(id, name, members) + for x in members: + if not isinstance(x, (Metabolite, Reaction)): + raise(TypeError, f"Compartments should have only " + f"reactions or metabolites. {x} is a {type(x)}.") self._members = DictList() if members is None else DictList(members) self._compartment_type = None - self.compartment_type = "" if compartment_type is None else compartment_type + self.__delattr__("kind") # Compartments don't have kind # self.model is None or refers to the cobra.Model that # contains self self._dimensions = dimensions self._model = None - def __len__(self) -> int: - """Get length of group. - - Returns - ------- - int - An int with the length of the group. - - """ - return len(self._members) - - # read-only - @property - def members(self) -> DictList: - """Get members of the group. - - Returns - ------- - DictList - A dictlist containing the members of the group. - """ - return self._members - - @property - def compartment_type(self) -> str: - """Return the compartment type. - - Returns - ------- - str - The compartment type. - - """ - return self._compartment_type - - @compartment_type.setter - def compartment_type(self, compartment_type: str) -> None: - """Set the compartment type. - - Parameters - ---------- - compartment_type: str - - """ - self._compartment_type = compartment_type - def add_members(self, new_members: Union[str, Metabolite, Reaction, List[Union[Reaction, Metabolite]]]) -> None: - """Add objects to the group. + """Add objects to the compartment. Parameters ---------- @@ -137,7 +80,7 @@ def add_members(self, new_members: Union[str, Metabolite, Reaction, Raises ------ - TypeError - if given any object other than Metaoblite or Gene + TypeError - if given any object other than Metaoblite or Reaction """ if isinstance(new_members, str) or hasattr(new_members, "id"): warn("need to pass in a list") @@ -180,25 +123,64 @@ def metabolites(self) -> DictList[Metabolite]: return self._members.query(lambda x: isinstance(x, Metabolite)) @property - def reactions(self) -> Optional[FrozenSet[Reaction]]: + def inferred_reactions(self) -> FrozenSet[Reaction]: """Return the reactions whose metabolites are in the compartment. This is returned as a FrozenSet of reactions for each metabolite in the compartment, if any. + Returns + ------- + FrozenSet of cobra.Reactions + Reactions that have metabolites that belong to this compartment. + Returns ------- FrozenSet of cobra.Reactions Reactions that have metabolites that belong to this compartment. """ - direct_set = set(self._members.query(lambda x: isinstance(x, Reaction))) rxn_set = set() for met in self.metabolites: rxn_set.update(met._reactions) - return frozenset(rxn_set.union(direct_set)) + return frozenset(rxn_set) + + @property + def assigned_reactions(self) -> FrozenSet[Reaction]: + """Return the reactions who were assigned to this compartment. + + This is returned as a FrozenSet of reactions for each metabolite in the + compartment, if any. + + Returns + ------- + FrozenSet of cobra.Reactions + Reactions that have metabolites that belong to this compartment. + + Returns + ------- + FrozenSet of cobra.Reactions + Reactions that were assigned to this compartment, if any. + """ + return frozenset(self._members.query(lambda x: isinstance(x, Reaction))) + + @property + def reactions(self) -> FrozenSet[Reaction]: + """Return the reactions who belong to this compartment. + + This is returned as a FrozenSet of reactions for each metabolite in the + compartment, if any, and the reactions that were assigned to this compartment + directly. + + Returns + ------- + FrozenSet of cobra.Reactions + Reactions that belong to this compartment, both assigned and inferred. + """ + direct_set = set(self.assigned_reactions) + return frozenset(direct_set.union(self.inferred_reactions)) - def __contains__(self, member: Union[Metabolite, Gene]): - return member.compartment is self + def __contains__(self, member: Union[Metabolite, Reaction]): + return self.members.__contains__(member) def merge(self, other): warn("Not implemented yet") From a80e23c1a30623f13faa6660767e8f5812e3835d Mon Sep 17 00:00:00 2001 From: "uri.akavia" Date: Mon, 28 Mar 2022 20:23:01 -0400 Subject: [PATCH 3/4] modified draft based on discussion --- src/cobra/core/compartment.py | 4 ++-- src/cobra/core/reaction.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/cobra/core/compartment.py b/src/cobra/core/compartment.py index 67e09ed10..932f2132a 100644 --- a/src/cobra/core/compartment.py +++ b/src/cobra/core/compartment.py @@ -176,8 +176,8 @@ def reactions(self) -> FrozenSet[Reaction]: FrozenSet of cobra.Reactions Reactions that belong to this compartment, both assigned and inferred. """ - direct_set = set(self.assigned_reactions) - return frozenset(direct_set.union(self.inferred_reactions)) + assigned_reactions = set(self.assigned_reactions) + return frozenset(assigned_reactions.union(self.inferred_reactions)) def __contains__(self, member: Union[Metabolite, Reaction]): return self.members.__contains__(member) diff --git a/src/cobra/core/reaction.py b/src/cobra/core/reaction.py index 52e2d723a..a1e64afd0 100644 --- a/src/cobra/core/reaction.py +++ b/src/cobra/core/reaction.py @@ -23,6 +23,7 @@ ) from warnings import warn +from .compartment import Compartment if TYPE_CHECKING: from optlang.interface import Variable @@ -1439,6 +1440,18 @@ def get_compartments(self) -> list: warn("use Reaction.compartments instead", DeprecationWarning) return list(self.compartments) + @property + def location(self) -> Optional[Compartment]: + """Get assigned compartment, if any. + + Returns + ------- + Compartment + Gets the compartment this reaction was explicitly assigned to, if one + exists. If no compartment was assigned, return None. + """ + return self._compartment + def _associate_gene(self, cobra_gene: Gene) -> None: """Associates a cobra.Gene object with a cobra.Reaction. From 5168db37683a984e2d4105d7e6378061ee411759 Mon Sep 17 00:00:00 2001 From: "uri.akavia" Date: Tue, 29 Mar 2022 18:01:36 -0400 Subject: [PATCH 4/4] some minor corrections --- src/cobra/core/compartment.py | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/src/cobra/core/compartment.py b/src/cobra/core/compartment.py index 932f2132a..d308dee0d 100644 --- a/src/cobra/core/compartment.py +++ b/src/cobra/core/compartment.py @@ -10,7 +10,7 @@ class Compartment(Group): """ - Manage groups via this implementation of the SBML group specification. + Manage compartments via this implementation of the SBML group specification. `Compartment` is a class for holding information regarding a bounded space in which species are located. You can think of it as a location in a model, @@ -29,13 +29,14 @@ class Compartment(Group): dimensions: float, optional Compartments can have dimensions defined, if they are volume (3 dimensions) or 2 (a two-dimensional compartment, a surface, like a membrane). In theory, this - can be 1 or 0 dimensions, and even partial dimensions, but that will needlessly - complicate the math. The number of dimensions influences the size and units - used. + can be 1 or 0 dimensions, and even partial dimensions (fractal dimensions for + trees and vessels). For almost every application in constraint based modeling + this will be an integer. The number of dimensions influences the size and + units used. """ def __init__(self, id: str, name: str = "", members: Optional[Iterable] = None, dimensions: Optional[float] = None): - """Initialize the group object. + """Initialize the Compartment object. id : str The identifier to associate with this group @@ -57,8 +58,8 @@ def __init__(self, id: str, name: str = "", members: Optional[Iterable] = None, super().__init__(id, name, members) for x in members: if not isinstance(x, (Metabolite, Reaction)): - raise(TypeError, f"Compartments should have only " - f"reactions or metabolites. {x} is a {type(x)}.") + raise(TypeError, f"Compartments should only have reactions or " + f"metabolites as members. {x} is a {type(x)}.") self._members = DictList() if members is None else DictList(members) self._compartment_type = None self.__delattr__("kind") # Compartments don't have kind @@ -163,22 +164,6 @@ def assigned_reactions(self) -> FrozenSet[Reaction]: """ return frozenset(self._members.query(lambda x: isinstance(x, Reaction))) - @property - def reactions(self) -> FrozenSet[Reaction]: - """Return the reactions who belong to this compartment. - - This is returned as a FrozenSet of reactions for each metabolite in the - compartment, if any, and the reactions that were assigned to this compartment - directly. - - Returns - ------- - FrozenSet of cobra.Reactions - Reactions that belong to this compartment, both assigned and inferred. - """ - assigned_reactions = set(self.assigned_reactions) - return frozenset(assigned_reactions.union(self.inferred_reactions)) - def __contains__(self, member: Union[Metabolite, Reaction]): return self.members.__contains__(member)