{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.12.12"},"kaggle":{"accelerator":"gpu","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":10855324,"datasetId":6742586,"databundleVersionId":11219268}],"dockerImageVersionId":31260,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":165.02465,"end_time":"2026-03-02T08:06:01.924463","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-03-02T08:03:16.899813","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"f978dae1","cell_type":"markdown","source":"# Stanford RNA 3D Folding — Protenix + TBM Pipeline\n\n<div style=\"background:linear-gradient(135deg,#0f172a 0%,#1e3a5f 50%,#1d4ed8 100%); border-radius:12px; padding:24px 28px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#ffffff; line-height:1.7; margin-bottom:8px;\">\n<b style=\"font-size:18px; letter-spacing:0.4px;\">What this notebook does</b><br><br>\nThis notebook implements a <b>three-phase hybrid prediction pipeline</b> for the Stanford RNA 3D Folding Kaggle competition. It combines classical Template-Based Modeling (TBM) with <b>Protenix</b> — ByteDance Research's open-source reimplementation of AlphaFold3 — to predict the 3D atomic coordinates of RNA molecules.\n<br><br>\nThe key insight driving this design is that <b>TBM dramatically outperforms pure neural network prediction</b> when close structural templates exist in the training data. As shown in the version table below, the combined approach scores <b style=\"color:#93c5fd;\">0.408</b> on the leaderboard versus <b style=\"color:#fca5a5;\">0.249</b> for Protenix alone.\n<br><br>\n<b style=\"color:#93c5fd;\">References and prior work:</b>\n<ul style=\"margin-top:6px; padding-left:20px; color:#cbd5e1;\">\n  <li><a href=\"https://www.kaggle.com/code/qiweiyin/protenix-v1-inference-2026\" style=\"color:#93c5fd;\">Protenix v1 inference 2026 — qiweiyin</a></li>\n  <li><a href=\"https://www.kaggle.com/code/nihilisticneuralnet/0-409-stanford-rna-folding-2-protenix-template\" style=\"color:#93c5fd;\">0.409 Stanford RNA Folding — nihilisticneuralnet</a></li>\n  <li><a href=\"https://www.kaggle.com/code/alexxanderlarko/protenix-v1\" style=\"color:#93c5fd;\">Protenix v1 — alexxanderlarko</a></li>\n</ul>\n</div>","metadata":{"papermill":{"duration":0.003962,"end_time":"2026-03-02T08:03:19.254051","exception":false,"start_time":"2026-03-02T08:03:19.250089","status":"completed"},"tags":[]}},{"id":"f1674fd7-384e-4647-983a-efc5c3491de2","cell_type":"markdown","source":"<div style='background:#080c14;border:1px solid #1a2540;border-radius:12px;padding:24px 28px;font-family:monospace;max-width:820px;margin-bottom:8px;'>\n  <div style='border-bottom:1px solid #1a2540;padding-bottom:18px;margin-bottom:20px;'>\n    <div style='font-size:10px;letter-spacing:0.15em;text-transform:uppercase;color:#3b82f6;border:1px solid #1d3a6b;padding:3px 9px;border-radius:3px;background:#0d1628;display:inline-block;margin-bottom:10px;'>// AI · ML · Data Science</div>\n    <div style='font-size:32px;font-weight:800;color:#fff;letter-spacing:-0.02em;margin-bottom:6px;'>Gourab <span style='color:#3b82f6;'>Roy</span></div>\n    <div style='font-size:13px;color:#64748b;font-style:italic;'>AI/ML practitioner shipping notebooks &amp; code daily — built in the open.</div>\n  </div>\n  <div style='display:grid;grid-template-columns:repeat(4,1fr);gap:10px;'>\n    <a href='https://www.kaggle.com/gourabr0y555' target='_blank' style='text-decoration:none;background:#0d1420;border:1px solid #1a2540;border-radius:8px;padding:14px;display:block;'>\n      <div style='font-size:16px;margin-bottom:8px;'>🏅</div>\n      <div style='font-size:9px;letter-spacing:0.15em;text-transform:uppercase;color:#20beff;margin-bottom:3px;'>Kaggle</div>\n      <div style='font-size:13px;font-weight:700;color:#fff;margin-bottom:3px;'>gourabr0y555</div>\n      <div style='font-size:10px;color:#475569;'>Notebooks · competitions</div>\n    </a>\n    <a href='https://github.com/crimsonKn1ght' target='_blank' style='text-decoration:none;background:#0d1420;border:1px solid #1a2540;border-radius:8px;padding:14px;display:block;'>\n      <div style='font-size:16px;margin-bottom:8px;'>⚡</div>\n      <div style='font-size:9px;letter-spacing:0.15em;text-transform:uppercase;color:#a78bfa;margin-bottom:3px;'>GitHub</div>\n      <div style='font-size:13px;font-weight:700;color:#fff;margin-bottom:3px;'>crimsonKn1ght</div>\n      <div style='font-size:10px;color:#475569;'>Code · implementations</div>\n    </a>\n    <a href='https://royml.dev/' target='_blank' style='text-decoration:none;background:#0d1420;border:1px solid #1a2540;border-radius:8px;padding:14px;display:block;'>\n      <div style='font-size:16px;margin-bottom:8px;'>✍️</div>\n      <div style='font-size:9px;letter-spacing:0.15em;text-transform:uppercase;color:#f59e0b;margin-bottom:3px;'>Blog</div>\n      <div style='font-size:13px;font-weight:700;color:#fff;margin-bottom:3px;'>royml.dev</div>\n      <div style='font-size:10px;color:#475569;'>Writing · tutorials</div>\n    </a>\n    <a href='https://resume-gr.vercel.app/' target='_blank' style='text-decoration:none;background:#0d1420;border:1px solid #1a2540;border-radius:8px;padding:14px;display:block;'>\n      <div style='font-size:16px;margin-bottom:8px;'>📄</div>\n      <div style='font-size:9px;letter-spacing:0.15em;text-transform:uppercase;color:#34d399;margin-bottom:3px;'>Resume</div>\n      <div style='font-size:13px;font-weight:700;color:#fff;margin-bottom:3px;'>resume-gr</div>\n      <div style='font-size:10px;color:#475569;'>Experience · skills</div>\n    </a>\n  </div>\n</div>","metadata":{}},{"id":"1d93ed2d","cell_type":"markdown","source":"## Experiment Log\n\n<div style=\"background:#f0fdf4; border-left:6px solid #16a34a; padding:16px 20px; border-radius:8px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#111111; line-height:1.6; margin-bottom:16px;\">\n<b style=\"color:#14532d; font-size:15px;\">Key finding</b><br>\nTBM is not a fallback — it is the primary driver of performance. Pure Protenix without template initialization scores <b>0.159 lower</b>, showing how much the neural network relies on structural priors to converge on a good fold.\n</div>\n\n<table style=\"border-collapse:collapse; width:100%; font-size:0.95em; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; background:white; border-radius:8px; overflow:hidden;\">\n<thead>\n<tr style=\"background:#1d4ed8; color:white;\">\n<th style=\"padding:10px 16px; text-align:left;\">Version</th>\n<th style=\"padding:10px 16px; text-align:left;\">Description</th>\n<th style=\"padding:10px 16px; text-align:left;\">LB Score</th>\n</tr>\n</thead>\n<tbody>\n<tr style=\"background:#ffffff;\">\n<td style=\"padding:10px 16px; color:#374151;\">3</td>\n<td style=\"padding:10px 16px; color:#374151;\">Protenix + TBM (current best)</td>\n<td style=\"padding:10px 16px; font-weight:700; color:#16a34a;\">0.408</td>\n</tr>\n<tr style=\"background:#f9fafb;\">\n<td style=\"padding:10px 16px; color:#374151;\">4</td>\n<td style=\"padding:10px 16px; color:#374151;\">Pure Protenix, no TBM</td>\n<td style=\"padding:10px 16px; color:#dc2626;\">0.249</td>\n</tr>\n<tr style=\"background:#ffffff;\">\n<td style=\"padding:10px 16px; color:#374151;\">5</td>\n<td style=\"padding:10px 16px; color:#374151;\">USE_MSA + USE_RNA_MSA</td>\n<td style=\"padding:10px 16px; color:#6b7280;\">TBC</td>\n</tr>\n<tr style=\"background:#f9fafb;\">\n<td style=\"padding:10px 16px; color:#374151;\">6</td>\n<td style=\"padding:10px 16px; color:#374151;\">USE_MSA only</td>\n<td style=\"padding:10px 16px; color:#6b7280;\">TBC</td>\n</tr>\n<tr style=\"background:#ffffff;\">\n<td style=\"padding:10px 16px; color:#374151;\">7</td>\n<td style=\"padding:10px 16px; color:#374151;\">USE_RNA_MSA only</td>\n<td style=\"padding:10px 16px; color:#6b7280;\">TBC</td>\n</tr>\n</tbody>\n</table>","metadata":{"papermill":{"duration":0.002917,"end_time":"2026-03-02T08:03:19.260043","exception":false,"start_time":"2026-03-02T08:03:19.257126","status":"completed"},"tags":[]}},{"id":"90626c3e","cell_type":"markdown","source":"## Part 1 — Environment Setup\n\n### Dependencies\n\n<div style=\"background:#fffbeb; border-left:6px solid #d97706; padding:16px 20px; border-radius:8px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#111111; line-height:1.6;\">\n<b style=\"color:#92400e; font-size:15px;\">Biopython install</b><br><br>\nBiopython is required for pairwise sequence alignment. On Kaggle it is available as a pre-uploaded wheel to avoid internet access restrictions. The install line is commented out below since it may already be present in the environment. Uncomment if you get an import error for <code style=\"background:#fef3c7; padding:2px 5px; border-radius:4px;\">Bio.Align</code>.\n</div>","metadata":{"papermill":{"duration":0.002932,"end_time":"2026-03-02T08:03:19.265829","exception":false,"start_time":"2026-03-02T08:03:19.262897","status":"completed"},"tags":[]}},{"id":"27525d0f","cell_type":"code","source":"# !pip install /kaggle/input/datasets/ogurtsov/biopython/biopython-1.85-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl","metadata":{"execution":{"iopub.status.busy":"2026-03-16T04:34:47.449723Z","iopub.execute_input":"2026-03-16T04:34:47.450514Z","iopub.status.idle":"2026-03-16T04:34:47.454576Z","shell.execute_reply.started":"2026-03-16T04:34:47.450482Z","shell.execute_reply":"2026-03-16T04:34:47.453952Z"},"papermill":{"duration":0.009173,"end_time":"2026-03-02T08:03:19.277804","exception":false,"start_time":"2026-03-02T08:03:19.268631","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"31030886","cell_type":"markdown","source":"### Competition vs. Local Mode\n\n<div style=\"background:#eef2ff; border-left:6px solid #4f46e5; padding:16px 20px; border-radius:8px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#111111; line-height:1.6;\">\n<b style=\"color:#3730a3; font-size:15px;\">How the IS_KAGGLE flag works</b><br><br>\n<code style=\"background:#e0e7ff; padding:2px 6px; border-radius:4px;\">IS_KAGGLE</code> detects whether the notebook is running as an official Kaggle competition submission (when <code style=\"background:#e0e7ff; padding:2px 6px; border-radius:4px;\">KAGGLE_IS_COMPETITION_RERUN</code> is set by Kaggle's infrastructure) or interactively during development.\n<ul style=\"margin-top:10px;\">\n  <li><b>Local mode:</b> only <code style=\"background:#e0e7ff; padding:2px 6px; border-radius:4px;\">LOCAL_N_SAMPLES</code> sequences are processed, keeping iteration fast.</li>\n  <li><b>Competition mode:</b> all test sequences are processed.</li>\n</ul>\nThis single flag controls branching throughout the notebook — no manual commenting or uncommenting needed.\n</div>","metadata":{"papermill":{"duration":0.002847,"end_time":"2026-03-02T08:03:19.28922","exception":false,"start_time":"2026-03-02T08:03:19.286373","status":"completed"},"tags":[]}},{"id":"70040046","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 = bool(os.environ.get(\"KAGGLE_IS_COMPETITION_RERUN\", \"\"))\n\n# How many test samples to use when running locally\nLOCAL_N_SAMPLES = 2\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.\")\n","metadata":{"execution":{"iopub.status.busy":"2026-03-16T04:34:47.470091Z","iopub.execute_input":"2026-03-16T04:34:47.470308Z","iopub.status.idle":"2026-03-16T04:34:47.745539Z","shell.execute_reply.started":"2026-03-16T04:34:47.470288Z","shell.execute_reply":"2026-03-16T04:34:47.744919Z"},"papermill":{"duration":0.752522,"end_time":"2026-03-02T08:03:20.044525","exception":false,"start_time":"2026-03-02T08:03:19.292003","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"7bceb19e","cell_type":"markdown","source":"### Imports and Environment Variables\n\n<div style=\"background:#eef2ff; border-left:6px solid #1d4ed8; padding:18px 22px; border-radius:8px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#111111; line-height:1.6;\">\n<b style=\"color:#1e3a8a; font-size:15px;\">Two environment variables are set before any imports</b>\n<table style=\"border-collapse:collapse; width:100%; font-size:0.93em; background:white; border-radius:8px; overflow:hidden; margin-top:12px;\">\n<thead>\n<tr style=\"background:#1d4ed8; color:white;\">\n<th style=\"padding:10px 16px; text-align:left;\">Variable</th>\n<th style=\"padding:10px 16px; text-align:left;\">Value</th>\n<th style=\"padding:10px 16px; text-align:left;\">Reason</th>\n</tr>\n</thead>\n<tbody>\n<tr style=\"background:#ffffff;\">\n<td style=\"padding:10px 16px; font-weight:600; color:#111827;\"><code style=\"background:#e0e7ff; padding:3px 7px; border-radius:5px;\">LAYERNORM_TYPE</code></td>\n<td style=\"padding:10px 16px; color:#374151;\"><code>\"torch\"</code></td>\n<td style=\"padding:10px 16px; color:#374151;\">Forces PyTorch's native layer normalisation instead of the custom CUDA kernel (<code>fast_layernorm</code>). Slower but works on P100 GPUs where the kernel may not be available.</td>\n</tr>\n<tr style=\"background:#f9fafb;\">\n<td style=\"padding:10px 16px; font-weight:600; color:#111827;\"><code style=\"background:#e0e7ff; padding:3px 7px; border-radius:5px;\">RNA_MSA_DEPTH_LIMIT</code></td>\n<td style=\"padding:10px 16px; color:#374151;\"><code>\"512\"</code></td>\n<td style=\"padding:10px 16px; color:#374151;\">Caps the number of aligned sequences fed into the MSA module. Deeper MSAs improve accuracy but increase memory and runtime significantly.</td>\n</tr>\n</tbody>\n</table>\n</div>","metadata":{"papermill":{"duration":0.002949,"end_time":"2026-03-02T08:03:20.050597","exception":false,"start_time":"2026-03-02T08:03:20.047648","status":"completed"},"tags":[]}},{"id":"b4acb24f","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":{"execution":{"iopub.status.busy":"2026-03-16T04:34:47.746888Z","iopub.execute_input":"2026-03-16T04:34:47.747223Z","iopub.status.idle":"2026-03-16T04:34:51.245753Z","shell.execute_reply.started":"2026-03-16T04:34:47.747198Z","shell.execute_reply":"2026-03-16T04:34:51.244565Z"},"papermill":{"duration":3.456763,"end_time":"2026-03-02T08:03:23.510268","exception":false,"start_time":"2026-03-02T08:03:20.053505","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"fb77af68","cell_type":"markdown","source":"### C1' Atom Mask Helper (Preliminary Definition)\n\n<div style=\"background:#fdf4ff; border-left:6px solid #9333ea; padding:16px 20px; border-radius:8px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#111111; line-height:1.6;\">\n<b style=\"color:#6b21a8; font-size:15px;\">What get_c1_mask does</b><br><br>\nThis is an early definition of the <code style=\"background:#ede9fe; padding:2px 6px; border-radius:4px;\">get_c1_mask</code> utility, which is redefined with additional logic inside the main code block. It extracts a boolean mask identifying which atoms in Protenix's output correspond to <b>C1' atoms</b> — the sugar carbons that serve as the representative coordinate for each nucleotide.\n<br><br>\nThe function tries three approaches in order of reliability:\n<ol style=\"margin-top:8px;\">\n  <li>The <code style=\"background:#ede9fe; padding:2px 5px; border-radius:4px;\">centre_atom_mask</code> attribute on the atom array.</li>\n  <li>The <code style=\"background:#ede9fe; padding:2px 5px; border-radius:4px;\">atom_name</code> attribute.</li>\n  <li>A heuristic that picks whichever index value (11 or 12) in <code style=\"background:#ede9fe; padding:2px 5px; border-radius:4px;\">atom_to_tokatom_idx</code> produces a count closest to the sequence length.</li>\n</ol>\nThis defensive design handles API differences across Protenix versions.\n</div>","metadata":{"papermill":{"duration":0.002919,"end_time":"2026-03-02T08:03:23.516328","exception":false,"start_time":"2026-03-02T08:03:23.513409","status":"completed"},"tags":[]}},{"id":"10144e2f","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\n","metadata":{"execution":{"iopub.status.busy":"2026-03-16T04:34:51.24627Z","iopub.status.idle":"2026-03-16T04:34:51.246517Z","shell.execute_reply.started":"2026-03-16T04:34:51.2464Z","shell.execute_reply":"2026-03-16T04:34:51.246413Z"},"papermill":{"duration":0.011787,"end_time":"2026-03-02T08:03:23.531196","exception":false,"start_time":"2026-03-02T08:03:23.519409","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"30b6e1c5","cell_type":"markdown","source":"## Part 2 — Full Pipeline Implementation\n\n<div style=\"background:#f8fafc; border-left:6px solid #0f172a; padding:16px 20px; border-radius:8px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#111111; line-height:1.6; margin-bottom:20px;\">\nThe entire pipeline is defined in this cell as a collection of functions culminating in <code style=\"background:#e2e8f0; padding:2px 6px; border-radius:4px;\">main()</code>. It is structured into five logical sections described below.\n</div>\n\n### 2.1 — Paths, Constants, and Configuration\n\n<div style=\"background:#fffbeb; border-left:6px solid #d97706; padding:16px 20px; border-radius:8px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#111111; line-height:1.6; margin-bottom:20px;\">\nAll file paths and tunable parameters are defined at the top. Key values:\n<ul style=\"margin-top:8px;\">\n  <li><code style=\"background:#fef3c7; padding:2px 5px; border-radius:4px;\">MAX_SEQ_LEN = 512</code> — sequences are truncated to this length before being passed to Protenix, preventing GPU out-of-memory errors. Full-length TBM predictions are still generated for all sequences regardless of length.</li>\n  <li><code style=\"background:#fef3c7; padding:2px 5px; border-radius:4px;\">MIN_SIMILARITY</code> and <code style=\"background:#fef3c7; padding:2px 5px; border-radius:4px;\">MIN_PERCENT_IDENTITY</code> — quality gates for template acceptance. Setting <code style=\"background:#fef3c7; padding:2px 5px; border-radius:4px;\">MIN_PERCENT_IDENTITY = 50.0</code> means only templates sharing at least half their nucleotide letters with the query are accepted.</li>\n  <li><code style=\"background:#fef3c7; padding:2px 5px; border-radius:4px;\">USE_PROTENIX = True</code> — set to <code style=\"background:#fef3c7; padding:2px 5px; border-radius:4px;\">False</code> to skip the neural network stage entirely and fall back to de novo structures, useful for rapid debugging.</li>\n  <li>MSA and template flags (<code style=\"background:#fef3c7; padding:2px 5px; border-radius:4px;\">USE_MSA</code>, <code style=\"background:#fef3c7; padding:2px 5px; border-radius:4px;\">USE_TEMPLATE</code>, <code style=\"background:#fef3c7; padding:2px 5px; border-radius:4px;\">USE_RNA_MSA</code>) are read from environment variables, enabling ablation experiments without code changes.</li>\n</ul>\n</div>\n\n### 2.2 — TBM Core Functions\n\n<div style=\"background:#eef2ff; border-left:6px solid #1d4ed8; padding:16px 20px; border-radius:8px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#111111; line-height:1.6; margin-bottom:20px;\">\n<b style=\"color:#1e3a8a;\">Sequence alignment</b> uses a <code style=\"background:#e0e7ff; padding:2px 5px; border-radius:4px;\">PairwiseAligner</code> with match score 2.0, mismatch penalty -1.5, and gap open -8. End gaps are penalised symmetrically on both query and target sides. The length-difference filter is 30%, so templates with very different lengths are rejected early.\n<br><br>\n<b style=\"color:#1e3a8a;\">find_similar_sequences_detailed</b> returns the top 30 candidates and computes two quality metrics per template: normalised alignment score and percent identity (fraction of aligned positions where query and template share the same nucleotide letter). Both are used as acceptance criteria.\n<br><br>\n<b style=\"color:#1e3a8a;\">adapt_template_to_query</b> maps template coordinates onto the query via the alignment's matched blocks, then fills gaps by linear interpolation. The fallback for edge residues with no valid anchor shifts by <code style=\"background:#e0e7ff; padding:2px 5px; border-radius:4px;\">[3, 0, 0]</code> — a deterministic 3 Å step along the x-axis — making results fully reproducible.\n<br><br>\n<b style=\"color:#1e3a8a;\">adaptive_rna_constraints</b> applies three tiers of physical corrections per chain segment, run for multiple passes:\n<ol style=\"margin-top:8px;\">\n  <li><b>Bond constraint (i to i+1):</b> drives consecutive C1'-C1' distances toward 5.95 Å.</li>\n  <li><b>Next-neighbour constraint (i to i+2):</b> drives second-neighbour distances toward 10.2 Å, resisting sharp zigzag backbone conformations.</li>\n  <li><b>Laplacian smoothing:</b> nudges each internal residue toward the midpoint of its two neighbours, removing jagged kinks.</li>\n  <li><b>Self-avoidance:</b> for chains of 25+ residues, repels non-adjacent atoms closer than 3.2 Å. Uses a sampled subset of positions for chains longer than 220 residues to keep the O(n²) computation tractable.</li>\n</ol>\nConstraint strength scales as <code style=\"background:#e0e7ff; padding:2px 5px; border-radius:4px;\">0.75 × (1 - min(confidence, 0.97))</code>, meaning very high-similarity templates receive only minimal correction while de novo structures receive the full correction force.\n</div>\n\n### 2.3 — Diversity Transforms\n\n<div style=\"background:#fdf4ff; border-left:6px solid #9333ea; padding:16px 20px; border-radius:8px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#111111; line-height:1.6; margin-bottom:20px;\">\nRather than adding undifferentiated Gaussian noise to generate 5 diverse candidates, each prediction slot gets a qualitatively distinct structural perturbation:\n<ul style=\"margin-top:8px;\">\n  <li><b>Slot 0:</b> raw adapted template coordinates, unmodified.</li>\n  <li><b>Slot 1:</b> small Gaussian noise, magnitude proportional to <code style=\"background:#ede9fe; padding:2px 5px; border-radius:4px;\">max(0.01, (0.40 - similarity) × 0.06)</code>. Better templates get less perturbation.</li>\n  <li><b>Slot 2:</b> <b>hinge rotation</b> — a random pivot within the longest chain segment rotates all downstream residues by up to ±22°. This mimics a physical domain hinge motion.</li>\n  <li><b>Slot 3:</b> <b>chain jitter</b> — each chain segment is independently rotated by ±12° and translated by up to 1.5 Å, then the global centre of mass is restored. This simulates inter-chain conformational variability in multi-copy complexes.</li>\n  <li><b>Slot 4:</b> <b>smooth wiggle</b> — a smooth displacement field built from 6 interpolated random control points is applied to each chain, producing a global wave-like deformation.</li>\n</ul>\n</div>\n\n### 2.4 — Protenix Phase\n\n<div style=\"background:#f0fdf4; border-left:6px solid #16a34a; padding:16px 20px; border-radius:8px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#111111; line-height:1.6; margin-bottom:20px;\">\nProtenix only runs on sequences where TBM produced fewer than 5 predictions. It is asked to generate <b>exactly as many samples as are still missing</b> (<code style=\"background:#dcfce7; padding:2px 5px; border-radius:4px;\">N_sample = n_needed</code>), rather than always generating 5 and discarding extras. This conserves GPU time significantly.\n<br><br>\nSequences are truncated to <code style=\"background:#dcfce7; padding:2px 5px; border-radius:4px;\">MAX_SEQ_LEN</code> before being passed to Protenix. After inference, C1' coordinates are extracted from the raw all-atom output using the defensive masking logic described above. Extensive debug prints are retained in this section — added during development to diagnose coordinate extraction failures and detect model collapse (all residues converging to the same point).\n<br><br>\nIf Protenix fails for any sequence (exception, data error, or detected collapse), that target's slots remain empty and are filled in Phase 3.\n</div>\n\n### 2.5 — Combine and Save\n\n<div style=\"background:#fff1f2; border-left:6px solid #e11d48; padding:16px 20px; border-radius:8px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#111111; line-height:1.6;\">\nTBM predictions are taken first, Protenix predictions fill remaining slots in order, and any still-empty slots receive de novo helical structures. The final coordinates are clipped to <code style=\"background:#ffe4e6; padding:2px 5px; border-radius:4px;\">[-999.999, 9999.999]</code> to stay within the range the PDB format can represent — a requirement imposed by the competition's scoring metric.\n</div>","metadata":{"papermill":{"duration":0.002845,"end_time":"2026-03-02T08:03:23.536929","exception":false,"start_time":"2026-03-02T08:03:23.534084","status":"completed"},"tags":[]}},{"id":"1669cad3","cell_type":"code","source":"\n# ─────────────── Paths & Constants ───────────────────────────────────────────\nDATA_BASE              = \"/kaggle/input/stanford-rna-3d-folding-2\"\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\nDEFAULT_CODE_DIR = (\n    \"/kaggle/input/datasets/qiweiyin/protenix-v1-adjusted\"\n    \"/Protenix-v1-adjust-v2/Protenix-v1-adjust-v2/Protenix-v1\"\n)\nDEFAULT_ROOT_DIR = DEFAULT_CODE_DIR\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\"))\nCHUNK_OVERLAP = int(os.environ.get(\"CHUNK_OVERLAP\",  \"64\"))\n\n# TBM quality thresholds — sequences below these get routed to Protenix\nMIN_SIMILARITY       = float(os.environ.get(\"MIN_SIMILARITY\",       \"0.0\"))\nMIN_PERCENT_IDENTITY = float(os.environ.get(\"MIN_PERCENT_IDENTITY\", \"50.0\"))\n\n# Set False to skip Protenix and use de-novo fallback instead\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\"))\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[\"PYTHONHASHSEED\"] = str(seed)\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    # 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    # CASE A: center_atom_mask exists\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        \n    # CASE B: Use atom_name\n    if \"atom_name\" in f:\n        # Check against \"C1'\" (byte encoded or string?)\n        # For now assume typical behavior is center_atom_mask is present.\n        pass\n\n    # CASE C: atom_to_tokatom_idx fallback\n    # The index for C1' is typically 11 or 12 depending on featurizer.\n    # Let's try to match exactly C1' if possible.\n    # But usually 'centre_atom_mask' should be there.\n    \n    # If we fall through, assume standard mask\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    \"\"\"coords shape: (N_SAMPLE, seq_len, 3)\"\"\"\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\n# ─────────────── TBM Core Functions ──────────────────────────────────────────\ndef _make_aligner() -> PairwiseAligner:\n    al = PairwiseAligner()\n    al.mode                           = \"global\"\n    al.match_score                    = 2\n    al.mismatch_score                 = -1.5\n    al.open_gap_score                 = -8\n    al.extend_gap_score               = -0.4\n    al.query_left_open_gap_score      = -8\n    al.query_left_extend_gap_score    = -0.4\n    al.query_right_open_gap_score     = -8\n    al.query_right_extend_gap_score   = -0.4\n    al.target_left_open_gap_score     = -8\n    al.target_left_extend_gap_score   = -0.4\n    al.target_right_open_gap_score    = -8\n    al.target_right_extend_gap_score  = -0.4\n    return al\n\n\n_aligner = _make_aligner()\n\n\ndef parse_stoichiometry(stoich: str) -> list:\n    if pd.isna(stoich) or str(stoich).strip() == \"\":\n        return []\n    return [(ch.strip(), int(cnt)) for part in str(stoich).split(\";\")\n            for ch, cnt in [part.split(\":\")]]\n\n\ndef parse_fasta(fasta_content: str) -> dict:\n    out, cur, parts = {}, None, []\n    for line in str(fasta_content).splitlines():\n        line = line.strip()\n        if not line:\n            continue\n        if line.startswith(\">\"):\n            if cur is not None:\n                out[cur] = \"\".join(parts)\n            cur = line[1:].split()[0]\n            parts = []\n        else:\n            parts.append(line.replace(\" \", \"\"))\n    if cur is not None:\n        out[cur] = \"\".join(parts)\n    return out\n\n\ndef get_chain_segments(row) -> list:\n    seq    = row[\"sequence\"]\n    stoich = row.get(\"stoichiometry\", \"\")\n    all_sq = row.get(\"all_sequences\", \"\")\n    if (pd.isna(stoich) or pd.isna(all_sq)\n            or str(stoich).strip() == \"\" or str(all_sq).strip() == \"\"):\n        return [(0, len(seq))]\n    try:\n        chain_dict = parse_fasta(all_sq)\n        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:\n                return [(0, len(seq))]\n            for _ in range(cnt):\n                segs.append((pos, pos + len(base)))\n                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: pd.DataFrame) -> tuple:\n    seg_map, stoich_map = {}, {}\n    for _, r in df.iterrows():\n        tid               = r[\"target_id\"]\n        seg_map[tid]      = get_chain_segments(r)\n        raw_s             = r.get(\"stoichiometry\", \"\")\n        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: pd.DataFrame) -> dict:\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:\n            continue\n        if abs(len(tseq) - len(query_seq)) / max(len(tseq), len(query_seq)) > 0.3:\n            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(\n            1 for (qs, qe), (ts, te) in zip(*aln.aligned)\n            for qp, tp in zip(range(qs, qe), range(ts, te))\n            if query_seq[qp] == tseq[tp]\n        )\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) -> np.ndarray:\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):\n            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)\n                new_coords[i] = (1 - w) * new_coords[pv] + w * new_coords[nv]\n            elif pv >= 0:\n                new_coords[i] = new_coords[pv] + [3, 0, 0]\n            elif nv >= 0:\n                new_coords[i] = new_coords[nv] + [3, 0, 0]\n            else:\n                new_coords[i] = [i * 3, 0, 0]\n    return np.nan_to_num(new_coords)\n\n\ndef adaptive_rna_constraints(coords, target_id, segments_map, confidence=1.0, passes=2) -> np.ndarray:\n    X        = coords.copy()\n    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:\n                continue\n            # bond i–i+1  ~5.95 Å\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)\n            C[:-1] -= adj; C[1:] += adj\n            # soft i–i+2  ~10.2 Å\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)\n            C[:-2] -= adj2; C[2:] += adj2\n            # Laplacian smoothing\n            C[1:-1] += (0.06 * strength) * (0.5 * (C[:-2] + C[2:]) - C[1:-1])\n            # self-avoidance\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\n                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)\n        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: str, seed=None) -> np.ndarray:\n    \"\"\"Idealized A-form RNA helix — last-resort de-novo fallback.\"\"\"\n    if seed is not None:\n        np.random.seed(seed)\n    n = len(sequence); coords = np.zeros((n, 3))\n    for i in range(n):\n        ang = i * 0.6\n        coords[i] = [10.0 * np.cos(ang), 10.0 * np.sin(ang), i * 2.5]\n    return coords\n\n\n# ─────────────── TBM Phase ───────────────────────────────────────────────────\ndef tbm_phase(test_df, train_seqs_df, train_coords_dict, segments_map):\n    \"\"\"\n    Phase 1 — Template-Based Modeling.\n\n    Returns\n    -------\n    template_predictions : {target_id: [np.ndarray(seq_len, 3), ...]}\n        0 to N_SAMPLE predictions per target, from real templates.\n    protenix_queue : {target_id: (n_needed, full_sequence)}\n        Targets that still need more predictions.\n    \"\"\"\n    print(f\"\\n{'='*60}\")\n    print(f\"PHASE 1: Template-Based Modeling\")\n    print(f\"  MIN_SIMILARITY = {MIN_SIMILARITY}  |  MIN_PCT_IDENTITY = {MIN_PERCENT_IDENTITY}\")\n    print(f\"{'='*60}\")\n    t0 = time.time()\n\n    template_predictions: dict = {}\n    protenix_queue:       dict = {}\n\n    for _, row in test_df.iterrows():\n        tid = row[\"target_id\"]\n        seq = row[\"sequence\"]\n        segs = segments_map.get(tid, [(0, len(seq))])\n\n        similar = find_similar_sequences_detailed(seq, train_seqs_df, train_coords_dict, top_n=30)\n        preds   = []\n        used    = set()\n\n        for i, (tmpl_id, tmpl_seq, sim, tmpl_coords, pct_id, _, _) in enumerate(similar):\n            if len(preds) >= N_SAMPLE:\n                break\n            if sim < MIN_SIMILARITY or pct_id < MIN_PERCENT_IDENTITY:\n                break           # list is sorted by sim, so no point continuing\n            if tmpl_id in used:\n                continue\n\n            rng     = np.random.default_rng((abs(hash(tid)) + i * 10007) % (2**32))\n            adapted = adapt_template_to_query(seq, tmpl_seq, tmpl_coords)\n\n            # Diversity transforms (same strategy as the 0-409 TBM notebook)\n            slot = len(preds)\n            if slot == 0:\n                X = adapted\n            elif slot == 1:\n                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])\n                X = apply_hinge(adapted, longest, rng)\n            elif slot == 3:\n                X = jitter_chains(adapted, segs, rng)\n            else:\n                X = smooth_wiggle(adapted, segs, rng)\n\n            refined = adaptive_rna_constraints(X, tid, segments_map, confidence=sim)\n            preds.append(refined)\n            used.add(tmpl_id)\n\n        template_predictions[tid] = preds\n        n_needed = N_SAMPLE - len(preds)\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\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    print(f\"  Fully covered by TBM : {n_full}\")\n    print(f\"  Need Protenix        : {len(protenix_queue)}\")\n    return template_predictions, protenix_queue\n\n\n# ─────────────── Main ────────────────────────────────────────────────────────\ndef main() -> None:\n    test_csv, output_csv, code_dir, root_dir = resolve_paths()\n\n    if not os.path.isdir(code_dir):\n        raise FileNotFoundError(\n            f\"Missing PROTENIX_CODE_DIR: {code_dir}. \"\n            \"Set PROTENIX_CODE_DIR to the repo path.\"\n        )\n\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\n    # ── Load test data ──────────────────────────────────────────────────────\n    test_df_full = pd.read_csv(test_csv)\n    test_df      = (test_df_full.head(LOCAL_N_SAMPLES) if not IS_KAGGLE\n                    else test_df_full).reset_index(drop=True)\n    print(f\"Test targets : {len(test_df)}\"\n          + (\" (LOCAL MODE)\" if not IS_KAGGLE else \"\"))\n\n    seq_by_id = dict(zip(test_df[\"target_id\"], test_df[\"sequence\"]))\n\n    # Truncated copy for Protenix (Protenix has token limits)\n    test_df_trunc = test_df.copy()\n    test_df_trunc[\"sequence\"] = test_df_trunc[\"sequence\"].str[:MAX_SEQ_LEN]\n\n    # ── Load training data for TBM ──────────────────────────────────────────\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\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\n    print(f\"Template pool: {len(combined_seqs)} sequences, {len(train_coords)} structures\")\n\n    # ─── PHASE 1: TBM ──────────────────────────────────────────────────────\n    template_preds, protenix_queue = tbm_phase(\n        test_df, combined_seqs, train_coords, segments_map\n    )\n\n    # ─── PHASE 2: Protenix (only for targets that need extra predictions) ──\n    protenix_preds: dict = {}   # target_id -> np.ndarray (n_needed, seq_len, 3)\n\n    if protenix_queue and USE_PROTENIX:\n        print(f\"\\n{'='*60}\")\n        print(f\"PHASE 2: Protenix for {len(protenix_queue)} targets\")\n        print(f\"{'='*60}\")\n\n        work_dir = Path(\"/kaggle/working\")\n        work_dir.mkdir(parents=True, exist_ok=True)\n\n        # Build input JSON only for queued targets\n        queue_df = (test_df_trunc[test_df_trunc[\"target_id\"].isin(protenix_queue)]\n                    .reset_index(drop=True))\n        input_json_path = str(work_dir / \"protenix_queue_input.json\")\n        build_input_json(queue_df, input_json_path)\n\n        from protenix.data.inference.infer_dataloader import InferenceDataset\n        from runner.inference import (InferenceRunner,\n                                      update_gpu_compatible_configs,\n                                      update_inference_configs)\n\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\n        for i in tqdm(range(len(dataset)), desc=\"Protenix\"):\n            data, atom_array, error_message = dataset[i]\n            target_id = data.get(\"sample_name\", f\"sample_{i}\")\n\n            if target_id not in protenix_queue:\n                continue\n\n            n_needed, full_seq = protenix_queue[target_id]\n\n            if error_message:\n                print(f\"  {target_id}: data error — {error_message}\")\n                protenix_preds[target_id] = None\n                del data, atom_array, error_message\n                gc.collect(); torch.cuda.empty_cache(); gc.collect()\n                continue\n\n            try:\n                new_cfg = update_inference_configs(configs, data[\"N_token\"].item())\n                # Only generate as many samples as we actually need to fill the slots\n                new_cfg.sample_diffusion.N_sample = n_needed\n                runner.update_model_configs(new_cfg)\n\n                prediction = runner.predict(data)\n                raw_coords = prediction[\"coordinate\"] # Shape: [N_sample, all_atoms, 3]\n\n                # -----------------------------------------------------------\n                # DEBUG PRINT START\n                # -----------------------------------------------------------\n                print(f\"\\n[DEBUG] {target_id} | n_needed: {n_needed} | SeqLen: {len(full_seq)}\")\n                print(f\"[DEBUG] raw_coords shape: {raw_coords.shape}\")\n                \n                feat = data[\"input_feature_dict\"]\n                \n                # Check potential masks\n                mask_candidates = {}\n                if \"centre_atom_mask\" in feat:\n                    m = feat[\"centre_atom_mask\"]\n                    mask_candidates['centre_atom_mask'] = (m.sum().item(), m.shape)\n                \n                if \"atom_to_tokatom_idx\" in feat:\n                    idx_11 = (feat[\"atom_to_tokatom_idx\"] == 11).sum().item()\n                    idx_12 = (feat[\"atom_to_tokatom_idx\"] == 12).sum().item()\n                    mask_candidates['idx_11'] = idx_11\n                    mask_candidates['idx_12'] = idx_12\n                \n                print(f\"[DEBUG] Mask candidates counts: {mask_candidates}\")\n                # -----------------------------------------------------------\n                # DEBUG PRINT END\n                # -----------------------------------------------------------\n\n                # ─────────────────────────────────────────────────────────────\n                # DEBUG / FIX: Explicit C1' masking logic\n                # ─────────────────────────────────────────────────────────────\n                # Try to use 'centre_atom_mask' from features if possible\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                    # Heuristic: pick the one closest to sequence length\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                    \n                    c11, c12 = m11.sum(), m12.sum()\n                    target_len = len(full_seq) # closer to N_token usually\n                    \n                    if abs(c11 - target_len) < abs(c12 - target_len):\n                         mask = m11\n                         print(f\"[DEBUG] Selected idx 11 mask (count={c11})\")\n                    else:\n                         mask = m12\n                         print(f\"[DEBUG] Selected idx 12 mask (count={c12})\")\n                else:\n                    # Should not happen\n                    mask = torch.zeros(raw_coords.shape[1], dtype=torch.bool, device=raw_coords.device)\n                \n                # Extract\n                coords = raw_coords[:, mask, :].detach().cpu().numpy()\n                print(f\"[DEBUG] Extracted coords shape: {coords.shape}\")\n\n                # If we get duplicate coordinates (collapsed), this is bad.\n                # Check for duplications in first sample\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):\n                         print(f\"  WARNING: {target_id} has identical coordinates for all residues! (Model collapse?)\")\n                \n                # Pad/trim to full (un-truncated) sequence length\n                if coords.shape[1] != len(full_seq):\n                    # Check for broadcast issue or model collapse\n                    if coords.shape[1] == 1 and len(full_seq) > 1:\n                        # Model outputted only 1 residue/atom but we need many?\n                        # Broadcast the single coord to all positions just in case (though highly suspicious)\n                        # Or perhaps mask was wrong and selected only 1 atom.\n                        # Do NOT broadcast, fill with zeros to be safe.\n                        print(f\"[DEBUG] WARNING: {target_id}: mask selected only 1 atom, but sequence is {len(full_seq)}\")\n                        # padded = np.zeros(...) -> kept as zeros\n                    else:\n                        padded  = np.zeros((coords.shape[0], len(full_seq), 3), dtype=np.float32)\n                        min_len = min(coords.shape[1], len(full_seq))\n                        if min_len > 0:\n                            padded[:, :min_len, :] = coords[:, :min_len, :]\n                        coords = padded\n\n                # Final check for identical coordinates (indicative of model failure)\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):\n                         print(f\"  WARNING: {target_id}: Identical coordinates detected! Resetting to zeros.\")\n                         coords = np.zeros_like(coords)\n\n                protenix_preds[target_id] = coords\n                print(f\"  {target_id}: {coords.shape[0]} Protenix predictions generated\")\n\n            except Exception as exc:\n                print(f\"  {target_id}: Protenix FAILED — {exc}\")\n                import traceback\n                traceback.print_exc()\n                protenix_preds[target_id] = None\n\n            finally:\n                del prediction, raw_coords, mask, data, atom_array\n                gc.collect(); torch.cuda.empty_cache(); gc.collect()\n# ...existing code...\n\n    elif protenix_queue and not USE_PROTENIX:\n        print(f\"\\nPHASE 2 skipped (USE_PROTENIX=False). \"\n              f\"De-novo fallback will cover {len(protenix_queue)} targets.\")\n\n    # ─── PHASE 3: Combine everything ───────────────────────────────────────\n    print(f\"\\n{'='*60}\")\n    print(\"PHASE 3: Combine TBM + Protenix + de-novo fallback\")\n    print(f\"{'='*60}\")\n\n    all_rows = []\n\n    for _, row in test_df.iterrows():\n        tid = row[\"target_id\"]\n        seq = row[\"sequence\"]\n\n        combined: list = list(template_preds.get(tid, []))  # TBM predictions\n\n        # Append Protenix predictions to fill remaining slots\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:\n                    break\n                combined.append(ptx[j])  # (seq_len, 3)\n\n        # De-novo fallback for any still-empty slots\n        n_denovo = 0\n        while len(combined) < N_SAMPLE:\n            seed_val = hash(tid) % 10000 + 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            n_denovo += 1\n\n        if n_denovo:\n            print(f\"  {tid}: {n_denovo} slot(s) filled with de-novo fallback\")\n\n        # Stack to (N_SAMPLE, seq_len, 3) and write rows\n        stacked = np.stack(combined[:N_SAMPLE], axis=0)\n        all_rows.extend(coords_to_rows(tid, seq, stacked))\n\n    # ── Save ───────────────────────────────────────────────────────────────\n    sub = pd.DataFrame(all_rows)\n    cols = [\"ID\", \"resname\", \"resid\"] + [\n        f\"{c}_{i}\" for i in range(1, N_SAMPLE + 1) for c in [\"x\", \"y\", \"z\"]\n    ]\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\n    print(f\"\\n✓ Saved submission to {output_csv}  ({len(sub):,} rows)\")\n\n","metadata":{"execution":{"iopub.status.busy":"2026-03-16T04:34:51.248137Z","iopub.status.idle":"2026-03-16T04:34:51.248375Z","shell.execute_reply.started":"2026-03-16T04:34:51.248267Z","shell.execute_reply":"2026-03-16T04:34:51.248281Z"},"papermill":{"duration":0.08427,"end_time":"2026-03-02T08:03:23.624011","exception":false,"start_time":"2026-03-02T08:03:23.539741","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"a4e1eb12","cell_type":"markdown","source":"## Part 3 — Entry Point\n\n<div style=\"background:#f8fafc; border-left:6px solid #475569; padding:16px 20px; border-radius:8px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#111111; line-height:1.6;\">\nThe <code style=\"background:#e2e8f0; padding:2px 6px; border-radius:4px;\">main()</code> function defined above is called here. The <code style=\"background:#e2e8f0; padding:2px 6px; border-radius:4px;\">if __name__ == \"__main__\"</code> guard is preserved for compatibility with running the file as a standard Python script. When run as a Kaggle notebook cell, the guard evaluates to <code style=\"background:#e2e8f0; padding:2px 6px; border-radius:4px;\">True</code> and <code style=\"background:#e2e8f0; padding:2px 6px; border-radius:4px;\">main()</code> executes immediately.\n</div>","metadata":{"papermill":{"duration":0.002945,"end_time":"2026-03-02T08:03:23.630081","exception":false,"start_time":"2026-03-02T08:03:23.627136","status":"completed"},"tags":[]}},{"id":"9fa592b8","cell_type":"code","source":"\nif __name__ == \"__main__\":\n    main()\n","metadata":{"execution":{"iopub.status.busy":"2026-03-16T04:34:51.249692Z","iopub.status.idle":"2026-03-16T04:34:51.250007Z","shell.execute_reply.started":"2026-03-16T04:34:51.249836Z","shell.execute_reply":"2026-03-16T04:34:51.249856Z"},"papermill":{"duration":154.940035,"end_time":"2026-03-02T08:05:58.573765","exception":false,"start_time":"2026-03-02T08:03:23.63373","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"8160ed2f","cell_type":"markdown","source":"## Part 4 — Verify Output\n\n<div style=\"background:#f0fdf4; border-left:6px solid #16a34a; padding:16px 20px; border-radius:8px; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#111111; line-height:1.6;\">\n<b style=\"color:#14532d; font-size:15px;\">Sanity check</b><br><br>\nA quick check to confirm the submission file was written correctly and has the expected structure. Each row represents one nucleotide of one RNA sequence, with columns for the 5 predicted (x, y, z) coordinate sets.\n</div>","metadata":{"papermill":{"duration":0.003926,"end_time":"2026-03-02T08:05:58.581703","exception":false,"start_time":"2026-03-02T08:05:58.577777","status":"completed"},"tags":[]}},{"id":"d03d491a","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":{"execution":{"iopub.status.busy":"2026-03-16T04:34:51.251142Z","iopub.status.idle":"2026-03-16T04:34:51.25144Z","shell.execute_reply.started":"2026-03-16T04:34:51.251274Z","shell.execute_reply":"2026-03-16T04:34:51.251297Z"},"papermill":{"duration":0.023399,"end_time":"2026-03-02T08:05:58.608796","exception":false,"start_time":"2026-03-02T08:05:58.585397","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null}]}