From 7c80ca04d28b4c00f0e1b3e2cafa32a9d5b270c2 Mon Sep 17 00:00:00 2001 From: Peter Schubert <Peter.Schubert@hhu.de> Date: Tue, 7 Jun 2022 17:17:00 +0200 Subject: [PATCH] OptResults added and small adjustments --- xbanalysis/_version.py | 2 +- xbanalysis/model/xba_parameter.py | 3 +- xbanalysis/model/xba_reaction.py | 40 +- xbanalysis/model/xba_species.py | 23 +- xbanalysis/problems/gba_problem.py | 823 ++++++++++++++++++++++------- xbanalysis/utils/__init__.py | 5 +- xbanalysis/utils/hessians.py | 125 +++++ xbanalysis/utils/opt_results.py | 66 +++ 8 files changed, 845 insertions(+), 242 deletions(-) create mode 100644 xbanalysis/utils/hessians.py create mode 100644 xbanalysis/utils/opt_results.py diff --git a/xbanalysis/_version.py b/xbanalysis/_version.py index 60596f0..2a1d8ea 100644 --- a/xbanalysis/_version.py +++ b/xbanalysis/_version.py @@ -1,5 +1,5 @@ """ Definition of version string. """ -__version__ = "0.1" +__version__ = "0.2.0" program_name = 'xbanalysis' diff --git a/xbanalysis/model/xba_parameter.py b/xbanalysis/model/xba_parameter.py index 656dd5a..812301c 100644 --- a/xbanalysis/model/xba_parameter.py +++ b/xbanalysis/model/xba_parameter.py @@ -3,6 +3,7 @@ Peter Schubert, HHU Duesseldorf, May 2022 """ + class XbaParameter: def __init__(self, s_parameter): @@ -11,4 +12,4 @@ class XbaParameter: self.sboterm = s_parameter.get('sboterm', '') self.value = s_parameter['value'] self.constant = s_parameter['constant'] - self.units = s_parameter['units'] \ No newline at end of file + self.units = s_parameter['units'] diff --git a/xbanalysis/model/xba_reaction.py b/xbanalysis/model/xba_reaction.py index 1303bf6..985af26 100644 --- a/xbanalysis/model/xba_reaction.py +++ b/xbanalysis/model/xba_reaction.py @@ -31,14 +31,14 @@ class XbaReaction: def set_enzyme(self, species): for sid in self.modifiers: - if species[sid].sboterm == 'SBO:0000250' or species[sid].sboterm == 'SBO:0000252': + if species[sid].sboterm in ('SBO:0000252', 'SBO:0000250'): self.enzyme = sid break def add_species_usage(self, species): # for metabolic reactions add to the species consumed/produced the metabolic reaction id # !!! we could also enter the Reaction Object references instead !!!!!!!!!! - if (self.sboterm == 'SBO:0000167') or (self.sboterm == 'SBO:0000179') or (self.sboterm == 'SBO:0000182'): + if self.sboterm in ('SBO:0000167', 'SBO:0000179', 'SBO:0000182'): for sid, val in self.products.items(): species[sid].add_m_reaction(self.id, val) for sid, val in self.reactants.items(): @@ -46,35 +46,31 @@ class XbaReaction: # protein degradation reaction, assume that first protein in reactants is getting degraded # a protein degradation reaction is still a metabolic reaction if self.sboterm == 'SBO:0000179': - for sid in self.reactants: - if (species[sid].sboterm == 'SBO:0000252') or (species[sid].sboterm == 'SBO:0000250'): - species[sid].set_p_degrad_rid(self.id) + for sid, val in self.reactants.items(): + if species[sid].sboterm in ('SBO:0000252', 'SBO:0000250'): + species[sid].add_p_degrad_rid(self.id, -val) break - # protein interconversion reactions + # protein transitions if self.sboterm == 'SBO:0000182': - for sid in self.reactants: - if (species[sid].sboterm == 'SBO:0000252') or (species[sid].sboterm == 'SBO:0000250'): - species[sid].set_p_conversion_rid(self.id) + for sid, val in self.reactants.items(): + if species[sid].sboterm in ('SBO:0000252', 'SBO:0000250'): + species[sid].add_p_transition_rid(self.id, -val) break - for sid in self.products: - if (species[sid].sboterm == 'SBO:0000252') or (species[sid].sboterm == 'SBO:0000250'): - species[sid].set_p_conversion_rid(self.id) + for sid, val in self.products.items(): + if species[sid].sboterm in ('SBO:0000252', 'SBO:0000250'): + species[sid].add_p_transition_rid(self.id, val) break # for protein synthesis reactions identify the enzyme being produced from reaction.products # to species being consumed add the enzyme product with corresponding stoichiometry # to species being produced add the enzyme product with corresponding stoichiometry, unless # the species is itself the enzyme product, in which case we add the ps reaction producing it. - if (self.sboterm == 'SBO:0000589') or (self.sboterm == 'SBO:0000205') : - enz_sid = '' - for sid in self.products: - if (species[sid].sboterm == 'SBO:0000252') or (species[sid].sboterm == 'SBO:0000250'): - enz_sid = sid - self.ps_product = enz_sid - species[enz_sid].set_ps_rid(self.id) + if self.sboterm in ('SBO:0000589', 'SBO:0000205'): + for sid, val in self.products.items(): + if species[sid].sboterm in ('SBO:0000252', 'SBO:0000250'): + self.ps_product = sid break for sid, val in self.products.items(): - if sid != enz_sid: - species[sid].add_ps_reaction(enz_sid, val) + species[sid].add_ps_reaction(self.id, val) for sid, val in self.reactants.items(): - species[sid].add_ps_reaction(enz_sid, -val) + species[sid].add_ps_reaction(self.id, -val) diff --git a/xbanalysis/model/xba_species.py b/xbanalysis/model/xba_species.py index e18fdc1..cf94b57 100644 --- a/xbanalysis/model/xba_species.py +++ b/xbanalysis/model/xba_species.py @@ -15,14 +15,18 @@ class XbaSpecies: self.compartment = s_species['compartment'] self.initial_conc = s_species['initialConcentration'] self.constant = s_species['constant'] + self.boundary = s_species['boundaryCondition'] self.units = s_species['substanceUnits'] attrs = sbmlxdf.misc.extract_xml_attrs(s_species['xmlAnnotation'], token='molecule') - self.weight = float(attrs['weight_Da']) + self.mw = float(attrs['weight_Da']) + self.ps_rid = None self.m_reactions = {} self.ps_reactions = {} - self.ps_rid = None - self.p_degrad_rid = None - self.p_convert_rid = None + self.p_degrad_rids = {} + self.p_trans_rids = {} + + def set_ps_rid(self, rid, stoic): + self.ps_rid = {'rid': rid, 'stoic': stoic} def add_m_reaction(self, rid, stoic): self.m_reactions[rid] = stoic @@ -30,11 +34,8 @@ class XbaSpecies: def add_ps_reaction(self, rid, stoic): self.ps_reactions[rid] = stoic - def set_ps_rid(self, rid): - self.ps_rid = rid - - def set_p_degrad_rid(self, rid): - self.p_degrad_rid = rid + def add_p_degrad_rid(self, rid, stoic): + self.p_degrad_rids[rid] = stoic - def set_p_conversion_rid(self, rid): - self.p_convert_rid = rid + def add_p_transition_rid(self, rid, stoic): + self.p_trans_rids[rid] = stoic diff --git a/xbanalysis/problems/gba_problem.py b/xbanalysis/problems/gba_problem.py index 1f242fb..3f73fc7 100644 --- a/xbanalysis/problems/gba_problem.py +++ b/xbanalysis/problems/gba_problem.py @@ -1,62 +1,93 @@ -"""Implementation of XBAmodel class. +"""Implementation of GBA Problem class. Peter Schubert, HHU Duesseldorf, May 2022 """ -import re import numpy as np -import sympy +import re import types +import sympy + +from xbanalysis.utils.hessians import (hess_from_grad_grad, hess_from_v_hesss, hess_from_jac_jac, + hesss_from_matrix_hesss, hesss_from_v_hess, + hesss_from_grad_jac) + + +# Decorator to cache last result +def cache_last(func): + def wrapper(self, x): + fname = wrapper.__name__ + if fname not in GbaProblem.last_values or np.array_equal(GbaProblem.last_values[fname][0], x) is False: + GbaProblem.last_values[fname] = [x, func(self, x)] + return GbaProblem.last_values[fname][1] + wrapper.__name__ = func.__name__ + return wrapper class GbaProblem: + last_values = {} + def __init__(self, xba_model): - self.xba_model = xba_model + """Initialize GbaProblem from a xba_model. + :param xba_model: xba model with all required GBA parameters + :type xba_model: XbaModel + """ + self.xba_model = xba_model + # self.cache = True + self.last_gr0_x = np.zeros(1) + self.last_gr0 = None # metabolites and enzymes in the model metab_sids = [s.id for s in xba_model.species.values() if s.sboterm == 'SBO:0000247'] - enz_sids = [s.id for s in xba_model.species.values() if (s.sboterm == 'SBO:0000250' or - s.sboterm == 'SBO:0000252')] + enz_sids = [s.id for s in xba_model.species.values() if s.sboterm in ('SBO:0000250', 'SBO:0000252')] # optimization variables self.var_metab_sids = [sid for sid in metab_sids if xba_model.species[sid].constant is False] - self.var_enz_sids = [sid for sid in enz_sids if xba_model.species[sid].constant is False] + self.var_enz_sids = enz_sids self.var_sids = self.var_metab_sids + self.var_enz_sids - self.var_n_metabs = len(self.var_metab_sids) + self.n_vars = len(self.var_sids) self.var_sid2idx = {sid: idx for idx, sid in enumerate(self.var_sids)} + self.var_mask_metab = [True if sid in self.var_metab_sids else False for sid in self.var_sids] + self.var_mask_enz = [True if sid in self.var_enz_sids else False for sid in self.var_sids] self.var_initial = np.array([xba_model.species[sid].initial_conc for sid in self.var_sids]) - # self.var_lower_bounds = np.zeros(len(self.var_sids)) * 1 - # self.var_upper_bounds = np.ones(len(self.var_sids)) * 0.1 + self.var_vols = np.array([xba_model.compartments[xba_model.species[sid].compartment].size + for sid in self.var_sids]) # constant concentrations self.const_metab_sids = [sid for sid in metab_sids if xba_model.species[sid].constant is True] self.const_sid2idx = {sid: idx for idx, sid in enumerate(self.const_metab_sids)} - self.const_enz_sids = [sid for sid in enz_sids if xba_model.species[sid].constant is True] - - self.all_enz_sids = self.var_enz_sids + self.const_enz_sids - # reactions - yet to implement, accept child SBO terms, e.g. here of 00167 + # TODO: accept child SBO terms (i.e. SBO hierarchy parsing) self.mr_rids = [r.id for r in xba_model.reactions.values() - if ((r.sboterm == 'SBO:0000167') or - (r.sboterm == 'SBO:0000179') or - (r.sboterm == 'SBO:0000182'))] + if r.sboterm in ('SBO:0000167', 'SBO:0000179', 'SBO:0000182')] self.ps_rids = [r.id for r in xba_model.reactions.values() - if (r.sboterm == 'SBO:0000589') or (r.sboterm == 'SBO:0000205')] - spont_rids = [rid for rid in self.mr_rids if xba_model.reactions[rid].enzyme == ''] - - # calculate stoichiometric for mass balance equality constraints - s_mat = self.get_stoic_matrix(self.var_metab_sids, self.mr_rids) - var_sids_vol = np.array([self.xba_model.compartments[self.xba_model.species[sid].compartment].size - for sid in self.var_metab_sids]) - s_scaled_mat = s_mat/var_sids_vol.reshape(-1, 1) - - all_enz_ps_rids = [xba_model.species[sid].ps_rid for sid in self.all_enz_sids] - p_mat = self.get_stoic_matrix(self.var_metab_sids, all_enz_ps_rids) - all_enz_vol = np.array([self.xba_model.compartments[self.xba_model.species[sid].compartment].size - for sid in self.all_enz_sids]) - vol_scale_p_mat = np.outer(1.0/var_sids_vol, all_enz_vol) - p_scaled_mat = p_mat * vol_scale_p_mat + if r.sboterm in ('SBO:0000589', 'SBO:0000205')] + # spont_rids = [rid for rid in self.mr_rids if xba_model.reactions[rid].enzyme == ''] + # calculate stoichiometric sub-matrices and scale them accoring to volume + # Note: reordering of P submatrix + # - vector of enzyme concentrations is extracted from optimization vector x + # - sequenze of enzyme concentrations is according to var_enz_sids + # - there is a one-to-one relationship between enzymes and protein synthesis reactions + # - columns of the P submatrix have to be ordered along var_enz_sids for correct P.dot(ce) + # TODO: sparce matrix storage and calculations + + self.var_enz_ps_rids = [list(xba_model.species[sid].ps_reactions.keys())[0] + for sid in self.var_enz_sids] + self.ps_stoic = np.array([list(xba_model.species[sid].ps_reactions.values())[0] + for sid in self.var_enz_sids]) + + metab_x_mrs = self.get_stoic_matrix(self.var_metab_sids, self.mr_rids) + metab_x_psrs = self.get_stoic_matrix(self.var_metab_sids, self.var_enz_ps_rids) + self.enz_x_mrs = self.get_stoic_matrix(self.var_enz_sids, self.mr_rids) + enz_vols = self.var_vols[self.var_mask_enz] + metab_vols = self.var_vols[self.var_mask_metab] + + self.pstma_scale = enz_vols / self.ps_stoic + self.metab_x_mras = metab_x_mrs / metab_vols.reshape((-1, 1)) + self.metab_x_psras = metab_x_psrs / metab_vols.reshape((-1, 1)) + self.enz_x_mras = self.enz_x_mrs / enz_vols.reshape((-1, 1)) + self.pstm_scale = enz_vols / self.ps_stoic # model parameters self.model_params = {pid: p.value for pid, p in xba_model.parameters.items()} @@ -64,28 +95,24 @@ class GbaProblem: self.model_params['macro'] = self.model_params.get('macro', 0.0) > 0.0 self.model_params['ext_conc'] = np.array([xba_model.species[sid].initial_conc for sid in self.const_metab_sids]) - self.model_params['const_enz_conc'] = np.array([xba_model.species[sid].initial_conc - for sid in self.const_enz_sids]) - self.model_params['S_scaled_mat'] = s_scaled_mat - self.model_params['P_scaled_mat'] = p_scaled_mat - # self.model_params['P_scaled_csr'] = scipy.sparse.csr_matrix(p_mat * vol_scale_p_mat) self.model_params['np'] = np - self.model_params['weights'] = np.array([xba_model.species[sid].weight for sid in self.var_sids]) - self.model_params['const_enz_weights'] = np.array([xba_model.species[sid].weight - for sid in self.const_enz_sids]) - self.model_params['weights'] = np.array([xba_model.species[sid].weight for sid in self.var_sids]) + self.model_params['mws'] = np.array([xba_model.species[sid].mw for sid in self.var_sids]) # create functions - self.mrs, self.mrs_jac = self.get_reactions(self.mr_rids) - self.spontrs, self.spontrs_jac = self.get_reactions(spont_rids) - self.pss, self.pss_jac = self.get_reactions(self.ps_rids) - self.inv_ps_rates, self.inv_ps_rates_jac = self.get_inv_ps_rates() - # self.gammas, self.gammas_jac = self.get_gammas() - self.p_degrads, self.p_degrads_jac = self.get_p_degrad_rates() - self.enz_conv_mb, self.enz_conv_mb_jac = self.get_enz_interconv_mb() + self.mras, self.mras_jac, self.mras_hesss = self.get_reactions_apt(self.mr_rids) + self.pstmas, self.pstmas_jac, self.pstmas_hesss = self.get_psm_times_apt() + self.enz_trans, self.enz_trans_jac, self.enz_trans_hesss = self.get_enz_transitions() def get_stoic_matrix(self, sids, rids): - # matrix for free metabolites and list of reactions + """retrieve stoichiometric sub-matrix [sids x rids]. + + :param sids: species ids - rows of sub-matrix + :type sids: list of strings + :param rids: reaction ids - columns of sub-matrix + :type rids: list of strings + :returns: stoic_mat, stoichmetric sub-matrix + :rtype: np.ndarray + """ stoic_mat = np.zeros((len(sids), len(rids))) map_metab_row = {sid: idx for idx, sid in enumerate(sids)} for col, rid in enumerate(rids): @@ -101,94 +128,147 @@ class GbaProblem: self.model_params[key] = val def _to_vectors(self, rs_str): - # replace free and constant metabolites by vector elements + """replace free and constant metabolites by vector elements. + + :param rs_str: reaction string (extracted from the xba model). + :type rs_str: list of strings + :returns: rs_v_str + :rtype: list of reaction stings with some parameters replaced by vector elements + """ rs_v_str = [] for r_v_str in rs_str: for idx, sid in enumerate(self.var_sids): r_v_str = re.sub(r'\b' + sid + r'\b', f'x[{idx}]', r_v_str) for idx, sid in enumerate(self.const_metab_sids): r_v_str = re.sub(r'\b' + sid + r'\b', f'ext_conc[{idx}]', r_v_str) - for idx, sid in enumerate(self.const_enz_sids): - r_v_str = re.sub(r'\b' + sid + r'\b', f'const_enz_conc[{idx}]', r_v_str) rs_v_str.append(r_v_str) return rs_v_str def _gradients(self, rs_v_str): - # generate derivatives + """generate code object for Jacobian and Hessians (lower triangle) of reactions. + + :param rs_v_str: reaction strings with concentrations replaced by vector elements. + :type rs_v_str: list of string + :returns: jac + :rtype: code object to generate Jacobian + :returns: hess + :rtype: code object to generate Hessians (lower triangle) + """ x = sympy.IndexedBase('x') ext_conc = sympy.IndexedBase('ext_conc') - const_enz_conc = sympy.IndexedBase('const_enz_conc') grads_str = [] + hesss_str = [] for i, r_v_str in enumerate(rs_v_str): - func_sym = sympy.sympify(r_v_str, locals={'x': x, - 'ext_conc': ext_conc, - 'const_enz_conc': const_enz_conc}) - partial_der = [str(func_sym.diff(x[idx])) for idx in range(len(self.var_sids))] - grads_str.append('[' + ', '.join(partial_der) + ']') + func_sym = sympy.sympify(r_v_str, locals={'x': x, 'ext_conc': ext_conc}) + partials_x = [] + hess = [] + for idx1 in range(self.n_vars): + partials_x.append(str(func_sym.diff(x[idx1]))) + partials_xx = [] + for idx2 in range(self.n_vars): + if idx2 <= idx1: + partials_xx.append(str(func_sym.diff(x[idx1], x[idx2]))) + else: + partials_xx.append('0') + hess.append('[' + ', '.join(partials_xx) + ']') + grads_str.append('[' + ', '.join(partials_x) + ']') + hesss_str.append('[' + ', '.join(hess) + ']') # progress_bar(i, len(rs_v_str)) jac = compile('def jac(x): return np.array([' + ', '.join(grads_str) + '])', '<string>', 'exec') - return jac - - def get_reactions(self, rids): + hesss = compile('def hess(x): return np.array([' + ', '.join(hesss_str) + '])', + '<string>', 'exec') + return jac, hesss + + def get_reactions_apt(self, rids): + """create function objects for reactions rates, Jacobian and Hessian + + extracted from kinetic laws for SBML model + + Note: reactions in the SBML model are in amounts per time (not concentration per time) + I.e. for mass balances equations, we have to devide by volumes where metabolite is located + + :param rids: reaction ids for reactions in amount per time to be processed. + :type rids: list of strings + :returns: ras + :rtype: function object, returning an 1D array of function results + :returns: ras_jac + :rtype: function object, returning a 2D array with the Jacobian matrix ∂/∂xi + :returns: ras_hesss + :rtype: function object, returning a 3D array (or sparce/lower triangele) of the Hessian + matrices for each individual function + """ kinetics_str = [self.xba_model.reactions[rid].expanded_kl for rid in rids] v_str = self._to_vectors(kinetics_str) - func_code = compile('def reactions(x): return np.array([' + ', '.join(v_str) + '])', - '<string>', 'exec') - rs = types.FunctionType(func_code.co_consts[0], self.model_params) - rs_jac = types.FunctionType(self._gradients(v_str).co_consts[0], self.model_params) - return rs, rs_jac - - def get_inv_ps_rates(self): - # inverse of max protein synthesis rates as function of variable enzymes and constant enzymes - ps_inv_rs_str = [] - for sid in self.all_enz_sids: - sp = self.xba_model.species[sid] - ps_rid = sp.ps_rid - ps_inv_rs_str.append(f'{sp.compartment}/({self.xba_model.reactions[ps_rid].expanded_kl})') - v_str = self._to_vectors(ps_inv_rs_str) - func_code = compile('def ps_inv(x): return np.array([' + ', '.join(v_str) + '])', + func_code = compile('def rs_apt(x): return np.array([' + ', '.join(v_str) + '])', '<string>', 'exec') - inv_ps_rates = types.FunctionType(func_code.co_consts[0], self.model_params) - inv_ps_rates_jac = types.FunctionType(self._gradients(v_str).co_consts[0], self.model_params) - return inv_ps_rates, inv_ps_rates_jac - - def get_p_degrad_rates(self): - # protein degradation rates - degrads_str = [] - for sid in self.all_enz_sids: - sp = self.xba_model.species[sid] - if sp.p_degrad_rid is not None: - degrads_str.append(f'({self.xba_model.reactions[sp.p_degrad_rid].expanded_kl})/' + - f'{sp.compartment}') - else: - degrads_str.append('0') - v_str = self._to_vectors(degrads_str) - func_code = compile('def p_degrads(x): return np.array([' + ', '.join(v_str) + '])', + jac_code, hesss_code = self._gradients(v_str) + ras = types.FunctionType(func_code.co_consts[0], self.model_params) + ras_jac = types.FunctionType(jac_code.co_consts[0], self.model_params) + ras_hesss = types.FunctionType(hesss_code.co_consts[0], self.model_params) + return ras, ras_jac, ras_hesss + + def get_psm_times_apt(self): + """Create function objects for reaction times, Jacobian and Hessian sorted along var_enz_sids + + Maximum protein synthesis rates from model converted to minimum protein synthesis times + + Note: reactions in the SBML model are in amounts per time (not concentration per time) + + :returns: pstmas, returning a 1D array of function results + :rtype: function object + :returns: pstmas_jac, returning a 2D array with the Jacobian matrix ∂/∂xi + :rtype: function object + :returns: pstmas_hesss , returning a 3D array (or sparce/lower triangele) of the Hessian + matrices for each individual function + :rtype: function object + """ + pstmas_str = [] + for idx, sid in enumerate(self.var_enz_sids): + ps_rid = self.var_enz_ps_rids[idx] + pstmas_str.append(f'1.0/({self.xba_model.reactions[ps_rid].expanded_kl})') + v_str = self._to_vectors(pstmas_str) + func_code = compile('def pstms_apt(x): return np.array([' + ', '.join(v_str) + '])', '<string>', 'exec') - degrads = types.FunctionType(func_code.co_consts[0], self.model_params) - degrads_jac = types.FunctionType(self._gradients(v_str).co_consts[0], self.model_params) - return degrads, degrads_jac - - def get_enz_interconv_mb(self): + jac_code, hesss_code = self._gradients(v_str) + pstmas = types.FunctionType(func_code.co_consts[0], self.model_params) + pstmas_jac = types.FunctionType(jac_code.co_consts[0], self.model_params) + pstmas_hesss = types.FunctionType(hesss_code.co_consts[0], self.model_params) + return pstmas, pstmas_jac, pstmas_hesss + + def get_enz_transitions(self): + """Create function objects for protein interconversion, Jacobian and Hessian + + Assumed, that there are only zero of few such conversion in the model + + Note: reactions in the SBML model are in amounts per time (not concentration per time) + + :returns: enz_trans, returning a 1D array of function results + :rtype: function object + :returns: enz_trans_jac, returning a 2D array with the Jacobian matrix ∂/∂xi + :rtype: function object + :returns: enz_trans_hesss, returning a 3D array of the Hessian + matrices for each individual function + :rtype: function object + """ # mass balance for interconvering enzymes - # identify enzymes in ExMr submatrix (enzymes x metabolic reactions) + # identify enzymes in enz x mra submatrix (enzymes x metabolic reactions) # identify sets of enzymes that interconvert (consider unique sets) enz_in_mrs_sids = [] - for enz_sid in self.all_enz_sids: + for enz_sid in self.var_enz_sids: sp = self.xba_model.species[enz_sid] partner_enz_sids = {enz_sid} - for mr_rid, enz_stoic in sp.m_reactions.items(): - rid = self.xba_model.reactions[mr_rid] + for p_trans_rid, enz_stoic in sp.p_trans_rids.items(): + p_trans_r = self.xba_model.reactions[p_trans_rid] if enz_stoic < 0: - for other_sid, other_stoic in rid.products.items(): + for other_sid, other_stoic in p_trans_r.products.items(): sp_other = self.xba_model.species[other_sid] - if (sp_other.sboterm == 'SBO:0000250') or (sp_other.sboterm == 'SBO:0000252'): + if sp_other.sboterm in ('SBO:0000250', 'SBO:0000252'): partner_enz_sids.add(other_sid) if enz_stoic > 0: - for other_sid, other_stoic in rid.reactants.items(): + for other_sid, other_stoic in p_trans_r.reactants.items(): sp_other = self.xba_model.species[other_sid] - if (sp_other.sboterm == 'SBO:0000250') or (sp_other.sboterm == 'SBO:0000252'): + if sp_other.sboterm in ('SBO:0000250', 'SBO:0000252'): partner_enz_sids.add(other_sid) # look for enzyme interconversion if len(partner_enz_sids) > 1: @@ -202,128 +282,463 @@ class GbaProblem: enz_sid = partner_enz_sids.pop() sp = self.xba_model.species[enz_sid] parts_str = [] - for mr_rid, enz_stoic in sp.m_reactions.items(): - parts_str.append((f'({enz_stoic} * {self.xba_model.reactions[mr_rid].expanded_kl})/' + + for p_trans_rid, enz_stoic in sp.p_trans_rids.items(): + parts_str.append((f'({enz_stoic} * {self.xba_model.reactions[p_trans_rid].expanded_kl})/' + f'{sp.compartment}')) conversions_str.append(' + '.join(parts_str)) v_str = self._to_vectors(conversions_str) - func_code = compile('def enz_conv_mb(x): return np.array([' + ', '.join(v_str) + '])', - '<string>', 'exec') - enz_conv_mb = types.FunctionType(func_code.co_consts[0], self.model_params) - enz_conv_mb_jac = types.FunctionType(self._gradients(v_str).co_consts[0], self.model_params) - return enz_conv_mb, enz_conv_mb_jac - - def get_gammas(self): - # ratios between protein degradation and max protein synthesis rates - gammas_str = [] - for sid in self.all_enz_sids: - sp = self.xba_model.species[sid] - if sp.p_degrad_rid is not None: - degrad_rid = sp.p_degrad_rid - ps_rid = sp.ps_rid - gammas_str.append(f'({self.xba_model.reactions[degrad_rid].expanded_kl})/' + - f'({self.xba_model.reactions[ps_rid].expanded_kl})') - else: - gammas_str.append('0') - v_str = self._to_vectors(gammas_str) - func_code = compile('def gammas(x): return np.array([' + ', '.join(v_str) + '])', + if len(v_str) == 0: + v_str = '0' + func_code = compile('def enz_trans(x): return np.array([' + ', '.join(v_str) + '])', '<string>', 'exec') - gammas = types.FunctionType(func_code.co_consts[0], self.model_params) - gammas_jac = types.FunctionType(self._gradients(v_str).co_consts[0], - self.model_params) - return gammas, gammas_jac + jac_code, hesss_code = self._gradients(v_str) + enz_trans = types.FunctionType(func_code.co_consts[0], self.model_params) + enz_trans_jac = types.FunctionType(jac_code.co_consts[0], self.model_params) + enz_trans_hesss = types.FunctionType(hesss_code.co_consts[0], self.model_params) + return enz_trans, enz_trans_jac, enz_trans_hesss + +# @cache_last + def get_growth_rate_0(self, x): + """Calculate growth rate at x, assuming no protein degradation + + considering stoichiometric coefficiens for enzyme turnover + + :param x: optimization variables + :type x: numpy.ndarray + :returns: growth rate at x (without protein degradation) + :rtype: float + """ + # if self.cache: + # # fname = inspect.stack()[0][3] + # if np.array_equal(self.last_gr0_x, x): + # return self.last_gr0 + # gr0 = 1/tau + # tau = ce_1 * pstms_1 + ce_2 * pstms_2 + ... + ce_r * pstms_r = sum(ce_i * pstms_i) + ce_var = x[self.var_mask_enz] + pstms = self.pstma_scale * (self.pstmas(x)) + gr0 = 1.0 / pstms.dot(ce_var) + # if self.cache: + # self.last_gr0_x = x.copy() + # self.last_gr0 = gr0 + return gr0 + + def get_growth_rate_0_grad(self, x): + """Calculate growth rate gradient at x, assuming no protein degradation + + gr0 = 1/tau(x) + tau = ce.dot(pstms(x)) + gr0_grad = ∂(1/tau)/∂x = -(∂tau/∂x)/tau**2 = -gr0**2 * tau_grad + tau_grad = ce_jac.dot(pstms(x)) + ce.dot(pstms_jac(x)) + + :param x: optimization variables + :type x: numpy.ndarray + :returns: growth rate gradient at x (without protein degradation) + :rtype: numpy.ndarray (1D) + """ + gr0 = self.get_growth_rate_0(x) + ce = x[self.var_mask_enz] + ce_jac = np.hstack((np.zeros((len(ce), len(x) - len(ce))), np.eye(len(ce)))) + enz_to_pstms = self.pstma_scale * self.pstmas(x) + enz_to_pstms_jac = self.pstma_scale.reshape((-1, 1)) * self.pstmas_jac(x) + tau_grad = ce.dot(enz_to_pstms_jac) + enz_to_pstms.dot(ce_jac) + gr0_grad = - gr0 ** 2 * tau_grad + return np.nan_to_num(gr0_grad, nan=0.0) + + def get_growth_rate_0_hess(self, x): + """Calculate growth rate Hessian at x, assuming no protein degradation + + gr0 = 1/tau(x) + tau = ce.dot(pstms(x)) + + gr0_grad = -gr0**2 * tau_grad + tau_grad = ce.dot(pstms_jac(x)) + pstms(x).dot(ce_jac) + + gr0_hess = -2 * gr0 * gr0_grad * tau_grad - gr0**2 * tau_hess + gr0_hess = -2 gr0 * -gr0**2 * tau_grad^2 - gr0**2 * tau_hess + gr0_hess = gr0**2 * (2 * gr0 * tau_grad^2 - tau_hess) + tau_hess = ∂(tau_grad)/∂x = ce.dot(pstms_hesss(x)) + ce_jac.dot(pstms_jac(x)) + + pstms_jac(x).dot(ce_jac) + pstms.dot(ce_hess) + ce_hess = 0 + + :param x: optimization variables + :type x: numpy.ndarray + :returns: growth rate Hessian at x (without protein degradation) + :rtype: numpy.ndarray (2D) with shape [x,x] + """ + gr0 = self.get_growth_rate_0(x) + gr0_grad = self.get_growth_rate_0_grad(x) + + ce = x[self.var_mask_enz] + ce_jac = np.hstack((np.zeros((len(ce), len(x) - len(ce))), np.eye(len(ce)))) + enz_to_pstms = self.pstma_scale * self.pstmas(x) + enz_to_pstms_jac = self.pstma_scale.reshape((-1, 1)) * self.pstmas_jac(x) + enz_to_pstms_hesss = (self.pstma_scale ** 2).reshape((-1, 1, 1)) * self.pstmas_hesss(x) + tau_grad = ce.dot(enz_to_pstms_jac) + enz_to_pstms.dot(ce_jac) + + hess_a = hess_from_grad_grad(gr0_grad, tau_grad) + hess_b1 = hess_from_v_hesss(ce, enz_to_pstms_hesss) + hess_b2 = hess_from_jac_jac(ce_jac, enz_to_pstms_jac) + hess_b3 = hess_from_jac_jac(enz_to_pstms_jac, ce_jac) + # gr0_hess = -2 * gr0 * gr0_grad * tau_grad - gr0**2 * tau_hess + # gr0_hess = -2 gr0 * -gr0**2 * tau_grad^2 - gr0**2 * tau_hess + # gr0_hess = gr0**2 * (2 * gr0 * tau_grad^2 - tau_hess) + # tau_hess = ∂(tau_grad)/∂x = ce.dot(pstms_hesss(x)) + ce_jac.dot(pstms_jac(x)) + # + pstms_jac(x).dot(ce_jac) + pstms.dot(ce_hess) + + gr0_hess = - 2 * gr0 * hess_a - gr0 ** 2 * (hess_b1 + hess_b2 + hess_b3) + return np.nan_to_num(gr0_hess, nan=0.0) def get_growth_rate(self, x): - # gr = gr0 * (1 - sum(gammas)) - # gr0 = 1/tau - # tau = sum(ce/psme) = sum(ce * 1/psme) - ce_all = np.append(x[self.var_n_metabs:], self.model_params['const_enz_conc']) - tau = self.inv_ps_rates(x).dot(ce_all) - gr0 = 1.0/tau - # gr = gr0 * (1.0 - sum(gammas(x))) - gr = gr0 * (1.0 - sum(self.p_degrads(x) * self.inv_ps_rates(x))) + """Calculate growth rate at x, considering protein degradation + + considering stoichiometric coefficiens unequal unity for enzyme turnover + stoichiometric sub-matrices do not require volume scaling (as Vol_enz gets factored out) + + gr = gr0 * (1 + enz_to_mras(x).dot(enz_to_pstmas(x))) + enz_to_mras = enz_x_mrs.dot(mras(x)) + enz_to_pstmas = 1/ps_stoic * pstmas(x) + + :param x: optimization variables + :type x: numpy.ndarray + :returns: growth rate at x (considering protein degradation) + :rtype: float + """ + gr0 = self.get_growth_rate_0(x) + if np.count_nonzero(self.enz_x_mrs) == 0: + gr = gr0 + else: + enz_to_mras = self.enz_x_mrs.dot(self.mras(x)) + enz_to_pstmas = 1.0 / self.ps_stoic * self.pstmas(x) + gr = gr0 * (1.0 + enz_to_mras.dot(enz_to_pstmas)) return gr def get_growth_rate_grad(self, x): - # gr0_grad = d(1/tau)dx = (-dtau/dx)/tau**2 = - dtau/dx * gr0**2 - # gr_grad = gr0_grad * (1 - sum(gammas)) - gr0 * sum(gammas_jac) - ce_all = np.append(x[self.var_n_metabs:], self.model_params['const_enz_conc']) - tau = self.inv_ps_rates(x).dot(ce_all) - gr0 = 1.0/tau - dtau_dx = ce_all.dot(self.inv_ps_rates_jac(x)) - dtau_dx[self.var_n_metabs:] += self.inv_ps_rates(x)[:len(self.var_enz_sids)] - gr0_grad = -dtau_dx * gr0**2 - deg_term = (1.0 - sum(self.p_degrads(x) * self.inv_ps_rates(x))) - deg_term_jac = (self.p_degrads(x).reshape(-1, 1) * self.inv_ps_rates_jac(x) + - self.inv_ps_rates(x).reshape(-1, 1) - * self.p_degrads_jac(x)).sum(axis=0) - gr_grad = gr0_grad * deg_term - gr0 * deg_term_jac + """Calculate growth rate gradient at x, considering protein degradation + + gr = gr0 * (1 + enz_to_mras(x).dot(enz_to_pstmas(x))) + enz_to_mras = enz_x_mrs.dot(mras(x)) + enz_to_pstmas = 1/ps_stoic * pstmas(x) + gr_grad = gr0_grad * (1 + enz_to_mras(x).dot(enz_to_pstmas(x))) + + gr0 * (enz_to_mras(x).dot(enz_to_pstmas_jac(x)) + enz_to_pstmas(x).dot(enz_to_mras_jac)) + + :param x: optimization variables + :type x: numpy.ndarray + :returns: growth rate gradient at x (considering protein degradation) + :rtype: numpy.ndarray (1D) + """ + gr0_grad = self.get_growth_rate_0_grad(x) + if np.count_nonzero(self.enz_x_mrs) == 0: + gr_grad = gr0_grad + else: + gr0 = self.get_growth_rate_0(x) + enz_to_mras = self.enz_x_mrs.dot(self.mras(x)) + enz_to_mras_jac = self.enz_x_mrs.dot(self.mras_jac(x)) + enz_to_pstmas = 1.0 / self.ps_stoic * self.pstmas(x) + enz_to_pstmas_jac = 1.0 / self.ps_stoic.reshape((-1, 1)) * self.pstmas_jac(x) + gr_grad = (gr0_grad * (1.0 + enz_to_mras.dot(enz_to_pstmas)) + + gr0 * (enz_to_mras.dot(enz_to_pstmas_jac) + enz_to_pstmas.dot(enz_to_mras_jac))) return np.nan_to_num(gr_grad, nan=0.0) + def get_growth_rate_hess(self, x): + """Calculate growth rate Hessian at x, considering protein degradation + + gr = gr0 * (1 + enz_to_mras(x).dot(enz_to_pstmas(x))) + enz_to_mras = enz_x_mrs.dot(mras(x)) + enz_to_pstmas = 1/ps_stoic * pstmas(x) + + gr_grad = gr0_grad * (1 + enz_to_mras(x).dot(enz_to_pstmas(x))) + + gr0 * (enz_to_mras(x).dot(enz_to_pstmas_jac(x)) + enz_to_pstmas(x).dot(enz_to_mras_jac(x))) + + gr_hess = gr0_hess * (1 + enz_to_mras(x).dot(enz_to_pstmas(x))) + + gr0_grad * (enz_to_mras.dot(enz_to_pstmas_jac) + enz_to_pstmas.dot(enz_to_mras_jac)) + + gr0_grad * (enz_to_mras.dot(enz_to_pstmas_jac) + enz_to_pstmas.dot(enz_to_mras_jac)) + + gr0 * (enz_to_mras_jac.dot(enz_to_pstmas_jac + enz_to_mras.dot(enz_to_pstmas_hesss) + + enz_to_pstmas_jac.dot(enz_to_mras_jac) + enz_to_pstmas.dot(enz_to_mras_hesss) ) + + :param x: optimization variables + :type x: numpy.ndarray + :returns: growth rate Hessian at x (considering protein degradation) + :rtype: numpy.ndarray (2D) with shape [x,x] + """ + gr0_hess = self.get_growth_rate_0_hess(x) + if np.count_nonzero(self.enz_x_mrs) == 0: + gr_hess = gr0_hess + else: + gr0 = self.get_growth_rate_0(x) + gr0_grad = self.get_growth_rate_0_grad(x) + enz_to_mras = self.enz_x_mrs.dot(self.mras(x)) + enz_to_mras_jac = self.enz_x_mrs.dot(self.mras_jac(x)) + enz_to_mras_hesss = hesss_from_matrix_hesss(self.enz_x_mrs, self.mras_hesss(x)) + enz_to_pstmas = 1.0 / self.ps_stoic * self.pstmas(x) + enz_to_pstmas_jac = 1.0 / self.ps_stoic.reshape((-1, 1)) * self.pstmas_jac(x) + enz_to_pstmas_hesss = 1.0 / (self.ps_stoic ** 2).reshape((-1, 1, 1)) * self.pstmas_hesss(x) + + hess_a = gr0_hess * (1.0 + enz_to_mras.dot(enz_to_pstmas)) + tau_grad = enz_to_mras.dot(enz_to_pstmas_jac) + enz_to_pstmas.dot(enz_to_mras_jac) + hess_b = hess_from_grad_grad(gr0_grad, tau_grad) + hess_c1 = hess_from_jac_jac(enz_to_mras_jac, enz_to_pstmas_jac) + hess_c2 = hess_from_v_hesss(enz_to_mras, enz_to_pstmas_hesss) + hess_c3 = hess_from_jac_jac(enz_to_pstmas_jac, enz_to_mras_jac) + hess_c4 = hess_from_v_hesss(enz_to_pstmas, enz_to_mras_hesss) + # gr_hess = gr0_hess * (1 + enz_to_mras(x).dot(enz_to_pstmas(x))) + # + gr0_grad * (enz_to_mras.dot(enz_to_pstmas_jac) + enz_to_pstmas.dot(enz_to_mras_jac)) + # + gr0_grad * (enz_to_mras.dot(enz_to_pstmas_jac) + enz_to_pstmas.dot(enz_to_mras_jac)) + # + gr0 * (enz_to_mras_jac.dot(enz_to_pstmas_jac + enz_to_mras.dot(enz_to_pstmas_hesss) + # + enz_to_pstmas_jac.dot(enz_to_mras_jac) + enz_to_pstmas.dot(enz_to_mras_hesss)) + + gr_hess = hess_a + 2 * hess_b + gr0 * (hess_c1 + hess_c2 + hess_c3 + hess_c4) + return np.nan_to_num(gr_hess, nan=0.0) + def get_alphas(self, x): - # alpha_e = gr * c_e/psm_e + gamma_e - ce_all = np.append(x[self.var_n_metabs:], self.model_params['const_enz_conc']) - alpha0 = self.get_growth_rate(x) * self.inv_ps_rates(x)*ce_all - deg_term = self.p_degrads(x) * self.inv_ps_rates(x) - alphas = alpha0 + deg_term + """Calculate ribosomal allocations at x, considering protein degradation + + alpha_e = gr * ce_e * pstm_e + p_degr_e * pstm_e + alpha = gr * ce.dot(pstms(x)) + p_degrs(x).dot(pstms(x)) + + :param x: optimization variables + :type x: numpy.ndarray + :returns: ribosomal allocations at x (considering protein degradation) + :rtype: numpy.ndarray (1D) + """ + ce = x[self.var_mask_enz] + gr = self.get_growth_rate(x) + + enz_to_pstms = self.pstma_scale * self.pstmas(x) + alphas0 = gr * ce * enz_to_pstms + if np.count_nonzero(self.enz_x_mrs) == 0: + alphas = alphas0 + else: + enz_to_mras = self.enz_x_mrs.dot(self.mras(x)) + enz_to_pstmas = 1.0 / self.ps_stoic * self.pstmas(x) + alphas = alphas0 - enz_to_mras * enz_to_pstmas return alphas def heq_mass_balance(self, x): - # considering syntesis costs for protein degradation - # mass balance for species involved in metabolic reactions: - # dMb/dt: S.dot(mrs) + deg_term + gr*(P.dot(ce) - cm) = 0 - # deg_term: P.dot(pdegs) - # assuming protein degradation functions (pdegs) have soichiomtry of -1 for enzyme - cm_var = x[:self.var_n_metabs] - ce_all = np.append(x[self.var_n_metabs:], self.model_params['const_enz_conc']) - degrad_costs = self.model_params['P_scaled_mat'].dot(self.p_degrads(x)) - metabolism = self.model_params['S_scaled_mat'].dot(self.mrs(x)) - pem = self.model_params['P_scaled_mat'].dot(ce_all) - cm_var - dilution = self.get_growth_rate(x) * pem - metabs_mb = metabolism + dilution + degrad_costs - return metabs_mb + """Calculate mass balance equations at x for metabolites + + incuding protein degradation , which consume metabolites + Note: protein degradation is also part of metabolic reactions (mras), which return + metabolites. + + metab_mb = dcm/dt = metabolic_balance + growth_dilution + sythesis_enzyme_balance + metabolic_balance = metab_x_mras.dot(mras(x)) + growth_dilution = λ * (metab_x_psras.dot(ce_scaled) - cm) + ce_scaled = pstm_scale * ce # convert concentration to amounts, considering stoichiometry + sythesis_enzyme_balance = - metab_x_psras.dot(pstm_scale * enzyme_balance) + enzyme_balance = enz_x_mras.dot(mras(x)) + + :param x: optimization variables + :type x: numpy.ndarray + :returns: mass balance calculated at x + :rtype: numpy.ndarray (1D) + """ + gr = self.get_growth_rate(x) + cm = x[self.var_mask_metab] + ce_scaled = self.pstm_scale * x[self.var_mask_enz] + + metabolic_mb = self.metab_x_mras.dot(self.mras(x)) + ps_drain = self.metab_x_psras.dot(ce_scaled) + + if np.count_nonzero(self.enz_x_mras) == 0: + metab_mb = metabolic_mb + gr * (ps_drain - cm) + else: + enz_mb = self.enz_x_mras.dot(self.mras(x)) + synth_enz_mb = - self.metab_x_psras.dot(self.pstm_scale * enz_mb) + metab_mb = metabolic_mb + gr * (ps_drain - cm) + synth_enz_mb + return metab_mb def heq_mass_balance_jac(self, x): - cm_var = x[:self.var_n_metabs] - ce_all = np.append(x[self.var_n_metabs:], self.model_params['const_enz_conc']) - - metabolism_jac = self.model_params['S_scaled_mat'].dot(self.mrs_jac(x)) - degrad_costs_jac = self.model_params['P_scaled_mat'].dot(self.p_degrads_jac(x)) - - # jacobian of dilution term: d/dx gr * (P.dot(ce) - cm) - # jac = gr_jac * (P.dot(ce) - cm) + gr * (jac(P.dot(ce)) - jac(cm)) - pem = self.model_params['P_scaled_mat'].dot(ce_all) - cm_var - # construct jacobian of dilution term (P.dot(ce) - cm): - # d/dx (P.dot(ce) - cm) = d(P.dot(ce) - cm)/dx = P.dot(dce/dx) - dcm/dx - cm_jac = -np.eye(self.var_n_metabs) - pe_jac = self.model_params['P_scaled_mat'][:, :len(self.var_enz_sids)] - pem_jac = np.hstack([cm_jac, pe_jac]) - dilution_jac = np.outer(pem, self.get_growth_rate_grad(x)) + self.get_growth_rate(x) * pem_jac - metabs_mb_jac = metabolism_jac + dilution_jac + degrad_costs_jac - return np.nan_to_num(metabs_mb_jac, nan=0.0) - - def heq_enz_conv_mb(self, x): - return self.enz_conv_mb(x) - - def heq_enz_conv_mb_jac(self, x): - return self.enz_conv_mb_jac(x) + """Calculate Jacobian of mass balance equations at x for metabolites + + incuding protein degradation , which consume metabolites + Note: protein degradation is also part of metabolic reactions (mras), which return + metabolites. + + metab_mb = dcm/dt = metabolic_mb + growth_dilution + sythesis_enzyme_balance + metabolic_mb = metab_x_mras.dot(mras(x)) + growth_dilution = λ * (metab_x_psras.dot*(ce_scaled) - cm) + ce_scaled = pstm_scale * ce # convert concentration to amounts, considering stoichiometry + sythesis_enzyme_balance = - metab_x_psras.dot(pstm_scale * enzyme_balance) + enzyme_balance = enz_x_mras.dot(mras(x)) + + ∂metab_mb/∂x = metabolic_mb_jac + dilution_jac + sythesis_enzyme_balance_jac + metabolic_mb_jac = metab_x_mras.dot(mras_jac(x)) + dilution_jac = gr_grad * (metab_x_psras.dot(ce_scaled) - cm) + + gr * (metab_x_psras.dot(ce_scaled_jac) - cm_jac) + ce_scaled = pstm_scale * ce # convert concentration to amounts, considering stoichiometry + ce_jac = np.hstack( (np.zeros((len(ce), len(cm))), np.eye(len(ce)) )) + cm_jac = np.hstack( (np.eye(len(cm)), np.zeros((len(cm), len(ce))) )) + ce_scaled_jac = pstm_scale.reshape((-1,1)) * ce_jac + sythesis_enzyme_balance_jac = - metab_x_psras.dot(pstm_scale * enzyme_balance_jac) + enzyme_balance_jac = enz_x_mras.dot(mras_jac(x)) + + :param x: optimization variables + :type x: numpy.ndarray + :returns: Jacobian of mass balance equations at x + :rtype: numpy.ndarray (2D) + """ + gr = self.get_growth_rate(x) + gr_grad = self.get_growth_rate_grad(x) + + cm = x[self.var_mask_metab] + ce = x[self.var_mask_enz] + ce_scaled = self.pstm_scale * ce + cm_jac = np.hstack((np.eye(len(cm)), np.zeros((len(cm), len(x) - len(cm))))) + ce_jac = np.hstack((np.zeros((len(ce), len(x) - len(ce))), np.eye(len(ce)))) + ce_jac_scaled = self.pstm_scale.reshape((-1, 1)) * ce_jac + + metabolic_jac = self.metab_x_mras.dot(self.mras_jac(x)) + ps_drain = self.metab_x_psras.dot(ce_scaled) + dilute_1_jac = np.outer((ps_drain - cm), gr_grad) + dilute_2_jac = gr * (self.metab_x_psras.dot(ce_jac_scaled) - cm_jac) + + if np.count_nonzero(self.enz_x_mras) == 0: + metab_mb_jac = metabolic_jac + dilute_1_jac + dilute_2_jac + else: + enz_mb_jac = self.enz_x_mras.dot(self.mras_jac(x)) + synth_enz_mb_jac = -self.metab_x_psras.dot(self.pstm_scale.reshape((-1, 1)) * enz_mb_jac) + metab_mb_jac = metabolic_jac + dilute_1_jac + dilute_2_jac + synth_enz_mb_jac + return np.nan_to_num(metab_mb_jac, nan=0.0) + + def heq_mass_balance_hess(self, x): + """Calculate Hessians of mass balance equations at x + + incuding protein degradation , which consume metabolites + Here: assuming protein degradation has stoichometry of -1 for relevant enzyme + Note: protein degradation is also part of metabolic reactions (mras), which return + metabolites. + + metab_mb = dcm/dt = metabolic_mb + growth_dilution + sythesis_enzyme_balance + metabolic_mb = metab_x_mras.dot(mras(x)) + growth_dilution = λ * (metab_x_psras.dot*(ce_scaled) - cm) + ce_scaled = pstm_scale * ce # convert concentration to amounts, considering stoichiometry + sythesis_enzyme_balance = - metab_x_psras.dot(pstm_scale * enzyme_balance) + enzyme_balance = enz_x_mras.dot(mras(x)) + + ∂metab_mb/∂x = metabolic_mb_jac + dilution_jac + sythesis_enzyme_balance_jac + metabolic_mb_jac = metab_x_mras.dot(mras_jac(x)) + dilution_jac = gr_grad * (metab_x_psras.dot(ce_scaled) - cm) + + gr * (metab_x_psras.dot(ce_scaled_jac) - cm_jac) + ce_scaled = pstm_scale * ce # convert concentration to amounts, considering stoichiometry + ce_jac = np.hstack( (np.zeros((len(ce), len(cm))), np.eye(len(ce)) )) + cm_jac = np.hstack( (np.eye(len(cm)), np.zeros((len(cm), len(ce))) )) + ce_scaled_jac = pstm_scale.reshape((-1,1)) * ce_jac + sythesis_enzyme_balance_jac = - metab_x_psras.dot(pstm_scale * enzyme_balance_jac) + enzyme_balance_jac = enz_x_mras.dot(mras_jac(x)) + + array of n Hessians + metab_mb_hesss = metabolic_mb_hess + dilution_hess + sythesis_enzyme_balance_hess + metabolic_mb_hesss = metab_x_mras.dot(mras_hesss(x)) + dilution_hessd = gr_hess * (metab_x_psras.dot(ce_scaled) - cm) + + gr_grad * (metab_x_psras.dot(ce_scaled_jac) - cm_jac) + + gr_grad * (metab_x_psras.dot(ce_scaled_jac) - cm_jac) + + gr * (metab_x_psras.dot(ce_scaled_hess) - cm_hess) + dilution_hesss = gr_hess * (metab_x_psras.dot(ce_scaled) - cm) + + 2 * gr_grad * (metab_x_psras.dot(ce_scaled_jac) - cm_jac) + + ce_scaled = pstm_scale * ce # convert concentration to amounts, considering stoichiometry + ce_jac = np.hstack( (np.zeros((len(ce), len(cm))), np.eye(len(ce)) )) + cm_jac = np.hstack( (np.eye(len(cm)), np.zeros((len(cm), len(ce))) )) + ce_scaled_jac = pstm_scale.reshape((-1,1)) * ce_jac + cm_hess = np.zeros((x,x)) # as cm_jac only contains constants + ce_hess = np.zeros((x,x)) + + sythesis_enzyme_balance_hesss = - metab_x_psras.dot(pstm_scale * enzyme_balance_hesss) + + enzyme_balance_jac = enz_x_mras.dot(mras_hesss(x)) + + sythesis_enzyme_balance_jac = - metab_x_psras.dot(pstm_scale * enzyme_balance_jac) + enzyme_balance_jac = enz_x_mras.dot(mras_jac(x)) + + :param x: optimization variables + :type x: numpy.ndarray + :returns: Hessians of mass balance equations at x + :rtype: numpy.ndarray (3D) with shape [n,x,x] + """ + gr_grad = self.get_growth_rate_grad(x) + gr_hess = self.get_growth_rate_hess(x) + + cm = x[self.var_mask_metab] + ce = x[self.var_mask_enz] + ce_scaled = self.pstm_scale * ce + cm_jac = np.hstack((np.eye(len(cm)), np.zeros((len(cm), len(x) - len(cm))))) + ce_jac = np.hstack((np.zeros((len(ce), len(x) - len(ce))), np.eye(len(ce)))) + ce_jac_scaled = self.pstm_scale.reshape((-1, 1)) * ce_jac + + metabolic_hesss = hesss_from_matrix_hesss(self.metab_x_mras, self.mras_hesss(x)) + ps_drain = self.metab_x_psras.dot(ce_scaled) + dilute_1_hesss = hesss_from_v_hess(ps_drain - cm, gr_hess) + dilute_2_hesss = hesss_from_grad_jac(gr_grad, self.metab_x_psras.dot(ce_jac_scaled) - cm_jac) + + if np.count_nonzero(self.enz_x_mras) == 0: + metab_mb_hesss = metabolic_hesss + dilute_1_hesss + 2 * dilute_2_hesss + else: + enz_mb_hesss = hesss_from_matrix_hesss(self.enz_x_mras, self.mras_hesss(x)) + + synth_enz_mb_hesss = -hesss_from_matrix_hesss(self.metab_x_psras, + (self.pstm_scale ** 2).reshape((-1, 1, 1)) * enz_mb_hesss) + metab_mb_hesss = metabolic_hesss + dilute_1_hesss + 2 * dilute_2_hesss + synth_enz_mb_hesss + return np.nan_to_num(metab_mb_hesss, nan=0.0) + + def heq_enz_trans(self, x): + return self.enz_trans(x) + + def heq_enz_trans_jac(self, x): + return self.enz_trans_jac(x) + + def heq_enz_trans_hess(self, x): + return self.enz_trans_hesss(x) def heq_density(self, x): - ce_var = x[self.var_n_metabs:] + """Calculate density (g/l) constraint at x + + Could be extended having several density constraints + + macromolecular density is total protein mass density + alternalively, dry mass density also adds total metabolite mass density + formulated in a way to + + using ipopt, the constraint bounds could be set to rho (only add up masses) + density = sum(species_concentration * molecular_weight) + + :param x: optimization variables + :type x: numpy.ndarray + :returns: mass balance calculated at x + :rtype: numpy.ndarray (1D) + """ + ce = x[self.var_mask_enz] if self.model_params.get('macro', True) is True: - density = self.model_params['weights'][self.var_n_metabs:].dot(ce_var) + density = self.model_params['mws'][self.var_mask_enz].dot(ce) else: - density = self.model_params['weights'].dot(x) - density += self.model_params['const_enz_weights'].dot( - self.model_params['const_enz_conc']) - return self.model_params['rho'] - density + density = self.model_params['mws'].dot(x) + # !!! when using Ipopt, the rho could be specified as a bound! + return np.array(self.model_params['rho'] - density) + # noinspection PyUnusedLocal def heq_density_jac(self, x): + """Calculate gradient of density constraint at x + + :param x: optimization variables + :type x: numpy.ndarray + :returns: mass balance jacobian at x + :rtype: numpy.ndarray (1D) + """ if self.model_params.get('macro', True) is True: - density_grad = np.zeros(len(self.model_params['weights'])) - density_grad[self.var_n_metabs:] = \ - -self.model_params['weights'][self.var_n_metabs:] + density_grad = np.zeros(len(x)) + density_grad[self.var_mask_enz] = -self.model_params['mws'][self.var_mask_enz] else: - density_grad = -self.model_params['weights'] + density_grad = -self.model_params['mws'] return density_grad + + # noinspection PyUnusedLocal + def heq_density_hess(self, x): + """Calculate hessian of density constraint at x + + :param x: optimization variables + :type x: numpy.ndarray + :returns: mass balance Hessian at x + :rtype: numpy.ndarray (2D), shape [x,x] + """ + hess = np.zeros((self.n_vars, self.n_vars)) + return np.tril(hess) diff --git a/xbanalysis/utils/__init__.py b/xbanalysis/utils/__init__.py index d75f80f..7a3e1e8 100644 --- a/xbanalysis/utils/__init__.py +++ b/xbanalysis/utils/__init__.py @@ -1,6 +1,5 @@ """Subpackage with utility functions """ -#rom .xba_model import XBAmodel - -__all__ = [] +from .opt_results import OptResults +__all__ = ['OptResults'] diff --git a/xbanalysis/utils/hessians.py b/xbanalysis/utils/hessians.py new file mode 100644 index 0000000..9c63f41 --- /dev/null +++ b/xbanalysis/utils/hessians.py @@ -0,0 +1,125 @@ +"""Implementation of Hessians calculations. + +Peter Schubert, HHU Duesseldorf, May 2022 +""" + +import numpy as np + + +def hess_from_grad_grad(grad_l, grad_r): + """Calculate Hessian from two gradiends wrt x + + :param grad_l: left scalar gradient wrt x variables + :param grad_l: numpy.ndarray (1D) with length of x + :param grad_r: rigth scalar gradient wrt x variables + :param grad_r: numpy.ndarray (1D) with length of x + :returns: result_hess Hessian wrt to x variables, lower triangle matrix + :rtype: numpy.ndarray (2D) with shape [x,x] + """ + assert grad_l.shape[0] == grad_r.shape[0] + size = grad_l.shape[0] + result_hess = np.zeros((size, size)) + for i in range(size): + for j in range(i+1): + result_hess[i, j] = grad_l[i] * grad_r[j] + return result_hess + + +def hess_from_v_hesss(v, hesss): + """Calculate Hessian from a vector and Hessians + + :param v: vector of length n + :param v: numpy.ndarray (1D) + :param hesss: Hessians of n vectors wrt to x variables + :param hesss: numpy.ndarray (3D) with shape [n,x,x] + :returns: result_hess Hessian wrt to x variables, lower triangle matrix + :rtype: numpy.ndarray (2D) with shape [x,x] + """ + assert hesss.shape[1] == hesss.shape[2] + size = hesss.shape[1] + result_hess = np.zeros((size, size)) + for i in range(size): + for j in range(i + 1): + result_hess[i, j] = v.dot(hesss[:, i, j]) + return result_hess + + +def hess_from_jac_jac(jac_l, jac_r): + """Calculate Hessian from two Jacobians + + :param jac_l: left Jacobian for vector of size n wrt to x variables + :param jac_l: numpy.ndarray (2D) with shape [n,x] + :param jac_r: right Jacobian for vector of size n wrt to x variables + :param jac_r: numpy.ndarray (2D) with shape [n,x] + :returns: result_hess Hessian wrt to x variables, lower triangle matrix + :rtype: numpy.ndarray (2D) with shape [x,x] + """ + assert jac_l.shape == jac_r.shape + size = jac_l.shape[1] + result_hess = np.zeros((size, size)) + for i in range(size): + for j in range(i + 1): + result_hess[i, j] = jac_l[:, i].dot(jac_r[:, j]) + return result_hess + + +def hesss_from_matrix_hesss(matrix, hesss): + """Calculate dot product of a stoichiometric matrix with Hessians + + :param matrix: stoichiometric sub-matrix with shape (m, n) + :param matrix: numpy.ndarray (2D) with shape [m,n] + :param hesss: Hessians of n vectors wrt to x variables + :param hesss: numpy.ndarray (3D) with shape [n,x,x] + :returns: result_hess Hessians wrt to x variables, lower triangle matrix + :rtype: numpy.ndarray (3D) with shape [n,x,x] + """ + assert matrix.shape[1] == hesss.shape[0], "expect as many Hessians as there are matrix columns" + assert hesss.shape[1] == hesss.shape[2] + size = hesss.shape[1] + n_rows, n_cols = matrix.shape + result_hesss = np.zeros((n_rows, size, size)) + for i in range(n_rows): + for j in range(n_cols): + if matrix[i, j] != 0: + result_hesss[i] += matrix[i, j] * hesss[j] + return result_hesss + + +def hesss_from_v_hess(v, hess): + """Calculate Hessians from vector and a Hessian + + :param v: vector of length m + :param v: numpy.ndarray (1D) + :param hess: Hessian of scalar wrt to x variables + :param hess: numpy.ndarray (2D) with shape [x,x] + :returns: result_hesss Hessians wrt to x variables, lower triangle matrix + :rtype: numpy.ndarray (3D) with shape [n,x,x] + """ + assert hess.shape[0] == hess.shape[1] + size = hess.shape[1] + n_rows = v.shape[0] + + result_hesss = np.zeros((n_rows, size, size)) + for i in range(n_rows): + result_hesss[i] = v[i] * hess + return result_hesss + + +def hesss_from_grad_jac(grad, jac): + """Calculate Hessians from gradient and a Jacobian + + :param grad: gradient of scalar wrt to x variables + :param grad: numpy.ndarray (1D) with shape [x] + :param jac: Jacobian of a vector size m wrt to x variables + :param jac: numpy.ndarray (2D) with shape [m,x] + :returns: result_hesss Hessians wrt to x variables, lower triangle matrix + :rtype: numpy.ndarray (3D) with shape [m,x,x] + """ + assert grad.shape[0] == jac.shape[1] + size = jac.shape[1] + n_rows = jac.shape[0] + + result_hesss = np.zeros((n_rows, size, size)) + for i in range(n_rows): + result_hesss[i] = hess_from_grad_grad(jac[i], grad) + return result_hesss diff --git a/xbanalysis/utils/opt_results.py b/xbanalysis/utils/opt_results.py new file mode 100644 index 0000000..df73f03 --- /dev/null +++ b/xbanalysis/utils/opt_results.py @@ -0,0 +1,66 @@ +"""Implementation of OptResults class. + +Peter Schubert, HHU Duesseldorf, June 2022 +""" + +import numpy as np + + +class OptResults: + + def __init__(self, problem, n_points): + """Initialize OptResults. + + :param problem: GbaProblem from where to extract relevant parameters + :type problem: GbaProblem + :param n_points: number of points to record + :type n_points: integer + """ + self.problem = problem + self._n_points = n_points + self._idx = 0 + self._n_m = len(self.problem.var_metab_sids) + self._n_e = len(self.problem.var_enz_sids) + + self.ress = {} + self.ress['metabolites'] = self.problem.var_metab_sids + self.ress['enzymes'] = self.problem.var_enz_sids + self.ress['mr_reactions'] = self.problem.mr_rids + self.ress['enz_sid2idx'] = {sid: idx for idx, sid in enumerate(self.problem.var_enz_sids)} + self.ress['var_mask_metab'] = self.problem.var_mask_metab + self.ress['var_mask_enz'] = self.problem.var_mask_enz + + self.ress['grs_per_h'] = np.zeros(n_points) + self.ress['status'] = np.zeros(n_points) + self.ress['x_opt'] = np.zeros((n_points, self.problem.n_vars)) + self.ress['cm_M'] = np.zeros((n_points, self._n_m)) + self.ress['rho_m'] = np.zeros((n_points, self._n_m)) + self.ress['ce_M'] = np.zeros((n_points, self._n_e)) + self.ress['rho_e'] = np.zeros((n_points, self._n_e)) + self.ress['alphas'] = np.zeros((n_points, self._n_e)) + self.ress['mr_fluxes'] = np.zeros((n_points, len(self.problem.mr_rids))) + self.ress['psm_fluxes'] = np.zeros((n_points, self._n_e)) + + def add(self, info): + if self._idx < self._n_points: + i = self._idx + cx = info['x'] + self.ress['x_opt'][i] = cx + self.ress['status'][i] = info['status'] + self.ress['grs_per_h'][i] = -3600 * info['obj_val'] + self.ress['cm_M'][i] = cx[self.problem.var_mask_metab] + self.ress['ce_M'][i] = cx[self.problem.var_mask_enz] + rho = self.problem.model_params['mws'] * cx + self.ress['rho_m'][i] = rho[self.problem.var_mask_metab] + self.ress['rho_e'][i] = rho[self.problem.var_mask_enz] + + # !!! what volume to consider, possibly the volume of the ribosome ?? + scale_factor = 3600 * 1e3 / self.problem.model_params['rho'] + self.ress['mr_fluxes'][i] = scale_factor * self.problem.mras(cx) / self.problem.var_vols[-1] + self.ress['psm_fluxes'][i] = scale_factor / (self.problem.pstm_scale * self.problem.pstmas(cx)) + + self.ress['alphas'][i] = self.problem.get_alphas(cx) + self._idx += 1 + + def get(self): + return self.ress -- GitLab