CoolFace
Apppublic

v2431/FormulaShowcase

sourceHugging Faceupdated 7mo agoView on Hugging Face
0likes
modules.py2242 linesDownload Raw Back to root
1# modules.py2# -*- coding: utf-8 -*-3from __future__ import annotations4 5from dataclasses import dataclass, field6from pathlib import Path7import pickle8from typing import Iterable, Optional, Set, Dict, Any, Tuple, List, Sequence9import collections10 11import networkx as nx12 13import netmedpy14import pandas as pd  # type: ignore15import numpy as np16from collections import deque17from tqdm import tqdm18from concurrent.futures import ThreadPoolExecutor, as_completed19 20import pulp21import math22from pyscipopt import Model, quicksum23 24# from utils import _sample_preserving_degrees_fixed25# netmedpy.NetMedPy._sample_preserving_degrees = _sample_preserving_degrees_fixed26 27# ----------------------------------------------------------------------28# Proximity 部分(Step 2)29# ----------------------------------------------------------------------30@dataclass31class ProximityResult:32    """33    存放网络邻近度计算结果的数据类(支持单模块和多模块场景)。34 35    Attributes36    ----------37    module_name : str38        模块名称或场景名称39    full_stats : dict40        NetMedPy screening 返回的完整结果字典41    """42 43    module_name: str44    full_stats: Dict[str, Any]45    thres_z: Optional[float] = None46 47    def __post_init__(self):48        """自动构建 z_scores_by_module"""49        z_df = self.full_stats.get("z_score")50        if z_df is None or not isinstance(z_df, pd.DataFrame):51            raise ValueError("full_stats 中未找到 'z_score' DataFrame")52        self._z_scores_by_module = z_df.apply(pd.to_numeric, errors="coerce")53 54    @property55    def z_scores_by_module(self) -> pd.DataFrame:56        """返回多模块的 z-score 矩阵(行=候选实体,列=模块)"""57        return self._z_scores_by_module58 59    def aggregate_z_scores(self, mode: str = "min") -> pd.Series:60        """61        聚合多模块的 z-score 为场景级 z-score。62 63        Parameters64        ----------65        mode : str, default "min"66            聚合模式:'min', 'mean', 'max'67 68        Returns69        -------70        pd.Series71            聚合后的 z-score(index=候选实体id)72        """73        if mode == "min":74            z_agg = self._z_scores_by_module.min(axis=1)75        elif mode == "mean":76            z_agg = self._z_scores_by_module.mean(axis=1)77        elif mode == "max":78            z_agg = self._z_scores_by_module.max(axis=1)79        else:80            raise ValueError(f"不支持的聚合模式: {mode},请使用 'min', 'mean', 或 'max'")81        82        z_agg.name = self.module_name83        return z_agg84 85    def filter_by_proximity(86        self,87        ns_targets: Dict[str, Iterable[str]],88        thres_z: float = -1.6,89        agg_mode: str = "min",90        max_candidates: Optional[int] = None,91    ) -> Dict[str, Iterable[str]]:92        """93        基于聚合 z-score 筛选天然来源。94 95        Parameters96        ----------97        ns_targets : dict98            天然来源 id -> 靶标蛋白列表99        thres_z : float, default -1.6100            z-score 阈值(越小越好)101        agg_mode : str, default "min"102            聚合模式:'min', 'mean', 'max'103        max_candidates : int or None, default None104            最大候选数量限制。如果不为 None,在阈值筛选后按 z-score 排序取前 N 个105 106        Returns107        -------108        dict109            筛选后的天然来源字典110        """111        self.thres_z = thres_z112        z_agg = self.aggregate_z_scores(mode=agg_mode)113        selected = z_agg[z_agg <= thres_z].index.tolist()114        115        # 如果指定了 max_candidates,进一步限制数量(保留 z 最小的前 N 个)116        if max_candidates is not None and len(selected) > max_candidates:117            z_selected = z_agg.loc[selected].sort_values()118            selected = z_selected.index[:max_candidates].tolist()119            print(f"[ProximityResult] Filtered by z<={thres_z} ({agg_mode}): {len(z_agg[z_agg <= thres_z])} candidates, limited to top {max_candidates}")120        else:121            print(f"[ProximityResult] Filtered by z<={thres_z} ({agg_mode}): {len(selected)} candidates")122 123        # 同步更新 z_scores_by_module,只保留筛选后的候选124        self._z_scores_by_module = self._z_scores_by_module.loc[selected].copy()125        126        return {ns: ns_targets[ns] for ns in selected if ns in ns_targets}127 128    @property129    def significant_hits(self) -> Dict[str, List[str]]:130        """返回每个候选实体显著命中的 module 列表。131        Returns132        -------133        Dict[str, List[str]]134            {candidate_id: [module_name1, module_name2, ...]}135        """136        z_df = self.z_scores_by_module137        result: Dict[str, List[str]] = {}138        139        for cid in z_df.index:140            hit_modules = []141            for module_name in z_df.columns:142                z = z_df.loc[cid, module_name]143                if pd.notna(z) and z <= self.thres_z:144                    hit_modules.append(module_name)145            if hit_modules:146                result[cid] = hit_modules147        148        return result149 150 151class ProximityAnalyzer:152    """153    基于 NetMedPy 的网络邻近度计算器。154 155    主要职责:156    ----------157    1. 预计算并缓存 PPI 的距离矩阵 (all_pair_distances)158    2. 使用 screening 统一计算一批候选实体相对于指定模块的 proximity z-score159    """160 161    def __init__(162        self,163        ppi_graph: nx.Graph,164        *,165        distance_metric: str = "shortest_path",166        null_model: str = "soft_log_binning",167        repeat_allowed: bool = False,168        n_iter: int = 1000,169        n_procs: int = 1,170        bin_size: int = 100,171    ) -> None:172        """173        Parameters174        ----------175        ppi_graph : networkx.Graph176            PPI 子图177        distance_metric : str, default "shortest_path"178            NetMedPy 支持的距离度量:179            - "shortest_path"180            - "random_walk"181            - "biased_random_walk"182            - "communicability"183184        null_model : str, default "soft_log_binning"185            空模型类型(与 LCC 一致)186        repeat_allowed : bool, default False187            空模型采样时是否允许重复选择节点,True则只允许松弛一次bin,False则反复扩大bin188        n_iter : int, default 1000189            空模型随机采样次数190        n_procs : int, default 1191            并行进程数192        """193 194        self.ppi_graph = ppi_graph195        self.distance_metric = distance_metric196        self.null_model = null_model197        self.repeat_allowed = repeat_allowed198        self.n_iter = n_iter199        self.n_procs = n_procs200        self.bin_size = bin_size201 202        # 距离矩阵缓存203        self._distance_matrix = None204 205    # ----------------------------206    # 内部:懒加载距离矩阵207    # ----------------------------208    def _ensure_distance_matrix(self):209        """210        懒加载 PPI 的全对距离矩阵,仅计算一次。211        """212        if self._distance_matrix is None:213            self._distance_matrix = netmedpy.all_pair_distances(214                self.ppi_graph,215                distance=self.distance_metric,216                n_processors=self.n_procs,217            )218        return self._distance_matrix219 220    def load_distance_matrix(self, path: str):221        """222        从指定路径加载预计算的距离矩阵。223 224        Parameters225        ----------226        path : str227            距离矩阵文件的路径 (pkl)228        """229        self._distance_matrix = pickle.load(open(path, "rb"))230 231    @property232    def distance_matrix(self):233        """234        获取距离矩阵。235 236        Returns237        -------238        pd.DataFrame239            PPI 的全对距离矩阵240        """241        if self._distance_matrix is None:242            self._distance_matrix = self._ensure_distance_matrix()243        return self._distance_matrix244 245    # ----------------------------246    # 核心 API:单模块247    # ----------------------------248    def compute_for_sources(249        self,250        source_sets: Dict[str, Iterable[str]],251        module_nodes: Iterable[str],252        module_name: str = "module",253    ) -> ProximityResult:254        """255        计算一批候选实体相对于某模块的 proximity z-score(单模块版)。256 257        Parameters258        ----------259        source_sets : dict260            key: 候选实体 id(如 natural_source id)261            value: 对应的蛋白靶标列表 / 可迭代262        module_nodes : Iterable[str]263            模块的蛋白节点集合(建议使用 LCCResult.lcc_nodes)264        module_name : str, default "module"265            模块名称(用于 NetMedPy 输出的行名)266 267        Returns268        -------269        ProximityResult270        """271        graph_nodes = set(self.ppi_graph.nodes())272 273        # 1) 清洗模块节点:只保留存在于 PPI 的节点274        module_nodes_set = set(module_nodes) & graph_nodes275        if len(module_nodes_set) == 0:276            raise ValueError("module_nodes 在 PPI 中没有任何有效节点,无法计算 proximity。")277 278        # 2) 清洗 source_sets:只保留至少有 1 个节点在 PPI 中的候选实体279        cleaned_sources: Dict[str, list] = {}280        for sid, nodes in source_sets.items():281            nodes_in_graph = list(set(nodes) & graph_nodes)282            if len(nodes_in_graph) > 0:283                cleaned_sources[sid] = nodes_in_graph284 285        if not cleaned_sources:286            raise ValueError("source_sets 中没有任何候选实体在 PPI 中有有效靶标。")287 288        # 3) 准备 NetMedPy 所需输入格式289        distance_matrix = self.distance_matrix290        module_dict = {module_name: list(module_nodes_set)}  # 列:模块291 292        # 4) 调用 screening 计算 proximity z-score293        screen_data = netmedpy.screening(294            cleaned_sources,295            module_dict,296            self.ppi_graph,297            distance_matrix,298            score="proximity",299            properties=[300                "z_score",301                "p_value_single_tail",302                "p_value_double_tail",303                "raw_amspl",304            ],305            null_model=self.null_model,306            repeat_allowed=self.repeat_allowed,307            n_iter=self.n_iter,308            bin_size=self.bin_size,309            n_procs=self.n_procs,310        )311 312        z_df: pd.DataFrame = screen_data["z_score"]313 314        if module_name not in z_df.columns:315            raise RuntimeError(f"NetMedPy screening result does not contain column: {module_name}")316 317        # 取出这一列,就是每个候选实体的 z-score318        z_series: pd.Series = z_df[module_name]319        z_series = pd.to_numeric(z_series, errors="coerce")320 321        return ProximityResult(322            module_name=module_name,323            z_scores=z_series,324            full_stats=screen_data,325        )326 327    # ----------------------------328    # 核心 API:多模块329    # ----------------------------330    def compute_for_modules(331        self,332        source_sets: Dict[str, Iterable[str]],333        modules: Dict[str, Iterable[str]],334        scenario_name: str = "scenario",335    ) -> Dict[str, Any]:336        """337        多模块版本的 proximity 计算,直接返回 NetMedPy 的 screen_data。338 339        Parameters340        ----------341        source_sets : dict342            key: 候选实体 id343            value: 对应的蛋白靶标列表344        modules : dict345            key: 模块名346            value: 模块蛋白集合347        scenario_name : str, default "scenario"348            场景名称(未使用,保留用于日志)349 350        Returns351        -------352        dict353            NetMedPy screening 的完整结果字典354        """355        graph_nodes = set(self.ppi_graph.nodes())356 357        # 清洗模块358        cleaned_modules: Dict[str, List[str]] = {}359        for mname, nodes in modules.items():360            nodes_in_graph = list(set(nodes) & graph_nodes)361            if nodes_in_graph:362                cleaned_modules[mname] = nodes_in_graph363 364        if not cleaned_modules:365            raise ValueError("modules 中没有任何模块在 PPI 中有有效节点。")366 367        # 清洗候选实体368        cleaned_sources: Dict[str, List[str]] = {}369        for sid, nodes in source_sets.items():370            nodes_in_graph = list(set(nodes) & graph_nodes)371            if len(nodes_in_graph) > 0:372                cleaned_sources[sid] = nodes_in_graph373 374        if not cleaned_sources:375            raise ValueError("source_sets 中没有任何候选实体在 PPI 中有有效靶标。")376 377        distance_matrix = self.distance_matrix378 379        screen_data = netmedpy.screening(380            cleaned_sources,381            cleaned_modules,382            self.ppi_graph,383            distance_matrix,384            score="proximity",385            properties=[386                "z_score",387                "p_value_single_tail",388                "p_value_double_tail",389                "raw_amspl",390            ],391            null_model=self.null_model,392            n_iter=self.n_iter,393            n_procs=self.n_procs,394        )395 396        print('[ProximityAnalyzer] Proximity screening finished')397        return screen_data398 399 400# ----------------------------------------------------------------------401# Separation 部分(Step 3)402# ----------------------------------------------------------------------403class SeparationAnalyzer:404    """405    基于 NetMedPy 的网络分离度(separation)计算器。406 407    用途(Step 3):408    -------------409    - 给定一组候选实体(草药 / 食物) -> protein 靶标集合,410      计算它们之间的 pairwise separation(默认 source_sets 和 target_sets 相同),411      并返回 separation 的矩阵。412    """413 414    def __init__(415        self,416        ppi_graph: nx.Graph,417        distance_matrix,418        n_procs: int = 4,419    ) -> None:420        """421        Parameters422        ----------423        ppi_graph : nx.Graph424            与 Step 1/2 相同的 PPI 网络。425        distance_matrix :426            netmedpy.all_pair_distances 返回的 DistanceMatrix 对象。427        n_procs : int428            并行进程数。429        """430        self.ppi_graph = ppi_graph431        self.distance_matrix = distance_matrix432        self.n_procs = n_procs433 434    def _clean_sets(435        self,436        sets: Dict[str, Iterable[str]],437    ) -> Dict[str, list]:438        """439        辅助函数:把每个实体的 protein 集合限制在 PPI 节点里,440        丢掉在 PPI 中完全没有靶标的实体。441        """442        graph_nodes = set(self.ppi_graph.nodes())443        cleaned: Dict[str, list] = {}444        for sid, nodes in sets.items():445            nodes_in_graph = list(set(nodes) & graph_nodes)446            if nodes_in_graph:447                cleaned[sid] = nodes_in_graph448        return cleaned449 450    def compute_pairwise(451        self,452        source_sets: Dict[str, Iterable[str]],453        target_sets: Optional[Dict[str, Iterable[str]]] = None,454    ):455        """456        计算候选实体之间(或 source vs target)的 pairwise separation。457 458        Parameters459        ----------460        source_sets : dict461            {实体id -> [protein 节点列表]},例如 {herb1: [P1, P2, ...], herb2: [...]}。462        target_sets : dict or None463            如果为 None,则默认 target_sets = source_sets,464            即计算所有候选之间的两两 separation。465 466        Returns467        -------468        dict469            NetMedPy screening 的原始结果。470        """471        # 1) 清洗输入472        cleaned_sources = self._clean_sets(source_sets)473        if not cleaned_sources:474            raise ValueError("source_sets 中没有任何实体在 PPI 中有有效靶标。")475 476        if target_sets is None:477            cleaned_targets = cleaned_sources478        else:479            cleaned_targets = self._clean_sets(target_sets)480            if not cleaned_targets:481                raise ValueError("target_sets 中没有任何实体在 PPI 中有有效靶标。")482 483        # 2) 直接调用 NetMedPy 的 screening(score=\"separation\")484        screen_data = netmedpy.screening(485            cleaned_sources,486            cleaned_targets,487            self.ppi_graph,488            self.distance_matrix,489            score="separation",490            properties=["raw_separation"],491            n_procs=self.n_procs,492        )493 494        return screen_data495 496# ----------------------------------------------------------------------497# 组方优化 部分(Step 4)- Unified FormulaOptimizer (SCIP / CBC / GRASP)498# ----------------------------------------------------------------------499 500@dataclass501class FormulaSolution:502    """一个候选组方解(支持同构/异构 size)。"""503    selected: List[str]504    objective: float505 506    # size 信息507    size: int508    size_by_group: Optional[Dict[str, int]] = None509 510    # 一些可读指标(不一定等同于优化目标)511    mean_z: float = float("nan")512    mean_separation: float = float("nan")513    coverage: float = float("nan")  # [0,1] 或 weighted 版本514 515    # 目标分解(便于 debug/报告)516    obj_components: Dict[str, float] = field(default_factory=dict)517 518    # 其他元信息(例如:method, seed, scheme_id, raw_obj)519    meta: Dict[str, Any] = field(default_factory=dict)520 521 522class FormulaOptimizer:523    """524    Step4:多味组方组合优化器(SCIP / CBC(CBC) / GRASP)。525    - scip: PySCIPOpt + SCIP,线性加性目标 + 近优域 + overlap 多样化526    - cbc : PuLP + CBC,模块级 proximity(min-linearization) + no-good(overlap) 多样化527    - grasp: 子模 GRASP(log1p prox + facility-location diversity + coverage)528 529    说明:530    - optimize() 只保留“公共参数”;求解器/多样性等参数通过 optimizer_setup() 预先配置。531    - target_size 支持:532        * int 或 list[int]:同构总味数533        * dict[str,int] 或 list[dict]:异构配额(需 source_groups: candidate -> set[group])534    """535 536    def __init__(537        self,538        *,539        prox_result: Any,540        sep_result: Dict[str, Any],541        ns_targets: Dict[str, Iterable[str]],542        module_nodes: Set[str],543        modules: Optional[Dict[str, Set[str]]] = None,544        agg_mode: str = "min",545    ) -> None:546        self.prox_result = prox_result547        self.sep_result = sep_result548        self.ns_targets = {k: list(v) for k, v in ns_targets.items()}549        self.module_nodes: Set[str] = set(module_nodes)550        self.modules = modules551        self.agg_mode = agg_mode552 553        # --- z-score matrix ---554        z_df = getattr(prox_result, "z_scores_by_module", None)555        if z_df is None:556            raise ValueError("prox_result 必须包含 z_scores_by_module 属性(DataFrame)。")557        if not isinstance(z_df, pd.DataFrame):558            raise TypeError("prox_result.z_scores_by_module 必须是 pandas.DataFrame。")559        self.z_scores_by_module: pd.DataFrame = z_df560 561        # --- separation matrix ---562        if "raw_separation" not in sep_result:563            raise ValueError("sep_result 必须包含 key='raw_separation' 的 DataFrame。")564        self.sep_df: pd.DataFrame = sep_result["raw_separation"]565        if not isinstance(self.sep_df, pd.DataFrame):566            raise TypeError("sep_result['raw_separation'] 必须是 pandas.DataFrame。")567 568        # --- solver config ---569        self._method: str = "scip"  # default570        self._cfg: Dict[str, Any] = {571            # multi-solution quality/diversity572            "quality_rel_gap": 0.10,573            "quality_abs_gap": None,574            "min_hamming": 2,575 576            # scip/cbc577            "time_limit": 30.0,578            "mip_rel_gap": 0.01,579            "threads": 0,580            "verbose": False,581 582            # grasp583            "grasp_pool_factor": 50,584            "rcl_size": 10,585            "random_seed": 2025,586 587            # cbc (CBC no-good overlap ratio)588            "diversity_ratio": 0.0,   # 0.0 -> 至少差 1 味;0.3 -> 至少 30% 不同589        }590 591    # ============================592    # Setup API593    # ============================594 595    def optimizer_setup(self, *, method: str = "scip", **solver_params: Any) -> None:596        """597        在调用 optimize() 之前配置求解器与其参数。598 599        Parameters600        ----------601        method:602            "scip" | "cbc" | "grasp"603            - 兼容旧写法:method="ilp" 会被映射到 "scip"604        solver_params:605            - 通用:quality_rel_gap, quality_abs_gap, min_hamming, verbose606            - scip/cbc:time_limit, mip_rel_gap, threads607            - grasp:grasp_pool_factor, rcl_size, random_seed608            - cbc:diversity_ratio609        """610        m = str(method).lower().strip()611        if m == "ilp":612            m = "scip"613        if m not in {"scip", "cbc", "grasp"}:614            raise ValueError("method 仅支持 'scip' / 'cbc' / 'grasp'(兼容 'ilp'->'scip')。")615        self._method = m616 617        for k, v in solver_params.items():618            self._cfg[k] = v619 620    # ============================621    # Public API622    # ============================623 624    def optimize(625        self,626        *,627        candidate_ids: Optional[Sequence[str]] = None,628        target_size: Optional[Any] = None,629        source_groups: Optional[Dict[str, Set[str]]] = None,630        n_solutions: int = 3,631        lambda_prox: float = 1.0,632        lambda_sep: float = 1.0,633        lambda_neg_sep: float = 0.0,634        lambda_cov: float = 0.0,635        weighted_coverage: bool = False,636        node_weights: Optional[Dict[str, float]] = None,637        module_weights: Optional[Dict[str, float]] = None,638    ) -> List[FormulaSolution]:639        """返回多样解列表(长度 <= n_solutions * len(size_schemes or 1))。"""640        641        # 提前读取 setup config,保证 verbose 等变量在后续任何地方可用642        method = self._method643        cfg = self._cfg644        verbose = bool(cfg.get("verbose", False))645 646        quality_rel_gap = float(cfg.get("quality_rel_gap", 0.10))647        quality_abs_gap = cfg.get("quality_abs_gap", None)648        quality_abs_gap = float(quality_abs_gap) if quality_abs_gap is not None else None649        min_hamming = int(cfg.get("min_hamming", 2))650 651        time_limit = float(cfg.get("time_limit", 30.0))652        mip_rel_gap = float(cfg.get("mip_rel_gap", 0.01))653        threads = int(cfg.get("threads", 0))654 655        grasp_pool_factor = int(cfg.get("grasp_pool_factor", 50))656        rcl_size = int(cfg.get("rcl_size", 10))657        random_seed = int(cfg.get("random_seed", 2025))658 659        diversity_ratio = float(cfg.get("diversity_ratio", 0.0))660        661        if candidate_ids is None:662            candidate_ids = list(self.ns_targets.keys())663        candidate_ids = list(candidate_ids)664        if not candidate_ids:665            raise ValueError("candidate_ids 为空。")666 667        # 保证候选存在于矩阵中668        candidate_ids = [cid for cid in candidate_ids if cid in self.z_scores_by_module.index]669        if not candidate_ids:670            raise ValueError("candidate_ids 与 z_scores_by_module 无交集。")671 672        # separation 可能只包含子集673        row_ids = set(self.sep_df.index)674        col_ids = set(self.sep_df.columns)675        candidate_ids = [cid for cid in candidate_ids if (cid in row_ids) or (cid in col_ids)]676        if not candidate_ids:677            raise ValueError("候选草药在 separation 矩阵中不存在有效的行/列。")678 679        # ---------- 解析 target_size(支持 int / list[int] / dict / list[dict]) ----------680        use_group_sizes = False681        schemes: List[Optional[Dict[str, int]]] = []682        size_list: List[int] = []683 684        if isinstance(target_size, dict):685            use_group_sizes = True686            schemes = [self._normalize_size_dict(target_size)]687        elif isinstance(target_size, (list, tuple)) and target_size and all(isinstance(x, dict) for x in target_size):688            use_group_sizes = True689            schemes = [self._normalize_size_dict(s) for s in target_size]690 691        if use_group_sizes:692            if source_groups is None:693                raise ValueError("使用 dict 形式的 target_size 时必须提供 source_groups 映射。")694        else:695            if isinstance(target_size, (list, tuple, set, np.ndarray)):696                size_list = [int(s) for s in target_size if int(s) > 0]697                size_list = sorted(set(size_list))698            elif isinstance(target_size, (int, np.integer)):699                if int(target_size) > 0:700                    size_list = [int(target_size)]701            else:702                raise ValueError("target_size 必须是 int, list[int], dict 或 list[dict]。")703 704            if not size_list:705                raise ValueError("无法从 target_size 确定需要优化的味数。")706 707            schemes = [None] * len(size_list)708 709        # ---------- 保险机制:调整 size 不超过候选数量 ----------710        if use_group_sizes:711            # 异构配额:统计每个 group 的候选数量712            group_candidate_count: Dict[str, int] = {}713            for cid in candidate_ids:714                groups = source_groups.get(cid, set())715                for g in groups:716                    group_candidate_count[g] = group_candidate_count.get(g, 0) + 1717            718            # 调整每个 scheme 的配额719            adjusted_schemes: List[Dict[str, int]] = []720            for scheme in schemes:721                adjusted_scheme: Dict[str, int] = {}722                total_adjusted = 0723                for g, requested in scheme.items():724                    available = group_candidate_count.get(g, 0)725                    adjusted = min(requested, available)726                    if adjusted > 0:727                        adjusted_scheme[g] = adjusted728                        total_adjusted += adjusted729                    if adjusted < requested and verbose:730                        print(f"[FormulaOptimizer] Safety mechanism: group '{g}' requested {requested} items, but only {available} candidates available, adjusted to {adjusted}")731                732                if total_adjusted > 0:733                    adjusted_schemes.append(adjusted_scheme)734                elif verbose:735                    print(f"[FormulaOptimizer] Safety mechanism: scheme {scheme} adjusted to total of 0, skipped")736            737            schemes = adjusted_schemes738            if not schemes:739                print("[FormulaOptimizer] All schemes adjusted to no available candidates, returning empty solution.")740                return []741        else:742            # 同构总味数:直接用 min(size, len(candidate_ids))743            n_candidates = len(candidate_ids)744            adjusted_size_list: List[int] = []745            for s in size_list:746                adjusted = min(s, n_candidates)747                if adjusted > 0:748                    adjusted_size_list.append(adjusted)749                if adjusted < s and verbose:750                    print(f"[FormulaOptimizer] Safety mechanism: requested {s} items, but only {n_candidates} candidates available, adjusted to {adjusted}")751            752            size_list = adjusted_size_list753            if not size_list:754                print("[FormulaOptimizer] All sizes adjusted to 0, returning empty solution.")755                return []756            757            schemes = [None] * len(size_list)758 759        # 构建 hit 映射760        herb_to_hits, gene_to_herbs = self._build_herb_gene_maps(candidate_ids)761 762        module_size = len(self.module_nodes)763        if module_size <= 0:764            module_size = max(1, len(gene_to_herbs))765 766        # separation lookup767        pos_sep, neg_sep = self._build_sep_lookup(candidate_ids)768 769        # 聚合 z(用于可读指标)770        agg_z = self._aggregate_z_scores(candidate_ids, mode=self.agg_mode)771 772        all_solutions: List[FormulaSolution] = []773 774        for scheme_idx, scheme in enumerate(schemes):775            k_val = None if use_group_sizes else (size_list[scheme_idx] if scheme_idx < len(size_list) else None)776 777            if method == "scip":778                sols = self._solve_scip_multi(779                    candidate_ids=candidate_ids,780                    scheme=scheme,781                    k=k_val,782                    n_solutions=n_solutions,783                    quality_rel_gap=quality_rel_gap,784                    quality_abs_gap=quality_abs_gap,785                    min_hamming=min_hamming,786                    lambda_prox=lambda_prox,787                    lambda_sep=lambda_sep,788                    lambda_neg_sep=lambda_neg_sep,789                    lambda_cov=lambda_cov,790                    weighted_coverage=weighted_coverage,791                    node_weights=node_weights,792                    module_weights=module_weights,793                    herb_to_hits=herb_to_hits,794                    gene_to_herbs=gene_to_herbs,795                    module_size=module_size,796                    pos_sep=pos_sep,797                    neg_sep=neg_sep,798                    agg_z=agg_z,799                    source_groups=source_groups,800                    time_limit=time_limit,801                    mip_rel_gap=mip_rel_gap,802                    threads=threads,803                    verbose=verbose,804                    scheme_idx=scheme_idx,805                )806                all_solutions.extend(sols)807 808            elif method == "cbc":809                sols = self._solve_cbc_multi(810                    candidate_ids=candidate_ids,811                    scheme=scheme,812                    k=k_val,813                    n_solutions=n_solutions,814                    quality_rel_gap=quality_rel_gap,815                    quality_abs_gap=quality_abs_gap,816                    diversity_ratio=diversity_ratio,817                    lambda_prox=lambda_prox,818                    lambda_sep=lambda_sep,819                    lambda_neg_sep=lambda_neg_sep,820                    lambda_cov=lambda_cov,821                    weighted_coverage=weighted_coverage,822                    node_weights=node_weights,823                    module_weights=module_weights,824                    herb_to_hits=herb_to_hits,825                    gene_to_herbs=gene_to_herbs,826                    module_size=module_size,827                    pos_sep=pos_sep,828                    neg_sep=neg_sep,829                    agg_z=agg_z,830                    source_groups=source_groups,831                    time_limit=time_limit,832                    mip_rel_gap=mip_rel_gap,833                    threads=threads,834                    verbose=verbose,835                    scheme_idx=scheme_idx,836                )837                all_solutions.extend(sols)838 839            elif method == "grasp":840                rng = np.random.default_rng(int(random_seed))841                sols = self._solve_grasp_multi(842                    candidate_ids=candidate_ids,843                    scheme=scheme,844                    k=k_val,845                    n_solutions=n_solutions,846                    quality_rel_gap=quality_rel_gap,847                    quality_abs_gap=quality_abs_gap,848                    min_hamming=min_hamming,849                    lambda_prox=lambda_prox,850                    lambda_sep=lambda_sep,851                    lambda_cov=lambda_cov,852                    weighted_coverage=weighted_coverage,853                    node_weights=node_weights,854                    module_weights=module_weights,855                    herb_to_hits=herb_to_hits,856                    gene_to_herbs=gene_to_herbs,857                    module_size=module_size,858                    agg_z=agg_z,859                    pos_sep=pos_sep,860                    source_groups=source_groups,861                    rng=rng,862                    pool_factor=grasp_pool_factor,863                    rcl_size=rcl_size,864                    verbose=verbose,865                    scheme_idx=scheme_idx,866                )867                all_solutions.extend(sols)868 869            else:870                raise RuntimeError(f"Unknown method '{method}'")871 872        return all_solutions873 874    # ============================875    # Common helpers876    # ============================877 878    def _normalize_size_dict(self, d: Dict[str, int]) -> Dict[str, int]:879        out: Dict[str, int] = {}880        for k, v in d.items():881            vv = int(v)882            if vv > 0:883                out[str(k)] = vv884        if not out:885            raise ValueError("size scheme 为空或全为非正数。")886        return out887 888    def _aggregate_z_scores(self, candidate_ids: List[str], mode: str = "min") -> pd.Series:889        sub = self.z_scores_by_module.loc[candidate_ids].copy()890        sub = sub.replace([np.inf, -np.inf], np.nan).fillna(0.0)891        if mode == "min":892            s = sub.min(axis=1)893        elif mode == "mean":894            s = sub.mean(axis=1)895        elif mode == "max":896            s = sub.max(axis=1)897        else:898            raise ValueError("agg_mode 仅支持 'min'/'mean'/'max'。")899        s.name = "agg_z"900        return s901 902    def _get_sep_value(self, cid_i: str, cid_j: str) -> float:903        try:904            if cid_i in self.sep_df.index and cid_j in self.sep_df.columns:905                return float(self.sep_df.loc[cid_i, cid_j])906            if cid_j in self.sep_df.index and cid_i in self.sep_df.columns:907                return float(self.sep_df.loc[cid_j, cid_i])908        except Exception:909            pass910        return float("nan")911 912    def _build_sep_lookup(self, candidate_ids: List[str]) -> Tuple[Dict[Tuple[str, str], float], Dict[Tuple[str, str], float]]:913        pos_sep: Dict[Tuple[str, str], float] = {}914        neg_sep: Dict[Tuple[str, str], float] = {}915        for i, cid_i in enumerate(candidate_ids):916            for j in range(i + 1, len(candidate_ids)):917                cid_j = candidate_ids[j]918                val = self._get_sep_value(cid_i, cid_j)919                if not np.isfinite(val):920                    continue921                if val > 0:922                    pos_sep[(cid_i, cid_j)] = float(val)923                elif val < 0:924                    neg_sep[(cid_i, cid_j)] = float(-val)  # store magnitude925        return pos_sep, neg_sep926 927    def _build_herb_gene_maps(self, candidate_ids: List[str]) -> Tuple[Dict[str, List[str]], Dict[str, List[str]]]:928        herb_to_hits: Dict[str, List[str]] = {}929        gene_to_herbs: Dict[str, List[str]] = {}930        module_nodes = self.module_nodes931 932        for cid in candidate_ids:933            hits = [g for g in self.ns_targets.get(cid, []) if (not module_nodes) or (g in module_nodes)]934            herb_to_hits[cid] = hits935            for g in hits:936                gene_to_herbs.setdefault(g, []).append(cid)937 938        for g, herbs in gene_to_herbs.items():939            gene_to_herbs[g] = sorted(set(herbs))940 941        return herb_to_hits, gene_to_herbs942 943    def _compute_readable_metrics(944        self,945        *,946        selected: List[str],947        agg_z: pd.Series,948        herb_to_hits: Dict[str, List[str]],949        module_size: int,950        weighted_coverage: bool,951        node_weights: Optional[Dict[str, float]],952    ) -> Tuple[float, float, float]:953        z_vals = agg_z.loc[selected].values if len(selected) > 0 else np.array([])954        mean_z = float(np.mean(z_vals)) if len(z_vals) > 0 else float("nan")955 956        sep_vals: List[float] = []957        for i in range(len(selected)):958            for j in range(i + 1, len(selected)):959                v = self._get_sep_value(selected[i], selected[j])960                if np.isfinite(v):961                    sep_vals.append(float(v))962        mean_sep = float(np.mean(sep_vals)) if sep_vals else float("nan")963 964        hit_genes: Set[str] = set()965        for cid in selected:966            hit_genes.update(herb_to_hits.get(cid, []))967 968        if weighted_coverage and node_weights is not None and len(hit_genes) > 0:969            denom = float(sum(node_weights.get(g, 1.0) for g in self.module_nodes)) or 1.0970            num = float(sum(node_weights.get(g, 1.0) for g in hit_genes))971            cov = num / denom972        else:973            cov = (len(hit_genes) / float(module_size)) if module_size > 0 else float("nan")974 975        return mean_z, mean_sep, cov976 977    # ============================978    # GRASP (multi-solution) - unchanged from modules1979    # ============================980 981    def _solve_grasp_multi(982        self,983        *,984        candidate_ids: List[str],985        scheme: Optional[Dict[str, int]],986        k: Optional[int],987        n_solutions: int,988        quality_rel_gap: float,989        quality_abs_gap: Optional[float],990        min_hamming: int,991        lambda_prox: float,992        lambda_sep: float,993        lambda_cov: float,994        weighted_coverage: bool,995        node_weights: Optional[Dict[str, float]],996        module_weights: Optional[Dict[str, float]],997        herb_to_hits: Dict[str, List[str]],998        gene_to_herbs: Dict[str, List[str]],999        module_size: int,1000        agg_z: pd.Series,1001        pos_sep: Dict[Tuple[str, str], float],1002        source_groups: Optional[Dict[str, Set[str]]],1003        rng: np.random.Generator,1004        pool_factor: int,1005        rcl_size: int,1006        verbose: bool,1007        scheme_idx: int,1008    ) -> List[FormulaSolution]:1009        if scheme is None:1010            if k is None:1011                raise ValueError("GRASP 同构 size 需要 k。")1012            k_total = int(k)1013            if k_total <= 0:1014                return []1015        else:1016            k_total = int(sum(scheme.values()))1017            if source_groups is None:1018                raise ValueError("GRASP 异构 size 需要 source_groups。")1019 1020        pool_size = max(int(n_solutions * pool_factor), 50)1021        pool: List[FormulaSolution] = []1022 1023        for t in range(pool_size):1024            seed_tag = int(rng.integers(0, 2**31 - 1))1025            sol_ids = self._grasp_construct(1026                candidate_ids=candidate_ids,1027                k_total=k_total,1028                scheme=scheme,1029                source_groups=source_groups,1030                lambda_prox=lambda_prox,1031                lambda_sep=lambda_sep,1032                lambda_cov=lambda_cov,1033                weighted_coverage=weighted_coverage,1034                node_weights=node_weights,1035                module_weights=module_weights,1036                herb_to_hits=herb_to_hits,1037                gene_to_herbs=gene_to_herbs,1038                module_size=module_size,1039                agg_z=agg_z,1040                rng=np.random.default_rng(seed_tag),1041                rcl_size=rcl_size,1042            )1043            if not sol_ids:1044                continue1045 1046            obj, comps = self._eval_submodular_objective(1047                selected=sol_ids,1048                candidate_ids=candidate_ids,1049                lambda_prox=lambda_prox,1050                lambda_sep=lambda_sep,1051                lambda_cov=lambda_cov,1052                weighted_coverage=weighted_coverage,1053                node_weights=node_weights,1054                module_weights=module_weights,1055                herb_to_hits=herb_to_hits,1056                gene_to_herbs=gene_to_herbs,1057                module_size=module_size,1058            )1059            mean_z, mean_sep, cov = self._compute_readable_metrics(1060                selected=sol_ids,1061                agg_z=agg_z,1062                herb_to_hits=herb_to_hits,1063                module_size=module_size,1064                weighted_coverage=weighted_coverage,1065                node_weights=node_weights,1066            )1067 1068            pool.append(1069                FormulaSolution(1070                    selected=sol_ids,1071                    objective=float(obj),1072                    size=len(sol_ids),1073                    size_by_group=scheme,1074                    mean_z=mean_z,1075                    mean_separation=mean_sep,1076                    coverage=cov,1077                    obj_components=comps,1078                    meta={"method": "grasp", "scheme_idx": scheme_idx, "seed": seed_tag},1079                )1080            )1081 1082        if not pool:1083            return []1084 1085        picked = self._select_diverse_subset(1086            pool=pool,1087            n=n_solutions,1088            min_hamming=min_hamming,1089            quality_rel_gap=quality_rel_gap,1090            quality_abs_gap=quality_abs_gap,1091        )1092 1093        if verbose:1094            print(f"[FormulaOptimizer][GRASP] scheme={scheme_idx} pool={len(pool)} picked={len(picked)}")1095 1096        return picked1097 1098    def _grasp_construct(1099        self,1100        *,1101        candidate_ids: List[str],1102        k_total: int,1103        scheme: Optional[Dict[str, int]],1104        source_groups: Optional[Dict[str, Set[str]]],1105        lambda_prox: float,1106        lambda_sep: float,1107        lambda_cov: float,1108        weighted_coverage: bool,1109        node_weights: Optional[Dict[str, float]],1110        module_weights: Optional[Dict[str, float]],1111        herb_to_hits: Dict[str, List[str]],1112        gene_to_herbs: Dict[str, List[str]],1113        module_size: int,1114        agg_z: pd.Series,1115        rng: np.random.Generator,1116        rcl_size: int,1117    ) -> List[str]:1118        if k_total <= 0:1119            return []1120 1121        use_group = scheme is not None1122        remaining: Optional[Dict[str, int]] = None1123        if use_group:1124            if source_groups is None:1125                raise ValueError("scheme 非空时必须提供 source_groups。")1126            remaining = {str(g): int(v) for g, v in scheme.items() if int(v) > 0}1127            if sum(remaining.values()) != k_total:1128                k_total = sum(remaining.values())1129 1130        def pick_group(cid: str) -> Optional[str]:1131            if not use_group or remaining is None:1132                return None1133            gs = source_groups.get(cid, set()) if source_groups is not None else set()1134            feasible = [str(g) for g in gs if str(g) in remaining and remaining[str(g)] > 0]1135            if not feasible:1136                return None1137            feasible.sort(key=lambda g: remaining.get(g, 0), reverse=True)1138            return feasible[0]1139 1140        z_df = self.z_scores_by_module1141        cols = list(z_df.columns)1142        sub = z_df.loc[candidate_ids, cols].replace([np.inf, -np.inf], np.nan).fillna(0.0)1143        a_mat = np.maximum(0.0, -sub.to_numpy(dtype=float))1144 1145        if module_weights is not None:1146            w = np.array([float(module_weights.get(c, 1.0)) for c in cols], dtype=float)1147        else:1148            w = np.ones(len(cols), dtype=float)1149        if not np.isfinite(w).all() or w.sum() <= 0.0:1150            w = np.ones(len(cols), dtype=float)1151        w = w / w.sum()1152 1153        n = len(candidate_ids)1154        idx_of = {cid: i for i, cid in enumerate(candidate_ids)}1155        sim_mat = np.zeros((n, n), dtype=float)1156        for i, cid_i in enumerate(candidate_ids):1157            for j in range(i + 1, n):1158                s = self._get_sep_value(cid_i, candidate_ids[j])1159                sim = 0.0 if (np.isnan(s) or s >= 0) else -float(s)1160                sim_mat[i, j] = sim1161                sim_mat[j, i] = sim1162 1163        selected: List[str] = []1164        selected_set: Set[str] = set()1165 1166        sum_a = np.zeros((len(cols),), dtype=float)1167        best_sim = np.zeros((n,), dtype=float)1168 1169        covered: Set[str] = set()1170        if weighted_coverage and node_weights is not None:1171            cov_total = float(sum(node_weights.get(g, 1.0) for g in self.module_nodes)) or 1.01172            cov_curr = 0.01173        else:1174            cov_total = float(module_size) or 1.01175            cov_curr = 0.01176 1177        for step in range(k_total):1178            gains: List[Tuple[float, str, Optional[str], Any]] = []1179 1180            for cid in candidate_ids:1181                if cid in selected_set:1182                    continue1183 1184                g_assigned = pick_group(cid)1185                if use_group and g_assigned is None:1186                    continue1187 1188                i = idx_of[cid]1189 1190                if lambda_prox != 0.0:1191                    new_sum_a = sum_a + a_mat[i, :]1192                    delta_prox = float(np.sum(w * (np.log1p(new_sum_a) - np.log1p(sum_a))))1193                else:1194                    new_sum_a = sum_a1195                    delta_prox = 0.01196 1197                genes = herb_to_hits.get(cid, [])1198                new_genes = [g for g in genes if g not in covered]1199                delta_cov = 0.01200                new_cov_curr = cov_curr

Showing the first 1,200 of 2242 lines. Download the file for the rest.