{"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":"tpuV5e8","dataSources":[{"sourceId":118765,"databundleVersionId":15231210,"sourceType":"competition"}],"dockerImageVersionId":31261,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ============================================================\n# Stanford RNA 3D Folding (Part 2) - TEMPLATE BASELINE\n# - Fast k-mer retrieval  + Alignment-based mapping \n# - Safe fallback to no-gap resampling if alignment is unreliable\n# - 5 diverse predictions + geometry constraints per chain\n# ============================================================\n\nimport os, sys, time, warnings\nfrom collections import defaultdict, Counter\n\nimport numpy as np\nimport pandas as pd\n\nwarnings.filterwarnings(\"ignore\")\n\n# --------------------------\n# Config\n# --------------------------\nDATA_PATH = \"/kaggle/input/stanford-rna-3d-folding-2/\"\nOUT_PATH  = \"/kaggle/working/submission.csv\"\n\n# Retrieval parameters\nKMER_K = 5\nPRE_TOPM = 800\nTOP_N_TEMPLATES = 30\nLENGTH_FILTER_RATIO = 0.40\n\n# Prediction parameters\nN_PRED = 5\nCLIP_MIN, CLIP_MAX = -999.999, 9999.999\n\n# Constraint defaults (angstrom)\nBOND_I_I1 = 5.95\nBOND_I_I2 = 10.2\n\n# Alignment reliability thresholds (HYBRID key)\nALIGN_MIN_COVERAGE = 0.60   # % of query residues filled directly from aligned chunks\nALIGN_MAX_BLOCKS   = 20     # too many blocks => too gappy/fragmented => fallback\n\n# --------------------------\n# Load data\n# --------------------------\ntrain_seqs   = pd.read_csv(os.path.join(DATA_PATH, \"train_sequences.csv\"))\ntest_seqs    = pd.read_csv(os.path.join(DATA_PATH, \"test_sequences.csv\"))\ntrain_labels = pd.read_csv(os.path.join(DATA_PATH, \"train_labels.csv\"))\n\n# --------------------------\n# FASTA + stoichiometry parsing (for chain segments)\n# --------------------------\nsys.path.append(os.path.join(DATA_PATH, \"extra\"))\n\ntry:\n    import typing as _typing\n    import builtins as _builtins\n    _builtins.Dict  = getattr(_typing, \"Dict\")\n    _builtins.Tuple = getattr(_typing, \"Tuple\")\n    _builtins.List  = getattr(_typing, \"List\")\n    from parse_fasta_py import parse_fasta as _parse_fasta_raw\n\n    def parse_fasta(fasta_content: str):\n        d = _parse_fasta_raw(fasta_content)\n        out = {}\n        for k, v in d.items():\n            out[k] = v[0] if isinstance(v, tuple) else v\n        return out\n\nexcept Exception:\n    def parse_fasta(fasta_content: str):\n        out = {}\n        cur = None\n        seq_parts = []\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(seq_parts)\n                header = line[1:]\n                cur = header.split()[0]\n                seq_parts = []\n            else:\n                seq_parts.append(line.replace(\" \", \"\"))\n        if cur is not None:\n            out[cur] = \"\".join(seq_parts)\n        return out\n\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\n\ndef normalize_seq_keep_len(s: str) -> str:\n    s = str(s).strip().upper().replace(\"T\", \"U\").replace(\" \", \"\")\n    return \"\".join([c if c in \"ACGU\" else \"N\" for c in s])\n\n\ntrain_seqs[\"sequence_norm\"] = train_seqs[\"sequence\"].apply(normalize_seq_keep_len)\ntest_seqs[\"sequence_norm\"]  = test_seqs[\"sequence\"].apply(normalize_seq_keep_len)\n\ntrain_seq_dict = dict(zip(train_seqs[\"target_id\"].values, train_seqs[\"sequence_norm\"].values))\n\n\ndef get_chain_segments(row):\n    \"\"\"\n    Returns chain segments in the concatenated `sequence` corresponding to chain copies.\n    Output: list of (start_idx, end_idx).\n    \"\"\"\n    seq = row[\"sequence_norm\"]\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        chain_dict = {k: normalize_seq_keep_len(v) for k, v in chain_dict.items()}\n\n        order = parse_stoichiometry(stoich)\n\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\n        if pos != len(seq):\n            return [(0, len(seq))]\n        return segs\n\n    except Exception:\n        return [(0, len(seq))]\n\n\n\ndef build_segments_map(df):\n    seg_map = {}\n    for _, r in df.iterrows():\n        seg_map[r[\"target_id\"]] = get_chain_segments(r)\n    return seg_map\n\n\ntrain_segs_map = build_segments_map(train_seqs)\ntest_segs_map  = build_segments_map(test_seqs)\n\n# --------------------------\n# Prepare training coordinate dictionary\n# --------------------------\ndef process_labels(labels_df):\n    coords_dict = {}\n    prefixes = labels_df[\"ID\"].str.rsplit(\"_\", n=1).str[0]\n    for tid, group in labels_df.groupby(prefixes):\n        group = group.sort_values(\"resid\")\n        coords_dict[tid] = group[[\"x_1\", \"y_1\", \"z_1\"]].values.astype(np.float32)\n    return coords_dict\n\n\ntrain_coords_dict = process_labels(train_labels)\n\n# --------------------------\n# Biopython alignment\n# --------------------------\nimport subprocess\nfrom Bio.Align import PairwiseAligner\n\naligner = PairwiseAligner()\naligner.mode = \"global\"\naligner.match_score = 2.0\naligner.mismatch_score = -1.5\n\n# strong gap penalties reduce sliding\naligner.open_gap_score   = -8.0\naligner.extend_gap_score = -0.4\n\n# penalize terminal gaps\naligner.query_left_open_gap_score     = -8.0\naligner.query_left_extend_gap_score   = -0.4\naligner.query_right_open_gap_score    = -8.0\naligner.query_right_extend_gap_score  = -0.4\naligner.target_left_open_gap_score    = -8.0\naligner.target_left_extend_gap_score  = -0.4\naligner.target_right_open_gap_score   = -8.0\naligner.target_right_extend_gap_score = -0.4\n\n# ============================================================\n# 1) FAST k-mer retrieval index\n# ============================================================\n\n\n\ndef build_kmer_index(train_df, k=5, min_len=20):\n    kmer_to_ids = defaultdict(list)\n    for _, r in train_df.iterrows():\n        tid = r[\"target_id\"]\n        s = r[\"sequence_norm\"]\n        if len(s) < max(min_len, k):\n            continue\n        seen = set(\n    s[i:i+k]\n    for i in range(len(s)-k+1)\n    if \"N\" not in s[i:i+k]\n)\n\n        for km in seen:\n            kmer_to_ids[km].append(tid)\n    return kmer_to_ids\n\n\nkmer_index = build_kmer_index(train_seqs, k=KMER_K)\n\n\nprint(\"N train =\", len(train_seqs))\nprint(\"Ex train seq head =\", train_seqs[\"sequence\"].iloc[0][:60], \"len=\", len(train_seqs[\"sequence\"].iloc[0]))\nprint(\"kmer_index size =\", len(kmer_index))\n\nprint(\"unique chars in test seq (norm):\", set(test_seqs[\"sequence_norm\"].iloc[0]))\nprint(\"len test seq (norm):\", len(test_seqs[\"sequence_norm\"].iloc[0]))\n\n\n\n\ndef fast_candidates_by_kmer(query_seq, topM=300):\n    if len(query_seq) < KMER_K:\n        return []\n    kmers = [query_seq[i:i+KMER_K] for i in range(len(query_seq)-KMER_K+1)]\n    hits = Counter()\n    for km in kmers:\n        for tid in kmer_index.get(km, []):\n            hits[tid] += 1\n    if not hits:\n        return []\n    denom = max(1, (len(query_seq) - KMER_K + 1))\n    scored = [(tid, hits[tid] / denom) for tid in hits]\n    scored.sort(key=lambda x: x[1], reverse=True)\n    return [tid for tid, _ in scored[:topM]]\n\ndef find_similar_sequences(query_seq, train_coords_dict, top_n=5, pre_topM=300):\n    cand_ids = fast_candidates_by_kmer(query_seq, topM=pre_topM)\n    if not cand_ids:\n        return []\n    qL = len(query_seq)\n    candidates = []\n    for tid in cand_ids:\n        if tid not in train_coords_dict:\n            continue\n        t_seq = train_seq_dict.get(tid)\n        if t_seq is None:\n            continue\n        if abs(len(t_seq) - qL) / max(len(t_seq), qL) > LENGTH_FILTER_RATIO:\n            continue\n\n        raw = aligner.score(query_seq, t_seq)\n        norm = raw / (2.0 * min(qL, len(t_seq)))\n        t_coords = train_coords_dict[tid]\n        if len(t_coords) != len(t_seq):\n            continue\n\n        candidates.append((tid, t_seq, float(norm), t_coords))\n\n    candidates.sort(key=lambda x: x[2], reverse=True)\n    return candidates[:top_n]\n\n# ============================================================\n# 2) HYBRID coordinate transfer: alignment-chunks OR fallback nogap\n# ============================================================\ndef adapt_template_to_query_nogap(query_seq, template_seq, template_coords):\n    Lq, Lt = len(query_seq), len(template_seq)\n    if Lq == Lt:\n        return template_coords.copy().astype(np.float32)\n    idx = np.round(np.linspace(0, Lt - 1, Lq)).astype(int)\n    X = template_coords[idx].copy().astype(np.float32)\n    # avoid duplicated indices collapsing geometry\n    for i in range(1, Lq):\n        if idx[i] == idx[i - 1]:\n            X[i] = X[i - 1] + np.array([BOND_I_I1, 0.0, 0.0], dtype=np.float32)\n    return X\n\ndef _interpolate_nans_in_trace(X):\n    \"\"\"Fill NaNs in an (L,3) trace by linear interpolation; extrapolate gently if needed.\"\"\"\n    L = len(X)\n    for i in range(L):\n        if np.isnan(X[i, 0]):\n            prev_v = next((j for j in range(i-1, -1, -1) if not np.isnan(X[j, 0])), -1)\n            next_v = next((j for j in range(i+1, L) if not np.isnan(X[j, 0])), -1)\n            if prev_v >= 0 and next_v >= 0:\n                w = (i - prev_v) / (next_v - prev_v)\n                X[i] = (1-w)*X[prev_v] + w*X[next_v]\n            elif prev_v >= 0:\n                X[i] = X[prev_v] + np.array([BOND_I_I1, 0.0, 0.0], dtype=np.float32)\n            elif next_v >= 0:\n                X[i] = X[next_v] + np.array([BOND_I_I1, 0.0, 0.0], dtype=np.float32)\n            else:\n                X[i] = np.array([i*BOND_I_I1, 0.0, 0.0], dtype=np.float32)\n    return np.nan_to_num(X).astype(np.float32)\n\ndef adapt_template_to_query_hybrid(query_seq, template_seq, template_coords,\n                                  min_coverage=ALIGN_MIN_COVERAGE,\n                                  max_blocks=ALIGN_MAX_BLOCKS):\n    \"\"\"\n    Hybrid mapping:\n    - Try alignment-based chunk mapping (alignment.aligned) -> best quality when reliable\n    - Measure coverage + fragmentation -> if unreliable, fallback to no-gap resampling\n    \"\"\"\n    Lq, Lt = len(query_seq), len(template_seq)\n    if Lq == Lt:\n        return template_coords.copy().astype(np.float32)\n\n    # Build alignment object (first optimal)\n    try:\n        alignment = next(iter(aligner.align(query_seq, template_seq)))\n    except Exception:\n        return adapt_template_to_query_nogap(query_seq, template_seq, template_coords)\n\n    new_coords = np.full((Lq, 3), np.nan, dtype=np.float32)\n\n    # alignment.aligned: (q_blocks, t_blocks) where blocks are (start,end)\n    try:\n        q_blocks, t_blocks = alignment.aligned\n    except Exception:\n        return adapt_template_to_query_nogap(query_seq, template_seq, template_coords)\n\n    n_blocks = min(len(q_blocks), len(t_blocks))\n    filled = 0\n\n    for (q_start, q_end), (t_start, t_end) in zip(q_blocks, t_blocks):\n        if q_end <= q_start or t_end <= t_start:\n            continue\n        # Safety: chunk length must match\n        if (q_end - q_start) != (t_end - t_start):\n            continue\n        t_chunk = template_coords[t_start:t_end]\n        if len(t_chunk) != (q_end - q_start):\n            continue\n        new_coords[q_start:q_end] = t_chunk.astype(np.float32)\n        filled += (q_end - q_start)\n\n    covered_ratio = filled / max(1, Lq)\n    if (covered_ratio < min_coverage) or (n_blocks > max_blocks):\n        # unreliable alignment => stable fallback\n        return adapt_template_to_query_nogap(query_seq, template_seq, template_coords)\n\n    return _interpolate_nans_in_trace(new_coords)\n\n# ============================================================\n# 3) Geometry refinement / constraints (shape-safe curv)\n# ============================================================\ndef adaptive_rna_constraints(coordinates, target_id, confidence=1.0, passes=2):\n    coords = coordinates.copy().astype(np.float32)\n    segments = test_segs_map.get(target_id, [(0, len(coords))])\n\n    strength = 0.75 * (1.0 - min(confidence, 0.97))\n    strength = max(strength, 0.02)\n\n    for _ in range(passes):\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) i,i+1 bond ~5.95Å\n            d = X[1:] - X[:-1]\n            dist = np.linalg.norm(d, axis=1) + 1e-6\n            target = BOND_I_I1\n            scale = (target - dist) / dist\n            adj = (d * scale[:, None]) * (0.22 * strength)\n            X[:-1] -= adj\n            X[1:]  += adj\n\n            # (2) i,i+2 ~10.2Å\n            d2 = X[2:] - X[:-2]\n            dist2 = np.linalg.norm(d2, axis=1) + 1e-6\n            target2 = BOND_I_I2\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\n            lap = 0.5 * (X[:-2] + X[2:]) - X[1:-1]\n            X[1:-1] += (0.06 * strength) * lap\n\n            # (4) Frame regularization (shape-safe)\n            v = X[1:] - X[:-1]  # (L-1,3)\n            vn = v / (np.linalg.norm(v, axis=1, keepdims=True) + 1e-6)  # (L-1,3)\n            if vn.shape[0] >= 2:\n                curv = vn[1:] - vn[:-1]  # (L-2,3)\n                # internal residues 1..L-2 => X[1:-1] has shape (L-2,3)\n                X[1:-1] -= (0.04 * strength) * curv\n\n            # (5) Light self-avoidance\n            if L >= 25:\n                k = min(L, 160) if L > 220 else L\n                idx = np.linspace(0, L - 1, k).astype(int) if k < L else np.arange(L)\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                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.astype(np.float32)\n\n            coords[s:e] = X\n\n    return coords\n\n# ============================================================\n# 4) Diversity transforms\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.astype(np.float32)\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.astype(np.float32)\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.astype(np.float32)\n    return X.astype(np.float32)\n\n# ============================================================\n# 5) Main prediction function (HYBRID)\n# ============================================================\ndef predict_rna_structures(row, n_predictions=5):\n    tid = row[\"target_id\"]\n    seq = row[\"sequence_norm\"]   # <- IMPORTANT\n\n    assert len(seq) > 0 and set(seq).issubset(set(\"ACGU\")), f\"Bad seq in {tid}\"\n\n    segments = test_segs_map.get(tid, [(0, len(seq))])\n\n    # Fast candidate pool (k-mer prefilter + alignment scoring on only topM)\n    cands = find_similar_sequences(\n        query_seq=seq,\n        train_coords_dict=train_coords_dict,\n        top_n=TOP_N_TEMPLATES,\n        pre_topM=PRE_TOPM\n    )\n\n    predictions = []\n    used_templates = 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        # Fallback if no templates at all\n        if not cands:\n            coords = np.zeros((len(seq), 3), dtype=np.float32)\n            for (s, e) in segments:\n                for j in range(s+1, e):\n                    coords[j] = coords[j-1] + np.array([BOND_I_I1, 0.0, 0.0], dtype=np.float32)\n            predictions.append(coords)\n            continue\n\n        # Choose template (best then weighted sampling for diversity)\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)], dtype=np.float32)\n            w = np.exp((sims - sims.max()) / 0.08)\n            for k in range(K):\n                if cands[k][0] in used_templates:\n                    w[k] *= 0.10\n            w = w / (w.sum() + 1e-10)\n            k = int(rng.choice(np.arange(K), p=w))\n            t_id, t_seq, sim, t_coords = cands[k]\n\n        used_templates.add(t_id)\n\n        # HYBRID transfer: alignment chunks if reliable, else no-gap\n        adapted = adapt_template_to_query_hybrid(seq, t_seq, t_coords)\n\n        # Diversity transforms\n        if i == 0:\n            X = adapted\n        elif i == 1:\n            sigma = max(0.01, (0.40 - float(sim)) * 0.06)\n            X = (adapted + rng.normal(0, sigma, adapted.shape)).astype(np.float32)\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=float(sim), passes=2)\n        predictions.append(refined)\n\n    return predictions\n\n\n# ============================================================\n# 6) Build submission.csv (FIXED LOOP)\n# ============================================================\nall_rows = []\nt0 = time.time()\n\nfor idx, row in test_seqs.iterrows():\n    tid = row[\"target_id\"]\n    seq = row[\"sequence_norm\"]      # pour retrieval + predict\n    seq_raw = row[\"sequence\"]       # pour resname (submission)\n\n    if idx % 10 == 0:\n        print(f\"[{idx:4d}/{len(test_seqs)}] elapsed {time.time()-t0:.1f}s\")\n\n    # Debug candidates (optionnel)\n    cands_dbg = find_similar_sequences(seq, train_coords_dict, top_n=TOP_N_TEMPLATES, pre_topM=PRE_TOPM)\n    print(tid, \"len(seq)=\", len(seq), \"cands=\", len(cands_dbg), \"best_sim=\", (cands_dbg[0][2] if cands_dbg else None))\n\n    preds = predict_rna_structures(row, n_predictions=N_PRED)\n\n    # Write per-residue rows (use RAW sequence for resname)\n    for j in range(len(seq_raw)):\n        out = {\"ID\": f\"{tid}_{j+1}\", \"resname\": seq_raw[j], \"resid\": j+1}\n        for p in range(N_PRED):\n            out[f\"x_{p+1}\"], out[f\"y_{p+1}\"], out[f\"z_{p+1}\"] = preds[p][j]\n        all_rows.append(out)\n\nsub = pd.DataFrame(all_rows)\n\ncols = [\"ID\", \"resname\", \"resid\"] + [f\"{c}_{i}\" for i in range(1, 6) for c in [\"x\", \"y\", \"z\"]]\ncoord_cols = [c for c in cols if c.startswith((\"x_\", \"y_\", \"z_\"))]\n\nsub[coord_cols] = sub[coord_cols].clip(CLIP_MIN, CLIP_MAX)\nsub[cols].to_csv(OUT_PATH, index=False)\n\nprint(f\"Saved: {OUT_PATH}\")\nprint(sub.head(3))\n\n\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-02-07T19:01:49.321139Z","iopub.execute_input":"2026-02-07T19:01:49.321366Z","iopub.status.idle":"2026-02-07T19:04:40.010187Z","shell.execute_reply.started":"2026-02-07T19:01:49.32135Z","shell.execute_reply":"2026-02-07T19:04:40.009307Z"}},"outputs":[],"execution_count":null}]}