{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"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":"none","dataSources":[{"sourceId":118765,"databundleVersionId":15231210,"sourceType":"competition"},{"sourceId":14604295,"sourceType":"datasetVersion","datasetId":9328538}],"dockerImageVersionId":31260,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false},"papermill":{"default_parameters":{},"duration":2354.045049,"end_time":"2026-01-13T06:54:44.524542","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-01-13T06:15:30.479493","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# *Stanford RNA 3D Parse_fasta Meta Data***","metadata":{}},{"cell_type":"code","source":"!pip install --no-index /kaggle/input/biopython-cp312/biopython-1.86-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-15T21:00:10.108893Z","iopub.execute_input":"2026-02-15T21:00:10.109178Z","iopub.status.idle":"2026-02-15T21:00:15.199364Z","shell.execute_reply.started":"2026-02-15T21:00:10.109153Z","shell.execute_reply":"2026-02-15T21:00:15.198352Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport random\nimport time\nimport warnings\nimport os, sys\n\nwarnings.filterwarnings('ignore')\n\nDATA_PATH = '/kaggle/input/stanford-rna-3d-folding-2/'\ntrain_seqs = pd.read_csv(DATA_PATH + 'train_sequences.csv')\ntest_seqs  = pd.read_csv(DATA_PATH + 'test_sequences.csv')\ntrain_labels = pd.read_csv(DATA_PATH + 'train_labels.csv')\n\n# ============================================================\n# Write patched helper scripts into /kaggle/working/extra\n# (Kaggle input is read-only; working is writable)\n# ============================================================\nINPUT_EXTRA = os.path.join(DATA_PATH, \"extra\")\nWORK_EXTRA  = \"/kaggle/working/extra\"\nos.makedirs(WORK_EXTRA, exist_ok=True)\n\n# Ensure our patched modules win import resolution\nif WORK_EXTRA not in sys.path:\n    sys.path.insert(0, WORK_EXTRA)\nif INPUT_EXTRA not in sys.path:\n    sys.path.append(INPUT_EXTRA)\n\n# ------------------------------------------------------------\n# 1) extra/parse_fasta_py.py  (PATCHED)\n# ------------------------------------------------------------\nparse_fasta_py_path = os.path.join(WORK_EXTRA, \"parse_fasta_py.py\")\nwith open(parse_fasta_py_path, \"w\", encoding=\"utf-8\") as f:\n    f.write(\nr'''\"\"\"\nRobust FASTA parser for Stanford RNA 3D Folding Part 2 sequences metadata.\n\nPrimary use-case:\n  - Parse the `all_sequences` field from {train/test/validation}_sequences.csv\n  - Return: dict[str, str] mapping chain_id -> sequence\n\nNotes:\n  - Handles multi-line FASTA sequences\n  - Handles literal '\\\\n' encodings (common when FASTA is stored as a CSV cell)\n  - Tries to infer chain ID from header tokens; default behavior matches competition convention:\n      chain_id := last token after '_' in the first header token\n    e.g. header token \"7ABC_A\" -> chain_id \"A\"\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Dict, Optional, Iterable, Union\n\n\ndef _normalize_fasta_text(fasta_content: Union[str, bytes, Iterable[str]]) -> str:\n    if fasta_content is None:\n        return \"\"\n    if isinstance(fasta_content, bytes):\n        text = fasta_content.decode(\"utf-8\", \"ignore\")\n    elif isinstance(fasta_content, str):\n        text = fasta_content\n    else:\n        # Iterable of lines\n        try:\n            text = \"\\n\".join([str(x) for x in fasta_content])\n        except Exception:\n            text = str(fasta_content)\n\n    text = text.replace(\"\\r\\n\", \"\\n\").replace(\"\\r\", \"\\n\")\n    # If CSV stored FASTA uses literal backslash-n sequences instead of real newlines\n    if (\"\\n\" not in text) and (\"\\\\n\" in text):\n        text = text.replace(\"\\\\n\", \"\\n\")\n    return text\n\n\ndef _extract_chain_id(header: str) -> str:\n    \"\"\"\n    Heuristic chain-id extractor for competition FASTA headers.\n\n    We primarily use the *first* token, then:\n      - strip any pipe section\n      - if token contains '_' => chain_id is token.split('_')[-1]\n      - else chain_id is token\n    \"\"\"\n    h = (header or \"\").strip()\n    if not h:\n        return \"\"\n    tok0 = h.split()[0].strip().strip(\":,;\")\n\n    # Handle common pipe formats, e.g.:\n    #   pdb|7ABC|A   -> chain \"A\"\n    #   7ABC|A       -> chain \"A\"\n    if \"|\" in tok0:\n        parts = [p for p in tok0.split(\"|\") if p]\n        # heuristic: last part is usually chain id\n        cand = parts[-1].strip(\":,;\")\n        if cand:\n            tok0 = cand\n\n    # Handle \"PDBID_CHAIN\" formats: 7ABC_A -> chain \"A\"\n    if \"_\" in tok0:\n        tok0 = tok0.split(\"_\")[-1].strip()\n\n    return tok0.strip(\":,;\")\n\ndef parse_fasta(\n    fasta_content: Union[str, bytes, Iterable[str]],\n    *,\n    keep_full_header: bool = False,\n    chain_id_fn=None,\n) -> Dict[str, str]:\n    \"\"\"\n    Parse a FASTA string (or bytes / iterable of lines) and return {chain_id: sequence}.\n    \"\"\"\n    text = _normalize_fasta_text(fasta_content)\n    if not text:\n        return {}\n\n    out: Dict[str, str] = {}\n    cur_key: Optional[str] = None\n    seq_parts = []\n\n    for raw in text.splitlines():\n        line = (raw or \"\").strip()\n        if not line:\n            continue\n        if line.startswith(\">\"):\n            if cur_key is not None:\n                out[cur_key] = \"\".join(seq_parts).replace(\" \", \"\").upper()\n            header = line[1:].strip()\n            if keep_full_header:\n                cur_key = header\n            else:\n                if chain_id_fn is not None:\n                    try:\n                        cur_key = str(chain_id_fn(header)).strip()\n                    except Exception:\n                        cur_key = _extract_chain_id(header)\n                else:\n                    cur_key = _extract_chain_id(header)\n            seq_parts = []\n        else:\n            seq_parts.append(line.replace(\" \", \"\"))\n\n    if cur_key is not None:\n        out[cur_key] = \"\".join(seq_parts).replace(\" \", \"\").upper()\n\n    return out\n'''\n    )\n\n# ------------------------------------------------------------\n# 2) extra/build_rna_metadata.py  (PATCHED)\n# ------------------------------------------------------------\nbuild_meta_path = os.path.join(WORK_EXTRA, \"build_rna_metadata.py\")\nwith open(build_meta_path, \"w\", encoding=\"utf-8\") as f:\n    f.write(\nr'''#!/usr/bin/env python3\n\"\"\"\nBuild rna_metadata.csv from PDB nucleic-acid seqres FASTA + release dates CSV.\n\nOutputs (minimum useful schema):\n  pdb_id, chain_id, pdb_chain_id, polymer_guess, length, gc_frac, release_date, sequence\n\nUsage:\n  python build_rna_metadata.py --data-root /kaggle/input/stanford-rna-3d-folding-2 --out /kaggle/working/extra/rna_metadata.csv\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport argparse\nimport pandas as pd\n\n\ndef iter_fasta_records(path: str):\n    header = None\n    seq_parts = []\n    with open(path, \"r\", encoding=\"utf-8\", errors=\"ignore\") as f:\n        for line in f:\n            line = line.strip()\n            if not line:\n                continue\n            if line.startswith(\">\"):\n                if header is not None:\n                    yield header, \"\".join(seq_parts).replace(\" \", \"\").upper()\n                header = line[1:].strip()\n                seq_parts = []\n            else:\n                seq_parts.append(line)\n        if header is not None:\n            yield header, \"\".join(seq_parts).replace(\" \", \"\").upper()\n\n\ndef guess_polymer(seq: str) -> str:\n    has_u = \"U\" in seq\n    has_t = \"T\" in seq\n    if has_u and has_t:\n        return \"HYBRID\"\n    if has_t and (not has_u):\n        return \"DNA\"\n    return \"RNA\"\n\n\ndef extract_pdb_chain(header: str):\n    tok0 = header.split()[0]\n    tok0 = tok0.split(\"|\")[0]\n    tok0 = tok0.strip(\":,;\")\n    # expected \"PDBID_CHAIN\"\n    if \"_\" in tok0 and len(tok0.split(\"_\")[0]) >= 4:\n        pdb = tok0.split(\"_\")[0][:4].upper()\n        ch = tok0.split(\"_\")[-1]\n        return pdb, ch, f\"{pdb}_{ch}\"\n    # fallback\n    pdb = tok0[:4].upper()\n    ch = tok0[4:].strip() or \"?\"\n    return pdb, ch, f\"{pdb}_{ch}\"\n\n\ndef load_release_dates(path: str) -> dict:\n    df = pd.read_csv(path)\n    cols = {c.lower(): c for c in df.columns}\n    id_col = cols.get(\"entry_id\") or cols.get(\"pdb_id\") or cols.get(\"pdb\") or list(df.columns)[0]\n    date_col = None\n    for k in [\"release_date\", \"released\", \"release\", \"date\"]:\n        if k in cols:\n            date_col = cols[k]\n            break\n    if date_col is None:\n        date_col = list(df.columns)[1] if len(df.columns) > 1 else list(df.columns)[0]\n    df[id_col] = df[id_col].astype(str).str.upper().str.strip()\n    rel = pd.to_datetime(df[date_col], errors=\"coerce\")\n    return dict(zip(df[id_col].values, rel.dt.strftime(\"%Y-%m-%d\").fillna(\"\").values))\n\n\ndef main():\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"--data-root\", required=True, help=\"Competition dataset root (contains pdb_seqres_NA.fasta etc.)\")\n    ap.add_argument(\"--out\", default=None, help=\"Output path for rna_metadata.csv\")\n    args = ap.parse_args()\n\n    data_root = args.data_root\n    out_path = args.out or os.path.join(data_root, \"extra\", \"rna_metadata.csv\")\n\n    fasta_path = os.path.join(data_root, \"pdb_seqres_NA.fasta\")\n    rel_path   = os.path.join(data_root, \"pdb_release_dates_NA.csv\")\n\n    if not os.path.exists(fasta_path):\n        raise FileNotFoundError(f\"Missing FASTA: {fasta_path}\")\n    if not os.path.exists(rel_path):\n        raise FileNotFoundError(f\"Missing release dates CSV: {rel_path}\")\n\n    release_map = load_release_dates(rel_path)\n\n    rows = []\n    for header, seq in iter_fasta_records(fasta_path):\n        pdb_id, chain_id, pdb_chain_id = extract_pdb_chain(header)\n        L = len(seq)\n        if L == 0:\n            continue\n        gc = (seq.count(\"G\") + seq.count(\"C\")) / max(1, L)\n        rows.append({\n            \"pdb_id\": pdb_id,\n            \"chain_id\": chain_id,\n            \"pdb_chain_id\": pdb_chain_id,\n            \"polymer_guess\": guess_polymer(seq),\n            \"length\": L,\n            \"gc_frac\": float(gc),\n            \"release_date\": release_map.get(pdb_id, \"\"),\n            \"sequence\": seq,\n        })\n\n    df = pd.DataFrame(rows)\n    os.makedirs(os.path.dirname(out_path), exist_ok=True)\n    df.to_csv(out_path, index=False)\n    print(f\"[OK] wrote {out_path} ({len(df)} rows)\")\n\n\nif __name__ == \"__main__\":\n    main()\n'''\n    )\n\n# ------------------------------------------------------------\n# 3) extra/README.md  (PATCHED)\n# ------------------------------------------------------------\nreadme_path = os.path.join(WORK_EXTRA, \"README.md\")\nwith open(readme_path, \"w\", encoding=\"utf-8\") as f:\n    f.write(\n\"\"\"# extra/rna_metadata.csv\n\nThe file `extra/rna_metadata.csv` contains metadata for nucleic-acid polymer chains (RNA and RNA/DNA hybrids)\nextracted from PDB entries included in the competition’s nucleic-acid corpus (up to Dec 17, 2025).\n\nThis metadata is primarily useful for:\n- Temporal cutoff filtering: avoid selecting template structures released after a target’s temporal_cutoff.\n- Quick template priors (e.g., prefer chains of similar length / polymer type).\n\n## Minimal schema (guaranteed by build_rna_metadata.py)\n\n- pdb_id: 4-character PDB accession (upper-case)\n- chain_id: chain identifier in the PDB entry\n- pdb_chain_id: convenience key \"{pdb_id}_{chain_id}\"\n- polymer_guess: heuristic label from alphabet: RNA, DNA, HYBRID\n- length: SEQRES length\n- gc_frac: fraction of G/C\n- release_date: PDB entry release date (YYYY-MM-DD), from pdb_release_dates_NA.csv\n- sequence: SEQRES sequence (uppercase)\n\n## How to (re)generate\n\npython build_rna_metadata.py --data-root /kaggle/input/stanford-rna-3d-folding-2 --out /kaggle/working/extra/rna_metadata.csv\n\"\"\"\n    )\n\n# ============================================================\n# Import patched parse_fasta (NO try/except block)\n# ============================================================\nfrom parse_fasta_py import parse_fasta\n\n# ============================================================\n# RNA metadata: load from input if present else build minimal\n# ============================================================\nimport subprocess\n\nRNA_META_INPUT = os.path.join(INPUT_EXTRA, \"rna_metadata.csv\")\nRNA_META_WORK  = os.path.join(WORK_EXTRA, \"rna_metadata.csv\")\n\nif os.path.exists(RNA_META_INPUT):\n    RNA_META_PATH = RNA_META_INPUT\nelse:\n    RNA_META_PATH = RNA_META_WORK\n    if not os.path.exists(RNA_META_PATH):\n        try:\n            subprocess.check_call([\n                sys.executable,\n                build_meta_path,\n                \"--data-root\", DATA_PATH,\n                \"--out\", RNA_META_PATH\n            ])\n        except Exception as e:\n            print(\"Warning: could not build rna_metadata.csv:\", repr(e))\n\nrelease_by_pdb = {}\nif os.path.exists(RNA_META_PATH):\n    try:\n        rna_meta = pd.read_csv(RNA_META_PATH)\n        cols = {c.lower(): c for c in rna_meta.columns}\n        pdb_col  = cols.get(\"pdb_id\") or cols.get(\"entry_id\") or cols.get(\"pdb\")\n        date_col = cols.get(\"release_date\") or cols.get(\"released\") or cols.get(\"release\") or cols.get(\"date\")\n        if pdb_col and date_col:\n            tmp = rna_meta[[pdb_col, date_col]].copy()\n            tmp[pdb_col]  = tmp[pdb_col].astype(str).str.upper().str.strip()\n            tmp[date_col] = pd.to_datetime(tmp[date_col], errors=\"coerce\")\n            release_by_pdb = dict(zip(tmp[pdb_col].values, tmp[date_col].values))\n    except Exception as e:\n        print(\"Warning: could not load rna_metadata.csv:\", repr(e))\n        release_by_pdb = {}\n\ndef target_id_to_pdb_id(tid: str) -> str:\n    return str(tid).split(\"_\", 1)[0].upper().strip()\n\n# ============================================================\n# Your existing code (unchanged except: no try/except parser)\n# ============================================================\ndef parse_stoichiometry(stoich: str):\n    if pd.isna(stoich) or str(stoich).strip() == \"\":\n        return []\n    out = []\n    for part in str(stoich).split(';'):\n        ch, cnt = part.split(':')\n        out.append((ch.strip(), int(cnt)))\n    return out\n\ndef get_chain_segments(row):\n    seq = row['sequence']\n    stoich = row.get('stoichiometry', '')\n    all_seq = row.get('all_sequences', '')\n\n    if pd.isna(stoich) or pd.isna(all_seq) or str(stoich).strip()==\"\" or str(all_seq).strip()==\"\":\n        return [(0, len(seq))]\n\n    try:\n        chain_dict = parse_fasta(all_seq)\n        order = parse_stoichiometry(stoich)\n        segs = []\n        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                L = len(base)\n                segs.append((pos, pos + L))\n                pos += L\n        if pos != len(seq):\n            return [(0, len(seq))]\n        return segs\n    except Exception:\n        return [(0, len(seq))]\n\ndef build_segments_map(df):\n    seg_map = {}\n    stoich_map = {}\n    for _, r in df.iterrows():\n        tid = r['target_id']\n        seg_map[tid] = get_chain_segments(r)\n        stoich_map[tid] = str(r.get('stoichiometry', '') if not pd.isna(r.get('stoichiometry', '')) else '')\n    return seg_map, stoich_map\n\ntrain_segs_map, train_stoich_map = build_segments_map(train_seqs)\ntest_segs_map,  test_stoich_map  = build_segments_map(test_seqs)\n\ndef process_labels(labels_df):\n    coords_dict = {}\n    prefixes = labels_df['ID'].str.rsplit('_', n=1).str[0]\n    for id_prefix, group in labels_df.groupby(prefixes):\n        coords_dict[id_prefix] = group.sort_values('resid')[['x_1', 'y_1', 'z_1']].values\n    return coords_dict\n\ntrain_coords_dict = process_labels(train_labels)\n\nfrom Bio.Align import PairwiseAligner\n\naligner = PairwiseAligner()\naligner.mode = 'global'\naligner.match_score = 2\naligner.mismatch_score = -1.5\n\naligner.open_gap_score   = -8\naligner.extend_gap_score = -0.4\n\naligner.query_left_open_gap_score  = -8\naligner.query_left_extend_gap_score = -0.4\naligner.query_right_open_gap_score = -8\naligner.query_right_extend_gap_score = -0.4\naligner.target_left_open_gap_score = -8\naligner.target_left_extend_gap_score = -0.4\naligner.target_right_open_gap_score = -8\naligner.target_right_extend_gap_score = -0.4\n\ndef find_similar_sequences(query_seq, train_seqs_df, train_coords_dict, top_n=5):\n    \"\"\"\n    Returns top-N (target_id, train_seq, normalized_score, coords) templates.\n\n    Adds two safety filters:\n      1) Temporal cutoff (if find_similar_sequences._cutoff is set and release_by_pdb available)\n      2) Multichain signature compatibility: match #segments and roughly match segment lengths\n         using train_segs_map/test_segs_map (cheap but high-value)\n    \"\"\"\n    similar_seqs = []\n\n    cutoff = getattr(find_similar_sequences, \"_cutoff\", None)\n    q_tid = getattr(find_similar_sequences, \"_tid\", \"\")\n    q_segs = test_segs_map.get(q_tid, None)\n\n    # Pre-filter: iterate only valid train targets\n    # Note: aligner.score is much faster than generating full alignments\n    for _, row in train_seqs_df.iterrows():\n        target_id = row[\"target_id\"]\n        train_seq = row[\"sequence\"]\n\n        if target_id not in train_coords_dict:\n            continue\n\n        # ------------------------------------------------------------\n        # (A) Temporal cutoff enforcement (if available)\n        # ------------------------------------------------------------\n        if cutoff is not None and release_by_pdb:\n            pdb = target_id_to_pdb_id(target_id)\n            rel = release_by_pdb.get(pdb, None)\n            if rel is not None and pd.notna(rel) and rel > cutoff:\n                continue\n\n        # ------------------------------------------------------------\n        # (B) Multichain signature compatibility (segments)\n        # ------------------------------------------------------------\n        t_segs = train_segs_map.get(target_id, None)\n        if (q_segs is not None) and (t_segs is not None):\n            if len(t_segs) != len(q_segs):\n                continue\n\n            # segment-length mismatch threshold (relative)\n            q_lens = np.array([e - s for (s, e) in q_segs], dtype=float)\n            t_lens = np.array([e - s for (s, e) in t_segs], dtype=float)\n\n            # guard against weird zeros (shouldn't happen)\n            denom = np.maximum(q_lens, 1.0)\n            rel_diff = np.abs(q_lens - t_lens) / denom\n            if float(np.max(rel_diff)) > 0.25:\n                continue\n        # ------------------------------------------------------------\n        # (C) Length filter (your existing logic)\n        # ------------------------------------------------------------\n        if abs(len(train_seq) - len(query_seq)) / max(len(train_seq), len(query_seq)) > 0.3:\n            continue\n\n        # ------------------------------------------------------------\n        # (D) Fast alignment score\n        # ------------------------------------------------------------\n        raw_score = aligner.score(query_seq, train_seq)\n        normalized_score = raw_score / (2 * min(len(query_seq), len(train_seq)))\n        similar_seqs.append((target_id, train_seq, normalized_score, train_coords_dict[target_id]))\n\n    similar_seqs.sort(key=lambda x: x[2], reverse=True)\n    return similar_seqs[:top_n]\n\ndef adapt_template_to_query(query_seq, template_seq, template_coords):\n    alignment = next(iter(aligner.align(query_seq, template_seq)))\n    new_coords = np.full((len(query_seq), 3), np.nan)\n\n    for (q_start, q_end), (t_start, t_end) in zip(*alignment.aligned):\n        t_chunk = template_coords[t_start:t_end]\n        if len(t_chunk) == (q_end - q_start):\n            new_coords[q_start:q_end] = t_chunk\n\n    for i in range(len(new_coords)):\n        if np.isnan(new_coords[i, 0]):\n            prev_v = next((j for j in range(i-1, -1, -1) if not np.isnan(new_coords[j, 0])), -1)\n            next_v = next((j for j in range(i+1, len(new_coords)) if not np.isnan(new_coords[j, 0])), -1)\n            if prev_v >= 0 and next_v >= 0:\n                w = (i - prev_v) / (next_v - prev_v)\n                new_coords[i] = (1-w)*new_coords[prev_v] + w*new_coords[next_v]\n            elif prev_v >= 0:\n                new_coords[i] = new_coords[prev_v] + [3, 0, 0]\n            elif next_v >= 0:\n                new_coords[i] = new_coords[next_v] + [3, 0, 0]\n            else:\n                new_coords[i] = [i*3, 0, 0]\n\n    return np.nan_to_num(new_coords)\n\ndef adaptive_rna_constraints(coordinates, target_id, confidence=1.0, passes=2):\n    \"\"\"\n    Evaluation-driven constraints:\n    - US-align is rigid-body only => internal geometry errors are fatal\n    - Apply constraints within each chain segment (no fake bonds across chain breaks)\n    - NEW: weak inter-chain anchoring to prevent chain drift in multimers\n    \"\"\"\n    coords = coordinates.copy()\n    segments = test_segs_map.get(target_id, [(0, len(coords))])\n\n    # stronger corrections when confidence is low\n    strength = 0.75 * (1.0 - min(confidence, 0.90))\n    strength = max(strength, 0.02)\n\n    for _ in range(passes):\n        # Capture per-pass reference centroids BEFORE we modify chains\n        ref_cents = None\n        if len(segments) >= 2:\n            ref_cents = [coords[s:e].mean(axis=0) for (s, e) in segments]\n\n        # -----------------------------\n        # Per-chain constraints\n        # -----------------------------\n        for (s, e) in segments:\n            X = coords[s:e]\n            L = e - s\n            if L < 3:\n                coords[s:e] = X\n                continue\n\n            # (1) bond i,i+1 to ~5.95Å (vectorized, symmetric)\n            d = X[1:] - X[:-1]\n            dist = np.linalg.norm(d, axis=1) + 1e-6\n            target = 5.95\n            scale = (target - dist) / dist\n            adj = (d * scale[:, None]) * (0.22 * strength)\n            X[:-1] -= adj\n            X[1:]  += adj\n\n            # (2) soft i,i+2 to ~10.2Å (vectorized, symmetric)\n            d2 = X[2:] - X[:-2]\n            dist2 = np.linalg.norm(d2, axis=1) + 1e-6\n            target2 = 10.2\n            scale2 = (target2 - dist2) / dist2\n            adj2 = (d2 * scale2[:, None]) * (0.10 * strength)\n            X[:-2] -= adj2\n            X[2:]  += adj2\n\n            # (3) Laplacian smoothing (removes kinks US-align cannot fix)\n            lap = 0.5 * (X[:-2] + X[2:]) - X[1:-1]\n            X[1:-1] += (0.06 * strength) * lap\n\n            # (4) light self-avoidance (prevents steric collapse)\n            if L >= 25:\n                k = min(L, 160) if L > 220 else L\n                if k < L:\n                    idx = np.linspace(0, L - 1, k).astype(int)\n                else:\n                    idx = np.arange(L)\n\n                P = X[idx]\n                diff = P[:, None, :] - P[None, :, :]\n                distm = np.linalg.norm(diff, axis=2) + 1e-6\n                sep = np.abs(idx[:, None] - idx[None, :])\n\n                mask = (sep > 2) & (distm < 3.2)\n                if np.any(mask):\n                    force = (3.2 - distm) / distm\n                    vec = (diff * force[:, :, None] * mask[:, :, None]).sum(axis=1)\n                    X[idx] += (0.015 * strength) * vec\n\n            coords[s:e] = X\n\n        # ------------------------------------------------\n        # NEW: weak inter-chain anchoring (prevents drift)\n        # ------------------------------------------------\n        if len(segments) >= 2 and ref_cents is not None:\n            # very light: keeps relative chain placement similar to pass-start\n            anchor_w = 0.02 * strength\n\n            # Use neighbor offsets from reference centroids, but compute c0/c1 after refinement\n            for k in range(1, len(segments)):\n                (s0, e0) = segments[k - 1]\n                (s1, e1) = segments[k]\n\n                c0 = coords[s0:e0].mean(axis=0)\n                c1 = coords[s1:e1].mean(axis=0)\n\n                old_off = ref_cents[k] - ref_cents[k - 1]\n                desired_c1 = c0 + old_off\n                delta = desired_c1 - c1\n\n                # shift whole chain slightly toward desired centroid\n                coords[s1:e1] += anchor_w * delta\n\n    return coords\n\ndef _rotmat(axis, ang):\n    axis = np.asarray(axis, float)\n    axis = axis / (np.linalg.norm(axis) + 1e-12)\n    x, y, z = axis\n    c, s = np.cos(ang), np.sin(ang)\n    C = 1.0 - c\n    return np.array([\n        [c + x*x*C,     x*y*C - z*s, x*z*C + y*s],\n        [y*x*C + z*s,   c + y*y*C,   y*z*C - x*s],\n        [z*x*C - y*s,   z*y*C + x*s, c + z*z*C]\n    ], dtype=float)\n\ndef apply_hinge(coords, seg, rng, max_angle_deg=25):\n    s, e = seg\n    L = e - s\n    if L < 30:\n        return coords\n    pivot = s + int(rng.integers(10, L - 10))\n    axis = rng.normal(size=3)\n    ang = np.deg2rad(float(rng.uniform(-max_angle_deg, max_angle_deg)))\n    R = _rotmat(axis, ang)\n    X = coords.copy()\n    p0 = X[pivot].copy()\n    X[pivot+1:e] = (X[pivot+1:e] - p0) @ R.T + p0\n    return X\n\ndef jitter_chains(coords, segments, rng, max_angle_deg=12, max_trans=1.5):\n    X = coords.copy()\n    global_center = X.mean(axis=0, keepdims=True)\n    for (s, e) in segments:\n        axis = rng.normal(size=3)\n        ang = np.deg2rad(float(rng.uniform(-max_angle_deg, max_angle_deg)))\n        R = _rotmat(axis, ang)\n        shift = rng.normal(size=3)\n        shift = shift / (np.linalg.norm(shift) + 1e-12) * float(rng.uniform(0.0, max_trans))\n        c = X[s:e].mean(axis=0, keepdims=True)\n        X[s:e] = (X[s:e] - c) @ R.T + c + shift\n    X -= X.mean(axis=0, keepdims=True) - global_center\n    return X\n\ndef smooth_wiggle(coords, segments, rng, amp=0.8):\n    X = coords.copy()\n    for (s, e) in segments:\n        L = e - s\n        if L < 20:\n            continue\n        n_ctrl = 6\n        ctrl_x = np.linspace(0, L - 1, n_ctrl)\n        ctrl_disp = rng.normal(0, amp, size=(n_ctrl, 3))\n        t = np.arange(L)\n        disp = np.vstack([np.interp(t, ctrl_x, ctrl_disp[:, k]) for k in range(3)]).T\n        X[s:e] += disp\n    return X\n\ndef predict_rna_structures(row, train_seqs_df, train_coords_dict, n_predictions=5):\n    tid = row['target_id']\n    seq = row['sequence']\n\n    assert set(seq).issubset(set(\"ACGU\")), f\"Non-ACGU in {tid}; do not remap here.\"\n\n    segments = test_segs_map.get(tid, [(0, len(seq))])\n\n    # Set per-query temporal cutoff for template selection\n    q_cutoff = pd.to_datetime(row.get(\"temporal_cutoff\", None), errors=\"coerce\")\n    find_similar_sequences._cutoff = (q_cutoff if pd.notna(q_cutoff) else None)\n\n    cands = find_similar_sequences(query_seq=seq, train_seqs_df=train_seqs_df, train_coords_dict=train_coords_dict, top_n=30)\n    assert all(len(c[3]) == len(c[1]) for c in cands), \"Template coords/seq length mismatch\"\n\n    predictions = []\n    used = set()\n\n    for i in range(n_predictions):\n        seed = (abs(hash(tid)) + i * 10007) % (2**32)\n        rng = np.random.default_rng(seed)\n\n        if not cands:\n            coords = np.zeros((len(seq), 3), dtype=float)\n            for (s, e) in segments:\n                for j in range(s+1, e):\n                    coords[j] = coords[j-1] + [5.95, 0, 0]\n            predictions.append(coords)\n            continue\n\n        if i == 0:\n            t_id, t_seq, sim, t_coords = cands[0]\n        else:\n            K = min(12, len(cands))\n            sims = np.array([cands[k][2] for k in range(K)], float)\n            w = np.exp((sims - sims.max()) / 0.08)\n            for k in range(K):\n                if cands[k][0] in used:\n                    w[k] *= 0.10\n            w = w / (w.sum() + 1e-12)\n            k = int(rng.choice(np.arange(K), p=w))\n            t_id, t_seq, sim, t_coords = cands[k]\n\n        used.add(t_id)\n\n        adapted = adapt_template_to_query(query_seq=seq, template_seq=t_seq, template_coords=t_coords)\n\n        if i == 0:\n            X = adapted\n        elif i == 1:\n            X = adapted + rng.normal(0, max(0.01, (0.40 - sim) * 0.06), adapted.shape)\n        elif i == 2:\n            longest = max(segments, key=lambda se: se[1] - se[0])\n            X = apply_hinge(adapted, longest, rng, max_angle_deg=22)\n        elif i == 3:\n            X = jitter_chains(adapted, segments, rng, max_angle_deg=10, max_trans=1.0)\n        else:\n            X = smooth_wiggle(adapted, segments, rng, amp=0.7)\n\n        refined = adaptive_rna_constraints(X, tid, confidence=sim, passes=2)\n        predictions.append(refined)\n\n    return predictions\n\nall_predictions = []\nstart_time = time.time()\nfor idx, row in test_seqs.iterrows():\n    if idx % 10 == 0:\n        print(f\"Processing {idx} | {time.time()-start_time:.1f}s\")\n    tid, seq = row['target_id'], row['sequence']\n    preds = predict_rna_structures(row, train_seqs, train_coords_dict)\n    for j in range(len(seq)):\n        res = {'ID': f\"{tid}_{j+1}\", 'resname': seq[j], 'resid': j+1}\n        for i in range(5):\n            res[f'x_{i+1}'], res[f'y_{i+1}'], res[f'z_{i+1}'] = preds[i][j]\n        all_predictions.append(res)\n\nsub = pd.DataFrame(all_predictions)\ncols = ['ID', 'resname', 'resid'] + [f'{c}_{i}' for i in range(1,6) for c in ['x','y','z']]\n\ncoord_cols = [c for c in cols if c.startswith(('x_','y_','z_'))]\nsub[coord_cols] = sub[coord_cols].clip(-999.999, 9999.999)\n\nsub[cols].to_csv('submission.csv', index=False)\nprint(\"submission.csv saved\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-15T21:00:15.201024Z","iopub.execute_input":"2026-02-15T21:00:15.201246Z","iopub.status.idle":"2026-02-15T21:02:23.554453Z","shell.execute_reply.started":"2026-02-15T21:00:15.201224Z","shell.execute_reply":"2026-02-15T21:02:23.553762Z"}},"outputs":[],"execution_count":null}]}