CoolFace
Datasetpublic

PerturbReason/PerturbReason_dataset_code

sourceHugging Facemitupdated 5mo agoView on Hugging Face
0likes12downloads
summer_vllm_v2.py538 linesDownload Raw Back to Baselines
1"""2SUMMER Pipeline with vLLM (v2 - 3way Dataset)3Uses the same 3way dataset as the Qwen baseline (dataset_0410_disambig).4Enhances prompts with knowledge-graph-derived gene descriptions.5 6Aligned with the official PerturbQA SUMMER implementation:7  https://github.com/Genentech/PerturbQA/tree/main/examples/summer8 9Input: dataset_0410_disambig/{context_type}/{split}/*.jsonl  (task=3way)10Output: {source_file, prompt, ground_truth_response, model_output}  (matches Qwen)11"""12import argparse13import json14import os15import re16import glob17import sys18from tqdm import tqdm19from vllm import LLM, SamplingParams20 21sys.path.append(os.path.dirname(os.path.abspath(__file__)))22try:23    import summer_pipeline24except ImportError:25    print("ERROR: summer_pipeline.py not found in the same directory.")26    sys.exit(1)27 28# ==============================================================================29# PROMPT TEMPLATES30# ==============================================================================31 32PROMPT_SUMMARIZE_GENE = """You are an expert molecular biologist who studies how genes are related using Perturb-seq.33 34Task: You are writing a brief overview of the human gene {name}, with a focus on its molecular and cellular functions. You will be provided a set of database entries about the gene. Ensure that your overview remains faithful to this domain knowledge.35 36Format:37- Write one to two sentences describing the primary molecular and cellular function of gene {name}.38- Write one sentence describing what types of perturbations might impact the expression of gene {name}. For example, you might consider pathways that are upstream of the gene or compensatory mechanisms.39 40Constraints:41- Maintain a professional tone throughout.42- Do not comment on your own writing.43- Do not add any notes or references. Do not make up additional information.44- Do not discuss the importance or impact of the gene. Focus only on its function.45 46Domain knowledge:47{entries}48 49Brief overview of gene {name}:50"""51 52# --- Genetic 3-way (up / down / unchanged) ---53PROMPT_GENETIC_3WAY = """You are an expert molecular biologist who studies how genes are related using Perturb-seq. Your goal is to determine: Is the expression of {gene} up, down, or unchanged after CRISPRi knockdown of {pert} in {cell_short}?54 55You are given as input:56- Description of perturbed gene ({pert}): description of gene that is perturbed via CRISPRi knockdown57- Description of gene of interest ({gene}): description of gene, the impact on which you wish to infer58- Context: description of cell line in which the genes are expressed59- Basal expression levels: baseline expression levels of genes in the pathway60- Reasoning paths: known molecular relationships between the perturbed gene and the gene of interest61 62Output: Please analyze the following aspects step by step.63 641) What is the function of {pert}? What downstream pathways or genes might be affected when {pert} is knocked down?652) What is the function of {gene}? What upstream pathways regulate its expression?663) Based on the reasoning paths and basal expression levels, trace the relationship from {pert} to {gene}. Consider:67   - If {pert} activates a pathway that activates {gene}, knockdown would likely decrease {gene} (down)68   - If {pert} activates a pathway that represses {gene}, knockdown would likely increase {gene} (up)69   - If there is no clear or strong causal relationship, expression would likely be unchanged704) Is the expression of {gene} up, down, or unchanged after CRISPRi knockdown of {pert} in {cell_short}?71 72Structure your response in exactly three blocks:731. <thinking>Your step-by-step biological reasoning here.</thinking>742. <answer>up</answer> OR <answer>down</answer> OR <answer>unchanged</answer>753. <triplet>A JSON list of causal edges from your reasoning, e.g., [["A", "inhibits", "B"], ["B", "activates", "C"]].</triplet>76 77[Start of Input]78- Description of perturbed gene ({pert}): {pert_desc}79- Description of gene of interest ({gene}): {gene_desc}80- Context: {cell_desc}81- Basal expression levels:82{basal_text}83- Reasoning paths:84{reasoning_paths}85[End of Input]"""86 87# --- Chemical 3-way (up / down / unchanged) ---88PROMPT_CHEMICAL_3WAY = """You are an expert molecular biologist who studies how drugs and chemical perturbations affect gene expression. Your goal is to determine: Is the expression of {gene} up, down, or unchanged after chemical perturbation {pert} in {cell_short}?89 90You are given as input:91- Description of drug ({pert}): mechanism of action and known targets92- Description of gene of interest ({gene}): molecular and cellular functions93- Context: description of cell line in which the experiment is conducted94- Basal expression levels: baseline expression levels of genes in the pathway95- Reasoning paths: known molecular relationships between the drug and the gene96 97Output: Please analyze the following aspects step by step.98 991) What are the primary targets of {pert}? What is its mechanism of action?1002) What is the function of {gene}? What upstream pathways regulate its expression?1013) Based on the reasoning paths and basal expression levels, trace the relationship from {pert} to {gene}. Consider:102   - If {pert} inhibits a target that activates {gene}, expression would likely decrease (down)103   - If {pert} inhibits a target that represses {gene}, expression would likely increase (up)104   - If there is no clear or strong causal relationship, expression would likely be unchanged1054) Is the expression of {gene} up, down, or unchanged after chemical perturbation {pert} in {cell_short}?106 107Structure your response in exactly three blocks:1081. <thinking>Your step-by-step biological reasoning here.</thinking>1092. <answer>up</answer> OR <answer>down</answer> OR <answer>unchanged</answer>1103. <triplet>A JSON list of causal edges from your reasoning, e.g., [["A", "inhibits", "B"], ["B", "activates", "C"]].</triplet>111 112[Start of Input]113- Description of drug ({pert}): {pert_desc}114- Description of gene of interest ({gene}): {gene_desc}115- Context: {cell_desc}116- Basal expression levels:117{basal_text}118- Reasoning paths:119{reasoning_paths}120[End of Input]"""121 122# Cell line descriptions (from official SUMMER)123CELL_LINES = {124    "K562 cells": "K562 cells are immortalised myelogenous leukemia cells of the erythroleukemia type.",125    "K562": "K562 cells are immortalised myelogenous leukemia cells of the erythroleukemia type.",126    "k562": "K562 cells are immortalised myelogenous leukemia cells of the erythroleukemia type.",127    "RPE1 cells": "RPE1 cells are a non-cancerous, hTERT-immortalized, near-euploid, adherent, and p53-positive cell line.",128    "rpe1": "RPE1 cells are a non-cancerous, hTERT-immortalized, near-euploid, adherent, and p53-positive cell line.",129    "Jurkat cells": "Jurkat cells are an immortalized line of human T lymphocyte cells.",130    "jurkat": "Jurkat cells are an immortalized line of human T lymphocyte cells.",131    "HepG2 cells": "HepG2 cells are a human liver cancer cell line, derived from a patient with a well-differentiated hepatocellular carcinoma.",132    "hepg2": "HepG2 cells are a human liver cancer cell line, derived from a patient with a well-differentiated hepatocellular carcinoma.",133    "HepG2": "HepG2 cells are a human liver cancer cell line, derived from a patient with a well-differentiated hepatocellular carcinoma.",134    "B cells": "B cells are lymphocytes that develop in the bone marrow and play a central role in the adaptive immune response by producing antibodies.",135}136 137# ==============================================================================138# Helper Functions139# ==============================================================================140 141def get_gene_summary_prompt(gene: str, resources: tuple) -> str:142    """Build a summarization prompt for a gene using KG data."""143    ensembl_dict, uniprot_dict, go_desc, all_graphs = resources144    kg_desc = summer_pipeline.get_gene_descriptions(145        gene, ensembl_dict, uniprot_dict, go_desc, all_graphs146    )147    if len(kg_desc) == 0:148        return None149    MAX_CHARS = 24000150    current_chars = 0151    truncated_desc = []152    for entry in kg_desc:153        entry_len = len(entry)154        if current_chars + entry_len > MAX_CHARS:155            break156        truncated_desc.append(f"- {entry}")157        current_chars += entry_len158    entries = "\n".join(truncated_desc)159    return PROMPT_SUMMARIZE_GENE.format(name=gene, entries=entries)160 161 162def format_reasoning_paths(triplets):163    """Convert triplets to reasoning path text."""164    if not triplets:165        return "  No reasoning paths available."166    paths = []167    seen = set()168    for t in triplets:169        s = f"{t[0]} {t[1]} {t[2]}"170        if s not in seen:171            paths.append(f"  - {s}")172            seen.add(s)173    return "\n".join(paths)174 175 176def format_basal_context(basal_context):177    """Convert basal expression dict {gene: float} to text like the dataset prompt."""178    if not basal_context:179        return "  No basal expression data available."180    lines = []181    for gene, val in sorted(basal_context.items(), key=lambda x: -x[1]):182        if val >= 0.6:183            level = "high"184        elif val >= 0.2:185            level = "medium"186        else:187            level = "low"188        lines.append(f"  - {gene}: {level}")189    return "\n".join(lines)190 191 192def get_pert_description_chemical(item):193    """Generate a description for a chemical perturbation from triplets."""194    pert = item["input"]["pert"]195    triplets = item["output"].get("triplets_cell_conditioned",196                                   item["output"].get("triplets_all", []))197    targets = set()198    mechanisms = []199    for triplet in triplets:200        if triplet[0] == pert:201            targets.add(triplet[2])202            mechanisms.append(f"{pert} {triplet[1]} {triplet[2]}")203    if targets:204        desc = f"{pert} targets {', '.join(list(targets)[:5])}."205        if mechanisms:206            desc += f" Known interactions: {'; '.join(mechanisms[:3])}."207    else:208        desc = f"{pert} is a chemical compound used for perturbation studies."209        smiles = item["input"].get("smiles", "")210        if smiles:211            desc += f" SMILES: {smiles[:80]}"212    return desc213 214 215def detect_pert_type(item, filename):216    """Detect pert_type from the input data or filename."""217    pert_type = item.get("pert_type", "")218    if not pert_type:219        pert_type = item.get("input", {}).get("pert_type", "")220    if not pert_type:221        if "chemical" in filename:222            pert_type = "chemical"223        elif "genetic" in filename:224            pert_type = "genetic"225    # Normalize: geneticRNv3 -> genetic226    if "genetic" in pert_type.lower():227        pert_type = "genetic"228    return pert_type229 230 231def construct_prediction_prompt(item, gene_summary_cache, pert_type):232    """Build the SUMMER-enhanced QA prompt for a 3way item."""233    inp = item["input"]234    pert_raw = inp["pert"]235    gene = inp["gene"]236    cell_type = inp["cell_type"]237 238    # Handle combo genetic perturbation (pert is a list)239    is_combo = isinstance(pert_raw, list)240    if is_combo:241        pert = "+".join(pert_raw)  # e.g. "CBL+TGFBR2"242    else:243        pert = pert_raw244 245    # Gene summary (from KG - SUMMER's core enhancement)246    gene_desc = gene_summary_cache.get(247        gene, f"No knowledge graph information available for gene {gene}."248    )249 250    # Pert description251    if pert_type == "genetic":252        if is_combo:253            # Combine summaries for all genes in the combo254            parts = []255            for p in pert_raw:256                desc = gene_summary_cache.get(257                    p, f"No knowledge graph information available for gene {p}."258                )259                parts.append(f"{p}: {desc}")260            pert_desc = "\n".join(parts)261        else:262            pert_desc = gene_summary_cache.get(263                pert, f"No knowledge graph information available for gene {pert}."264            )265    else:266        pert_desc = get_pert_description_chemical(item)267 268    # Reasoning paths from triplets269    triplets = item["output"].get("triplets_cell_conditioned",270                                   item["output"].get("triplets_all", []))271    reasoning_paths = format_reasoning_paths(triplets)272 273    # Basal expression context274    basal_context = item.get("basal_context", {})275    basal_text = format_basal_context(basal_context)276 277    # Cell description278    cell_desc = CELL_LINES.get(cell_type, f"{cell_type} cells.")279 280    # Select template281    if pert_type == "genetic":282        template = PROMPT_GENETIC_3WAY283    else:284        template = PROMPT_CHEMICAL_3WAY285 286    prompt = template.format(287        pert=pert,288        gene=gene,289        cell_short=cell_type,290        pert_desc=pert_desc,291        gene_desc=gene_desc,292        cell_desc=cell_desc,293        basal_text=basal_text,294        reasoning_paths=reasoning_paths,295    )296    return prompt297 298 299def parse_prediction(response):300    """Parse the 3-way prediction (up/down/unchanged) from LLM response."""301    # Primary: collect ALL <answer> tags and take the LAST one.302    # The model often echoes the template first (e.g. '<answer>up</answer> OR303    # <answer>down</answer> OR <answer>unchanged</answer>') before giving the real304    # answer, so re.search / the first match would be wrong.305    matches = re.findall(r'<answer>\s*(up|down|unchanged)\s*</answer>', response, re.IGNORECASE)306    if matches:307        return matches[-1].lower()308 309    # Fallback: look in last few lines310    response_lower = response.lower()311    last_lines = response_lower.split('\n')[-6:]312    last_text = ' '.join(last_lines)313 314    if "unchanged" in last_text:315        return "unchanged"316 317    has_down = bool(re.search(r'\bdown\b', last_text) and not re.search(r'knockdown', last_text)) or "decrease" in last_text318    has_up = bool(re.search(r'\bup\b', last_text)) or "increase" in last_text319 320    if has_down and not has_up:321        return "down"322    if has_up and not has_down:323        return "up"324 325    # Count-based fallback (full response)326    dec = response_lower.count("decrease") + response_lower.count("downregulat")327    inc = response_lower.count("increase") + response_lower.count("upregulat")328    unc = response_lower.count("unchanged") + response_lower.count("no significant")329 330    if unc > max(dec, inc):331        return "unchanged"332    if dec > inc:333        return "down"334    if inc > dec:335        return "up"336    return "unchanged"  # conservative default337 338 339# ==============================================================================340# Main341# ==============================================================================342 343def main():344    parser = argparse.ArgumentParser(description="SUMMER Pipeline with vLLM (3way)")345    parser.add_argument("--kg_path", type=str, required=True,346                        help="Path to knowledge graph directory")347    parser.add_argument("--input_dir", type=str, required=True,348                        help="Directory containing input .jsonl files (3way dataset)")349    parser.add_argument("--output_dir", type=str, required=True,350                        help="Directory to save output .jsonl files")351    parser.add_argument("--model_path", type=str, required=True,352                        help="Path to the LLM model")353    parser.add_argument("--temperature", type=float, default=0.2)354    parser.add_argument("--max_tokens", type=int, default=4096)355    parser.add_argument("--tp_size", type=int, default=1)356    parser.add_argument("--gpu_memory_utilization", type=float, default=0.9)357    args = parser.parse_args()358 359    # 1. Initialize vLLM360    print(f"\n[Init] Loading vLLM model from {args.model_path}...", flush=True)361    llm = LLM(362        model=args.model_path,363        tensor_parallel_size=args.tp_size,364        trust_remote_code=True,365        dtype="bfloat16",366        max_model_len=8192,367        gpu_memory_utilization=args.gpu_memory_utilization,368        enforce_eager=True,369    )370 371    summary_sampling = SamplingParams(temperature=args.temperature, max_tokens=512)372    prediction_sampling = SamplingParams(temperature=args.temperature, max_tokens=args.max_tokens)373 374    # 2. Load Knowledge Graphs375    print("\n[Init] Loading Knowledge Graphs...", flush=True)376    kg_data = summer_pipeline.load_knowledge_graphs(args.kg_path)377    resources = summer_pipeline.build_kg_lookups(kg_data, max_degree=1000)378 379    # 3. Scan input files380    print("\n[Phase 0] Scanning input files...", flush=True)381    input_files = sorted(glob.glob(os.path.join(args.input_dir, "*.jsonl")))382    if not input_files:383        print(f"ERROR: No .jsonl files found in {args.input_dir}")384        return385 386    print(f"  Found {len(input_files)} input files.", flush=True)387 388    # Collect all genes needing KG summaries389    all_genes = set()390    file_cache = {}391 392    for fpath in tqdm(input_files, desc="Reading files"):393        try:394            with open(fpath, 'r') as f:395                data = [json.loads(line) for line in f if line.strip()]396            file_cache[fpath] = data397            filename = os.path.basename(fpath)398            for item in data:399                inp = item["input"]400                all_genes.add(inp["gene"])401                pt = detect_pert_type(item, filename)402                if pt == "genetic":403                    pert = inp["pert"]404                    # pert may be a list for combo genetic perturbations405                    if isinstance(pert, list):406                        for p in pert:407                            all_genes.add(p)408                    else:409                        all_genes.add(pert)410        except Exception as e:411            print(f"  Error reading {fpath}: {e}")412 413    print(f"  Total unique genes to summarize: {len(all_genes)}", flush=True)414 415    # 4. Summarize genes (Phase 1 - SUMMER's KG enhancement)416    print(f"\n[Phase 1] Generating KG summaries for {len(all_genes)} genes...", flush=True)417    gene_summary_cache = {}418    genes_to_process = []419    prompts_to_process = []420 421    for gene in sorted(all_genes):422        prompt = get_gene_summary_prompt(gene, resources)423        if prompt:424            genes_to_process.append(gene)425            prompts_to_process.append(prompt)426        else:427            gene_summary_cache[gene] = f"No knowledge graph information available for gene {gene}."428 429    if genes_to_process:430        chunk_size = 50431        print(f"  Processing {len(genes_to_process)} genes in chunks of {chunk_size}...", flush=True)432 433        for i in range(0, len(genes_to_process), chunk_size):434            chunk_genes = genes_to_process[i:i + chunk_size]435            chunk_prompts = prompts_to_process[i:i + chunk_size]436            print(f"  > Batch {i}-{i + len(chunk_genes)} / {len(genes_to_process)}", flush=True)437 438            try:439                outputs = llm.generate(chunk_prompts, summary_sampling, use_tqdm=False)440                for gene, output in zip(chunk_genes, outputs):441                    if output.outputs:442                        gene_summary_cache[gene] = output.outputs[0].text.strip()443                    else:444                        gene_summary_cache[gene] = ""445            except Exception as e:446                print(f"  ! Error in batch {i}: {e}")447                for gene, prompt in zip(chunk_genes, chunk_prompts):448                    try:449                        out = llm.generate([prompt], summary_sampling, use_tqdm=False)450                        gene_summary_cache[gene] = out[0].outputs[0].text.strip()451                    except Exception:452                        print(f"    ! SKIPPING gene: {gene}")453                        gene_summary_cache[gene] = f"Error processing gene {gene}."454 455    print(f"  Cached summaries for {len(gene_summary_cache)} genes.", flush=True)456 457    # 5. Generate Predictions (Phase 2)458    print("\n[Phase 2] Generating Final Predictions...", flush=True)459    os.makedirs(args.output_dir, exist_ok=True)460 461    for fpath, qa_data in file_cache.items():462        filename = os.path.basename(fpath)463        output_path = os.path.join(args.output_dir, f"summer_pred_{filename}")464 465        if os.path.exists(output_path):466            print(f"  Skipping {filename} (output already exists).")467            continue468 469        print(f"\n  Processing {filename} ({len(qa_data)} items)...", flush=True)470 471        if not qa_data:472            continue473 474        pert_type = detect_pert_type(qa_data[0], filename)475        print(f"    pert_type={pert_type}", flush=True)476 477        # Build prompts478        prompts = []479        valid_items = []480        for item in qa_data:481            try:482                p = construct_prediction_prompt(item, gene_summary_cache, pert_type)483                if len(p) > 32000:484                    print(f"    Warning: Skipping item {item.get('id')} (prompt too long).")485                    continue486                prompts.append(p)487                valid_items.append(item)488            except Exception as e:489                print(f"    Skipping item {item.get('id')}: {e}")490 491        if not prompts:492            print(f"    No valid prompts for {filename}.")493            continue494 495        # Run inference in chunks496        chunk_size = 200497        all_outputs = []498        for ci in range(0, len(prompts), chunk_size):499            chunk_prompts = prompts[ci:ci + chunk_size]500            print(f"    Inference chunk {ci}-{ci + len(chunk_prompts)} / {len(prompts)}", flush=True)501            try:502                chunk_outputs = llm.generate(chunk_prompts, prediction_sampling, use_tqdm=True)503                all_outputs.extend(chunk_outputs)504            except Exception as e:505                print(f"    ! Error in chunk {ci}: {e}")506                for p in chunk_prompts:507                    try:508                        out = llm.generate([p], prediction_sampling, use_tqdm=False)509                        all_outputs.extend(out)510                    except Exception:511                        all_outputs.append(None)512 513        # Write output (matching Qwen's 4-key format)514        with open(output_path, "w", encoding="utf-8") as f_out:515            for item, summer_prompt, output in zip(valid_items, prompts, all_outputs):516                if output is None or not output.outputs:517                    model_output = "ERROR: Failed to generate output."518                else:519                    model_output = output.outputs[0].text520 521                ground_truth = item.get("response")522 523                result = {524                    "source_file": filename,525                    "prompt": summer_prompt,526                    "ground_truth_response": ground_truth,527                    "model_output": model_output,528                }529                f_out.write(json.dumps(result, ensure_ascii=False) + "\n")530 531        print(f"    Wrote {len(valid_items)} predictions to {output_path}", flush=True)532 533    print("\n[Done] SUMMER pipeline complete.", flush=True)534 535 536if __name__ == "__main__":537    main()538