{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":118765,"databundleVersionId":15231210}],"dockerImageVersionId":31328,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nfrom difflib import SequenceMatcher","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-25T18:58:43.807685Z","iopub.execute_input":"2026-03-25T18:58:43.809106Z","iopub.status.idle":"2026-03-25T18:58:43.814119Z","shell.execute_reply.started":"2026-03-25T18:58:43.809052Z","shell.execute_reply":"2026-03-25T18:58:43.813206Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nBASE = Path(\"/kaggle/input/competitions/stanford-rna-3d-folding-2\")\nWORKDIR = Path(\"/kaggle/working\")\n\nTEST_PATH = BASE / \"test_sequences.csv\"\nSAMPLE_SUB_PATH = BASE / \"sample_submission.csv\"\nOUTPUT_PATH = WORKDIR / \"submission.csv\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-25T18:58:43.816647Z","iopub.execute_input":"2026-03-25T18:58:43.817135Z","iopub.status.idle":"2026-03-25T18:58:43.833781Z","shell.execute_reply.started":"2026-03-25T18:58:43.817102Z","shell.execute_reply":"2026-03-25T18:58:43.832814Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def safe_read_csv(path: Path) -> pd.DataFrame:\n    if not path.exists():\n        raise FileNotFoundError(f\"Required file not found: {path}\")\n    return pd.read_csv(path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-25T18:58:43.835106Z","iopub.execute_input":"2026-03-25T18:58:43.836021Z","iopub.status.idle":"2026-03-25T18:58:43.85179Z","shell.execute_reply.started":"2026-03-25T18:58:43.83599Z","shell.execute_reply":"2026-03-25T18:58:43.850736Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_sequences = safe_read_csv(TEST_PATH)\nsample_submission = safe_read_csv(SAMPLE_SUB_PATH)\ntrain_sequences = safe_read_csv(BASE / \"train_sequences.csv\")\ntrain_labels = safe_read_csv(BASE / \"train_labels.csv\")\n\ntest_lookup = test_sequences.set_index(\"target_id\", drop=False)\n\nprint(\"test_sequences shape:\", test_sequences.shape)\nprint(\"sample_submission shape:\", sample_submission.shape)\nprint(\"train_sequences shape:\", train_sequences.shape)\nprint(\"train_labels shape:\", train_labels.shape)\n\ntest_sequences.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-25T18:58:43.85302Z","iopub.execute_input":"2026-03-25T18:58:43.853375Z","iopub.status.idle":"2026-03-25T18:58:52.487964Z","shell.execute_reply.started":"2026-03-25T18:58:43.853339Z","shell.execute_reply":"2026-03-25T18:58:52.486912Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SUBMISSION_COLUMNS = [\n    \"ID\", \"resname\", \"resid\",\n    \"x_1\", \"y_1\", \"z_1\",\n    \"x_2\", \"y_2\", \"z_2\",\n    \"x_3\", \"y_3\", \"z_3\",\n    \"x_4\", \"y_4\", \"z_4\",\n    \"x_5\", \"y_5\", \"z_5\",\n]\n\nCOORD_COLUMNS = SUBMISSION_COLUMNS[3:]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-25T18:58:52.490408Z","iopub.execute_input":"2026-03-25T18:58:52.490756Z","iopub.status.idle":"2026-03-25T18:58:52.49591Z","shell.execute_reply.started":"2026-03-25T18:58:52.490728Z","shell.execute_reply":"2026-03-25T18:58:52.494679Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def parse_submission_id(submission_id: str) -> tuple[str, int]:\n    target_id, resid = submission_id.rsplit(\"_\", 1)\n    return target_id, int(resid)\n\ndef clip_coords(coords: np.ndarray) -> np.ndarray:\n    return np.clip(coords, -999.999, 9999.999)\n\ndef one_hot_nt(nt: str) -> list[int]:\n    nts = [\"A\", \"U\", \"G\", \"C\"]\n    return [1 if nt == c else 0 for c in nts]\n\ndef get_sequence_info(target_id: str) -> dict:\n    row = test_lookup.loc[target_id]\n    seq = row[\"sequence\"]\n    return {\n        \"target_id\": target_id,\n        \"sequence\": seq,\n        \"seq_len\": len(seq),\n        \"stoichiometry\": row.get(\"stoichiometry\", None),\n        \"description\": row.get(\"description\", None),\n    }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-25T18:58:52.497562Z","iopub.execute_input":"2026-03-25T18:58:52.498429Z","iopub.status.idle":"2026-03-25T18:58:52.514566Z","shell.execute_reply.started":"2026-03-25T18:58:52.498376Z","shell.execute_reply":"2026-03-25T18:58:52.513273Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 7: stronger hybrid (multi-conformer template bank + scaffold)\n# ============================================================\n\nimport numpy as np\n\n\n# -----------------------------\n# Basic helpers\n# -----------------------------\ndef can_pair(a: str, b: str) -> bool:\n    return (a + b) in {\"AU\", \"UA\", \"GC\", \"CG\", \"GU\", \"UG\"}\n\n\ndef dotbracket_to_pairs(dotbracket: str) -> list[tuple[int, int]]:\n    stack = []\n    pairs = []\n    for i, ch in enumerate(dotbracket):\n        if ch == \"(\":\n            stack.append(i)\n        elif ch == \")\":\n            if stack:\n                j = stack.pop()\n                pairs.append((j, i))\n    pairs.sort()\n    return pairs\n\n\ndef pair_maps_from_pairs(L: int, pairs: list[tuple[int, int]]) -> np.ndarray:\n    pair_to = np.full(L, -1, dtype=int)\n    for i, j in pairs:\n        pair_to[i] = j\n        pair_to[j] = i\n    return pair_to\n\n\ndef normalize(v: np.ndarray, eps: float = 1e-8) -> np.ndarray:\n    n = np.linalg.norm(v)\n    if n < eps:\n        return np.array([1.0, 0.0, 0.0], dtype=np.float32)\n    return (v / n).astype(np.float32)\n\n\n# -----------------------------\n# Multi-conformer template bank\n# -----------------------------\ndef build_template_bank(train_seq_df: pd.DataFrame, train_lbl_df: pd.DataFrame):\n    seq_map = dict(zip(train_seq_df[\"target_id\"], train_seq_df[\"sequence\"]))\n    bank = {}\n\n    grouped = train_lbl_df.groupby(train_lbl_df[\"ID\"].str.rsplit(\"_\", n=1).str[0])\n\n    for tid, grp in grouped:\n        if tid not in seq_map:\n            continue\n\n        grp = grp.sort_values(\"resid\")\n        seq = seq_map[tid]\n        if len(grp) != len(seq):\n            continue\n\n        conformers = []\n        k = 1\n        while f\"x_{k}\" in grp.columns and f\"y_{k}\" in grp.columns and f\"z_{k}\" in grp.columns:\n            xyz = grp[[f\"x_{k}\", f\"y_{k}\", f\"z_{k}\"]].to_numpy(dtype=np.float32)\n            if not np.isnan(xyz).any():\n                xyz = xyz - xyz.mean(axis=0, keepdims=True)\n                conformers.append(xyz)\n            k += 1\n\n        if conformers:\n            bank[tid] = {\n                \"sequence\": seq,\n                \"conformers\": conformers,\n            }\n\n    return bank\n\n\nTEMPLATE_BANK = build_template_bank(train_sequences, train_labels)\n\n\n# -----------------------------\n# Needleman-Wunsch alignment\n# -----------------------------\ndef nw_align(seq1: str, seq2: str, match: int = 2, mismatch: int = -1, gap: int = -2):\n    n, m = len(seq1), len(seq2)\n    dp = np.zeros((n + 1, m + 1), dtype=np.int32)\n    tb = np.zeros((n + 1, m + 1), dtype=np.int8)  # 0=diag,1=up,2=left\n\n    for i in range(1, n + 1):\n        dp[i, 0] = i * gap\n        tb[i, 0] = 1\n    for j in range(1, m + 1):\n        dp[0, j] = j * gap\n        tb[0, j] = 2\n\n    for i in range(1, n + 1):\n        a = seq1[i - 1]\n        for j in range(1, m + 1):\n            b = seq2[j - 1]\n            diag = dp[i - 1, j - 1] + (match if a == b else mismatch)\n            up = dp[i - 1, j] + gap\n            left = dp[i, j - 1] + gap\n\n            best = diag\n            tbv = 0\n            if up > best:\n                best = up\n                tbv = 1\n            if left > best:\n                best = left\n                tbv = 2\n\n            dp[i, j] = best\n            tb[i, j] = tbv\n\n    # traceback\n    i, j = n, m\n    a1, a2 = [], []\n    while i > 0 or j > 0:\n        if i > 0 and j > 0 and tb[i, j] == 0:\n            a1.append(seq1[i - 1])\n            a2.append(seq2[j - 1])\n            i -= 1\n            j -= 1\n        elif i > 0 and (j == 0 or tb[i, j] == 1):\n            a1.append(seq1[i - 1])\n            a2.append(\"-\")\n            i -= 1\n        else:\n            a1.append(\"-\")\n            a2.append(seq2[j - 1])\n            j -= 1\n\n    a1.reverse()\n    a2.reverse()\n    return \"\".join(a1), \"\".join(a2), int(dp[n, m])\n\n\ndef aligned_identity(aln1: str, aln2: str) -> float:\n    denom = 0\n    matches = 0\n    for a, b in zip(aln1, aln2):\n        if a != \"-\" and b != \"-\":\n            denom += 1\n            if a == b:\n                matches += 1\n    return matches / max(1, denom)\n\n\ndef search_templates(q_seq: str, bank: dict, top_n: int = 5, rel_len_cap: float = 0.5):\n    hits = []\n\n    for tid, obj in bank.items():\n        t_seq = obj[\"sequence\"]\n        if abs(len(t_seq) - len(q_seq)) / max(len(t_seq), len(q_seq)) > rel_len_cap:\n            continue\n\n        aln_q, aln_t, score = nw_align(q_seq, t_seq)\n        ident = aligned_identity(aln_q, aln_t)\n        hits.append((tid, score, ident, aln_q, aln_t, obj[\"conformers\"]))\n\n    hits.sort(key=lambda x: (x[1], x[2]), reverse=True)\n    return hits[:top_n]\n\ndef search_template_conformers(q_seq: str, bank: dict, top_n: int = 8, rel_len_cap: float = 0.5):\n    hits = []\n\n    for tid, obj in bank.items():\n        t_seq = obj[\"sequence\"]\n\n        if abs(len(t_seq) - len(q_seq)) / max(len(t_seq), len(q_seq)) > rel_len_cap:\n            continue\n\n        aln_q, aln_t, score = nw_align(q_seq, t_seq)\n        ident = aligned_identity(aln_q, aln_t)\n\n        for conf_idx, conf_xyz in enumerate(obj[\"conformers\"]):\n            hits.append({\n                \"target_id\": tid,\n                \"conf_idx\": conf_idx,\n                \"score\": score,\n                \"ident\": ident,\n                \"aln_q\": aln_q,\n                \"aln_t\": aln_t,\n                \"template_seq\": t_seq,\n                \"coords\": conf_xyz,\n            })\n\n    hits.sort(key=lambda x: (x[\"score\"], x[\"ident\"]), reverse=True)\n    return hits[:top_n]\n    \n# -----------------------------\n# Template adaptation\n# -----------------------------\ndef adapt_template_to_query_from_alignment(\n    q_seq: str,\n    t_seq: str,\n    aln_q: str,\n    aln_t: str,\n    t_xyz: np.ndarray,\n) -> np.ndarray:\n    q_xyz = np.full((len(q_seq), 3), np.nan, dtype=np.float32)\n\n    q_idx = 0\n    t_idx = 0\n    for a, b in zip(aln_q, aln_t):\n        if a != \"-\" and b != \"-\":\n            q_xyz[q_idx] = t_xyz[t_idx]\n            q_idx += 1\n            t_idx += 1\n        elif a != \"-\" and b == \"-\":\n            q_idx += 1\n        elif a == \"-\" and b != \"-\":\n            t_idx += 1\n\n    # interpolate gaps\n    for i in range(len(q_xyz)):\n        if np.isnan(q_xyz[i, 0]):\n            left = i - 1\n            while left >= 0 and np.isnan(q_xyz[left, 0]):\n                left -= 1\n            right = i + 1\n            while right < len(q_xyz) and np.isnan(q_xyz[right, 0]):\n                right += 1\n\n            if left >= 0 and right < len(q_xyz):\n                w = (i - left) / (right - left)\n                q_xyz[i] = (1 - w) * q_xyz[left] + w * q_xyz[right]\n\n    # forward/backward fill\n    for i in range(len(q_xyz)):\n        if np.isnan(q_xyz[i, 0]) and i > 0 and not np.isnan(q_xyz[i - 1, 0]):\n            q_xyz[i] = q_xyz[i - 1] + np.array([0.0, 0.0, 4.0], dtype=np.float32)\n\n    for i in range(len(q_xyz) - 2, -1, -1):\n        if np.isnan(q_xyz[i, 0]) and not np.isnan(q_xyz[i + 1, 0]):\n            q_xyz[i] = q_xyz[i + 1] - np.array([0.0, 0.0, 4.0], dtype=np.float32)\n\n    if np.isnan(q_xyz).any():\n        center = np.zeros(3, dtype=np.float32)\n        for i in range(len(q_xyz)):\n            if np.isnan(q_xyz[i, 0]):\n                q_xyz[i] = center + np.array([0.0, 0.0, 4.0 * i], dtype=np.float32)\n\n    q_xyz = q_xyz - q_xyz.mean(axis=0, keepdims=True)\n    return q_xyz.astype(np.float32)\n\n\n# -----------------------------\n# Secondary structure prediction\n# -----------------------------\ndef predict_secondary_structure_vienna(sequence: str):\n    try:\n        import RNA\n        fc = RNA.fold_compound(sequence)\n        dotbracket, mfe = fc.mfe()\n        return dotbracket, mfe\n    except Exception:\n        return None, None\n\n\ndef predict_secondary_structure_windowed(sequence: str, window: int = 260, step: int = 140, margin: int = 20):\n    L = len(sequence)\n    pair_votes = {}\n\n    starts = list(range(0, max(1, L - window + 1), step))\n    if not starts or starts[-1] != max(0, L - window):\n        starts.append(max(0, L - window))\n\n    for start in starts:\n        end = min(L, start + window)\n        subseq = sequence[start:end]\n\n        dotbracket, _ = predict_secondary_structure_vienna(subseq)\n        if dotbracket is None:\n            continue\n\n        for i, j in dotbracket_to_pairs(dotbracket):\n            if i < margin or j >= len(subseq) - margin:\n                continue\n            gi, gj = start + i, start + j\n            pair_votes[(gi, gj)] = pair_votes.get((gi, gj), 0) + 1\n\n    voted_pairs = sorted(pair_votes.items(), key=lambda x: (-x[1], x[0][0], -x[0][1]))\n    used = np.zeros(L, dtype=bool)\n    pairs = []\n\n    for (i, j), _votes in voted_pairs:\n        if i >= j or used[i] or used[j]:\n            continue\n        pairs.append((i, j))\n        used[i] = True\n        used[j] = True\n\n    pairs.sort()\n    db = [\".\"] * L\n    for i, j in pairs:\n        db[i] = \"(\"\n        db[j] = \")\"\n\n    return \"\".join(db), pairs, \"windowed_vienna\"\n\n\ndef predict_secondary_structure(sequence: str):\n    if len(sequence) > 900:\n        return predict_secondary_structure_windowed(sequence)\n\n    dotbracket, _ = predict_secondary_structure_vienna(sequence)\n    if dotbracket is not None:\n        return dotbracket, dotbracket_to_pairs(dotbracket), \"ViennaRNA\"\n\n    return \".\" * len(sequence), [], \"fallback\"\n\n\ndef generate_pair_hypotheses(sequence: str, n_hypotheses: int = 5):\n    _, base_pairs, _ = predict_secondary_structure(sequence)\n    if not base_pairs:\n        return [[] for _ in range(n_hypotheses)]\n\n    pairs = sorted(base_pairs)\n    n = len(pairs)\n\n    hyps = [pairs]\n    hyps.append(pairs[: max(1, int(0.9 * n))] or pairs)\n    hyps.append([p for p in pairs if (p[1] - p[0]) >= 10] or pairs)\n    hyps.append([p for k, p in enumerate(pairs) if k % 3 != 0] or pairs)\n\n    spans = np.array([j - i for i, j in pairs], dtype=float)\n    cutoff = np.quantile(spans, 0.35) if len(spans) > 1 else spans[0]\n    hyps.append([p for p in pairs if (p[1] - p[0]) >= cutoff] or pairs)\n\n    return hyps[:n_hypotheses]\n\n\n# -----------------------------\n# Geometry scaffold\n# -----------------------------\ndef group_pairs_into_stems(pairs: list[tuple[int, int]]) -> list[list[tuple[int, int]]]:\n    if not pairs:\n        return []\n\n    pairs = sorted(pairs)\n    stems = []\n    cur = [pairs[0]]\n\n    for prev, nxt in zip(pairs[:-1], pairs[1:]):\n        if nxt[0] == prev[0] + 1 and nxt[1] == prev[1] - 1:\n            cur.append(nxt)\n        else:\n            stems.append(cur)\n            cur = [nxt]\n    stems.append(cur)\n    return stems\n\n\ndef find_unpaired_segments(L: int, pairs: list[tuple[int, int]]) -> list[tuple[int, int]]:\n    paired = np.zeros(L, dtype=bool)\n    for i, j in pairs:\n        paired[i] = True\n        paired[j] = True\n\n    segs = []\n    start = None\n    for idx in range(L):\n        if not paired[idx] and start is None:\n            start = idx\n        elif paired[idx] and start is not None:\n            segs.append((start, idx - 1))\n            start = None\n    if start is not None:\n        segs.append((start, L - 1))\n    return segs\n\n\ndef classify_unpaired_segment(seg_start: int, seg_end: int, pair_to: np.ndarray) -> str:\n    L = len(pair_to)\n    if seg_start == 0 or seg_end == L - 1:\n        return \"external\"\n\n    left_idx = seg_start - 1\n    right_idx = seg_end + 1\n    if left_idx < 0 or right_idx >= L:\n        return \"external\"\n\n    left_partner = pair_to[left_idx]\n    right_partner = pair_to[right_idx]\n\n    if left_partner == right_idx and right_partner == left_idx:\n        return \"hairpin\"\n\n    left_enclosed = (left_partner > right_idx) if left_partner != -1 else False\n    right_enclosed = (right_partner < left_idx) if right_partner != -1 else False\n\n    if left_enclosed and right_enclosed:\n        return \"internal_loop\"\n    if left_enclosed or right_enclosed:\n        return \"bulge\"\n    return \"other_loop\"\n\n\ndef classify_residues(L: int, stems: list[list[tuple[int, int]]], pairs: list[tuple[int, int]]) -> dict:\n    labels = np.array([\"other_loop\"] * L, dtype=object)\n    for stem in stems:\n        for i, j in stem:\n            labels[i] = \"stem\"\n            labels[j] = \"stem\"\n\n    pair_to = pair_maps_from_pairs(L, pairs)\n    segs = []\n    for a, b in find_unpaired_segments(L, pairs):\n        seg_type = classify_unpaired_segment(a, b, pair_to)\n        labels[a:b+1] = seg_type\n        segs.append({\"start\": a, \"end\": b, \"length\": b - a + 1, \"type\": seg_type})\n\n    return {\"labels\": labels, \"pair_to\": pair_to, \"segments\": segs}\n\n\ndef build_initial_backbone(L: int, variant_idx: int) -> np.ndarray:\n    coords = np.zeros((L, 3), dtype=np.float32)\n    radius = [7.8, 7.2, 8.4, 7.5, 8.0][variant_idx]\n    twist  = [0.14, 0.17, 0.11, 0.15, 0.13][variant_idx]\n    rise   = [2.1, 1.95, 2.25, 2.0, 2.15][variant_idx]\n\n    for i in range(L):\n        coords[i] = [\n            radius * np.cos(i * twist),\n            radius * np.sin(i * twist),\n            i * rise,\n        ]\n    return coords\n\n\ndef get_local_backbone_axis(coords: np.ndarray, left_idx: int, right_idx: int) -> np.ndarray:\n    L = len(coords)\n    lp = max(0, left_idx - 1)\n    ln = min(L - 1, left_idx + 1)\n    rp = max(0, right_idx - 1)\n    rn = min(L - 1, right_idx + 1)\n\n    axis = (coords[ln] - coords[lp]) + (coords[rn] - coords[rp])\n    if np.linalg.norm(axis) < 1e-8:\n        axis = np.array([0.0, 0.0, 1.0], dtype=np.float32)\n    return normalize(axis)\n\n\ndef make_perpendicular_basis(axis: np.ndarray):\n    trial = np.array([1.0, 0.0, 0.0], dtype=np.float32)\n    if abs(np.dot(axis, trial)) > 0.9:\n        trial = np.array([0.0, 1.0, 0.0], dtype=np.float32)\n\n    u = normalize(trial - np.dot(trial, axis) * axis)\n    v = normalize(np.cross(axis, u))\n    return u, v\n\n\ndef apply_stem_geometry_segmentwise(coords: np.ndarray, stems: list[list[tuple[int, int]]], variant_idx: int) -> np.ndarray:\n    coords = coords.copy()\n    stem_sep = [1.15, 1.0, 1.25, 1.1, 1.2][variant_idx]\n    axis_rise = [1.30, 1.15, 1.40, 1.20, 1.35][variant_idx]\n    twist_pair = [0.55, 0.62, 0.48, 0.58, 0.52][variant_idx]\n\n    for stem in stems:\n        if not stem:\n            continue\n\n        axis = get_local_backbone_axis(coords, stem[0][0], stem[0][1])\n        u, v = make_perpendicular_basis(axis)\n        mids = np.array([0.5 * (coords[i] + coords[j]) for i, j in stem], dtype=np.float32)\n        center = mids.mean(axis=0)\n\n        for k, (i, j) in enumerate(stem):\n            along = (k - (len(stem) - 1) / 2.0) * axis_rise\n            local_center = center + along * axis\n            ang = twist_pair * k + 0.20 * variant_idx\n            perp = normalize(np.cos(ang) * u + np.sin(ang) * v)\n\n            coords[i] = local_center + stem_sep * perp\n            coords[j] = local_center - stem_sep * perp\n\n    return coords\n\n\ndef apply_segment_arc(coords: np.ndarray, start: int, end: int, amplitude: float, z_bump: float, phase: float = 0.0) -> np.ndarray:\n    coords = coords.copy()\n    n = end - start + 1\n    if n <= 0:\n        return coords\n\n    left_anchor = coords[start - 1] if start - 1 >= 0 else coords[start]\n    right_anchor = coords[end + 1] if end + 1 < len(coords) else coords[end]\n\n    for t_idx, i in enumerate(range(start, end + 1)):\n        t = (t_idx + 1) / (n + 1)\n        base = (1 - t) * left_anchor + t * right_anchor\n        normal = np.array([np.sin(np.pi * t + phase), np.cos(np.pi * t + phase), 0.0], dtype=np.float32)\n        coords[i] = base + amplitude * np.sin(np.pi * t) * normal\n        coords[i, 2] += z_bump * np.sin(np.pi * t)\n\n    return coords\n\n\ndef apply_loop_segments(coords: np.ndarray, residue_info: dict, variant_idx: int) -> np.ndarray:\n    coords = coords.copy()\n\n    for seg in residue_info[\"segments\"]:\n        a, b, seg_type = seg[\"start\"], seg[\"end\"], seg[\"type\"]\n        scale = min(1.6, 0.8 + 0.12 * seg[\"length\"])\n\n        if seg_type == \"hairpin\":\n            coords = apply_segment_arc(coords, a, b, 1.8 * scale, 1.0 * scale, 0.1 * variant_idx)\n        elif seg_type == \"internal_loop\":\n            coords = apply_segment_arc(coords, a, b, 1.2 * scale, 0.7 * scale, 0.4 + 0.1 * variant_idx)\n        elif seg_type == \"bulge\":\n            coords = apply_segment_arc(coords, a, b, 1.4 * scale, 0.5 * scale, 0.6 + 0.1 * variant_idx)\n        elif seg_type == \"external\":\n            coords = apply_segment_arc(coords, a, b, 0.7 * scale, 0.2 * scale, 0.0)\n        else:\n            coords = apply_segment_arc(coords, a, b, 0.9 * scale, 0.4 * scale, 0.2)\n\n    return coords\n\n\ndef apply_sequence_offsets(coords: np.ndarray, sequence: str) -> np.ndarray:\n    coords = coords.copy()\n    for i, nt in enumerate(sequence):\n        if nt == \"G\":\n            coords[i, 0] += 0.15\n        elif nt == \"C\":\n            coords[i, 1] -= 0.15\n        elif nt == \"U\":\n            coords[i, 2] += 0.10\n        elif nt == \"A\":\n            coords[i, 1] += 0.08\n    return coords\n\n\ndef smooth_backbone(coords: np.ndarray, labels: np.ndarray, passes: int = 2) -> np.ndarray:\n    coords = coords.copy()\n    L = len(coords)\n\n    for _ in range(passes):\n        new_coords = coords.copy()\n        for i in range(1, L - 1):\n            if labels[i] == \"stem\":\n                w_prev, w_self, w_next = 0.12, 0.76, 0.12\n            else:\n                w_prev, w_self, w_next = 0.24, 0.52, 0.24\n\n            new_coords[i] = (\n                w_prev * coords[i - 1]\n                + w_self * coords[i]\n                + w_next * coords[i + 1]\n            )\n        coords = new_coords\n    return coords\n\n\ndef refine_backbone_constraints(coords: np.ndarray, pairs: list[tuple[int, int]], n_iter: int = 2) -> np.ndarray:\n    out = coords.copy()\n    L = len(out)\n\n    for _ in range(n_iter):\n        # sequential spacing\n        for i in range(L - 1):\n            d = out[i + 1] - out[i]\n            norm = np.linalg.norm(d)\n            if norm < 1e-6:\n                continue\n            target = 4.1\n            corr = 0.15 * (target - norm) * (d / norm)\n            out[i] -= 0.5 * corr\n            out[i + 1] += 0.5 * corr\n\n        # mild pair regularization\n        for i, j in pairs:\n            d = out[j] - out[i]\n            norm = np.linalg.norm(d)\n            if norm < 1e-6:\n                continue\n            target = 2.5\n            corr = 0.08 * (norm - target) * (d / norm)\n            out[i] += corr\n            out[j] -= corr\n\n    out = out - out.mean(axis=0, keepdims=True)\n    return out.astype(np.float32)\n\ndef scaffold_fill_from_alignment(\n    q_seq: str,\n    aln_q: str,\n    aln_t: str,\n    template_xyz: np.ndarray,\n    scaffold_xyz: np.ndarray,\n) -> np.ndarray:\n    \n    out = np.full((len(q_seq), 3), np.nan, dtype=np.float32)\n    source = np.array([\"\"] * len(q_seq), dtype=object)\n\n    q_idx = 0\n    t_idx = 0\n\n    for a, b in zip(aln_q, aln_t):\n        if a != \"-\" and b != \"-\":\n            out[q_idx] = template_xyz[q_idx]\n            source[q_idx] = \"template\"\n            q_idx += 1\n            t_idx += 1\n        elif a != \"-\" and b == \"-\":\n            out[q_idx] = scaffold_xyz[q_idx]\n            source[q_idx] = \"scaffold\"\n            q_idx += 1\n        elif a == \"-\" and b != \"-\":\n            t_idx += 1\n\n    # fill any remaining missing positions from scaffold\n    for i in range(len(out)):\n        if np.isnan(out[i, 0]):\n            out[i] = scaffold_xyz[i]\n            source[i] = \"scaffold\"\n\n    # smooth only at template/scaffold boundaries\n    for i in range(1, len(out) - 1):\n        if source[i - 1] != source[i] or source[i] != source[i + 1]:\n            out[i] = 0.2 * out[i - 1] + 0.6 * out[i] + 0.2 * out[i + 1]\n\n    out = out - out.mean(axis=0, keepdims=True)\n    return out.astype(np.float32)\n    \ndef generate_secondary_structure_baseline_from_pairs(sequence: str, pair_set: list[tuple[int, int]], variant_idx: int = 0) -> np.ndarray:\n    pairs = sorted(pair_set)\n    stems = group_pairs_into_stems(pairs)\n    residue_info = classify_residues(len(sequence), stems, pairs)\n\n    coords = build_initial_backbone(len(sequence), variant_idx)\n    coords = apply_stem_geometry_segmentwise(coords, stems, variant_idx)\n    coords = apply_loop_segments(coords, residue_info, variant_idx)\n    coords = apply_sequence_offsets(coords, sequence)\n    coords = smooth_backbone(coords, residue_info[\"labels\"], passes=2 if variant_idx in [0, 1, 3] else 3)\n    coords = refine_backbone_constraints(coords, pairs, n_iter=2)\n\n    coords = coords - coords.mean(axis=0, keepdims=True)\n    return coords.astype(np.float32)\n\n\n# -----------------------------\n# Hybrid template + scaffold\n# -----------------------------\ndef blend_template_with_scaffold(template_xyz: np.ndarray, scaffold_xyz: np.ndarray, alpha: float = 0.8) -> np.ndarray:\n    out = alpha * template_xyz + (1 - alpha) * scaffold_xyz\n    out = out - out.mean(axis=0, keepdims=True)\n    return out.astype(np.float32)\n\n\ndef add_template_noise(xyz: np.ndarray, noise_scale: float = 0.25, seed_shift: int = 0) -> np.ndarray:\n    rng = np.random.default_rng(2026 + seed_shift)\n    noise = rng.normal(0.0, noise_scale, size=xyz.shape).astype(np.float32)\n    out = xyz + noise\n    out = out - out.mean(axis=0, keepdims=True)\n    return out.astype(np.float32)\n\n\ndef refine_template_coords(xyz: np.ndarray, conf: float = 1.0) -> np.ndarray:\n    out = xyz.copy()\n    target = 4.1\n    strength = 0.20 + 0.25 * (1.0 - min(conf, 1.0))\n\n    for i in range(len(out) - 1):\n        d = out[i + 1] - out[i]\n        norm = np.linalg.norm(d)\n        if norm < 1e-6:\n            continue\n        direction = d / norm\n        out[i + 1] = out[i] + (1 - strength) * d + strength * target * direction\n\n    out = out - out.mean(axis=0, keepdims=True)\n    return out.astype(np.float32)\n\n\n# -----------------------------\n# Long-sequence stitching\n# -----------------------------\ndef kabsch_align(P: np.ndarray, Q: np.ndarray):\n    Pc = P.mean(axis=0)\n    Qc = Q.mean(axis=0)\n    P0 = P - Pc\n    Q0 = Q - Qc\n\n    H = Q0.T @ P0\n    U, S, Vt = np.linalg.svd(H)\n    R = U @ Vt\n    if np.linalg.det(R) < 0:\n        U[:, -1] *= -1\n        R = U @ Vt\n\n    t = Pc - Qc @ R\n    return R.astype(np.float32), t.astype(np.float32)\n\n\ndef stitch_window_coords(L: int, window_results: list[tuple[int, int, np.ndarray]]) -> np.ndarray:\n    window_results = sorted(window_results, key=lambda x: x[0])\n\n    accum = np.zeros((L, 3), dtype=np.float32)\n    weights = np.zeros(L, dtype=np.float32)\n    placed_end = -1\n\n    def weight_profile(n: int):\n        if n <= 2:\n            return np.ones(n, dtype=np.float32)\n        x = np.linspace(-1.0, 1.0, n, dtype=np.float32)\n        return np.clip(1.0 - 0.7 * np.abs(x), 0.3, None).astype(np.float32)\n\n    for start, end, coords in window_results:\n        coords = coords.copy()\n        n = end - start\n        w = weight_profile(n)\n\n        overlap_start = start\n        overlap_end = min(end, placed_end)\n\n        if overlap_start < overlap_end and (overlap_end - overlap_start) >= 4:\n            existing = accum[overlap_start:overlap_end] / np.maximum(weights[overlap_start:overlap_end, None], 1e-8)\n            new = coords[(overlap_start - start):(overlap_end - start)]\n            try:\n                R, t = kabsch_align(existing, new)\n                coords = coords @ R + t\n            except Exception:\n                coords += existing.mean(axis=0) - new.mean(axis=0)\n\n        accum[start:end] += coords[:n] * w[:, None]\n        weights[start:end] += w\n        placed_end = max(placed_end, end)\n\n    weights[weights == 0] = 1.0\n    out = accum / weights[:, None]\n    out = out - out.mean(axis=0, keepdims=True)\n    return out.astype(np.float32)\n\n\ndef build_long_sequence_coords(sequence: str, variant_idx: int = 0, window: int = 260, step: int = 140) -> np.ndarray:\n    L = len(sequence)\n\n    starts = list(range(0, max(1, L - window + 1), step))\n    if not starts or starts[-1] != max(0, L - window):\n        starts.append(max(0, L - window))\n\n    window_results = []\n\n    for start in starts:\n        end = min(L, start + window)\n        subseq = sequence[start:end]\n\n        pair_hypotheses = generate_pair_hypotheses(subseq, n_hypotheses=5)\n        local_pairs = pair_hypotheses[min(variant_idx, len(pair_hypotheses) - 1)]\n        local_coords = generate_secondary_structure_baseline_from_pairs(subseq, local_pairs, variant_idx=variant_idx)\n        local_coords[:, 2] += start * 0.012\n        window_results.append((start, end, local_coords))\n\n    coords = stitch_window_coords(L, window_results)\n    coords *= 0.90\n    coords = coords - coords.mean(axis=0, keepdims=True)\n    return coords.astype(np.float32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-25T18:58:52.516112Z","iopub.execute_input":"2026-03-25T18:58:52.516485Z","iopub.status.idle":"2026-03-25T18:59:06.993713Z","shell.execute_reply.started":"2026-03-25T18:58:52.516439Z","shell.execute_reply":"2026-03-25T18:59:06.992786Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_five_structures_for_target(target_row: pd.Series) -> list[np.ndarray]:\n    sequence = target_row[\"sequence\"]\n    L = len(sequence)\n    preds = []\n\n    # long targets: keep windowed scaffold\n    if L > 1000:\n        for k in range(5):\n            coords = build_long_sequence_coords(sequence, variant_idx=k, window=260, step=140)\n\n            if k == 1:\n                coords *= 0.96\n            elif k == 2:\n                coords *= 1.04\n            elif k == 3:\n                coords[:, 2] *= 0.97\n            elif k == 4:\n                coords[:, 0] *= 1.02\n                coords[:, 1] *= 1.02\n\n            coords = coords - coords.mean(axis=0, keepdims=True)\n            preds.append(coords.astype(np.float32))\n\n        return preds\n\n    # conformer-level retrieval\n    hits = search_template_conformers(sequence, TEMPLATE_BANK, top_n=8)\n\n    pair_hypotheses = generate_pair_hypotheses(sequence, n_hypotheses=5)\n    scaffold_cache = {}\n\n    def get_scaffold(k):\n        if k not in scaffold_cache:\n            scaffold_cache[k] = generate_secondary_structure_baseline_from_pairs(\n                sequence,\n                pair_hypotheses[min(k, len(pair_hypotheses) - 1)],\n                variant_idx=k,\n            )\n        return scaffold_cache[k]\n\n    # 1: best pure template\n    if hits:\n        hit = hits[0]\n        xyz = adapt_template_to_query_from_alignment(\n            sequence,\n            hit[\"template_seq\"],\n            hit[\"aln_q\"],\n            hit[\"aln_t\"],\n            hit[\"coords\"],\n        )\n        xyz = refine_template_coords(xyz, conf=hit[\"ident\"])\n        preds.append(xyz.astype(np.float32))\n    else:\n        preds.append(get_scaffold(0))\n\n    # 2: second-best pure template\n    if len(hits) > 1:\n        hit = hits[1]\n        xyz = adapt_template_to_query_from_alignment(\n            sequence,\n            hit[\"template_seq\"],\n            hit[\"aln_q\"],\n            hit[\"aln_t\"],\n            hit[\"coords\"],\n        )\n        xyz = refine_template_coords(xyz, conf=hit[\"ident\"])\n        preds.append(xyz.astype(np.float32))\n    else:\n        preds.append(get_scaffold(1))\n\n    # 3: best template + scaffold only in gaps\n    if hits:\n        hit = hits[0]\n        xyz = adapt_template_to_query_from_alignment(\n            sequence,\n            hit[\"template_seq\"],\n            hit[\"aln_q\"],\n            hit[\"aln_t\"],\n            hit[\"coords\"],\n        )\n        scaffold = get_scaffold(2)\n        xyz = scaffold_fill_from_alignment(\n            sequence,\n            hit[\"aln_q\"],\n            hit[\"aln_t\"],\n            xyz,\n            scaffold,\n        )\n        xyz = refine_template_coords(xyz, conf=hit[\"ident\"])\n        preds.append(xyz.astype(np.float32))\n    else:\n        preds.append(get_scaffold(2))\n\n    # 4: third-best template or noisy best template\n    if len(hits) > 2:\n        hit = hits[2]\n        xyz = adapt_template_to_query_from_alignment(\n            sequence,\n            hit[\"template_seq\"],\n            hit[\"aln_q\"],\n            hit[\"aln_t\"],\n            hit[\"coords\"],\n        )\n        xyz = refine_template_coords(xyz, conf=hit[\"ident\"])\n        preds.append(xyz.astype(np.float32))\n    elif hits:\n        hit = hits[0]\n        xyz = adapt_template_to_query_from_alignment(\n            sequence,\n            hit[\"template_seq\"],\n            hit[\"aln_q\"],\n            hit[\"aln_t\"],\n            hit[\"coords\"],\n        )\n        xyz = refine_template_coords(xyz, conf=hit[\"ident\"])\n        xyz = add_template_noise(\n            xyz,\n            noise_scale=max(0.08, 0.20 * (1 - hit[\"ident\"])),\n            seed_shift=4,\n        )\n        preds.append(xyz.astype(np.float32))\n    else:\n        preds.append(get_scaffold(3))\n\n    # 5: scaffold alternative for diversity\n    preds.append(get_scaffold(4))\n\n    # final normalization\n    out = []\n    for coords in preds[:5]:\n        coords = coords - coords.mean(axis=0, keepdims=True)\n        out.append(coords.astype(np.float32))\n\n    return out","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-25T18:59:06.994916Z","iopub.execute_input":"2026-03-25T18:59:06.995371Z","iopub.status.idle":"2026-03-25T18:59:07.014571Z","shell.execute_reply.started":"2026-03-25T18:59:06.995332Z","shell.execute_reply":"2026-03-25T18:59:07.013398Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_submission_from_test(test_df: pd.DataFrame, sample_sub: pd.DataFrame) -> pd.DataFrame:\n    test_lookup = test_df.set_index(\"target_id\", drop=False)\n\n    sub = sample_sub[[\"ID\", \"resname\", \"resid\"]].copy()\n\n    # cache predictions so each target is only processed once\n    pred_cache = {}\n\n    for idx, row in sub.iterrows():\n        submission_id = row[\"ID\"]\n        target_id, resid = parse_submission_id(submission_id)\n\n        if target_id not in pred_cache:\n            target_row = test_lookup.loc[target_id]\n            print(f\"Processing {target_id} (length={len(target_row['sequence'])})\")\n            pred_cache[target_id] = predict_five_structures_for_target(target_row)\n\n        preds = pred_cache[target_id]\n\n        for k in range(5):\n            x, y, z = preds[k][resid - 1]\n            sub.at[idx, f\"x_{k+1}\"] = float(x)\n            sub.at[idx, f\"y_{k+1}\"] = float(y)\n            sub.at[idx, f\"z_{k+1}\"] = float(z)\n\n    sub = sub[SUBMISSION_COLUMNS]\n    return sub","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-25T18:59:07.016054Z","iopub.execute_input":"2026-03-25T18:59:07.016571Z","iopub.status.idle":"2026-03-25T18:59:07.042761Z","shell.execute_reply.started":"2026-03-25T18:59:07.016407Z","shell.execute_reply":"2026-03-25T18:59:07.041573Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def validate_submission_format(sub: pd.DataFrame, sample_sub: pd.DataFrame) -> pd.DataFrame:\n    if list(sub.columns) != SUBMISSION_COLUMNS:\n        raise ValueError(\"Submission columns do not match required format.\")\n\n    if len(sub) != len(sample_sub):\n        raise ValueError(\n            f\"Submission row count {len(sub)} does not match sample row count {len(sample_sub)}.\"\n        )\n\n    if not sub[\"ID\"].equals(sample_sub[\"ID\"]):\n        raise ValueError(\"Submission IDs do not match sample submission IDs.\")\n\n    if not sub[\"resname\"].equals(sample_sub[\"resname\"]):\n        raise ValueError(\"Submission resname values do not match sample submission.\")\n\n    if not sub[\"resid\"].equals(sample_sub[\"resid\"]):\n        raise ValueError(\"Submission resid values do not match sample submission.\")\n\n    coord_values = sub[COORD_COLUMNS].to_numpy(dtype=np.float32)\n    if np.isnan(coord_values).any():\n        raise ValueError(\"Submission contains NaN coordinates.\")\n\n    sub.loc[:, COORD_COLUMNS] = clip_coords(coord_values)\n    return sub","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-25T18:59:07.044101Z","iopub.execute_input":"2026-03-25T18:59:07.044487Z","iopub.status.idle":"2026-03-25T18:59:07.066365Z","shell.execute_reply.started":"2026-03-25T18:59:07.044456Z","shell.execute_reply":"2026-03-25T18:59:07.065437Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission = build_submission_from_test(test_sequences, sample_submission)\nsubmission = validate_submission_format(submission, sample_submission)\n\nprint(submission.shape)\nprint(\"NaN count:\", submission[COORD_COLUMNS].isna().sum().sum())\nprint(\"Column check:\", list(submission.columns) == SUBMISSION_COLUMNS)\nsubmission.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-25T18:59:07.067763Z","iopub.execute_input":"2026-03-25T18:59:07.068167Z","iopub.status.idle":"2026-03-25T19:03:28.657882Z","shell.execute_reply.started":"2026-03-25T18:59:07.068119Z","shell.execute_reply":"2026-03-25T19:03:28.656642Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission.to_csv(\"/kaggle/working/submission.csv\", index=False)\nprint(\"Saved submission to /kaggle/working/submission.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-25T19:03:28.659134Z","iopub.execute_input":"2026-03-25T19:03:28.659518Z","iopub.status.idle":"2026-03-25T19:03:28.926892Z","shell.execute_reply.started":"2026-03-25T19:03:28.65949Z","shell.execute_reply":"2026-03-25T19:03:28.925857Z"}},"outputs":[],"execution_count":null}]}