diff --git a/setup.py b/setup.py index 2c04d6734c30e6928996e9ff4f15d7dc5485d051..fa9a43be13f533c5d79c8105852341927632c2ec 100755 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ setup( install_requires=['pandas>=1.3.0', 'numpy >= 1.20.0', 'sympy >= 1.9.0', + 'scipy >= 1.7.0', 'sbmlxdf>=0.2.5'], python_requires=">=3.7", keywords=['modeling', 'standardization', 'SBML'], diff --git a/xbanalysis/_version.py b/xbanalysis/_version.py index 120eb33ded3939e49674a86660c986ede1a23ee9..88646c942b503d2d5b6382a0b91033a5ed23f740 100644 --- a/xbanalysis/_version.py +++ b/xbanalysis/_version.py @@ -1,5 +1,5 @@ """ Definition of version string. """ -__version__ = "0.3.0" +__version__ = "0.3.1" program_name = 'xbanalysis' diff --git a/xbanalysis/model/xba_model.py b/xbanalysis/model/xba_model.py index 6e127885b53eb4da529d343648d05644401e0b7a..c72928d636f154d2abdd3d34722af143524b9f38 100644 --- a/xbanalysis/model/xba_model.py +++ b/xbanalysis/model/xba_model.py @@ -35,7 +35,7 @@ class XbaModel: for fid, row in model_dict['funcDefs'].iterrows()} self.species = {sid: XbaSpecies(row) for sid, row in model_dict['species'].iterrows()} - self.reactions = {rid: XbaReaction(row, self.functions) + self.reactions = {rid: XbaReaction(row, self.functions, self.compartments) for rid, row in model_dict['reactions'].iterrows()} for r in self.reactions.values(): diff --git a/xbanalysis/model/xba_reaction.py b/xbanalysis/model/xba_reaction.py index 985af2609255305bc7a13ce10bc766b3d4a6894c..94b3f966bbf8549e9eb88796e34052ac5d880844 100644 --- a/xbanalysis/model/xba_reaction.py +++ b/xbanalysis/model/xba_reaction.py @@ -10,7 +10,7 @@ from xbanalysis.utils.utils import get_species_stoic, get_local_params, expand_k class XbaReaction: - def __init__(self, s_reaction, functions): + def __init__(self, s_reaction, functions, compartments): self.id = s_reaction.name self.name = s_reaction.get('name', '') self.sboterm = s_reaction['sboterm'] @@ -26,6 +26,9 @@ class XbaReaction: for pid, val in self.local_params.items(): expanded_kl = re.sub(r'\b' + pid + r'\b', str(val), expanded_kl) self.expanded_kl = expand_kineticlaw(expanded_kl, functions) + for cid in compartments.keys(): + if re.search(r'\b'+cid+r'\b', self.expanded_kl) is not None: + self.compartment = cid self.ps_product = '' self.enzyme = '' @@ -37,40 +40,33 @@ class XbaReaction: 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 !!!!!!!!!! + # !!! we could also enter the Reaction Object references instead 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(): species[sid].add_m_reaction(self.id, -val) - # protein degradation reaction, assume that first protein in reactants is getting degraded - # a protein degradation reaction is still a metabolic reaction + # protein degradation reactions if self.sboterm == 'SBO:0000179': 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 transitions if self.sboterm == 'SBO:0000182': 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, 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. + # protein synthesis reactions 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(): - species[sid].add_ps_reaction(self.id, val) + species[sid].set_ps_rid(self.id) + else: + species[sid].add_ps_reaction(self.id, val) for sid, val in self.reactants.items(): species[sid].add_ps_reaction(self.id, -val) diff --git a/xbanalysis/model/xba_species.py b/xbanalysis/model/xba_species.py index cf94b5735abe0cd44c100999829998579551f527..436537012955b0e481588c15d52e226482d3a1ea 100644 --- a/xbanalysis/model/xba_species.py +++ b/xbanalysis/model/xba_species.py @@ -25,8 +25,8 @@ class XbaSpecies: self.p_degrad_rids = {} self.p_trans_rids = {} - def set_ps_rid(self, rid, stoic): - self.ps_rid = {'rid': rid, 'stoic': stoic} + def set_ps_rid(self, rid): + self.ps_rid = rid def add_m_reaction(self, rid, stoic): self.m_reactions[rid] = stoic diff --git a/xbanalysis/problems/gba_problem.py b/xbanalysis/problems/gba_problem.py index eaeb62b36f17d1342f30a1032cfd481b06685337..0eccbb6e93ab4464aa8b99b1404d0f7479abaf55 100644 --- a/xbanalysis/problems/gba_problem.py +++ b/xbanalysis/problems/gba_problem.py @@ -7,25 +7,32 @@ import numpy as np import re import types import sympy +import scipy -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) +from xbanalysis.utils.utils import progress_bar # Decorator to cache last result def cache_last(func): def wrapper(self, *args): fname = wrapper.__name__ - if fname not in GbaProblem.last_values or np.array_equal(GbaProblem.last_values[fname][0], args[0]) is False: + if (fname not in GbaProblem.last_values or + np.array_equal(GbaProblem.last_values[fname][0], args[0]) is False or + GbaProblem.cache is False): GbaProblem.last_values[fname] = [args[0], func(self, args[0])] return GbaProblem.last_values[fname][1] wrapper.__name__ = func.__name__ return wrapper +# TODO: cache last results from rate functions, Jacobians and Hessians +# TODO: call get_reaction_rates from solver ? +# TODO: split get_reaction_rates, get_reaction_jacs, get_reaction_hesses +# TODO: number of mb equations, enzyme transitions density constraints + class GbaProblem: + cache = True last_values = {} def __init__(self, xba_model): @@ -36,8 +43,7 @@ class GbaProblem: """ 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 in ('SBO:0000250', 'SBO:0000252')] @@ -58,36 +64,40 @@ class GbaProblem: 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)} - # TODO: accept child SBO terms (i.e. SBO hierarchy parsing) + # protein synthesis reactions get ordered according to the order of their enzyme products + # to facilitate matrix multiplication without further remapping + # - here we expect a one-to-one relationship between enzyme and corresponding protein synthesis reaction self.mr_rids = [r.id for r in xba_model.reactions.values() 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 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]) + self.ps_rids = [xba_model.species[sid].ps_rid for sid in self.var_enz_sids] + self.mr_vols = np.array([xba_model.compartments[xba_model.reactions[rid].compartment].size + for rid in self.mr_rids]) + self.ps_vols = np.array([xba_model.compartments[xba_model.reactions[rid].compartment].size + for rid in self.ps_rids]) + # calculate stoichiometric sub-matrices and scale them according to volume + # TODO: sparce matrix storage and calculations 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.metab_x_psrs = self.get_stoic_matrix(self.var_metab_sids, self.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] + self.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 + self.metab_x_psras = self.metab_x_psrs / metab_vols.reshape((-1, 1)) + self.enz_x_mras = self.enz_x_mrs / self.enz_vols.reshape((-1, 1)) + + # identify reactions ids for enzyme degradation and corresponding submatrix + enz_degrad_idxs = self.get_export_idxs(self.enz_x_mrs) + enz_degrad_rids = np.array(self.mr_rids)[enz_degrad_idxs] + self.enz_x_degrads = self.enz_x_mrs[:, enz_degrad_idxs] + + # identify reactions ids for enzyme transitions and corresponding submatrix + # for submatrix perform LU decomposition and remove empty rows from U matrix + enz_trans_idxs = self.get_trans_idxs(self.enz_x_mras) + enz_trans_rids = np.array(self.mr_rids)[enz_trans_idxs] + u = scipy.linalg.lu((self.enz_x_mras[:, enz_trans_idxs]))[2] + self.subenz_x_trans = u[abs(u).sum(axis=1) > 0] # model parameters self.model_params = {pid: p.value for pid, p in xba_model.parameters.items()} @@ -99,9 +109,10 @@ class GbaProblem: self.model_params['mws'] = np.array([xba_model.species[sid].mw for sid in self.var_sids]) # create functions - 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() + self.mras, self.mras_jac, self.mras_hesss = self.get_reactions(self.mr_rids, inv=False) + self.pstmas, self.pstmas_jac, self.pstmas_hesss = self.get_reactions(self.ps_rids, inv=True) + self.degradas, self.degradas_jac, self.degradas_hesss = self.get_reactions(enz_degrad_rids, inv=False) + self.enz_transas, self.enz_transas_jac, self.enz_transas_hesss = self.get_reactions(enz_trans_rids, inv=False) def get_stoic_matrix(self, sids, rids): """retrieve stoichiometric sub-matrix [sids x rids]. @@ -128,12 +139,14 @@ class GbaProblem: self.model_params[key] = val def _to_vectors(self, rs_str): - """replace free and constant metabolites by vector elements. + """replace variables and constant metabolites by vector elements. + + Variables converted to x[], external metabolites to ext_conc[]. - :param rs_str: reaction string (extracted from the xba model). + :param rs_str: reaction strings (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 + :rtype: list of reaction stings with parameters replaced by vector elements """ rs_v_str = [] for r_v_str in rs_str: @@ -163,25 +176,26 @@ class GbaProblem: partials_x = [] hess = [] for idx1 in range(self.n_vars): - partials_x.append(str(func_sym.diff(x[idx1]))) + partial_xsp = func_sym.diff(x[idx1]) + partials_x.append(str(partial_xsp)) partials_xx = [] for idx2 in range(self.n_vars): if idx2 <= idx1: - partials_xx.append(str(func_sym.diff(x[idx1], x[idx2]))) + partials_xx.append(str(partial_xsp.diff(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)) + progress_bar(i, len(rs_v_str)) jac = compile('def jac(x): return np.array([' + ', '.join(grads_str) + '])', '<string>', 'exec') 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 + def get_reactions(self, rids, inv=False): + """create function objects for reactions rates/times, Jacobian and Hessian extracted from kinetic laws for SBML model @@ -190,6 +204,8 @@ class GbaProblem: :param rids: reaction ids for reactions in amount per time to be processed. :type rids: list of strings + :param inv: flag indicating if inverse of reaction required (i.e. reaction times) + :type inv: boolean (default: False) :returns: ras :rtype: function object, returning an 1D array of function results :returns: ras_jac @@ -198,9 +214,12 @@ class GbaProblem: :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] + if inv is False: + kinetics_str = [self.xba_model.reactions[rid].expanded_kl for rid in rids] + else: + kinetics_str = [f'1.0/({self.xba_model.reactions[rid].expanded_kl})' for rid in rids] v_str = self._to_vectors(kinetics_str) - func_code = compile('def rs_apt(x): return np.array([' + ', '.join(v_str) + '])', + func_code = compile('def reactions(x): return np.array([' + ', '.join(v_str) + '])', '<string>', 'exec') jac_code, hesss_code = self._gradients(v_str) ras = types.FunctionType(func_code.co_consts[0], self.model_params) @@ -208,129 +227,74 @@ class GbaProblem: 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 + @staticmethod + def get_trans_idxs(smat): + """Identify metabolic reactions in S matrix with species transitions. - Maximum protein synthesis rates from model converted to minimum protein synthesis times + I.e. columns containing both positive and negative entries - 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 + :param smat: + :type smat: + :return: bolean vector identifying relevant columns + :rtype: 1D ndarray with dtype bool """ - 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') - 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 + mask_trans_idxs = np.zeros(smat.shape[1]) + for col_idx in range(smat.shape[1]): + if ((np.count_nonzero(smat[:, col_idx] < 0) > 0) and + np.count_nonzero(smat[:, col_idx] > 0) > 0): + mask_trans_idxs[col_idx] = 1.0 + return mask_trans_idxs > 0 + + @staticmethod + def get_export_idxs(smat): + """Identify metabolic reactions in S matrix with consumption only. + + I.e. columns containing only negative entries + + :param smat: + :type smat: + :return: bolean vector identifying relevant columns + :rtype: 1D ndarray with dtype bool """ - # mass balance for interconvering enzymes - # 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.var_enz_sids: - sp = self.xba_model.species[enz_sid] - partner_enz_sids = {enz_sid} - 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 p_trans_r.products.items(): - sp_other = self.xba_model.species[other_sid] - 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 p_trans_r.reactants.items(): - sp_other = self.xba_model.species[other_sid] - 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: - # consider unique interconversion sets - if partner_enz_sids not in enz_in_mrs_sids: - enz_in_mrs_sids.append(partner_enz_sids) - - # for each set interconverting enzymes create a mass balance equation - conversions_str = [] - for partner_enz_sids in enz_in_mrs_sids: - enz_sid = partner_enz_sids.pop() - sp = self.xba_model.species[enz_sid] - parts_str = [] - 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) - if len(v_str) == 0: - v_str = '0' - func_code = compile('def enz_trans(x): return np.array([' + ', '.join(v_str) + '])', - '<string>', 'exec') - 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 + mask_remove_idxs = np.zeros(smat.shape[1]) + for col_idx in range(smat.shape[1]): + if ((np.count_nonzero(smat[:, col_idx] < 0) > 0) and + np.count_nonzero(smat[:, col_idx] > 0) == 0): + mask_remove_idxs[col_idx] = 1.0 + return mask_remove_idxs > 0 @cache_last def get_growth_rate_0(self, x): """Calculate growth rate at x, assuming no protein degradation - considering stoichiometric coefficiens for enzyme turnover + Equations: + gr0(x) = 1/tau(x) + tau(x) = pstms(x).dot(ce) + pstms(x) = enz_vols * ptsmas(x) :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)) + pstms = self.enz_vols * 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 @cache_last 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)) + Equations: + gr0(x) = 1/tau(x) + tau(x) = pstms(x).dot(ce) + pstms(x) = enz_vols * ptsmas(x) + + gr0_grad(x) = -tau_grad(x)/tau(x)^2 = -gr0(x)^2 * tau_grad(x) + tau_grad(x) = ce.dot(pstms_jac(x)) + pstms(x).dot(ce_jac) + pstms_jac(x) = enz_vols.reshape(-1,1) * pstmas(x) :param x: optimization variables :type x: numpy.ndarray @@ -340,28 +304,29 @@ class GbaProblem: 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 + pstms = self.enz_vols * self.pstmas(x) + pstms_jac = self.enz_vols.reshape((-1, 1)) * self.pstmas_jac(x) + + tau_grad = ce.dot(pstms_jac) + pstms.dot(ce_jac) + gr0_grad = - gr0**2 * tau_grad return np.nan_to_num(gr0_grad, nan=0.0) @cache_last 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)) + Equations: + gr0(x) = 1/tau(x) + tau(x) = pstms(x).dot(ce) + pstms(x) = enz_vols * ptsmas(x) - gr0_grad = -gr0**2 * tau_grad - tau_grad = ce.dot(pstms_jac(x)) + pstms(x).dot(ce_jac) + gr0_grad(x) = -tau_grad(x)/tau(x)^2 = -gr0(x)^2 * tau_grad(x) + tau_grad(x) = ce.dot(pstms_jac(x)) + pstms(x).dot(ce_jac) + pstms_jac(x) = enz_vols.reshape(-1,1) * pstmas(x) - 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 + gr0_hess(x) = -2*gr0(x) * gr0_grad(x) * tau_grad(x) - gr0(x)^2 * tau_hess(x) + tau_hess = ce_jac.dot(pstms_jac(x)) + ce.dot(pstms_hesss(x)) + pstms_jac(x).dot(ce_jac) + ce_hess = 0 :param x: optimization variables :type x: numpy.ndarray @@ -370,26 +335,19 @@ class GbaProblem: """ 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) + pstms = self.enz_vols * self.pstmas(x) + pstms_jac = self.enz_vols.reshape((-1, 1)) * self.pstmas_jac(x) + pstms_hesss = self.enz_vols.reshape((-1, 1, 1)) * self.pstmas_hesss(x) + + tau_grad = ce.dot(pstms_jac) + pstms.dot(ce_jac) + hess_a = np.outer(gr0_grad, tau_grad) + hess_b1 = (ce.reshape((-1, 1, 1))*pstms_hesss).sum(axis=0) + hess_b2 = ce_jac.T.dot(pstms_jac) + hess_b3 = pstms_jac.T.dot(ce_jac) + gr0_hess = - 2 * gr0 * hess_a - gr0**2 * (hess_b1 + hess_b2 + hess_b3) + return np.nan_to_num(np.tril(gr0_hess), nan=0.0) @cache_last def get_growth_rate(self, x): @@ -397,10 +355,11 @@ class GbaProblem: considering stoichiometric coefficiens unequal unity for enzyme turnover stoichiometric sub-matrices do not require volume scaling (as Vol_enz gets factored out) + With dilution (negative stoichiometric coefficients in enz_x_mrs), growth rate reduces - 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) + Equations: + gr(x) = gr0(x) * (1 + enz_to_mras(x).dot(pstmas(x))) + enz_to_mras(x) = enz_x_mrs.dot(mras(x)) :param x: optimization variables :type x: numpy.ndarray @@ -411,20 +370,23 @@ class GbaProblem: 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)) + pstmas = self.pstmas(x) + enz_to_mras = self.enz_x_degrads.dot(self.degradas(x)) + + gr = gr0 * (1.0 + enz_to_mras.dot(pstmas)) return gr @cache_last def get_growth_rate_grad(self, x): """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)) + Equations: + gr(x) = gr0(x) * (1 + enz_to_mras(x).dot(pstmas(x))) + enz_to_mras(x) = enz_x_mrs.dot(mras(x)) + + gr_grad(x) = gr0_grad(x) * (1 + enz_to_mras(x).dot(pstmas(x))) + + gr0(x) * (pstmas(x).dot(enz_to_mras_jac(x)) + enz_to_mras(x).dot(pstmas_jac(x))) + enz_to_mras_jac(x) = enz_to_mrs.dot(mras_jac(x)) :param x: optimization variables :type x: numpy.ndarray @@ -436,30 +398,35 @@ class GbaProblem: 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))) + pstmas = self.pstmas(x) + pstmas_jac = self.pstmas_jac(x) + enz_to_mras = self.enz_x_degrads.dot(self.degradas(x)) + enz_to_mras_jac = self.enz_x_degrads.dot(self.degradas_jac(x)) + + gr_grad = (gr0_grad * (1.0 + enz_to_mras.dot(pstmas)) + + gr0 * (pstmas.dot(enz_to_mras_jac) + enz_to_mras.dot(pstmas_jac))) return np.nan_to_num(gr_grad, nan=0.0) @cache_last 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) + Equations: + gr(x) = gr0(x) * (1 + enz_to_mras(x).dot(pstmas(x))) + enz_to_mras(x) = enz_x_mrs.dot(mras(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_grad(x) = gr0_grad(x) * (1 + enz_to_mras(x).dot(pstmas(x))) + + gr0(x) * (pstmas(x).dot(enz_to_mras_jac(x)) + enz_to_mras(x).dot(pstmas_jac(x))) + enz_to_mras_jac(x) = enz_to_mrs.dot(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) ) + gr_hess(x) = gr0_hess(x) * (1 + enz_to_mras(x).dot(pstmas(x))) + + (pstmas(x).dot(enz_to_mras_jac(x)) + enz_to_mras(x).dot(pstmas_jac(x))) * gr0_grad(x) + + gr0_grad(x) * (pstmas(x).dot(enz_to_mras_jac(x)) + enz_to_mras(x).dot(pstmas_jac(x))) + + gr0(x) * (pstmas_jac(x).dot(enz_to_mras_jac(x)) + + enz_to_mras_hess(x).dot(pstmas(x)) + + enz_to_mras_jac(x).dot(pstmas_jac(x)) + + pstmas_hess(x).dot(enz_to_mras(x)) + ) :param x: optimization variables :type x: numpy.ndarray @@ -472,28 +439,24 @@ class GbaProblem: 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) + pstmas = self.pstmas(x) + pstmas_jac = self.pstmas_jac(x) + pstmas_hesss = self.pstmas_hesss(x) + enz_to_mras = self.enz_x_degrads.dot(self.degradas(x)) + enz_to_mras_jac = self.enz_x_degrads.dot(self.degradas_jac(x)) + enz_to_mras_hesss = np.array([(row.reshape((-1, 1, 1)) * self.degradas_hesss(x)).sum(axis=0) + for row in self.enz_x_degrads]) + + hess_a = gr0_hess * (1.0 + enz_to_mras.dot(pstmas)) + grad = pstmas.dot(enz_to_mras_jac) + enz_to_mras.dot(pstmas_jac) + hess_b1 = np.outer(gr0_grad, grad) + hess_b2 = np.outer(grad, gr0_grad) + hess_c1 = pstmas_jac.T.dot(enz_to_mras_jac) + hess_c2 = (pstmas.reshape((-1, 1, 1)) * enz_to_mras_hesss).sum(axis=0) + hess_c3 = enz_to_mras_jac.T.dot(pstmas_jac) + hess_c4 = (enz_to_mras.reshape((-1, 1, 1)) * pstmas_hesss).sum(axis=0) + gr_hess = hess_a + hess_b1 + hess_b2 + gr0 * (hess_c1 + hess_c2 + hess_c3 + hess_c4) + return np.nan_to_num(np.tril(gr_hess), nan=0.0) @cache_last def get_alphas(self, x): @@ -509,50 +472,46 @@ class GbaProblem: """ ce = x[self.var_mask_enz] gr = self.get_growth_rate(x) + pstms = self.enz_vols * self.pstmas(x) - enz_to_pstms = self.pstma_scale * self.pstmas(x) - alphas0 = gr * ce * enz_to_pstms + alphas0 = gr * ce * 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 + enz_to_mras = self.enz_x_degrads.dot(self.degradas(x)) + alphas = alphas0 - enz_to_mras * self.pstmas(x) return alphas @cache_last def heq_mass_balance(self, x): """Calculate mass balance equations at x for metabolites - incuding protein degradation , which consume 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) + growth_dilution = λ * (metab_x_psrs.dot(ce) - cm) + ce_scaled = enz_vols * ce # convert concentration to amounts, considering stoichiometry + sythesis_enzyme_balance = - metab_x_psras.dot(enz_vols * 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) + :rtype: numpy.ndarray (1D) with shape (len(var_mask_metab), 1) """ gr = self.get_growth_rate(x) cm = x[self.var_mask_metab] - ce_scaled = self.pstm_scale * x[self.var_mask_enz] + ce = 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: + dilution = self.metab_x_psrs.dot(ce) - cm + metab_mb = metabolic_mb + gr * dilution + if np.count_nonzero(self.enz_x_mras) > 0: 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 + metab_mb -= self.metab_x_psrs.dot(enz_mb) return metab_mb @cache_last @@ -566,19 +525,19 @@ class GbaProblem: 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) + ce_scaled = enz_vols * ce # convert concentration to amounts, considering stoichiometry + sythesis_enzyme_balance = - metab_x_psras.dot(enz_vols * 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_scaled = enz_vols * 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) + ce_scaled_jac = enz_vols.reshape((-1,1)) * ce_jac + sythesis_enzyme_balance_jac = - metab_x_psras.dot(enz_vols * enzyme_balance_jac) enzyme_balance_jac = enz_x_mras.dot(mras_jac(x)) :param x: optimization variables @@ -588,25 +547,21 @@ class GbaProblem: """ 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 + mras_jac = self.mras_jac(x) - 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) + metabolic_jac = self.metab_x_mras.dot(mras_jac) + dilution = self.metab_x_psrs.dot(ce) - cm + dilute_1_jac = np.outer(dilution, gr_grad) + dilute_2_jac = gr * (self.metab_x_psrs.dot(ce_jac) - 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 + metab_mb_jac = metabolic_jac + dilute_1_jac + dilute_2_jac + if np.count_nonzero(self.enz_x_mras) > 0: + enz_mb_jac = self.enz_x_mras.dot(mras_jac) + metab_mb_jac -= self.metab_x_psrs.dot(enz_mb_jac) return np.nan_to_num(metab_mb_jac, nan=0.0) @cache_last @@ -618,24 +573,6 @@ class GbaProblem: 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)) @@ -646,18 +583,18 @@ class GbaProblem: 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_scaled = enz_vols * 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 + ce_scaled_jac = enz_vols.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) + sythesis_enzyme_balance_hesss = - metab_x_psras.dot(enz_vols * 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) + sythesis_enzyme_balance_jac = - metab_x_psras.dot(enz_vols * enzyme_balance_jac) enzyme_balance_jac = enz_x_mras.dot(mras_jac(x)) :param x: optimization variables @@ -667,50 +604,71 @@ class GbaProblem: """ 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) + mras_hesss = self.mras_hesss(x) + metabolic_hesss = np.array([(row.reshape((-1, 1, 1)) * mras_hesss).sum(axis=0) + for row in self.metab_x_mras]) + dilution = self.metab_x_psrs.dot(ce) - cm + dilution_jac = self.metab_x_psrs.dot(ce_jac) - cm_jac + + dilution_1a_hesss = dilution.reshape((-1, 1, 1)) * gr_hess + dilution_1b_hesss = np.array([np.outer(row, gr_grad) for row in dilution_jac]) + dilution_2a_hesss = np.array([np.outer(gr_grad, row) for row in dilution_jac]) + # dilution_2b_hesss = np.zeros_like(metabolic_hesss) + + metab_mb_hesss = metabolic_hesss + dilution_1a_hesss + dilution_1b_hesss + dilution_2a_hesss + if np.count_nonzero(self.enz_x_mras) > 0: + enz_mb_hesss = np.array([(row.reshape((-1, 1, 1)) * mras_hesss).sum(axis=0) + for row in self.enz_x_mras]) + metab_mb_hesss -= np.array([(row.reshape((-1, 1, 1)) * enz_mb_hesss).sum(axis=0) + for row in self.metab_x_psrs]) + return np.nan_to_num(np.tril(metab_mb_hesss), nan=0.0) @cache_last def heq_enz_trans(self, x): - return self.enz_trans(x) + """Calculate enzyme transition equations at x + + :param x: optimization variables + :type x: numpy.ndarray + :returns: Jacobian of mass balance equations at x + :rtype: numpy.ndarray (1D) with shape (len(subenz_x_trans.shape[1]), 1) + """ + return self.subenz_x_trans.dot(self.enz_transas(x)) @cache_last def heq_enz_trans_jac(self, x): - return self.enz_trans_jac(x) + """Calculate Jacobian for enzyme transition equations at x + + :param x: optimization variables + :type x: numpy.ndarray + :returns: Jacobian of mass balance equations at x + :rtype: numpy.ndarray (2D) + """ + return self.subenz_x_trans.dot(self.enz_transas_jac(x)) @cache_last def heq_enz_trans_hess(self, x): - return self.enz_trans_hesss(x) + """Calculate Hessians of enzyme transition equations at 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] + """ + return np.array([(row.reshape((-1, 1, 1)) * self.enz_transas_hesss(x)).sum(axis=0) + for row in self.subenz_x_trans]) @cache_last def heq_density(self, x): """Calculate density (g/l) constraint at x - Could be extended having several density constraints + Could be extended having several density constraints (not yet implemented) 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) @@ -718,20 +676,23 @@ class GbaProblem: :param x: optimization variables :type x: numpy.ndarray :returns: mass balance calculated at x - :rtype: numpy.ndarray (1D) + :rtype: numpy.ndarray (1D) with shape (1,) for now """ ce = x[self.var_mask_enz] if self.model_params.get('macro', True) is True: density = self.model_params['mws'][self.var_mask_enz].dot(ce) else: density = self.model_params['mws'].dot(x) - # TODO check if rho should be specified as a bound! return np.array([self.model_params['rho'] - density]) @cache_last def heq_density_jac(self, x): """Calculate gradient of density constraint at x + for single density, this returns a gradient vector + for multiple densities (not implemented), this + would return a Jacobian + :param x: optimization variables :type x: numpy.ndarray :returns: mass balance jacobian at x diff --git a/xbanalysis/utils/hessians.py b/xbanalysis/utils/hessians.py index 9c63f41d805f768374338c4e0c0a4ea2a6c2df55..b3e7f97c0107cc7f6b21685415f170c4f48d303b 100644 --- a/xbanalysis/utils/hessians.py +++ b/xbanalysis/utils/hessians.py @@ -9,9 +9,11 @@ import numpy as np def hess_from_grad_grad(grad_l, grad_r): """Calculate Hessian from two gradiends wrt x + actually: np.tril(np.outer(grad_l, grad_r)) + :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: right 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] @@ -28,6 +30,8 @@ def hess_from_grad_grad(grad_l, grad_r): def hess_from_v_hesss(v, hesss): """Calculate Hessian from a vector and Hessians + actually: (v.reshape((-1,1,1))*hesss).sum(axis=0) + :param v: vector of length n :param v: numpy.ndarray (1D) :param hesss: Hessians of n vectors wrt to x variables @@ -47,6 +51,8 @@ def hess_from_v_hesss(v, hesss): def hess_from_jac_jac(jac_l, jac_r): """Calculate Hessian from two Jacobians + actually: # replaced by jac_l.T.dot(jac_r) + :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 @@ -66,6 +72,8 @@ def hess_from_jac_jac(jac_l, jac_r): def hesss_from_matrix_hesss(matrix, hesss): """Calculate dot product of a stoichiometric matrix with Hessians + actually: np.array([(row.reshape((-1,1,1)) * hesss).sum(axis=0) for row in matrix]) + :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 @@ -76,18 +84,18 @@ def hesss_from_matrix_hesss(matrix, hesss): 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 + n_rows = matrix.shape[0] 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] + for i, v in enumerate(matrix): + result_hesss[i] = (v.reshape((-1, 1, 1)) * hesss).sum(axis=0) return result_hesss def hesss_from_v_hess(v, hess): """Calculate Hessians from vector and a Hessian + actually: v.reshape((-1,1,1)) * hess + :param v: vector of length m :param v: numpy.ndarray (1D) :param hess: Hessian of scalar wrt to x variables @@ -108,6 +116,8 @@ def hesss_from_v_hess(v, hess): def hesss_from_grad_jac(grad, jac): """Calculate Hessians from gradient and a Jacobian + actually: np.array([np.outer(grad, row) for row in jac]) + :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 @@ -121,5 +131,27 @@ def hesss_from_grad_jac(grad, jac): result_hesss = np.zeros((n_rows, size, size)) for i in range(n_rows): - result_hesss[i] = hess_from_grad_grad(jac[i], grad) + result_hesss[i] = np.outer(grad, jac[i]) + return result_hesss + + +def hesss_from_jac_grad(jac, grad): + """Calculate Hessians from Jacobian and gradient + + actually: np.array([np.outer(row, grad) for row in jac]) + + :param jac: Jacobian of a vector size m wrt to x variables + :param jac: numpy.ndarray (2D) with shape [m,x] + :param grad: gradient of scalar wrt to x variables + :param grad: numpy.ndarray (1D) with shape [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] = np.outer(jac[i], grad) return result_hesss diff --git a/xbanalysis/utils/opt_results.py b/xbanalysis/utils/opt_results.py index ec6b9afce3d641274ffe4a67f39aedaf8352a28e..5a03ca3247d347639f5488c890d9ca20a64ddd04 100644 --- a/xbanalysis/utils/opt_results.py +++ b/xbanalysis/utils/opt_results.py @@ -22,24 +22,22 @@ class OptResults: 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)) + self.ress = {'metabolites': self.problem.var_metab_sids, + 'enzymes': self.problem.var_enz_sids, + 'mr_reactions': self.problem.mr_rids, + 'enz_sid2idx': {sid: idx for idx, sid in enumerate(self.problem.var_enz_sids)}, + 'var_mask_metab': self.problem.var_mask_metab, + 'var_mask_enz': self.problem.var_mask_enz, + 'grs_per_h': np.zeros(n_points), + 'status': np.zeros(n_points), + 'x_opt': np.zeros((n_points, self.problem.n_vars)), + 'cm_M': np.zeros((n_points, self._n_m)), + 'rho_m': np.zeros((n_points, self._n_m)), + 'ce_M': np.zeros((n_points, self._n_e)), + 'rho_e': np.zeros((n_points, self._n_e)), + 'alphas': np.zeros((n_points, self._n_e)), + 'mr_fluxes': np.zeros((n_points, len(self.problem.mr_rids))), + 'psm_fluxes': np.zeros((n_points, self._n_e))} def add(self, info): if self._idx < self._n_points: @@ -54,10 +52,9 @@ class OptResults: self.ress['rho_m'][i] = rho[self.problem.var_mask_metab] self.ress['rho_e'][i] = rho[self.problem.var_mask_enz] - # TODO extract reaction volume from kinetics to scale fluxes here 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['mr_fluxes'][i] = scale_factor * self.problem.mras(cx) / self.problem.mr_vols + self.ress['psm_fluxes'][i] = scale_factor / self.problem.pstmas(cx) / self.problem.ps_vols self.ress['alphas'][i] = self.problem.get_alphas(cx) self._idx += 1