From 37b710f93c8fa0531e877fbae00fad36d7c5a6a9 Mon Sep 17 00:00:00 2001 From: "samira.vandenbogaard" Date: Thu, 1 Aug 2024 10:54:18 +0200 Subject: [PATCH] ensured (un)pickling is possible for all configurations of the PAModel --- Scripts/pam_generation.py | 20 +++++- Scripts/pam_generation_uniprot_id.py | 15 +++- Scripts/toy_ec_pam.py | 3 +- src/PAModelpy/CatalyticEvent.py | 24 +++++++ src/PAModelpy/Enzyme.py | 69 ++++++++++++++++++- src/PAModelpy/EnzymeSectors.py | 11 +++ src/PAModelpy/PAModel.py | 28 +++++++- src/PAModelpy/__init__.py | 2 +- .../test_pamodel/test_pam_generation.py | 17 +++++ tests/unit_tests/test_pamodel/test_pamodel.py | 1 + 10 files changed, 181 insertions(+), 9 deletions(-) diff --git a/Scripts/pam_generation.py b/Scripts/pam_generation.py index 2a524a7..2951701 100644 --- a/Scripts/pam_generation.py +++ b/Scripts/pam_generation.py @@ -4,9 +4,11 @@ from typing import Union # load PAMpy modules -from PAModelpy.PAModel import PAModel -from PAModelpy.EnzymeSectors import ActiveEnzymeSector, UnusedEnzymeSector, TransEnzymeSector -from PAModelpy.configuration import Config +from src.PAModelpy.PAModel import PAModel +from src.PAModelpy.EnzymeSectors import ActiveEnzymeSector, UnusedEnzymeSector, TransEnzymeSector +from src.PAModelpy.configuration import Config + +from src.PAModelpy import EnzymeVariable from Scripts.toy_ec_pam import build_toy_gem, build_active_enzyme_sector, build_translational_protein_sector, build_unused_protein_sector @@ -319,3 +321,15 @@ def parse_coefficients(pamodel): def parse_esc(pamodel): return pamodel.enzyme_sensitivity_coefficients.coefficient.to_list() +if __name__ == '__main__': + ecoli_pam = set_up_ecoli_pam(sensitivity=False) + # ecoli_pam.objective = ecoli_pam.BIOMASS_REACTION + ecoli_pam.change_reaction_bounds('EX_glc__D_e', -10, 0) + ecoli_pam.optimize() + print(ecoli_pam.objective.value) + import pickle + + with open('path_to_your_pickle_file.pkl', 'wb') as file: + p = pickle.dump(ecoli_pam, file) + with open('path_to_your_pickle_file.pkl', 'rb') as file: + ob = pickle.load(file) \ No newline at end of file diff --git a/Scripts/pam_generation_uniprot_id.py b/Scripts/pam_generation_uniprot_id.py index b68f603..c6aef23 100644 --- a/Scripts/pam_generation_uniprot_id.py +++ b/Scripts/pam_generation_uniprot_id.py @@ -8,6 +8,7 @@ from src.PAModelpy.PAModel import PAModel from src.PAModelpy.EnzymeSectors import ActiveEnzymeSector, UnusedEnzymeSector, TransEnzymeSector from src.PAModelpy.configuration import Config +from src.PAModelpy import CatalyticEvent, EnzymeVariable from Scripts.toy_ec_pam import build_toy_gem, build_active_enzyme_sector, build_translational_protein_sector, build_unused_protein_sector import ast @@ -442,6 +443,18 @@ def filter_sublists(nested_list, target_string): import pickle with open('path_to_your_pickle_file.pkl', 'wb') as file: - p = pickle.dump(ecoli_pam, file) + pickle.dump(ecoli_pam, file) with open('path_to_your_pickle_file.pkl', 'rb') as file: ob = pickle.load(file) + + + # for enz_var in ecoli_pam.enzyme_variables: + # if enz_var not in ob.enzyme_variables: + # print(enz_var) + # for ce in ecoli_pam.catalytic_events: + # ce_ob = ob.catalytic_events.get_by_id(ce.id) + # for var in ce.enzyme_variables: + # if cobra.DictList(ce_ob.enzyme_variables).has_id(var.id): + # print(var, ce) + # print(ce.enzyme_variables) + # print(ce_ob.enzyme_variables) diff --git a/Scripts/toy_ec_pam.py b/Scripts/toy_ec_pam.py index d4c36b8..fa3f691 100644 --- a/Scripts/toy_ec_pam.py +++ b/Scripts/toy_ec_pam.py @@ -47,7 +47,7 @@ def build_toy_gem(): #set up model basics model = Model('toy_model') cobra_config = Configuration() - cobra_config.solver = 'gurobi' + # cobra_config.solver = 'gurobi' for i in range(1, n + 1): rxn = Reaction('R' + str(i)) lower_bound = 0 @@ -183,7 +183,6 @@ def print_heatmap(xaxis, matrix, yaxis = None): pamodel = PAModel(model, name='toy model MCA with enzyme constraints', active_sector=active_enzyme, translational_sector = translation_enzyme, unused_sector = unused_enzyme, p_tot=Etot, configuration=Config) - #optimize biomass formation pamodel.objective={pamodel.reactions.get_by_id('R7') :1} diff --git a/src/PAModelpy/CatalyticEvent.py b/src/PAModelpy/CatalyticEvent.py index f09564c..269b108 100644 --- a/src/PAModelpy/CatalyticEvent.py +++ b/src/PAModelpy/CatalyticEvent.py @@ -144,6 +144,7 @@ def model(self): @model.setter def model(self, model): self._model = model + # add reaction instance if self.rxn_id in self._model.reactions: self.rxn = self._model.reactions.get_by_id(self.rxn_id) @@ -196,6 +197,10 @@ def add_enzymes(self, enzyme_kcat_dict: dict): enzyme with the associated reaction. The kcat is another dictionary with `f` and `b` for the forward and backward reactions respectively. """ + # return lists back to dictlist after unpickling + if isinstance(self.enzymes, list): + self.enzymes = DictList(self.enzymes) + self.enzyme_variables = DictList(self.enzyme_variables) for enzyme, kcat in enzyme_kcat_dict.items(): # check if the enzyme is already associated to the catalytic event @@ -285,6 +290,10 @@ def remove_enzymes(self, enzyme_list: list): A list with PAModelpy.Package.Enzyme objects to be removed. If a list of identifiers (str) is provided, the corresponding enzyme will be obtained from the CatalyticEvent.enzymes attribute. """ + # return lists back to dictlist after unpickling + if isinstance(self.enzymes, list): + self.enzymes = DictList(self.enzymes) + self.enzyme_variables = DictList(self.enzyme_variables) # check the input if not hasattr(enzyme_list, "__iter__"): @@ -407,3 +416,18 @@ def __deepcopy__(self, memo: dict) -> "CatalyticEvent": cop = deepcopy(super(CatalyticEvent, self), memo) return cop + + def __getstate__(self): + # Return the state to be pickled + state = self.__dict__.copy() + # Handling non-serializable attributes + state['enzyme_variables'] = list(self.enzyme_variables) + state['enzymes'] = list(self.enzymes) + return state + + def __setstate__(self, state): + # Restore state from the unpickled state + self.__dict__.update(state) + + + diff --git a/src/PAModelpy/Enzyme.py b/src/PAModelpy/Enzyme.py index a4cdf0b..5057c10 100644 --- a/src/PAModelpy/Enzyme.py +++ b/src/PAModelpy/Enzyme.py @@ -18,6 +18,11 @@ from warnings import warn +def _change_catalytic_event_list_to_dictlist_after_unpickling(self): + # return lists back to dictlist after unpickling + if not isinstance(self.catalytic_events,DictList): + self.enzymes = DictList(self.catalytic_events) + class Enzyme(Object): """Upper level Enzyme object containing information about the enzyme and links to the EnzymeVariables for each reaction the enzyme catalyzes. @@ -152,6 +157,7 @@ def add_catalytic_event(self, ce: CatalyticEvent, kcats: Dict): Returns: NoneType: None """ + _change_catalytic_event_list_to_dictlist_after_unpickling(self) self.catalytic_events += [ce] self.enzyme_variable.add_catalytic_events([ce], [kcats]) @@ -186,7 +192,6 @@ def add_genes(self, gene_list: list, gene_length:list, relation:str = 'OR') -> N self.genes += genes_to_add if self._model is not None: - print(genes_to_add) self._model.add_genes(genes = genes_to_add, enzymes = [self], gene_lengths=gene_length) @@ -236,6 +241,7 @@ def change_kcat_values(self, rxn2kcat: Dict): rxn2kcat (Dict): A dictionary with reaction ID, kcat value pairs for the forward (f) and backward (b) reaction, e.g. `{'PGI': {'f': 30, 'b': 0.1}}` """ + _change_catalytic_event_list_to_dictlist_after_unpickling(self) # update the enzyme variables for rxn_id, kcats in rxn2kcat.items(): @@ -289,6 +295,7 @@ def remove_catalytic_event(self, catalytic_event: Union[CatalyticEvent, str]): Args: catalytic_event (Union[CatalyticEvent, str]): CatalyticEvent or str, catalytic event or identifier to remove. """ + _change_catalytic_event_list_to_dictlist_after_unpickling(self) if isinstance(catalytic_event, str): try: catalytic_event = self.catalytic_events.get_by_id(catalytic_event) @@ -300,6 +307,7 @@ def remove_catalytic_event(self, catalytic_event: Union[CatalyticEvent, str]): # remove the event from the DictList self.catalytic_events.remove(catalytic_event) + def __copy__(self) -> "Enzyme": """Copy the enzyme variable. @@ -324,6 +332,20 @@ def __deepcopy__(self, memo: dict) -> "Enzyme": return cop + def __getstate__(self): + # Return the state to be pickled + state = self.__dict__.copy() + state['catalytic_events'] = list(self.catalytic_events) + # Handle any non-serializable attributes here + return state + + def __setstate__(self, state): + # Restore state from the unpickled state + self.__dict__.update(state) + # Handle any attributes that require initialization or special handling here + + + class EnzymeComplex(Enzyme): """Upper-level EnzymeComplex object containing information about the enzymes in a complex and a link to the enzyme variables (CatalyticEvents) for each reaction the enzyme complex catalyzes. @@ -404,6 +426,21 @@ def __deepcopy__(self, memo: dict) -> "EnzymeComplex": return cop + def __getstate__(self): + # Return the state to be pickled + state = self.__dict__.copy() + state['catalytic_events'] = list(self.catalytic_events) + + # Handle any non-serializable attributes here + return state + + def __setstate__(self, state): + # Restore state from the unpickled state + self.__dict__.update(state) + # Handle any attributes that require initialization or special handling here + + + class EnzymeVariable(Reaction): """EnzymeVariable is a class for holding information regarding the variable representing an enzyme in the model. For each reaction, the enzyme variables are summarized in a CatalyticEvent. @@ -570,6 +607,8 @@ def model(self): @model.setter def model(self, model): + # _change_catalytic_event_list_to_dictlist_after_unpickling(self) + self._model = model # setting up the relations to the model # add enzyme instance @@ -710,6 +749,7 @@ def add_catalytic_events(self, catalytic_events:list, kcats:list): catalytic_events (list): A list of catalytic events to add. kcats (list): A list with dictionaries containing direction and kcat key-value pairs. """ + _change_catalytic_event_list_to_dictlist_after_unpickling(self) for i, ce in enumerate(catalytic_events): if ce not in self.catalytic_events: @@ -786,6 +826,8 @@ def remove_catalytic_event(self, catalytic_event: Union[CatalyticEvent, str]): Args: catalytic_event (Union[CatalyticEvent, str]): CatalyticEvent or str, catalytic event or identifier to remove. """ + _change_catalytic_event_list_to_dictlist_after_unpickling(self) + if isinstance(catalytic_event, str): try: catalytic_event = self.catalytic_events.get_by_id(catalytic_event) @@ -852,6 +894,8 @@ def change_kcat_values(self, reaction_kcat_dict: dict): enzyme with the associated reaction. The kcat is another dictionary with `f` and `b` for the forward and backward reactions, respectively. """ + _change_catalytic_event_list_to_dictlist_after_unpickling(self) + # apply changes to internal dicts (one by one to avoid deleting kcat values) kcats_change = {} for rxn, kcat_dict in reaction_kcat_dict.items(): @@ -875,6 +919,15 @@ def change_kcat_values(self, reaction_kcat_dict: dict): direction, kcat) self._model.solver.update() + def __str__(self) -> str: + """Return enzyme variable id as str. + + Returns: + str + A string comprised out of the enzyme id + """ + return f"{self.id}" + def __copy__(self) -> "PAModelpy.Enzyme.EnzymeVariable": """Copy the enzyme variable. @@ -897,3 +950,17 @@ def __deepcopy__(self, memo: dict) -> "PAModelpy.Enzyme.EnzymeVariable": cop = deepcopy(super(EnzymeVariable, self), memo) return cop + + def __getstate__(self): + # Return the state to be pickled + state = self.__dict__.copy() + state['catalytic_events'] = list(self.catalytic_events) + + # Handle any non-serializable attributes here + return state + + def __setstate__(self, state): + # Restore state from the unpickled state + + self.__dict__.update(state) + # Handle any attributes that require initialization or special handling here diff --git a/src/PAModelpy/EnzymeSectors.py b/src/PAModelpy/EnzymeSectors.py index 712ec48..85a7c90 100644 --- a/src/PAModelpy/EnzymeSectors.py +++ b/src/PAModelpy/EnzymeSectors.py @@ -355,6 +355,17 @@ def _get_model_genes_from_enzyme(self, enzyme_id: str, model: Model) -> list: gene_list.append(genes_and_list) return gene_list + # def __getstate__(self): + # # Return the state to be pickled + # state = self.__dict__.copy() + # # Handle any non-serializable attributes here + # return state + + def __setstate__(self, state): + # Restore state from the unpickled state + self.__dict__.update(state) + # Handle any attributes that require initialization or special handling here + class TransEnzymeSector(EnzymeSector): DEFAULT_MOL_MASS = 4.0590394e05 # default E. coli ribosome molar mass [g/mol] BIOMASS_RXNID = Config.BIOMASS_REACTION diff --git a/src/PAModelpy/PAModel.py b/src/PAModelpy/PAModel.py index d7674fb..c23dd00 100644 --- a/src/PAModelpy/PAModel.py +++ b/src/PAModelpy/PAModel.py @@ -2062,4 +2062,30 @@ def find_init_args(self, object): for param, default in inspect.signature(object.__init__).parameters.items(): if param != "self" and default.default == inspect.Parameter.empty: init_args[param] = getattr(object, param) - return init_args \ No newline at end of file + return init_args + + def __getstate__(self) -> Dict: + """Get state for serialization. + + Ensures that the context stack is cleared prior to serialization, + since partial functions cannot be pickled reliably. + + Returns + ------- + odict: Dict + A dictionary of state, based on self.__dict__. + """ + odict = self.__dict__.copy() + odict["_contexts"] = [] + return odict + + def __setstate__(self, state: Dict) -> None: + """Make sure all cobra.Objects an PAModel.Objects in the model point to the model. + + Parameters + ---------- + state: dict + """ + self.__dict__.update(state) + if not hasattr(self, "name"): + self.name = None diff --git a/src/PAModelpy/__init__.py b/src/PAModelpy/__init__.py index 4872dcc..4acfd69 100644 --- a/src/PAModelpy/__init__.py +++ b/src/PAModelpy/__init__.py @@ -1,4 +1,4 @@ -print('Loading PAModelpy modules version 0.0.3.13') +print('Loading PAModelpy modules version 0.0.3.14') from .Enzyme import * from .EnzymeSectors import * diff --git a/tests/unit_tests/test_pamodel/test_pam_generation.py b/tests/unit_tests/test_pamodel/test_pam_generation.py index d6f221b..9d0f995 100644 --- a/tests/unit_tests/test_pamodel/test_pam_generation.py +++ b/tests/unit_tests/test_pamodel/test_pam_generation.py @@ -1,4 +1,5 @@ import pytest +import pickle from src.PAModelpy.configuration import Config from src.PAModelpy.PAModel import PAModel @@ -119,6 +120,22 @@ def test_if_ecoli_pam_optimizes(): sut.optimize() assert sut.objective.value > 0 +def test_if_pamodel_can_be_pickled_and_unpickled(): + sut = set_up_ecoli_pam(sensitivity=False) + sut.change_reaction_bounds('EX_glc__D_e', -10, 0) + sut.optimize() + + # Act + sut_pickle = pickle.dumps(sut) + sut_unpickled = pickle.loads(sut_pickle) + + # sut_unpickled.change_reaction_bounds('EX_glc__D_e', -10, 0) + sut_unpickled.optimize() + + # Assert + assert sut.objective.value == pytest.approx(sut_unpickled.objective.value, rel = 1e-4) + assert all([enz_id in [e.id for e in sut_unpickled.enzymes] for enz_id in sut.enzymes]) + ######################################################################################################################### # HELPER FUNCTIONS ########################################################################################################################## diff --git a/tests/unit_tests/test_pamodel/test_pamodel.py b/tests/unit_tests/test_pamodel/test_pamodel.py index 280e651..e991d77 100644 --- a/tests/unit_tests/test_pamodel/test_pamodel.py +++ b/tests/unit_tests/test_pamodel/test_pamodel.py @@ -168,6 +168,7 @@ def test_if_pamodel_gets_catalyzing_enzymes_for_enzyme_object(): # Assert assert all(enz in catalyzing_enzymes for enz in associated_enzymes) + ####################################################################################################### #HELPER METHODS #######################################################################################################