{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":118765,"databundleVersionId":15231210},{"sourceType":"datasetVersion","sourceId":14604295,"datasetId":9328538,"databundleVersionId":15440074},{"sourceType":"datasetVersion","sourceId":14874339,"datasetId":9502242,"databundleVersionId":15736806},{"sourceType":"datasetVersion","sourceId":14962495,"datasetId":9577097,"databundleVersionId":15833858}],"dockerImageVersionId":31286,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import subprocess, os, glob\n\n# Try standard Kaggle dataset paths\nfor pkg_name, patterns in [\n    ('biopython', ['/kaggle/input/biopython-cp312/*.whl', '/kaggle/input/datasets/kami1976/biopython-cp312/*.whl']),\n    ('biotite', ['/kaggle/input/biotite/*.whl', '/kaggle/input/datasets/amirrezaaleyasin/biotite/*.whl']),\n    ('rdkit', ['/kaggle/input/rdkit-2025-9-5/*.whl', '/kaggle/input/datasets/amirrezaaleyasin/rdkit-2025-9-5/*.whl']),\n]:\n    installed = False\n    for pattern in patterns:\n        whls = glob.glob(pattern)\n        if whls:\n            subprocess.run(['pip', 'install', '--no-index', '--no-deps', whls[0]], check=True)\n            print(f'Installed {pkg_name} from {whls[0]}')\n            installed = True\n            break\n    if not installed:\n        print(f'WARNING: Could not find {pkg_name} wheel, trying pip install...')\n        subprocess.run(['pip', 'install', pkg_name], check=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T00:28:28.778446Z","iopub.execute_input":"2026-03-18T00:28:28.778762Z","iopub.status.idle":"2026-03-18T00:28:36.945377Z","shell.execute_reply.started":"2026-03-18T00:28:28.778737Z","shell.execute_reply":"2026-03-18T00:28:36.944504Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport sys\nimport pandas as pd\n\n# ── Local vs Kaggle mode ─────────────────────────────────────────────────────\n# On Kaggle competition rerun, KAGGLE_IS_COMPETITION_RERUN is set to a truthy value.\n# When running locally we do NOT exit — instead we cap the test set to a small\n# number of samples so the notebook finishes quickly.\n\nIS_KAGGLE = True #bool(os.environ.get(\"KAGGLE_IS_COMPETITION_RERUN\", \"\"))\n\n# How many test samples to use when running locally\nLOCAL_N_SAMPLES = None\n\nif IS_KAGGLE:\n    print(\"Running in KAGGLE COMPETITION mode — all test targets will be processed.\")\nelse:\n    print(f\"Running in LOCAL mode — only the first {LOCAL_N_SAMPLES} test targets \"\n          f\"will be processed to save time.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T00:28:37.269111Z","iopub.execute_input":"2026-03-18T00:28:37.270016Z","iopub.status.idle":"2026-03-18T00:28:37.275864Z","shell.execute_reply.started":"2026-03-18T00:28:37.269992Z","shell.execute_reply":"2026-03-18T00:28:37.274571Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import gc\nimport json\nimport os\nimport time\n\nos.environ[\"LAYERNORM_TYPE\"] = \"torch\"\nos.environ.setdefault(\"RNA_MSA_DEPTH_LIMIT\", \"512\")\n\nimport sys\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom Bio.Align import PairwiseAligner\nfrom tqdm import tqdm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T00:28:45.182241Z","iopub.execute_input":"2026-03-18T00:28:45.182602Z","iopub.status.idle":"2026-03-18T00:28:48.541363Z","shell.execute_reply.started":"2026-03-18T00:28:45.182573Z","shell.execute_reply":"2026-03-18T00:28:48.540601Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install ViennaRNA mdtraj openmm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T00:34:10.915551Z","iopub.execute_input":"2026-03-18T00:34:10.915895Z","iopub.status.idle":"2026-03-18T00:34:18.042454Z","shell.execute_reply.started":"2026-03-18T00:34:10.915865Z","shell.execute_reply":"2026-03-18T00:34:18.041142Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_c1_mask(data: dict, atom_array) -> torch.Tensor:\n    # 1. Try atom_array attributes first\n    if atom_array is not None:\n        try:\n            if hasattr(atom_array, \"centre_atom_mask\"):\n                m = atom_array.centre_atom_mask == 1\n                if hasattr(atom_array, \"is_rna\"):\n                    m = m & atom_array.is_rna\n                return torch.from_numpy(m).bool()\n            \n            if hasattr(atom_array, \"atom_name\"):\n                base = atom_array.atom_name == \"C1'\"\n                if hasattr(atom_array, \"is_rna\"):\n                    base = base & atom_array.is_rna\n                return torch.from_numpy(base).bool()\n        except Exception:\n            pass\n\n    # 2. Fallback to feature dict\n    f = data[\"input_feature_dict\"]\n    \n    if \"centre_atom_mask\" in f:\n        return (f[\"centre_atom_mask\"] == 1).bool()\n    if \"center_atom_mask\" in f:\n        return (f[\"center_atom_mask\"] == 1).bool()\n        \n    # Heuristic fallback: check which index gives us roughly N_token atoms\n    n_tokens = data.get(\"N_token\", torch.tensor(0)).item()\n    mask11 = (f[\"atom_to_tokatom_idx\"] == 11).bool()\n    mask12 = (f[\"atom_to_tokatom_idx\"] == 12).bool()\n    \n    c11 = mask11.sum().item()\n    c12 = mask12.sum().item()\n    \n    # Return the one closer to N_tokens (likely one per residue)\n    if abs(c11 - n_tokens) < abs(c12 - n_tokens):\n        return mask11\n    else:\n        return mask12","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T00:28:55.806365Z","iopub.execute_input":"2026-03-18T00:28:55.806689Z","iopub.status.idle":"2026-03-18T00:28:55.814832Z","shell.execute_reply.started":"2026-03-18T00:28:55.806664Z","shell.execute_reply":"2026-03-18T00:28:55.813725Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# Advanced Pipeline Imports\nimport subprocess\ntry:\n    import RNA # ViennaRNA for secondary structure\nexcept ImportError:\n    print(\"ViennaRNA not installed, secondary structure constraints will run in fallback mode\")\n\ntry:\n    import mdtraj as md\n    # import simtk.openmm as mm\n    # import simtk.openmm.app as app\n    # import simtk.unit as unit\nexcept ImportError:\n    print(\"OpenMM/MDTraj not installed, physical relaxation will run in fallback mode\")\n\n\n\n# --- Phase 2: Advanced Template-Based Modeling (TBM) ---\ndef predict_secondary_structure(sequence):\n    \"\"\"Uses ViennaRNA to predict the 2D dot-bracket structure to constrain 3D generation.\"\"\"\n    try:\n        import RNA\n        fc = RNA.fold_compound(sequence)\n        ss, mfe = fc.mfe()\n        return ss\n    except ImportError:\n        # Fallback dummy dot-bracket\n        return \".\" * len(sequence)\n\ndef enforce_ss_constraints(coords, ss_dotbracket, sequence):\n    \"\"\"\n    Forces the 3D generation to respect base-pairing rules derived from 2D secondary structure.\n    Pulls paired bases (like '(' and ')') closer to precise hydrogen bonding distances.\n    \"\"\"\n    X = coords.copy()\n    L = len(sequence)\n    stack = []\n    pairs = []\n    for i, char in enumerate(ss_dotbracket):\n        if char == '(': stack.append(i)\n        elif char == ')':\n            if stack:\n                j = stack.pop()\n                pairs.append((j, i))\n                \n    # Nudge paired atoms towards canonical A-form RNA distances (~2.8A for H-bonds)\n    # Simple spring constraint simulation\n    for j, i in pairs:\n        if j < L and i < L:\n            dist = np.linalg.norm(X[i] - X[j]) + 1e-6\n            # Ideal C1'-C1' distance in a pair is roughly 10.5 Angstroms\n            error = 10.5 - dist\n            correction = (X[i] - X[j]) / dist * (error * 0.1)\n            X[i] += correction\n            X[j] -= correction\n            \n    return X\n\n\n# --- Phase 3 & 4: Scoring and Physical Minimization ---\ndef mock_ares_score(coords, sequence):\n    \"\"\"\n    Placeholder for the Deep Learning Ares Selection model.\n    Evaluates thermodynamic feasibility. Lower is better.\n    \"\"\"\n    # In reality, runs coords through an SE(3) GNN\n    # For now, evaluate basic distance distributions\n    dist_mat = np.linalg.norm(coords[:, None, :] - coords[None, :, :], axis=2)\n    # Penalize steric clashes (atoms < 3.0A apart that aren't adjacent)\n    clashes = np.sum((dist_mat < 3.0) & (np.abs(np.arange(len(sequence))[:, None] - np.arange(len(sequence))[None, :]) > 1))\n    return clashes + np.random.uniform(0, 5.0)\n\ndef openmm_relax(coords, target_id):\n    \"\"\"\n    Runs Amber Molecular Dynamics implicit solvent relaxation on the coordinates.\n    Requires fully constructed topologies, so it's a heavy operation.\n    \"\"\"\n    # Full atom recreation and AMBER relaxation requires complex PDB construction.\n    # We apply our refined geometry constraints as a surrogate here for the Kaggle environment.\n    return coords # (Actual MD logic requires writing out standard residues, building LEaP files, and running OpenMM System)\n\n\n\n# ─────────────── Paths & Constants ───────────────────────────────────────────\n# Auto-detect competition data path\nimport glob as _glob\n\nDATA_BASE = None\n_candidates = [\n    \"/kaggle/input/competitions/stanford-rna-3d-folding-2\",\n    \"/kaggle/input/stanford-rna-3d-folding-2\",\n    \"/kaggle/input/stanford-rna-3d-folding-part-2\",\n    \"/kaggle/input/stanford-rna-3d-folding\",\n    \"/kaggle/input/competitions/stanford-rna-3d-folding-part-2\",\n    \"/kaggle/input/competitions/stanford-rna-3d-folding\",\n]\nfor _c in _candidates:\n    if os.path.isdir(_c) and os.path.exists(os.path.join(_c, \"test_sequences.csv\")):\n        DATA_BASE = _c\n        break\n\nif DATA_BASE is None:\n    _matches = _glob.glob(\"/kaggle/input/**/test_sequences.csv\", recursive=True)\n    if _matches:\n        DATA_BASE = os.path.dirname(_matches[0])\n    else:\n        print(\"WARNING: Could not find competition data!\")\n        print(\"Available /kaggle/input/ dirs:\")\n        if os.path.isdir(\"/kaggle/input\"):\n            for d in os.listdir(\"/kaggle/input\"):\n                full = os.path.join(\"/kaggle/input\", d)\n                if os.path.isdir(full):\n                    files = os.listdir(full)[:10]\n                    print(f\"  {d}/: {files}\")\n        DATA_BASE = \"/kaggle/input/competitions/stanford-rna-3d-folding-2\"\n\nprint(f\"DATA_BASE = {DATA_BASE}\")\nif os.path.isdir(DATA_BASE):\n    print(f\"  Data files: {os.listdir(DATA_BASE)[:10]}\")\nelse:\n    print(f\"  WARNING: Data dir does not exist!\")\n\nDEFAULT_TEST_CSV       = f\"{DATA_BASE}/test_sequences.csv\"\nDEFAULT_TRAIN_CSV      = f\"{DATA_BASE}/train_sequences.csv\"\nDEFAULT_TRAIN_LBLS     = f\"{DATA_BASE}/train_labels.csv\"\nDEFAULT_VAL_CSV        = f\"{DATA_BASE}/validation_sequences.csv\"\nDEFAULT_VAL_LBLS       = f\"{DATA_BASE}/validation_labels.csv\"\nDEFAULT_OUTPUT         = \"/kaggle/working/submission.csv\"\n\n# Auto-detect protenix code dir\nDEFAULT_CODE_DIR = None\n_protenix_candidates = [\n    \"/kaggle/input/datasets/qiweiyin/protenix-v1-adjusted/Protenix-v1-adjust-v2/Protenix-v1-adjust-v2/Protenix-v1\",\n    \"/kaggle/input/datasets/qiweiyin/protenix-v1-adjusted/Protenix-v1-adjust/Protenix-v1\",\n    \"/kaggle/input/datasets/qiweiyin/protenix-v1-adjusted/Protenix-v1\",\n    \"/kaggle/input/datasets/qiweiyin/protenix-v1-adjusted\",\n    \"/kaggle/input/protenix-v1-adjusted/Protenix-v1-adjust-v2/Protenix-v1-adjust-v2/Protenix-v1\",\n    \"/kaggle/input/protenix-v1-adjusted/Protenix-v1-adjust/Protenix-v1\",\n    \"/kaggle/input/protenix-v1-adjusted/Protenix-v1\",\n    \"/kaggle/input/protenix-v1-adjusted\",\n    \"/kaggle/input/pm-111457844-at-03-09-2026-10-59-40\",\n]\nfor _pc in _protenix_candidates:\n    if os.path.isdir(_pc):\n        has_ckpt = (os.path.isdir(os.path.join(_pc, \"checkpoint\")) or \n                    os.path.isdir(os.path.join(_pc, \"checkpoints\")) or\n                    len(_glob.glob(os.path.join(_pc, \"*.pt\"))) > 0 or\n                    len(_glob.glob(os.path.join(_pc, \"checkpoint\", \"*.pt\"))) > 0)\n        has_configs = (os.path.isdir(os.path.join(_pc, \"configs\")) or\n                      os.path.isdir(os.path.join(_pc, \"config\")))\n        has_runner = os.path.isdir(os.path.join(_pc, \"runner\"))\n        if has_ckpt or has_configs or has_runner:\n            DEFAULT_CODE_DIR = _pc\n            print(f\"Found protenix at: {_pc} (ckpt={has_ckpt}, configs={has_configs}, runner={has_runner})\")\n            break\n\nif DEFAULT_CODE_DIR is None:\n    _search_roots = [\"/kaggle/input/datasets/*/protenix*\", \"/kaggle/input/datasets/*/*/protenix*\",\n                     \"/kaggle/input/protenix*\", \"/kaggle/input/pm-*\"]\n    _ckpt_matches = []\n    for _sr in _search_roots:\n        _ckpt_matches.extend(_glob.glob(f\"{_sr}/**/checkpoint/*.pt\", recursive=True))\n        if _ckpt_matches:\n            break\n    if not _ckpt_matches:\n        for _sr in _search_roots:\n            _ckpt_matches.extend(_glob.glob(f\"{_sr}/**/*.pt\", recursive=True))\n            if _ckpt_matches:\n                break\n    if _ckpt_matches:\n        _ckpt_dir = os.path.dirname(_ckpt_matches[0])\n        if os.path.basename(_ckpt_dir) == \"checkpoint\":\n            DEFAULT_CODE_DIR = os.path.dirname(_ckpt_dir)\n        else:\n            DEFAULT_CODE_DIR = _ckpt_dir\n        print(f\"Found protenix via glob: {DEFAULT_CODE_DIR}\")\n    else:\n        print(\"WARNING: Could not find protenix checkpoint!\")\n        if os.path.isdir(\"/kaggle/input\"):\n            for d in sorted(os.listdir(\"/kaggle/input\")):\n                full = os.path.join(\"/kaggle/input\", d)\n                if os.path.isdir(full):\n                    contents = os.listdir(full)[:10]\n                    print(f\"  {d}/: {contents}\")\n                    for sub in os.listdir(full)[:5]:\n                        subfull = os.path.join(full, sub)\n                        if os.path.isdir(subfull):\n                            subcontents = os.listdir(subfull)[:8]\n                            print(f\"    {sub}/: {subcontents}\")\n        DEFAULT_CODE_DIR = \"/kaggle/input/datasets/qiweiyin/protenix-v1-adjusted/Protenix-v1-adjust-v2/Protenix-v1-adjust-v2/Protenix-v1\"\n\nprint(f\"DEFAULT_CODE_DIR = {DEFAULT_CODE_DIR}\")\nif os.path.isdir(DEFAULT_CODE_DIR):\n    print(f\"  Contents: {os.listdir(DEFAULT_CODE_DIR)[:15]}\")\nelse:\n    print(f\"  WARNING: Directory does not exist!\")\nDEFAULT_ROOT_DIR = DEFAULT_CODE_DIR\n\n# Symlink setup for Protenix\nif DEFAULT_CODE_DIR and os.path.isdir(DEFAULT_CODE_DIR):\n    _protenix_root = Path(DEFAULT_CODE_DIR)\n    _mmcif_candidates = [\n        _protenix_root / \"common\",\n        _protenix_root.parent / \"common\",\n        Path(\"/kaggle/input/protenix-v1-adjusted\") / \"common\",\n    ]\n    for _mc in _mmcif_candidates:\n        if _mc.is_dir() and list(_mc.glob(\"*.cif\"))[:1]:\n            if not (_protenix_root / \"common\").exists():\n                os.symlink(str(_mc), str(_protenix_root / \"common\"))\n                print(f\"Symlinked common/ -> {_mc}\")\n            break\n\nMODEL_NAME    = \"protenix_base_20250630_v1.0.0\"\nN_SAMPLE      = 5\nSEED          = 42\nMAX_SEQ_LEN   = int(os.environ.get(\"MAX_SEQ_LEN\",   \"512\"))\n# FIX 1: CHUNK_OVERLAP 128 -> 256 (top solution uses 256, better stitching)\nCHUNK_OVERLAP = int(os.environ.get(\"CHUNK_OVERLAP\",  \"256\"))\n\n# TBM quality thresholds — lowered to capture more templates\nMIN_SIMILARITY       = float(os.environ.get(\"MIN_SIMILARITY\",       \"0.0\"))\n# FIX 3: MIN_PERCENT_IDENTITY 50 -> 40 (capture more template matches)\nMIN_PERCENT_IDENTITY = float(os.environ.get(\"MIN_PERCENT_IDENTITY\", \"40.0\"))\n\nUSE_PROTENIX = True\n\n\ndef parse_bool(value: str, default: bool = False) -> str:\n    v = str(value).strip().lower()\n    if v in {\"1\", \"true\", \"t\", \"yes\", \"y\", \"on\"}:\n        return \"true\"\n    if v in {\"0\", \"false\", \"f\", \"no\", \"n\", \"off\"}:\n        return \"false\"\n    return \"true\" if default else \"false\"\n\n\nUSE_MSA      = parse_bool(os.environ.get(\"USE_MSA\",      \"false\"))\n# Templates disabled - requires mmcif directory not available on Kaggle\nUSE_TEMPLATE = parse_bool(os.environ.get(\"USE_TEMPLATE\", \"false\"))\nUSE_RNA_MSA  = parse_bool(os.environ.get(\"USE_RNA_MSA\",  \"true\"))\n\nMODEL_N_SAMPLE = int(os.environ.get(\"MODEL_N_SAMPLE\", str(N_SAMPLE)))\n\n\n# ─────────────── General Utilities ───────────────────────────────────────────\ndef seed_everything(seed: int) -> None:\n    os.environ[\"CUBLAS_WORKSPACE_CONFIG\"] = \":4096:8\"\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    np.random.seed(seed)\n    torch.backends.cudnn.benchmark = False\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.enabled = True\n    torch.use_deterministic_algorithms(True)\n\n\ndef resolve_paths():\n    test_csv   = os.environ.get(\"TEST_CSV\",           DEFAULT_TEST_CSV)\n    output_csv = os.environ.get(\"SUBMISSION_CSV\",     DEFAULT_OUTPUT)\n    code_dir   = os.environ.get(\"PROTENIX_CODE_DIR\",  DEFAULT_CODE_DIR)\n    root_dir   = os.environ.get(\"PROTENIX_ROOT_DIR\",  DEFAULT_ROOT_DIR)\n    return test_csv, output_csv, code_dir, root_dir\n\n\ndef ensure_required_files(root_dir: str) -> None:\n    for p, name in [\n        (Path(root_dir) / \"checkpoint\" / f\"{MODEL_NAME}.pt\",          \"checkpoint\"),\n        (Path(root_dir) / \"common\" / \"components.cif\",                \"CCD file\"),\n        (Path(root_dir) / \"common\" / \"components.cif.rdkit_mol.pkl\",  \"CCD cache\"),\n    ]:\n        if not p.exists():\n            raise FileNotFoundError(f\"Missing {name}: {p}\")\n\n\n# ─────────────── Protenix Input / Config Helpers ─────────────────────────────\ndef build_input_json(df: pd.DataFrame, json_path: str) -> None:\n    data = [\n        {\n            \"name\": row[\"target_id\"],\n            \"covalent_bonds\": [],\n            \"sequences\": [{\"rnaSequence\": {\"sequence\": row[\"sequence\"], \"count\": 1}}],\n        }\n        for _, row in df.iterrows()\n    ]\n    with open(json_path, \"w\", encoding=\"utf-8\") as f:\n        json.dump(data, f)\n\n\ndef build_configs(input_json_path: str, dump_dir: str, model_name: str):\n    from configs.configs_base import configs as configs_base\n    from configs.configs_data import data_configs\n    from configs.configs_inference import inference_configs\n    from configs.configs_model_type import model_configs\n    from protenix.config.config import parse_configs\n\n    base = {**configs_base, **{\"data\": data_configs}, **inference_configs}\n\n    def deep_update(t, p):\n        for k, v in p.items():\n            if isinstance(v, dict) and k in t and isinstance(t[k], dict):\n                deep_update(t[k], v)\n            else:\n                t[k] = v\n\n    deep_update(base, model_configs[model_name])\n    arg_str = \" \".join([\n        f\"--model_name {model_name}\",\n        f\"--input_json_path {input_json_path}\",\n        f\"--dump_dir {dump_dir}\",\n        f\"--use_msa {USE_MSA}\",\n        f\"--use_template {USE_TEMPLATE}\",\n        f\"--use_rna_msa {USE_RNA_MSA}\",\n        f\"--sample_diffusion.N_sample {MODEL_N_SAMPLE}\",\n        f\"--seeds {SEED}\",\n    ])\n    return parse_configs(configs=base, arg_str=arg_str, fill_required_with_null=True)\n\n\ndef get_c1_mask(data: dict, atom_array) -> torch.Tensor:\n    if atom_array is not None:\n        try:\n            if hasattr(atom_array, \"centre_atom_mask\"):\n                m = atom_array.centre_atom_mask == 1\n                if hasattr(atom_array, \"is_rna\"):\n                    m = m & atom_array.is_rna\n                return torch.from_numpy(m).bool()\n            if hasattr(atom_array, \"atom_name\"):\n                base = atom_array.atom_name == \"C1'\"\n                if hasattr(atom_array, \"is_rna\"):\n                    base = base & atom_array.is_rna\n                return torch.from_numpy(base).bool()\n        except Exception:\n            pass\n    f = data[\"input_feature_dict\"]\n    if \"center_atom_mask\" in f:\n        return (f[\"center_atom_mask\"] == 1).bool()\n    if \"centre_atom_mask\" in f:\n        return (f[\"centre_atom_mask\"] == 1).bool()\n    return (f[\"atom_to_tokatom_idx\"] == 11).bool()\n\n\ndef get_feature_c1_mask(data: dict) -> torch.Tensor:\n    f = data[\"input_feature_dict\"]\n    if \"centre_atom_mask\" in f:\n        return f[\"centre_atom_mask\"].long() == 1\n    return f[\"atom_to_tokatom_idx\"].long() == 12\n\n\ndef coords_to_rows(target_id: str, seq: str, coords: np.ndarray) -> list:\n    rows = []\n    for i in range(len(seq)):\n        row = {\"ID\": f\"{target_id}_{i + 1}\", \"resname\": seq[i], \"resid\": i + 1}\n        for s in range(N_SAMPLE):\n            if s < coords.shape[0] and i < coords.shape[1]:\n                x, y, z = coords[s, i]\n            else:\n                x, y, z = 0.0, 0.0, 0.0\n            row[f\"x_{s + 1}\"] = float(x)\n            row[f\"y_{s + 1}\"] = float(y)\n            row[f\"z_{s + 1}\"] = float(z)\n        rows.append(row)\n    return rows\n\n\ndef pad_samples(coords: np.ndarray, n: int) -> np.ndarray:\n    if coords.shape[0] >= n:\n        return coords[:n]\n    if coords.shape[0] == 0:\n        return np.zeros((n, coords.shape[1], 3), dtype=coords.dtype)\n    extra = np.repeat(coords[:1], n - coords.shape[0], axis=0)\n    return np.concatenate([coords, extra], axis=0)\n\n\ndef split_into_chunks(seq_len: int, max_len: int, overlap: int) -> list:\n    if seq_len <= max_len:\n        return [(0, seq_len)]\n    chunks = []\n    step = max_len - overlap\n    pos = 0\n    while pos < seq_len:\n        end = min(pos + max_len, seq_len)\n        chunks.append((pos, end))\n        if end == seq_len:\n            break\n        pos += step\n    return chunks\n\n\ndef kabsch_align(P: np.ndarray, Q: np.ndarray):\n    centroid_P = P.mean(axis=0)\n    centroid_Q = Q.mean(axis=0)\n    Pc = P - centroid_P\n    Qc = Q - centroid_Q\n    H = Pc.T @ Qc\n    U, _, Vt = np.linalg.svd(H)\n    d = np.linalg.det(Vt.T @ U.T)\n    S = np.eye(3)\n    if d < 0:\n        S[2, 2] = -1\n    R = Vt.T @ S @ U.T\n    t = centroid_Q - R @ centroid_P\n    return R, t\n\n\ndef stitch_chunk_coords(chunk_coords_list, chunk_ranges, seq_len):\n    if len(chunk_coords_list) == 1:\n        coords = chunk_coords_list[0]\n        if coords.shape[0] >= seq_len:\n            return coords[:seq_len]\n        out = np.zeros((seq_len, 3), dtype=coords.dtype)\n        out[:coords.shape[0]] = coords\n        return out\n    aligned = [chunk_coords_list[0].copy()]\n    for i in range(1, len(chunk_coords_list)):\n        prev_start, prev_end = chunk_ranges[i - 1]\n        cur_start, cur_end = chunk_ranges[i]\n        ov_start = cur_start\n        ov_end = min(prev_end, cur_end)\n        ov_len = ov_end - ov_start\n        if ov_len < 3:\n            aligned.append(chunk_coords_list[i].copy())\n            continue\n        prev_ov = aligned[i - 1][ov_start - prev_start: ov_end - prev_start]\n        cur_ov = chunk_coords_list[i][ov_start - cur_start: ov_end - cur_start]\n        valid = ~(np.isnan(prev_ov).any(axis=1) | np.isnan(cur_ov).any(axis=1))\n        if valid.sum() < 3:\n            aligned.append(chunk_coords_list[i].copy())\n            continue\n        R, t = kabsch_align(cur_ov[valid], prev_ov[valid])\n        transformed = (chunk_coords_list[i] @ R.T) + t\n        aligned.append(transformed)\n    full = np.zeros((seq_len, 3), dtype=np.float64)\n    weights = np.zeros(seq_len, dtype=np.float64)\n    for i, ((s, e), coords) in enumerate(zip(chunk_ranges, aligned)):\n        chunk_len = coords.shape[0]\n        actual_end = min(s + chunk_len, seq_len)\n        used_len = actual_end - s\n        w = np.ones(used_len, dtype=np.float64)\n        if i > 0:\n            ov_start = s\n            ov_end = min(chunk_ranges[i - 1][1], e)\n            ramp_len = ov_end - ov_start\n            if ramp_len > 0:\n                w[:ramp_len] = np.linspace(0.0, 1.0, ramp_len)\n        if i < len(chunk_ranges) - 1:\n            next_s = chunk_ranges[i + 1][0]\n            ramp_start = next_s - s\n            ramp_len = actual_end - next_s\n            if ramp_len > 0 and ramp_start < used_len:\n                w[ramp_start:used_len] = np.linspace(1.0, 0.0, ramp_len)\n        full[s:actual_end] += coords[:used_len] * w[:, None]\n        weights[s:actual_end] += w\n    mask = weights > 0\n    full[mask] /= weights[mask, None]\n    return full\n\n\ndef _make_aligner():\n    al = PairwiseAligner()\n    al.mode = \"global\"\n    al.match_score = 2; al.mismatch_score = -1.5\n    al.open_gap_score = -8; al.extend_gap_score = -0.3  # relaxed from -0.4\n    al.open_left_deletion_score = -8; al.extend_left_deletion_score = -0.3\n    al.open_right_deletion_score = -8; al.extend_right_deletion_score = -0.3\n    al.open_left_insertion_score = -8; al.extend_left_insertion_score = -0.3\n    al.open_right_insertion_score = -8; al.extend_right_insertion_score = -0.3\n    return al\n\n_aligner = _make_aligner()\n\n\ndef parse_stoichiometry(stoich):\n    if pd.isna(stoich) or str(stoich).strip() == \"\":\n        return []\n    return [(ch.strip(), int(cnt)) for part in str(stoich).split(\";\") for ch, cnt in [part.split(\":\")]]\n\n\ndef parse_fasta(fasta_content):\n    out, cur, parts = {}, None, []\n    for line in str(fasta_content).splitlines():\n        line = line.strip()\n        if not line: continue\n        if line.startswith(\">\"):\n            if cur is not None: out[cur] = \"\".join(parts)\n            cur = line[1:].split()[0]; parts = []\n        else:\n            parts.append(line.replace(\" \", \"\"))\n    if cur is not None: out[cur] = \"\".join(parts)\n    return out\n\n\ndef get_chain_segments(row):\n    seq = row[\"sequence\"]; stoich = row.get(\"stoichiometry\", \"\"); all_sq = row.get(\"all_sequences\", \"\")\n    if pd.isna(stoich) or pd.isna(all_sq) or str(stoich).strip() == \"\" or str(all_sq).strip() == \"\":\n        return [(0, len(seq))]\n    try:\n        chain_dict = parse_fasta(all_sq); order = parse_stoichiometry(stoich)\n        segs, pos = [], 0\n        for ch, cnt in order:\n            base = chain_dict.get(ch)\n            if base is None: return [(0, len(seq))]\n            for _ in range(cnt): segs.append((pos, pos + len(base))); pos += len(base)\n        return segs if pos == len(seq) else [(0, len(seq))]\n    except Exception:\n        return [(0, len(seq))]\n\n\ndef build_segments_map(df):\n    seg_map, stoich_map = {}, {}\n    for _, r in df.iterrows():\n        tid = r[\"target_id\"]; seg_map[tid] = get_chain_segments(r)\n        raw_s = r.get(\"stoichiometry\", \"\"); stoich_map[tid] = \"\" if pd.isna(raw_s) else str(raw_s)\n    return seg_map, stoich_map\n\n\ndef process_labels(labels_df):\n    coords = {}\n    prefixes = labels_df[\"ID\"].str.rsplit(\"_\", n=1).str[0]\n    for prefix, grp in labels_df.groupby(prefixes):\n        coords[prefix] = grp.sort_values(\"resid\")[[\"x_1\", \"y_1\", \"z_1\"]].values\n    return coords\n\n\ndef _build_aligned_strings(query_seq, template_seq, alignment):\n    q_segs, t_segs = alignment.aligned\n    aq, at, qi, ti = [], [], 0, 0\n    for (qs, qe), (ts, te) in zip(q_segs, t_segs):\n        while qi < qs: aq.append(query_seq[qi]); at.append(\"-\"); qi += 1\n        while ti < ts: aq.append(\"-\"); at.append(template_seq[ti]); ti += 1\n        for qp, tp in zip(range(qs, qe), range(ts, te)):\n            aq.append(query_seq[qp]); at.append(template_seq[tp])\n        qi, ti = qe, te\n    while qi < len(query_seq): aq.append(query_seq[qi]); at.append(\"-\"); qi += 1\n    while ti < len(template_seq): aq.append(\"-\"); at.append(template_seq[ti]); ti += 1\n    return \"\".join(aq), \"\".join(at)\n\n\ndef find_similar_sequences_detailed(query_seq, train_seqs_df, train_coords_dict, top_n=30):\n    results = []\n    for _, row in train_seqs_df.iterrows():\n        tid, tseq = row[\"target_id\"], row[\"sequence\"]\n        if tid not in train_coords_dict: continue\n        if abs(len(tseq) - len(query_seq)) / max(len(tseq), len(query_seq)) > 0.3: continue\n        aln = next(iter(_aligner.align(query_seq, tseq)))\n        norm_s = aln.score / (2 * min(len(query_seq), len(tseq)))\n        identical = sum(1 for (qs, qe), (ts, te) in zip(*aln.aligned)\n                        for qp, tp in zip(range(qs, qe), range(ts, te)) if query_seq[qp] == tseq[tp])\n        pct_id = 100 * identical / len(query_seq)\n        aq, at = _build_aligned_strings(query_seq, tseq, aln)\n        results.append((tid, tseq, norm_s, train_coords_dict[tid], pct_id, aq, at))\n    results.sort(key=lambda x: x[2], reverse=True)\n    return results[:top_n]\n\n\ndef adapt_template_to_query(query_seq, template_seq, template_coords):\n    aln = next(iter(_aligner.align(query_seq, template_seq)))\n    new_coords = np.full((len(query_seq), 3), np.nan)\n    for (qs, qe), (ts, te) in zip(*aln.aligned):\n        chunk = template_coords[ts:te]\n        if len(chunk) == (qe - qs): new_coords[qs:qe] = chunk\n    for i in range(len(new_coords)):\n        if np.isnan(new_coords[i, 0]):\n            pv = next((j for j in range(i - 1, -1, -1) if not np.isnan(new_coords[j, 0])), -1)\n            nv = next((j for j in range(i + 1, len(new_coords)) if not np.isnan(new_coords[j, 0])), -1)\n            if pv >= 0 and nv >= 0:\n                w = (i - pv) / (nv - pv); new_coords[i] = (1 - w) * new_coords[pv] + w * new_coords[nv]\n            elif pv >= 0: new_coords[i] = new_coords[pv] + [3, 0, 0]\n            elif nv >= 0: new_coords[i] = new_coords[nv] + [3, 0, 0]\n            else: new_coords[i] = [i * 3, 0, 0]\n    return np.nan_to_num(new_coords)\n\n\n# FIX 2: RNA constraint passes 2 -> 3 (top solution uses 3, more physically realistic)\ndef adaptive_rna_constraints(coords, target_id, segments_map, confidence=1.0, passes=3):\n    X = coords.copy(); segments = segments_map.get(target_id, [(0, len(X))])\n    strength = max(0.75 * (1.0 - min(confidence, 0.97)), 0.02)\n    for _ in range(passes):\n        for s, e in segments:\n            C = X[s:e]; L = e - s\n            if L < 3: continue\n            d = C[1:] - C[:-1]; dist = np.linalg.norm(d, axis=1) + 1e-6\n            adj = d * ((5.95 - dist) / dist)[:, None] * (0.22 * strength); C[:-1] -= adj; C[1:] += adj\n            d2 = C[2:] - C[:-2]; d2n = np.linalg.norm(d2, axis=1) + 1e-6\n            adj2 = d2 * ((10.2 - d2n) / d2n)[:, None] * (0.10 * strength); C[:-2] -= adj2; C[2:] += adj2\n            C[1:-1] += (0.06 * strength) * (0.5 * (C[:-2] + C[2:]) - C[1:-1])\n            if L >= 25:\n                idx = np.linspace(0, L - 1, min(L, 160)).astype(int) if L > 220 else np.arange(L)\n                P = C[idx]; diff = P[:, None, :] - P[None, :, :]\n                dm = np.linalg.norm(diff, axis=2) + 1e-6; sep = np.abs(idx[:, None] - idx[None, :])\n                mask = (sep > 2) & (dm < 3.2)\n                if np.any(mask):\n                    vec = (diff * ((3.2 - dm) / dm)[:, :, None] * mask[:, :, None]).sum(axis=1)\n                    C[idx] += (0.015 * strength) * vec\n            X[s:e] = C\n    return X\n\n\ndef _rotmat(axis, ang):\n    a = np.asarray(axis, float); a /= np.linalg.norm(a) + 1e-12\n    x, y, z = a; c, s = np.cos(ang), np.sin(ang); CC = 1 - c\n    return np.array([[c+x*x*CC, x*y*CC-z*s, x*z*CC+y*s],\n                     [y*x*CC+z*s, c+y*y*CC, y*z*CC-x*s],\n                     [z*x*CC-y*s, z*y*CC+x*s, c+z*z*CC]])\n\n\ndef apply_hinge(coords, seg, rng, deg=22):\n    s, e = seg; L = e - s\n    if L < 30: return coords\n    pivot = s + int(rng.integers(10, L - 10))\n    R = _rotmat(rng.normal(size=3), np.deg2rad(float(rng.uniform(-deg, deg))))\n    X = coords.copy(); p0 = X[pivot].copy()\n    X[pivot+1:e] = (X[pivot+1:e] - p0) @ R.T + p0\n    return X\n\n\ndef jitter_chains(coords, segs, rng, deg=12, trans=1.5):\n    X = coords.copy(); gc_ = X.mean(0, keepdims=True)\n    for s, e in segs:\n        R = _rotmat(rng.normal(size=3), np.deg2rad(float(rng.uniform(-deg, deg))))\n        shift = rng.normal(size=3); shift = shift / (np.linalg.norm(shift) + 1e-12) * float(rng.uniform(0, trans))\n        c = X[s:e].mean(0, keepdims=True); X[s:e] = (X[s:e] - c) @ R.T + c + shift\n    X -= X.mean(0, keepdims=True) - gc_\n    return X\n\n\ndef smooth_wiggle(coords, segs, rng, amp=0.8):\n    X = coords.copy()\n    for s, e in segs:\n        L = e - s\n        if L < 20: continue\n        ctrl = np.linspace(0, L - 1, 6); disp = rng.normal(0, amp, (6, 3)); t = np.arange(L)\n        X[s:e] += np.vstack([np.interp(t, ctrl, disp[:, k]) for k in range(3)]).T\n    return X\n\n\ndef generate_rna_structure(sequence, seed=None):\n    if seed is not None: np.random.seed(seed)\n    n = len(sequence); coords = np.zeros((n, 3))\n    for i in range(n):\n        ang = i * 0.6; coords[i] = [10.0 * np.cos(ang), 10.0 * np.sin(ang), i * 2.5]\n    return coords\n\n\ndef tbm_phase(test_df, train_seqs_df, train_coords_dict, segments_map):\n    print(f\"\\n{'='*60}\\nPHASE 1: Template-Based Modeling\")\n    print(f\"  MIN_SIMILARITY = {MIN_SIMILARITY}  |  MIN_PCT_IDENTITY = {MIN_PERCENT_IDENTITY}\\n{'='*60}\")\n    t0 = time.time()\n    template_predictions, protenix_queue = {}, {}\n    for _, row in test_df.iterrows():\n        tid, seq = row[\"target_id\"], row[\"sequence\"]\n        segs = segments_map.get(tid, [(0, len(seq))])\n        similar = find_similar_sequences_detailed(seq, train_seqs_df, train_coords_dict, top_n=30)\n        preds, used = [], set()\n        for i, (tmpl_id, tmpl_seq, sim, tmpl_coords, pct_id, _, _) in enumerate(similar):\n            if len(preds) >= 15: break  # Generate Decoys for ARES\n            if sim < MIN_SIMILARITY or pct_id < MIN_PERCENT_IDENTITY: break\n            if tmpl_id in used: continue\n            rng = np.random.default_rng((row.name * 10000000000 + i * 10007) % (2**32))\n            adapted = adapt_template_to_query(seq, tmpl_seq, tmpl_coords)\n            slot = len(preds)\n            if slot == 0: X = adapted\n            elif slot == 1: X = adapted + rng.normal(0, max(0.01, (0.40 - sim) * 0.06), adapted.shape)\n            elif slot == 2:\n                longest = max(segs, key=lambda se: se[1] - se[0]); X = apply_hinge(adapted, longest, rng)\n            elif slot == 3: X = jitter_chains(adapted, segs, rng)\n            else: X = smooth_wiggle(adapted, segs, rng)\n            \n            # New Advanced Physics logic\n            ss2d = predict_secondary_structure(seq)\n            constrained_X = enforce_ss_constraints(X, ss2d, seq)\n            relaxed_X = openmm_relax(constrained_X, tid)\n            refined = adaptive_rna_constraints(relaxed_X, tid, segments_map, confidence=sim, passes=15)\n            preds.append(refined); used.add(tmpl_id)\n\n        template_predictions[tid] = preds\n        \n        # Phase 3: ARES Selection on Decoys\n        if len(preds) > N_SAMPLE:\n            print(f\"  Selecting top {N_SAMPLE} from {len(preds)} decoys using ARES scorer...\")\n            scored_preds = [(p, mock_ares_score(p, seq)) for p in preds]\n            scored_preds.sort(key=lambda x: x[1]) # Sort by lowest energy\n            preds = [p for p, score in scored_preds[:N_SAMPLE]]\n            \n        n_needed = N_SAMPLE - len(preds)\n\n        if n_needed > 0:\n            protenix_queue[tid] = (n_needed, seq)\n            print(f\"  {tid} ({len(seq)} nt): {len(preds)} TBM -> need {n_needed} from Protenix\")\n        else:\n            print(f\"  {tid} ({len(seq)} nt): all {N_SAMPLE} from TBM\")\n    elapsed = time.time() - t0\n    n_full = len(test_df) - len(protenix_queue)\n    print(f\"\\nPhase 1 done in {elapsed:.1f}s\\n  Fully covered by TBM: {n_full}\\n  Need Protenix: {len(protenix_queue)}\")\n    return template_predictions, protenix_queue\n\n\ndef main():\n    test_csv, output_csv, code_dir, root_dir = resolve_paths()\n    if not os.path.isdir(code_dir):\n        raise FileNotFoundError(f\"Missing PROTENIX_CODE_DIR: {code_dir}. Set PROTENIX_CODE_DIR to the repo path.\")\n    os.environ[\"PROTENIX_ROOT_DIR\"] = root_dir\n    sys.path.append(code_dir)\n    ensure_required_files(root_dir)\n    seed_everything(SEED)\n    test_df_full = pd.read_csv(test_csv)\n    test_df = (test_df_full.head(LOCAL_N_SAMPLES) if not IS_KAGGLE else test_df_full).reset_index(drop=True)\n    print(f\"Test targets: {len(test_df)}\" + (\" (LOCAL MODE)\" if not IS_KAGGLE else \"\"))\n    seq_by_id = dict(zip(test_df[\"target_id\"], test_df[\"sequence\"]))\n    test_df_trunc = test_df.copy()\n    test_df_trunc[\"sequence\"] = test_df_trunc[\"sequence\"].str[:MAX_SEQ_LEN]\n    print(\"\\nLoading training data for TBM ...\")\n    train_seqs = pd.read_csv(DEFAULT_TRAIN_CSV)\n    val_seqs = pd.read_csv(DEFAULT_VAL_CSV)\n    train_labels = pd.read_csv(DEFAULT_TRAIN_LBLS)\n    val_labels = pd.read_csv(DEFAULT_VAL_LBLS)\n    combined_seqs = pd.concat([train_seqs, val_seqs], ignore_index=True)\n    combined_labels = pd.concat([train_labels, val_labels], ignore_index=True)\n    train_coords = process_labels(combined_labels)\n    segments_map, _ = build_segments_map(test_df)\n    print(f\"Template pool: {len(combined_seqs)} sequences, {len(train_coords)} structures\")\n    template_preds, protenix_queue = tbm_phase(test_df, combined_seqs, train_coords, segments_map)\n    protenix_preds = {}\n    if protenix_queue and USE_PROTENIX:\n        print(f\"\\n{'='*60}\\nPHASE 2: Protenix for {len(protenix_queue)} targets\\n{'='*60}\")\n        work_dir = Path(\"/kaggle/working\"); work_dir.mkdir(parents=True, exist_ok=True)\n        tasks, chunk_info = [], {}\n        for target_id, (n_needed, full_seq) in protenix_queue.items():\n            seq_len = len(full_seq)\n            if seq_len <= MAX_SEQ_LEN:\n                tasks.append({\"target_id\": target_id, \"sequence\": full_seq})\n                chunk_info[target_id] = [{\"name\": target_id, \"range\": (0, seq_len)}]\n            else:\n                chunks = split_into_chunks(seq_len, MAX_SEQ_LEN, CHUNK_OVERLAP)\n                chunk_info[target_id] = []\n                for ci, (cs, ce) in enumerate(chunks):\n                    chunk_name = f\"{target_id}_chunk{ci}\"\n                    tasks.append({\"target_id\": chunk_name, \"sequence\": full_seq[cs:ce]})\n                    chunk_info[target_id].append({\"name\": chunk_name, \"range\": (cs, ce)})\n        tasks_df = pd.DataFrame(tasks)\n        input_json_path = str(work_dir / \"protenix_queue_input.json\")\n        build_input_json(tasks_df, input_json_path)\n        from protenix.data.inference.infer_dataloader import InferenceDataset\n        from runner.inference import InferenceRunner, update_gpu_compatible_configs, update_inference_configs\n        configs = build_configs(input_json_path, str(work_dir / \"outputs\"), MODEL_NAME)\n        configs = update_gpu_compatible_configs(configs)\n        runner = InferenceRunner(configs)\n        dataset = InferenceDataset(configs)\n        raw_predictions = {}\n        def _extract_c1_coords(prediction, feat, chunk_seq_len, raw_coords):\n            if \"centre_atom_mask\" in feat:\n                mask = (feat[\"centre_atom_mask\"] == 1).to(raw_coords.device)\n            elif \"atom_to_tokatom_idx\" in feat:\n                m11 = (feat[\"atom_to_tokatom_idx\"] == 11).to(raw_coords.device)\n                m12 = (feat[\"atom_to_tokatom_idx\"] == 12).to(raw_coords.device)\n                mask = m11 if abs(m11.sum() - chunk_seq_len) < abs(m12.sum() - chunk_seq_len) else m12\n            else:\n                mask = torch.zeros(raw_coords.shape[1], dtype=torch.bool, device=raw_coords.device)\n            coords = raw_coords[:, mask, :].detach().cpu().numpy()\n            if coords.shape[1] > 1:\n                diffs = np.linalg.norm(coords[0, 1:] - coords[0, :-1], axis=-1)\n                if np.all(diffs < 1e-4): return None\n            if coords.shape[1] != chunk_seq_len:\n                if coords.shape[1] == 1 and chunk_seq_len > 1: return None\n                padded = np.zeros((coords.shape[0], chunk_seq_len, 3), dtype=np.float32)\n                ml = min(coords.shape[1], chunk_seq_len); padded[:, :ml, :] = coords[:, :ml, :]; coords = padded\n            return coords\n        for i in tqdm(range(len(dataset)), desc=\"Protenix Inference\"):\n            data, atom_array, err = dataset[i]\n            sample_name = data.get(\"sample_name\", f\"sample_{i}\")\n            if err:\n                raw_predictions[sample_name] = None\n                del data, atom_array, err; gc.collect(); torch.cuda.empty_cache(); continue\n            target_id = sample_name.split(\"_chunk\")[0] if \"_chunk\" in sample_name else sample_name\n            n_needed = protenix_queue.get(target_id, (N_SAMPLE, \"\"))[0]\n            sub_seq_len = data[\"N_token\"].item()\n            try:\n                new_cfg = update_inference_configs(configs, sub_seq_len)\n                new_cfg.sample_diffusion.N_sample = n_needed\n                runner.update_model_configs(new_cfg)\n                pred = runner.predict(data); raw_coords = pred[\"coordinate\"]\n                coords = _extract_c1_coords(pred, data[\"input_feature_dict\"], sub_seq_len, raw_coords)\n                raw_predictions[sample_name] = coords\n            except Exception as exc:\n                print(f\"  {sample_name} inference failed: {exc}\")\n                import traceback; traceback.print_exc()\n                raw_predictions[sample_name] = None\n            finally:\n                try: del pred, data, atom_array, raw_coords\n                except: pass\n                gc.collect(); torch.cuda.empty_cache()\n        for target_id, (n_needed, full_seq) in protenix_queue.items():\n            seq_len = len(full_seq); chunks = chunk_info.get(target_id, [])\n            if not chunks: continue\n            if len(chunks) == 1:\n                protenix_preds[target_id] = raw_predictions.get(target_id)\n            else:\n                chunk_results_per_sample = {s: [] for s in range(n_needed)}; all_ok = True\n                for cinfo in chunks:\n                    ccoords = raw_predictions.get(cinfo[\"name\"])\n                    if ccoords is None: all_ok = False; break\n                    for s_idx in range(n_needed):\n                        chunk_results_per_sample[s_idx].append(\n                            (ccoords[s_idx] if s_idx < ccoords.shape[0] else ccoords[-1], cinfo[\"range\"]))\n                if not all_ok: protenix_preds[target_id] = None; continue\n                stitched = []\n                for s_idx in range(n_needed):\n                    items = chunk_results_per_sample[s_idx]\n                    stitched.append(stitch_chunk_coords([c for c, _ in items], [r for _, r in items], seq_len))\n                protenix_preds[target_id] = np.stack(stitched, axis=0)\n    elif protenix_queue and not USE_PROTENIX:\n        print(f\"\\nPHASE 2 skipped. De-novo fallback for {len(protenix_queue)} targets.\")\n    print(f\"\\n{'='*60}\\nPHASE 3: Combine TBM + Protenix + de-novo fallback\\n{'='*60}\")\n    all_rows = []\n    for _, row in test_df.iterrows():\n        tid, seq = row[\"target_id\"], row[\"sequence\"]\n        combined = list(template_preds.get(tid, []))\n        ptx = protenix_preds.get(tid)\n        if ptx is not None and ptx.ndim == 3:\n            for j in range(ptx.shape[0]):\n                if len(combined) >= N_SAMPLE: break\n                combined.append(ptx[j])\n        while len(combined) < N_SAMPLE:\n            seed_val = row.name * 1000000 + len(combined) * 1000\n            dn = generate_rna_structure(seq, seed=seed_val)\n            combined.append(adaptive_rna_constraints(dn, tid, segments_map, confidence=0.2))\n        stacked = np.stack(combined[:N_SAMPLE], axis=0)\n        all_rows.extend(coords_to_rows(tid, seq, stacked))\n    sub = pd.DataFrame(all_rows)\n    cols = [\"ID\", \"resname\", \"resid\"] + [f\"{c}_{i}\" for i in range(1, N_SAMPLE + 1) for c in [\"x\", \"y\", \"z\"]]\n    coord_cols = [c for c in cols if c.startswith((\"x_\", \"y_\", \"z_\"))]\n    sub[coord_cols] = sub[coord_cols].clip(-999.999, 9999.999)\n    sub[cols].to_csv(output_csv, index=False)\n    print(f\"\\nSaved submission to {output_csv}  ({len(sub):,} rows)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T00:34:33.263884Z","iopub.execute_input":"2026-03-18T00:34:33.264197Z","iopub.status.idle":"2026-03-18T00:34:33.64661Z","shell.execute_reply.started":"2026-03-18T00:34:33.26417Z","shell.execute_reply":"2026-03-18T00:34:33.645544Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import traceback as _tb\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except Exception as e:\n        print(f\"\\n{'='*60}\")\n        print(f\"MAIN FAILED: {e}\")\n        _tb.print_exc()\n        print(f\"{'='*60}\")\n        print(\"Creating fallback dummy submission...\")\n        \n        # Try to read test CSV to get proper target IDs\n        import pandas as _pd\n        _output = \"/kaggle/working/submission.csv\"\n        _test_csv = None\n        for _tc in [\n            \"/kaggle/input/competitions/stanford-rna-3d-folding-2/test_sequences.csv\",\n            \"/kaggle/input/stanford-rna-3d-folding-2/test_sequences.csv\",\n            \"/kaggle/input/stanford-rna-3d-folding-part-2/test_sequences.csv\",\n        ]:\n            if os.path.exists(_tc):\n                _test_csv = _tc\n                break\n        \n        if _test_csv is None:\n            import glob as _g\n            _matches = _g.glob(\"/kaggle/input/**/test_sequences.csv\", recursive=True)\n            if _matches:\n                _test_csv = _matches[0]\n        \n        _rows = []\n        if _test_csv and os.path.exists(_test_csv):\n            _tdf = _pd.read_csv(_test_csv)\n            for _, _r in _tdf.iterrows():\n                _tid = _r[\"target_id\"]\n                _seq = _r[\"sequence\"]\n                for _i in range(len(_seq)):\n                    _row = {\"ID\": f\"{_tid}_{_i+1}\", \"resname\": _seq[_i], \"resid\": _i+1}\n                    for _s in range(1, 6):\n                        _ang = _i * 0.6\n                        _row[f\"x_{_s}\"] = float(10.0 * np.cos(_ang) + (_s-1)*0.1)\n                        _row[f\"y_{_s}\"] = float(10.0 * np.sin(_ang) + (_s-1)*0.1)\n                        _row[f\"z_{_s}\"] = float(_i * 2.5 + (_s-1)*0.1)\n                    _rows.append(_row)\n        else:\n            # Absolute last resort - create minimal valid submission\n            for _i in range(10):\n                _row = {\"ID\": f\"dummy_{_i+1}\", \"resname\": \"A\", \"resid\": _i+1}\n                for _s in range(1, 6):\n                    _row[f\"x_{_s}\"] = float(_i)\n                    _row[f\"y_{_s}\"] = 0.0\n                    _row[f\"z_{_s}\"] = 0.0\n                _rows.append(_row)\n        \n        _sub = _pd.DataFrame(_rows)\n        _cols = [\"ID\", \"resname\", \"resid\"] + [f\"{c}_{i}\" for i in range(1, 6) for c in [\"x\", \"y\", \"z\"]]\n        _sub[[c for c in _cols if c in _sub.columns]].to_csv(_output, index=False)\n        print(f\"Fallback submission saved: {_output} ({len(_sub)} rows)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T00:34:43.241917Z","iopub.execute_input":"2026-03-18T00:34:43.242427Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#read submission.csv\nsubmission_path = \"/kaggle/working/submission.csv\"\nsubmission_df = pd.read_csv(submission_path)\nprint(submission_df.head(20))","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}