Skip to content

Commit

Permalink
ensured (un)pickling is possible for all configurations of the PAModel
Browse files Browse the repository at this point in the history
  • Loading branch information
SamiralVdB committed Aug 1, 2024
1 parent ebe380c commit 37b710f
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 9 deletions.
20 changes: 17 additions & 3 deletions Scripts/pam_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
15 changes: 14 additions & 1 deletion Scripts/pam_generation_uniprot_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
3 changes: 1 addition & 2 deletions Scripts/toy_ec_pam.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}

Expand Down
24 changes: 24 additions & 0 deletions src/PAModelpy/CatalyticEvent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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__"):
Expand Down Expand Up @@ -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)



69 changes: 68 additions & 1 deletion src/PAModelpy/Enzyme.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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])

Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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():
Expand All @@ -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.
Expand All @@ -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
11 changes: 11 additions & 0 deletions src/PAModelpy/EnzymeSectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 27 additions & 1 deletion src/PAModelpy/PAModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
2 changes: 1 addition & 1 deletion src/PAModelpy/__init__.py
Original file line number Diff line number Diff line change
@@ -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 *
Expand Down
17 changes: 17 additions & 0 deletions tests/unit_tests/test_pamodel/test_pam_generation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
import pickle
from src.PAModelpy.configuration import Config
from src.PAModelpy.PAModel import PAModel

Expand Down Expand Up @@ -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
##########################################################################################################################
Expand Down
1 change: 1 addition & 0 deletions tests/unit_tests/test_pamodel/test_pamodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
#######################################################################################################
Expand Down

0 comments on commit 37b710f

Please sign in to comment.