adesomics.mapping.connection
1import ast 2import heapq 3import numpy as np 4from cobra.util.solver import linear_reaction_coefficients 5 6from adesomics.io.gsmm import Model 7from adesomics.io.count_matrix import CountMatrix 8 9 10class Connector: 11 """ 12 Binds an expression matrix to a GSMM via GPR rules. -> 13 integration with E-Flux 14 """ 15 16 def __init__(self, model: Model, counts: CountMatrix, max_bound: float = 1000.0): 17 self.model = model 18 self.counts = counts 19 self.tpm = counts.tpm() 20 self.max_bound = max_bound 21 self.gene_mapping = self._map_genes() 22 23 def _map_genes(self) -> dict: 24 model_ids = {g.id for g in self.model.model.genes} 25 count_ids = set(self.counts.counts.index) 26 mapping = {gid: gid for gid in count_ids if gid in model_ids} 27 print(f"{len(mapping)} / {len(model_ids)} model genes mapped to count matrix.") 28 return mapping 29 30 def _gpr_score(self, node, expr): 31 """Missing genes are unknown (NaN), not absent (0).""" 32 NA = float("nan") 33 if isinstance(node, ast.Expression): #usually not accessed 34 return self._gpr_score(node.body, expr) 35 if isinstance(node, ast.Name): 36 return expr.get(node.id, NA) 37 if isinstance(node, ast.BoolOp): 38 vals = [self._gpr_score(v, expr) for v in node.values] 39 known = [v for v in vals if not np.isnan(v)] 40 if not known: 41 return NA 42 return min(known) if isinstance(node.op, ast.And) else sum(known) 43 return NA 44 45 def _reaction_expression(self, sample): 46 expr = self.tpm[sample].to_dict() 47 scores = {} 48 for rxn in self.model.model.reactions: 49 rule = rxn.gene_reaction_rule.strip() 50 if not rule: 51 continue 52 tree = ast.parse(rule, mode="eval").body 53 scores[rxn.id] = self._gpr_score(tree, expr) 54 return scores 55 56 def _compute_scale(self, samples, percentile: float = 95) -> float: 57 pooled = [] 58 for s in samples: 59 pooled.extend(v for v in self._reaction_expression(s).values() 60 if not np.isnan(v)) 61 if not pooled: 62 raise ValueError("No reaction could be scored — check gene ID mapping") 63 return float(np.percentile(pooled, percentile)) 64 65 def eflux(self, sample, scale=None): 66 if not np.isfinite(scale) or scale <= 0: 67 raise ValueError(f"Invalid scale: {scale}") 68 if scale is None: 69 scale = self._compute_scale(self.tpm.columns) 70 if scale == 0: 71 raise ValueError("Expression scale is zero -> cannot apply E-Flux because all reactions would be unconstrained. Check gene ID mapping/overlap.") 72 scores = {k: v for k, v in self._reaction_expression(sample).items() 73 if not np.isnan(v)} # NaN -> unconstrained 74 75 m = self.model.model.copy() 76 for rxn in m.reactions: 77 if rxn.id not in scores: 78 continue 79 ratio = min(scores[rxn.id] / scale, 1.0) 80 lb, ub = rxn.lower_bound, rxn.upper_bound 81 rxn.bounds = (min(lb, 0) * ratio if lb < 0 else lb, 82 max(ub, 0) * ratio if ub > 0 else ub) 83 return m.optimize() 84 85 def compare(self, samples, percentile=95): 86 scale = self._compute_scale(samples, percentile) 87 eflux_results = {} 88 for sample in samples: 89 if sample not in self.tpm.columns: 90 raise ValueError(f"Sample '{sample}' not found in count matrix.") 91 eflux_results[sample] = self.eflux(sample, scale) 92 return eflux_results
class
Connector:
11class Connector: 12 """ 13 Binds an expression matrix to a GSMM via GPR rules. -> 14 integration with E-Flux 15 """ 16 17 def __init__(self, model: Model, counts: CountMatrix, max_bound: float = 1000.0): 18 self.model = model 19 self.counts = counts 20 self.tpm = counts.tpm() 21 self.max_bound = max_bound 22 self.gene_mapping = self._map_genes() 23 24 def _map_genes(self) -> dict: 25 model_ids = {g.id for g in self.model.model.genes} 26 count_ids = set(self.counts.counts.index) 27 mapping = {gid: gid for gid in count_ids if gid in model_ids} 28 print(f"{len(mapping)} / {len(model_ids)} model genes mapped to count matrix.") 29 return mapping 30 31 def _gpr_score(self, node, expr): 32 """Missing genes are unknown (NaN), not absent (0).""" 33 NA = float("nan") 34 if isinstance(node, ast.Expression): #usually not accessed 35 return self._gpr_score(node.body, expr) 36 if isinstance(node, ast.Name): 37 return expr.get(node.id, NA) 38 if isinstance(node, ast.BoolOp): 39 vals = [self._gpr_score(v, expr) for v in node.values] 40 known = [v for v in vals if not np.isnan(v)] 41 if not known: 42 return NA 43 return min(known) if isinstance(node.op, ast.And) else sum(known) 44 return NA 45 46 def _reaction_expression(self, sample): 47 expr = self.tpm[sample].to_dict() 48 scores = {} 49 for rxn in self.model.model.reactions: 50 rule = rxn.gene_reaction_rule.strip() 51 if not rule: 52 continue 53 tree = ast.parse(rule, mode="eval").body 54 scores[rxn.id] = self._gpr_score(tree, expr) 55 return scores 56 57 def _compute_scale(self, samples, percentile: float = 95) -> float: 58 pooled = [] 59 for s in samples: 60 pooled.extend(v for v in self._reaction_expression(s).values() 61 if not np.isnan(v)) 62 if not pooled: 63 raise ValueError("No reaction could be scored — check gene ID mapping") 64 return float(np.percentile(pooled, percentile)) 65 66 def eflux(self, sample, scale=None): 67 if not np.isfinite(scale) or scale <= 0: 68 raise ValueError(f"Invalid scale: {scale}") 69 if scale is None: 70 scale = self._compute_scale(self.tpm.columns) 71 if scale == 0: 72 raise ValueError("Expression scale is zero -> cannot apply E-Flux because all reactions would be unconstrained. Check gene ID mapping/overlap.") 73 scores = {k: v for k, v in self._reaction_expression(sample).items() 74 if not np.isnan(v)} # NaN -> unconstrained 75 76 m = self.model.model.copy() 77 for rxn in m.reactions: 78 if rxn.id not in scores: 79 continue 80 ratio = min(scores[rxn.id] / scale, 1.0) 81 lb, ub = rxn.lower_bound, rxn.upper_bound 82 rxn.bounds = (min(lb, 0) * ratio if lb < 0 else lb, 83 max(ub, 0) * ratio if ub > 0 else ub) 84 return m.optimize() 85 86 def compare(self, samples, percentile=95): 87 scale = self._compute_scale(samples, percentile) 88 eflux_results = {} 89 for sample in samples: 90 if sample not in self.tpm.columns: 91 raise ValueError(f"Sample '{sample}' not found in count matrix.") 92 eflux_results[sample] = self.eflux(sample, scale) 93 return eflux_results
Binds an expression matrix to a GSMM via GPR rules. -> integration with E-Flux
Connector( model: adesomics.io.gsmm.Model, counts: adesomics.io.count_matrix.CountMatrix, max_bound: float = 1000.0)
def
eflux(self, sample, scale=None):
66 def eflux(self, sample, scale=None): 67 if not np.isfinite(scale) or scale <= 0: 68 raise ValueError(f"Invalid scale: {scale}") 69 if scale is None: 70 scale = self._compute_scale(self.tpm.columns) 71 if scale == 0: 72 raise ValueError("Expression scale is zero -> cannot apply E-Flux because all reactions would be unconstrained. Check gene ID mapping/overlap.") 73 scores = {k: v for k, v in self._reaction_expression(sample).items() 74 if not np.isnan(v)} # NaN -> unconstrained 75 76 m = self.model.model.copy() 77 for rxn in m.reactions: 78 if rxn.id not in scores: 79 continue 80 ratio = min(scores[rxn.id] / scale, 1.0) 81 lb, ub = rxn.lower_bound, rxn.upper_bound 82 rxn.bounds = (min(lb, 0) * ratio if lb < 0 else lb, 83 max(ub, 0) * ratio if ub > 0 else ub) 84 return m.optimize()
def
compare(self, samples, percentile=95):
86 def compare(self, samples, percentile=95): 87 scale = self._compute_scale(samples, percentile) 88 eflux_results = {} 89 for sample in samples: 90 if sample not in self.tpm.columns: 91 raise ValueError(f"Sample '{sample}' not found in count matrix.") 92 eflux_results[sample] = self.eflux(sample, scale) 93 return eflux_results