|
| 1 | +"""tINIT model building — high-level pipeline. |
| 2 | +
|
| 3 | +Turn expression-derived scores into reaction scores (via the GPR), drop reactions that |
| 4 | +cannot carry flux, then run the INIT MILP to extract a context-specific model. Pass |
| 5 | +gene scores (typically from :func:`gene_scores_from_expression` or one of the omics |
| 6 | +loaders) or reaction scores directly. ``essential_rxns`` are forced kept. |
| 7 | +
|
| 8 | +For task-aware gap-filling on top of the resulting model, use ftINIT |
| 9 | +(:func:`raven_python.init.ftinit`); ``get_init_model`` itself does not run the task layer. |
| 10 | +""" |
| 11 | +from __future__ import annotations |
| 12 | + |
| 13 | +from collections.abc import Iterable, Mapping |
| 14 | +from dataclasses import dataclass |
| 15 | + |
| 16 | +import cobra |
| 17 | +from cobra.flux_analysis import find_blocked_reactions |
| 18 | + |
| 19 | +from raven_python.init.init import run_init |
| 20 | +from raven_python.init.score import score_reactions_from_genes |
| 21 | + |
| 22 | + |
| 23 | +@dataclass |
| 24 | +class InitModelResult: |
| 25 | + """Result of :func:`get_init_model`.""" |
| 26 | + |
| 27 | + model: cobra.Model |
| 28 | + reaction_scores: dict[str, float] |
| 29 | + deleted_dead_end_reactions: list[str] |
| 30 | + deleted_in_init: list[str] |
| 31 | + met_production: dict[str, bool] |
| 32 | + objective: float |
| 33 | + |
| 34 | + |
| 35 | +def get_init_model( |
| 36 | + ref_model: cobra.Model, |
| 37 | + *, |
| 38 | + rxn_scores: Mapping[str, float] | None = None, |
| 39 | + gene_scores: Mapping[str, float] | None = None, |
| 40 | + isozyme_scoring: str = "max", |
| 41 | + complex_scoring: str = "min", |
| 42 | + no_gene_score: float = -2.0, |
| 43 | + essential_rxns: Iterable[str] | None = None, |
| 44 | + present_mets: Iterable[str] | None = None, |
| 45 | + prod_weight: float = 0.5, |
| 46 | + allow_excretion: bool = True, |
| 47 | + no_rev_loops: bool = False, |
| 48 | + remove_dead_ends: bool = True, |
| 49 | + eps: float = 1.0, |
| 50 | + big_m: float | None = None, |
| 51 | + mip_gap: float | None = None, |
| 52 | + time_limit: float | None = None, |
| 53 | +) -> InitModelResult: |
| 54 | + """Extract a context-specific model with tINIT. |
| 55 | +
|
| 56 | + Provide either ``rxn_scores`` (reaction id → score) or ``gene_scores`` (gene id → |
| 57 | + score, converted via the GPR with :func:`score_reactions_from_genes`). Reactions |
| 58 | + that cannot carry flux (with exchanges open) are removed first unless |
| 59 | + ``remove_dead_ends=False``; ``essential_rxns`` are kept regardless. The remaining |
| 60 | + model is passed to :func:`run_init`. |
| 61 | + """ |
| 62 | + if (rxn_scores is None) == (gene_scores is None): |
| 63 | + raise ValueError("Provide exactly one of rxn_scores or gene_scores.") |
| 64 | + |
| 65 | + model = ref_model.copy() |
| 66 | + essential = set(essential_rxns or []) |
| 67 | + if gene_scores is not None: |
| 68 | + scores = score_reactions_from_genes( |
| 69 | + model, gene_scores, isozyme_scoring=isozyme_scoring, |
| 70 | + complex_scoring=complex_scoring, no_gene_score=no_gene_score, |
| 71 | + ) |
| 72 | + else: |
| 73 | + scores = dict(rxn_scores) |
| 74 | + |
| 75 | + deleted_dead_end: list[str] = [] |
| 76 | + if remove_dead_ends: |
| 77 | + # Identify and drop reactions that cannot carry flux even under the |
| 78 | + # *most permissive* boundary regime: every metabolite open for excretion |
| 79 | + # (when ``allow_excretion``) plus the exchange-opened FVA. That makes |
| 80 | + # the pre-filter conservative — only reactions blocked under both lax |
| 81 | + # and strict regimes are removed, so the strict run_init path never |
| 82 | + # loses a candidate it could have used. |
| 83 | + probe = model.copy() |
| 84 | + original_ids = {r.id for r in model.reactions} |
| 85 | + if allow_excretion: |
| 86 | + has_boundary = {m.id for r in probe.boundary for m in r.metabolites} |
| 87 | + for met in list(probe.metabolites): |
| 88 | + if met.id not in has_boundary: |
| 89 | + probe.add_boundary(met, type="demand") |
| 90 | + blocked = set(find_blocked_reactions(probe, open_exchanges=True)) |
| 91 | + deleted_dead_end = sorted((blocked & original_ids) - essential) |
| 92 | + model.remove_reactions(deleted_dead_end, remove_orphans=True) |
| 93 | + |
| 94 | + result = run_init( |
| 95 | + model, scores, |
| 96 | + present_mets=present_mets, |
| 97 | + essential_rxns=essential & {r.id for r in model.reactions}, |
| 98 | + prod_weight=prod_weight, |
| 99 | + allow_excretion=allow_excretion, |
| 100 | + no_rev_loops=no_rev_loops, |
| 101 | + eps=eps, |
| 102 | + big_m=big_m, |
| 103 | + mip_gap=mip_gap, |
| 104 | + time_limit=time_limit, |
| 105 | + ) |
| 106 | + return InitModelResult( |
| 107 | + model=result.model, |
| 108 | + reaction_scores=scores, |
| 109 | + deleted_dead_end_reactions=deleted_dead_end, |
| 110 | + deleted_in_init=result.deleted_reactions, |
| 111 | + met_production=result.met_production, |
| 112 | + objective=result.objective, |
| 113 | + ) |
0 commit comments