{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.10.12"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":118765,"databundleVersionId":15231210,"isSourceIdPinned":false},{"sourceType":"datasetVersion","sourceId":14604295,"datasetId":9328538,"databundleVersionId":15440074},{"sourceType":"datasetVersion","sourceId":14962460,"datasetId":9577079,"databundleVersionId":15833819},{"sourceType":"datasetVersion","sourceId":14874339,"datasetId":9502242,"databundleVersionId":15736806},{"sourceType":"datasetVersion","sourceId":14962495,"datasetId":9577097,"databundleVersionId":15833858},{"sourceType":"datasetVersion","sourceId":10855324,"datasetId":6742586,"databundleVersionId":11219268}],"dockerImageVersionId":31287,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Validation Evaluation Pipeline\n\nバリデーションセットでTBM+Protenixパイプラインを評価し、パラメータ最適化を行う。\n\n**評価指標**: TM-score  \n- 各ターゲット: max(5 predictions) × max(ground truth conformations)  \n- 最終スコア: 全ターゲットの平均TM-score","metadata":{}},{"cell_type":"code","source":"# ============================================================\n# Cell 1: Install & Environment\n# ============================================================\nimport os, sys\n\nIS_KAGGLE = os.path.exists('/kaggle/input')\nprint(f\"Environment: {'Kaggle' if IS_KAGGLE else 'Local'}\")\n\nif IS_KAGGLE:\n    # BioPythonのwhlがある場合\n    import glob\n    whl = glob.glob('/kaggle/input/**/biopython*.whl', recursive=True)\n    if whl:\n        !pip install -q {whl[0]}\n    else:\n        !pip install -q biopython\n    !pip install -q tqdm biotite rdkit","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-08T05:23:52.821123Z","iopub.execute_input":"2026-03-08T05:23:52.821449Z","iopub.status.idle":"2026-03-08T05:24:05.903303Z","shell.execute_reply.started":"2026-03-08T05:23:52.821423Z","shell.execute_reply":"2026-03-08T05:24:05.902568Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 2: Configuration\n# ============================================================\n\n# ★★★ ここを変更してパラメータ実験 ★★★\nVERSION = 13\nUSE_PROTENIX = True    # True: Protenixフォールバック使用, False: TBMのみ\nN_SAMPLE = 5\nSEED = 42\nMAX_SEQ_LEN = 512\n\n# TBM フィルタ\nMIN_SIMILARITY = 0.0\nMIN_PERCENT_IDENTITY = 50.0\nMIN_COVERAGE = 0.5\nNORM_SCORE_CUTOFF = 0.2\nMAX_NAN_RATE = 0.3\nLENGTH_FILTER = 0.3    # ±30%\nTOP_N = 30\n\n# 幾何学的制約\nBOND_TARGET_I_I1 = 5.95\nBOND_TARGET_I_I2 = 10.2\nSELF_AVOID_DIST = 3.2\nCONSTRAINT_PASSES = 2\nCONSTRAINT_BOND_STRENGTH = 0.22\n\n# ジヌクレオチド結合\nDINUC_BOND_TARGETS = {\n    ('C', 'G'): 5.70, ('C', 'U'): 5.70, ('G', 'A'): 6.10,\n    ('U', 'A'): 6.40, ('U', 'U'): 6.10, ('G', 'G'): 5.80,\n}\nDINUC_DEFAULT = 5.95\n\n# 多様性\nJITTER_TRANS = 3.0 if VERSION >= 11 else 1.5\nHINGE_DEG = 22\nJITTER_DEG = 12\nWIGGLE_AMP = 0.8\n\n# Protenix 設定\nos.environ['LAYERNORM_TYPE'] = 'torch'\nos.environ.setdefault('RNA_MSA_DEPTH_LIMIT', '512')\nMODEL_NAME = 'protenix_base_20250630_v1.0.0'\nMODEL_N_SAMPLE = N_SAMPLE\n\ndef parse_bool(value, default=False):\n    v = str(value).strip().lower()\n    if v in {'1','true','t','yes','y','on'}: return 'true'\n    if v in {'0','false','f','no','n','off'}: return 'false'\n    return 'true' if default else 'false'\n\nUSE_MSA = parse_bool(os.environ.get('USE_MSA', 'false'))\nUSE_TEMPLATE = parse_bool(os.environ.get('USE_TEMPLATE', 'false'))\nUSE_RNA_MSA = parse_bool(os.environ.get('USE_RNA_MSA', 'true'), default=True)\n\n# パス\nif IS_KAGGLE:\n    DATA_DIR = '/kaggle/input/stanford-rna-3d-folding-2'\n    PROTENIX_CODE_DIR = (\n        '/kaggle/input/datasets/qiweiyin/protenix-v1-adjusted'\n        '/Protenix-v1-adjust-v2/Protenix-v1-adjust-v2/Protenix-v1'\n    )\n    PROTENIX_ROOT_DIR = PROTENIX_CODE_DIR\nelse:\n    DATA_DIR = 'CSV'  # ローカル\n    PROTENIX_CODE_DIR = None\n\nprint(f'VERSION={VERSION}, USE_PROTENIX={USE_PROTENIX}, N_SAMPLE={N_SAMPLE}')\nprint(f'MIN_PERCENT_IDENTITY={MIN_PERCENT_IDENTITY}, MIN_COVERAGE={MIN_COVERAGE}')\nprint(f'CONSTRAINT_PASSES={CONSTRAINT_PASSES}, BOND_STRENGTH={CONSTRAINT_BOND_STRENGTH}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-08T05:24:05.905109Z","iopub.execute_input":"2026-03-08T05:24:05.905429Z","iopub.status.idle":"2026-03-08T05:24:05.915808Z","shell.execute_reply.started":"2026-03-08T05:24:05.905403Z","shell.execute_reply":"2026-03-08T05:24:05.915044Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 3: Imports\n# ============================================================\nimport gc\nimport json\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom Bio.Align import PairwiseAligner\nfrom tqdm.auto import tqdm\nimport warnings\nwarnings.filterwarnings('ignore', category=DeprecationWarning)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-08T05:24:05.916619Z","iopub.execute_input":"2026-03-08T05:24:05.916896Z","iopub.status.idle":"2026-03-08T05:24:05.929384Z","shell.execute_reply.started":"2026-03-08T05:24:05.916866Z","shell.execute_reply":"2026-03-08T05:24:05.928647Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 4: TM-score 計算\n# ============================================================\n\ndef kabsch_superpose(P, Q):\n    \"\"\"Kabsch algorithm: P→Q最適重ね合わせ。\"\"\"\n    cP, cQ = P.mean(axis=0), Q.mean(axis=0)\n    Pc, Qc = P - cP, Q - cQ\n    H = Pc.T @ Qc\n    U, S, Vt = np.linalg.svd(H)\n    d = np.linalg.det(Vt.T @ U.T)\n    sign_matrix = np.diag([1, 1, np.sign(d)])\n    R = Vt.T @ sign_matrix @ U.T\n    return (Pc @ R.T) + cQ\n\n\ndef tm_score(pred_coords, true_coords):\n    \"\"\"TM-score (0–1)。NaN残基は除外。\"\"\"\n    valid = (~np.isnan(pred_coords).any(axis=1)) & (~np.isnan(true_coords).any(axis=1))\n    if valid.sum() < 3:\n        return 0.0\n    P, Q = pred_coords[valid], true_coords[valid]\n    L = len(Q)\n    d0 = max(1.24 * (L - 15) ** (1.0/3.0) - 1.8, 0.5) if L > 15 else 0.5\n    P_aligned = kabsch_superpose(P, Q)\n    distances = np.linalg.norm(P_aligned - Q, axis=1)\n    return float(np.sum(1.0 / (1.0 + (distances / d0) ** 2)) / L)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-08T05:24:05.930357Z","iopub.execute_input":"2026-03-08T05:24:05.930648Z","iopub.status.idle":"2026-03-08T05:24:05.940169Z","shell.execute_reply.started":"2026-03-08T05:24:05.930621Z","shell.execute_reply":"2026-03-08T05:24:05.939418Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 5: データ読み込み\n# ============================================================\n\ndef load_validation_ground_truth(path):\n    \"\"\"バリデーション正解データ (複数コンフォメーション) を読み込み。\"\"\"\n    df = pd.read_csv(path)\n    x_cols = sorted([c for c in df.columns if c.startswith('x_')], key=lambda c: int(c.split('_')[1]))\n    y_cols = sorted([c for c in df.columns if c.startswith('y_')], key=lambda c: int(c.split('_')[1]))\n    z_cols = sorted([c for c in df.columns if c.startswith('z_')], key=lambda c: int(c.split('_')[1]))\n    n_conf = len(x_cols)\n    gt = {}\n    prefixes = df['ID'].str.rsplit('_', n=1).str[0]\n    for prefix, grp in df.groupby(prefixes):\n        grp_sorted = grp.sort_values('resid')\n        conformations = []\n        for ci in range(n_conf):\n            coords = grp_sorted[[x_cols[ci], y_cols[ci], z_cols[ci]]].values.astype(float)\n            coords[coords < -1e17] = np.nan\n            if np.isnan(coords).all():\n                continue\n            if (~np.isnan(coords).any(axis=1)).mean() > 0.5:\n                conformations.append(coords)\n        if conformations:\n            gt[prefix] = conformations\n    return gt\n\n\ndef load_train_data(train_csv, train_labels_csv):\n    \"\"\"テンプレートプール構築 (チャンク読み込み)。\"\"\"\n    train_seqs = pd.read_csv(train_csv, usecols=['target_id', 'sequence'])\n    coords = {}\n    print('  train_labels をチャンク読み込み中...')\n    for chunk in pd.read_csv(train_labels_csv, chunksize=200_000,\n                              usecols=['ID', 'resid', 'x_1', 'y_1', 'z_1'],\n                              dtype={'x_1': np.float32, 'y_1': np.float32, 'z_1': np.float32}):\n        prefixes = chunk['ID'].str.rsplit('_', n=1).str[0]\n        for prefix, grp in chunk.groupby(prefixes):\n            arr = grp.sort_values('resid')[['x_1', 'y_1', 'z_1']].values\n            coords[prefix] = np.vstack([coords[prefix], arr]) if prefix in coords else arr\n    for k in coords:\n        if coords[k].dtype != np.float32:\n            coords[k] = coords[k].astype(np.float32)\n    gc.collect()\n    print(f'  → {len(coords)} 構造を読み込み')\n    train_seqs['seq_len'] = train_seqs['sequence'].str.len()\n    return train_seqs, coords\n\n\n# 読み込み実行\nprint('データ読み込み中...')\nt0 = time.time()\n\ndata_dir = Path(DATA_DIR)\nval_seqs = pd.read_csv(data_dir / 'validation_sequences.csv')\nground_truth = load_validation_ground_truth(data_dir / 'validation_labels.csv')\ntrain_seqs, train_coords = load_train_data(\n    data_dir / 'train_sequences.csv',\n    data_dir / 'train_labels.csv'\n)\n\nprint(f'  バリデーション: {len(val_seqs)} ターゲット, {len(ground_truth)} に正解あり')\nprint(f'  テンプレートプール: {len(train_seqs)} 配列, {len(train_coords)} 構造')\nprint(f'  読み込み時間: {time.time()-t0:.1f}s')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-08T05:24:05.941854Z","iopub.execute_input":"2026-03-08T05:24:05.942242Z","iopub.status.idle":"2026-03-08T05:24:29.75784Z","shell.execute_reply.started":"2026-03-08T05:24:05.942212Z","shell.execute_reply":"2026-03-08T05:24:29.757176Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 6: TBM コア関数\n# ============================================================\n\ndef _make_aligner():\n    al = PairwiseAligner()\n    al.mode = 'global'\n    if VERSION >= 13:\n        al.match_score = 1; al.mismatch_score = -1\n        al.open_gap_score = -10; al.extend_gap_score = -0.5\n    else:\n        al.match_score = 2; al.mismatch_score = -1.5\n        al.open_gap_score = -8; al.extend_gap_score = -0.4\n    for side in ['left', 'right']:\n        for role in ['query', 'target']:\n            setattr(al, f'{role}_{side}_open_gap_score', al.open_gap_score)\n            setattr(al, f'{role}_{side}_extend_gap_score', al.extend_gap_score)\n    return al\n\n_aligner = _make_aligner()\n\n\ndef find_similar_sequences(query_seq, train_seqs_df, train_coords_dict):\n    results = []\n    query_len = len(query_seq)\n    min_len = query_len * (1 - LENGTH_FILTER)\n    max_len = query_len * (1 + LENGTH_FILTER)\n    norm_denom = 1 if VERSION >= 13 else 2\n\n    candidates = train_seqs_df[\n        (train_seqs_df['target_id'].isin(train_coords_dict.keys())) &\n        (train_seqs_df['seq_len'] >= min_len) &\n        (train_seqs_df['seq_len'] <= max_len)\n    ]\n\n    for _, row in candidates.iterrows():\n        tid, tseq = row['target_id'], row['sequence']\n        aln = next(iter(_aligner.align(query_seq, tseq)))\n        norm_s = aln.score / (norm_denom * min(query_len, len(tseq)))\n        if norm_s < NORM_SCORE_CUTOFF:\n            continue\n\n        identical, aligned_count = 0, 0\n        for (qs, qe), (ts, te) in zip(*aln.aligned):\n            for qp, tp in zip(range(qs, qe), range(ts, te)):\n                aligned_count += 1\n                if query_seq[qp] == tseq[tp]:\n                    identical += 1\n        pct_id = 100 * identical / query_len\n\n        if VERSION >= 13:\n            coverage = aligned_count / query_len\n            if coverage < MIN_COVERAGE:\n                continue\n            tmpl_coords = train_coords_dict[tid]\n            nan_rate = np.isnan(tmpl_coords).any(axis=1).mean() if tmpl_coords.size > 0 else 0\n            if nan_rate > MAX_NAN_RATE:\n                continue\n\n        results.append((tid, tseq, norm_s, train_coords_dict[tid], pct_id))\n\n    results.sort(key=lambda x: x[2], reverse=True)\n    return results[:TOP_N]\n\n\ndef _fix_reflection(adapted, template_coords):\n    n_shared = min(len(adapted), len(template_coords))\n    valid = (~np.isnan(adapted[:n_shared]).any(axis=1)) & (~np.isnan(template_coords[:n_shared]).any(axis=1))\n    idx = np.where(valid)[0]\n    if len(idx) < 4:\n        return adapted\n    A, B = adapted[idx], template_coords[idx]\n    cA, cB = A.mean(axis=0), B.mean(axis=0)\n    H = (A - cA).T @ (B - cB)\n    U, S, Vt = np.linalg.svd(H)\n    if np.linalg.det(Vt.T @ U.T) < 0:\n        result = adapted.copy()\n        result[:, 2] = -result[:, 2]\n        result -= result[idx].mean(axis=0) - cB\n        return result\n    return adapted\n\n\ndef adapt_template_to_query(query_seq, template_seq, template_coords):\n    aln = next(iter(_aligner.align(query_seq, template_seq)))\n    new_coords = np.full((len(query_seq), 3), np.nan)\n    for (qs, qe), (ts, te) in zip(*aln.aligned):\n        chunk = template_coords[ts:te]\n        if len(chunk) == (qe - qs):\n            new_coords[qs:qe] = chunk\n    for i in range(len(new_coords)):\n        if np.isnan(new_coords[i, 0]):\n            pv = next((j for j in range(i-1, -1, -1) if not np.isnan(new_coords[j, 0])), -1)\n            nv = next((j for j in range(i+1, len(new_coords)) if not np.isnan(new_coords[j, 0])), -1)\n            if pv >= 0 and nv >= 0:\n                w = (i - pv) / (nv - pv)\n                new_coords[i] = (1-w)*new_coords[pv] + w*new_coords[nv]\n            elif pv >= 0:\n                new_coords[i] = new_coords[pv] + [3, 0, 0]\n            elif nv >= 0:\n                new_coords[i] = new_coords[nv] + [3, 0, 0]\n            else:\n                new_coords[i] = [i*3, 0, 0]\n    result = np.nan_to_num(new_coords)\n    if VERSION >= 13:\n        result = _fix_reflection(result, template_coords)\n    return result\n\n\ndef _rotmat(axis, ang):\n    a = np.asarray(axis, float); a /= np.linalg.norm(a) + 1e-12\n    x, y, z = a; c, s = np.cos(ang), np.sin(ang); CC = 1 - c\n    return np.array([[c+x*x*CC, x*y*CC-z*s, x*z*CC+y*s],\n                     [y*x*CC+z*s, c+y*y*CC, y*z*CC-x*s],\n                     [z*x*CC-y*s, z*y*CC+x*s, c+z*z*CC]])\n\n\ndef adaptive_rna_constraints(coords, seq, confidence=1.0):\n    X = coords.copy()\n    L = len(X)\n    if L < 3:\n        return X\n    strength = max(0.75 * (1.0 - min(confidence, 0.97)), 0.02)\n    for _ in range(CONSTRAINT_PASSES):\n        d = X[1:] - X[:-1]\n        dist = np.linalg.norm(d, axis=1) + 1e-6\n        if VERSION >= 12 and seq is not None:\n            targets = np.array([\n                DINUC_BOND_TARGETS.get((seq[j], seq[j+1]), DINUC_DEFAULT)\n                for j in range(L - 1)\n            ])\n        else:\n            targets = BOND_TARGET_I_I1\n        adj = d * ((targets - dist) / dist)[:, None] * (CONSTRAINT_BOND_STRENGTH * strength)\n        X[:-1] -= adj; X[1:] += adj\n\n        d2 = X[2:] - X[:-2]; d2n = np.linalg.norm(d2, axis=1) + 1e-6\n        adj2 = d2 * ((BOND_TARGET_I_I2 - d2n) / d2n)[:, None] * (0.10 * strength)\n        X[:-2] -= adj2; X[2:] += adj2\n\n        X[1:-1] += (0.06 * strength) * (0.5 * (X[:-2] + X[2:]) - X[1:-1])\n\n        if L >= 25:\n            idx = np.linspace(0, L-1, min(L, 160)).astype(int) if L > 220 else np.arange(L)\n            P = X[idx]; diff = P[:, None, :] - P[None, :, :]\n            dm = np.linalg.norm(diff, axis=2) + 1e-6\n            sep = np.abs(idx[:, None] - idx[None, :])\n            mask = (sep > 2) & (dm < SELF_AVOID_DIST)\n            if np.any(mask):\n                vec = (diff * ((SELF_AVOID_DIST - dm) / dm)[:,:,None] * mask[:,:,None]).sum(axis=1)\n                X[idx] += (0.015 * strength) * vec\n    return X\n\n\ndef generate_rna_structure(sequence, seed):\n    np.random.seed(seed % (2**32))\n    n = len(sequence)\n    coords = np.zeros((n, 3))\n    if VERSION >= 11:\n        rise, radius, twist = 2.8, 10.0, 0.57\n        offsets = {'A': 0.1, 'C': -0.1, 'G': 0.15, 'U': -0.15}\n        for i in range(n):\n            ang = i * twist\n            off = offsets.get(sequence[i], 0.0)\n            coords[i] = [(radius+off)*np.cos(ang), (radius+off)*np.sin(ang), i*rise]\n    else:\n        for i in range(n):\n            ang = i * 0.6\n            coords[i] = [10.0*np.cos(ang), 10.0*np.sin(ang), i*2.5]\n    return coords\n\nprint('TBMコア関数を定義完了')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-08T05:24:29.758795Z","iopub.execute_input":"2026-03-08T05:24:29.75909Z","iopub.status.idle":"2026-03-08T05:24:29.786073Z","shell.execute_reply.started":"2026-03-08T05:24:29.759066Z","shell.execute_reply":"2026-03-08T05:24:29.785289Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 7: Protenix ヘルパー (Kaggle環境のみ)\n# ============================================================\n\ndef setup_protenix():\n    \"\"\"Protenix環境をセットアップ。Kaggle GPUが必要。\"\"\"\n    if not IS_KAGGLE or not USE_PROTENIX:\n        print('Protenix: 無効 (ローカル環境 or USE_PROTENIX=False)')\n        return None, None, None\n\n    import torch\n    if not torch.cuda.is_available():\n        print('WARNING: GPU未検出。Protenixは使用不可。')\n        return None, None, None\n\n    os.environ['PROTENIX_ROOT_DIR'] = PROTENIX_ROOT_DIR\n    sys.path.append(PROTENIX_CODE_DIR)\n\n    # ファイル確認\n    for p, name in [\n        (Path(PROTENIX_ROOT_DIR) / 'checkpoint' / f'{MODEL_NAME}.pt', 'checkpoint'),\n        (Path(PROTENIX_ROOT_DIR) / 'common' / 'components.cif', 'CCD file'),\n    ]:\n        if not p.exists():\n            print(f'WARNING: {name} not found: {p}')\n            return None, None, None\n\n    try:\n        from protenix.data.inference.infer_dataloader import InferenceDataset\n        from runner.inference import (\n            InferenceRunner,\n            update_gpu_compatible_configs,\n            update_inference_configs\n        )\n        from configs.configs_base import configs as configs_base\n        from configs.configs_data import data_configs\n        from configs.configs_inference import inference_configs\n        from configs.configs_model_type import model_configs\n        from protenix.config.config import parse_configs\n\n        # seed\n        torch.manual_seed(SEED)\n        torch.cuda.manual_seed_all(SEED)\n        np.random.seed(SEED)\n\n        print(f'Protenix: セットアップ完了 (GPU: {torch.cuda.get_device_name(0)})')\n        return InferenceDataset, InferenceRunner, {\n            'update_gpu_compatible_configs': update_gpu_compatible_configs,\n            'update_inference_configs': update_inference_configs,\n            'configs_base': configs_base,\n            'data_configs': data_configs,\n            'inference_configs': inference_configs,\n            'model_configs': model_configs,\n            'parse_configs': parse_configs,\n        }\n    except Exception as e:\n        print(f'Protenix セットアップ失敗: {e}')\n        return None, None, None\n\n\ndef build_input_json(target_ids, sequences, json_path):\n    data = [\n        {'name': tid, 'covalent_bonds': [],\n         'sequences': [{'rnaSequence': {'sequence': seq, 'count': 1}}]}\n        for tid, seq in zip(target_ids, sequences)\n    ]\n    with open(json_path, 'w') as f:\n        json.dump(data, f)\n    return json_path\n\n\ndef run_protenix_batch(target_ids, sequences, n_samples_per_target,\n                        InferenceDataset, InferenceRunner, ptx_helpers):\n    \"\"\"Protenixでバッチ推論。Returns: {target_id: (n_samples, L, 3) array}\"\"\"\n    import torch\n\n    work_dir = Path('/kaggle/working')\n    json_path = str(work_dir / 'val_protenix_input.json')\n\n    # 配列長をMAX_SEQ_LENに制限\n    trunc_seqs = [s[:MAX_SEQ_LEN] for s in sequences]\n    build_input_json(target_ids, trunc_seqs, json_path)\n\n    # configs構築\n    base = {**ptx_helpers['configs_base'], **{'data': ptx_helpers['data_configs']},\n            **ptx_helpers['inference_configs']}\n    def deep_update(t, p):\n        for k, v in p.items():\n            if isinstance(v, dict) and k in t and isinstance(t[k], dict):\n                deep_update(t[k], v)\n            else:\n                t[k] = v\n    deep_update(base, ptx_helpers['model_configs'][MODEL_NAME])\n\n    arg_str = ' '.join([\n        f'--model_name {MODEL_NAME}',\n        f'--input_json_path {json_path}',\n        f'--dump_dir {str(work_dir / \"val_outputs\")}',\n        f'--use_msa {USE_MSA}',\n        f'--use_template {USE_TEMPLATE}',\n        f'--use_rna_msa {USE_RNA_MSA}',\n        f'--sample_diffusion.N_sample {MODEL_N_SAMPLE}',\n        f'--seeds {SEED}',\n    ])\n    configs = ptx_helpers['parse_configs'](configs=base, arg_str=arg_str, fill_required_with_null=True)\n    configs = ptx_helpers['update_gpu_compatible_configs'](configs)\n\n    runner = InferenceRunner(configs)\n    dataset = InferenceDataset(configs)\n\n    results = {}\n    n_map = dict(zip(target_ids, n_samples_per_target))\n    full_seq_map = dict(zip(target_ids, sequences))\n\n    for i in tqdm(range(len(dataset)), desc='Protenix推論'):\n        data, atom_array, error_message = dataset[i]\n        target_id = data.get('sample_name', f'sample_{i}')\n\n        if target_id not in n_map:\n            continue\n\n        n_needed = n_map[target_id]\n        full_seq = full_seq_map[target_id]\n\n        if error_message:\n            print(f'  {target_id}: データエラー — {error_message}')\n            results[target_id] = None\n            del data, atom_array; gc.collect(); continue\n\n        try:\n            new_cfg = ptx_helpers['update_inference_configs'](configs, data['N_token'].item())\n            new_cfg.sample_diffusion.N_sample = n_needed\n            runner.update_model_configs(new_cfg)\n\n            prediction = runner.predict(data)\n            raw_coords = prediction['coordinate']\n            feat = data['input_feature_dict']\n\n            if 'centre_atom_mask' in feat:\n                mask = (feat['centre_atom_mask'] == 1).to(raw_coords.device)\n            elif 'atom_to_tokatom_idx' in feat:\n                m11 = (feat['atom_to_tokatom_idx'] == 11).to(raw_coords.device)\n                m12 = (feat['atom_to_tokatom_idx'] == 12).to(raw_coords.device)\n                mask = m11 if abs(m11.sum()-len(full_seq)) < abs(m12.sum()-len(full_seq)) else m12\n            else:\n                mask = torch.zeros(raw_coords.shape[1], dtype=torch.bool, device=raw_coords.device)\n\n            coords = raw_coords[:, mask, :].detach().cpu().numpy()\n\n            # collapse check\n            if coords.shape[1] > 1:\n                diffs = np.linalg.norm(coords[0, 1:] - coords[0, :-1], axis=-1)\n                if np.all(diffs < 1e-4):\n                    print(f'  WARNING: {target_id}: Model collapse')\n                    coords = np.zeros((coords.shape[0], len(full_seq), 3))\n\n            # length mismatch → pad\n            current_len = coords.shape[1]\n            target_len = len(full_seq)\n            if current_len != target_len:\n                padded = np.zeros((coords.shape[0], target_len, 3), dtype=np.float32)\n                ml = min(current_len, target_len)\n                if ml > 0:\n                    padded[:, :ml, :] = coords[:, :ml, :]\n                    if VERSION >= 11 and ml < target_len:\n                        padded[:, ml:, :] = coords[:, ml-1:ml, :]\n                coords = padded\n\n            results[target_id] = coords\n\n        except Exception as exc:\n            print(f'  {target_id}: Protenix失敗 — {exc}')\n            results[target_id] = None\n        finally:\n            del data, atom_array\n            if 'prediction' in dir(): del prediction\n            if 'raw_coords' in dir(): del raw_coords\n            torch.cuda.empty_cache(); gc.collect()\n\n    return results\n\n\n# セットアップ\nInferenceDataset, InferenceRunner, ptx_helpers = setup_protenix()\nPROTENIX_AVAILABLE = InferenceDataset is not None\nprint(f'Protenix利用可能: {PROTENIX_AVAILABLE}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-08T05:24:29.787085Z","iopub.execute_input":"2026-03-08T05:24:29.787354Z","iopub.status.idle":"2026-03-08T05:24:31.387604Z","shell.execute_reply.started":"2026-03-08T05:24:29.787332Z","shell.execute_reply":"2026-03-08T05:24:31.386855Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 8: 評価実行\n# ============================================================\n\ndef evaluate_pipeline(val_seqs_df, ground_truth, train_seqs_df, train_coords,\n                       use_protenix=False, verbose=True):\n    \"\"\"\n    バリデーション評価メインループ。\n    1. TBMでテンプレート検索・予測\n    2. 足りない分をProtenix (利用可能な場合)\n    3. それでも足りなければde-novoフォールバック\n    4. TM-score計算\n    \"\"\"\n    results = []\n    protenix_queue = {}  # {tid: (n_needed, seq)}\n    tbm_preds_cache = {}  # {tid: [pred1, pred2, ...]}\n\n    # Phase 1: TBM\n    print(f'\\n=== Phase 1: TBM (VERSION={VERSION}) ===')\n    for _, row in tqdm(val_seqs_df.iterrows(), total=len(val_seqs_df), desc='TBM'):\n        tid = row['target_id']\n        seq = row['sequence']\n        if tid not in ground_truth:\n            continue\n\n        similar = find_similar_sequences(seq, train_seqs_df, train_coords)\n        preds = []\n        used = set()\n\n        for i, (tmpl_id, tmpl_seq, sim, tmpl_coords, pct_id) in enumerate(similar):\n            if len(preds) >= N_SAMPLE:\n                break\n            if sim < MIN_SIMILARITY or pct_id < MIN_PERCENT_IDENTITY:\n                break\n            if tmpl_id in used:\n                continue\n\n            rng = np.random.default_rng((hash(tid) + i * 10007) % (2**32))\n            adapted = adapt_template_to_query(seq, tmpl_seq, tmpl_coords)\n\n            slot = len(preds)\n            if slot == 0:\n                X = adapted\n            elif slot == 1:\n                X = adapted + rng.normal(0, max(0.01, (0.40-sim)*0.06), adapted.shape)\n            elif slot == 2:\n                X = adapted.copy()\n                L = len(X)\n                if L >= 30:\n                    pivot = int(rng.integers(10, L-10))\n                    R = _rotmat(rng.normal(size=3), np.deg2rad(float(rng.uniform(-HINGE_DEG, HINGE_DEG))))\n                    p0 = X[pivot].copy()\n                    X[pivot+1:] = (X[pivot+1:] - p0) @ R.T + p0\n            elif slot == 3:\n                X = adapted.copy()\n                gc_ = X.mean(0, keepdims=True)\n                R = _rotmat(rng.normal(size=3), np.deg2rad(float(rng.uniform(-JITTER_DEG, JITTER_DEG))))\n                shift = rng.normal(size=3)\n                shift = shift / (np.linalg.norm(shift)+1e-12) * float(rng.uniform(0, JITTER_TRANS))\n                c = X.mean(0, keepdims=True)\n                X = (X - c) @ R.T + c + shift\n                X -= X.mean(0, keepdims=True) - gc_\n            else:\n                X = adapted.copy()\n                L = len(X)\n                if L >= 20:\n                    ctrl = np.linspace(0, L-1, 6)\n                    disp = rng.normal(0, WIGGLE_AMP, (6, 3))\n                    t = np.arange(L)\n                    X += np.vstack([np.interp(t, ctrl, disp[:, k]) for k in range(3)]).T\n\n            refined = adaptive_rna_constraints(X, seq, confidence=sim)\n            preds.append(refined)\n            used.add(tmpl_id)\n\n        tbm_preds_cache[tid] = preds\n        n_needed = N_SAMPLE - len(preds)\n        if n_needed > 0:\n            protenix_queue[tid] = (n_needed, seq)\n\n    n_full_tbm = sum(1 for v in tbm_preds_cache.values() if len(v) >= N_SAMPLE)\n    print(f'  TBMのみで完了: {n_full_tbm}/{len(tbm_preds_cache)}')\n    print(f'  Protenix必要: {len(protenix_queue)}')\n\n    # Phase 2: Protenix\n    protenix_results = {}\n    if protenix_queue and use_protenix and PROTENIX_AVAILABLE:\n        print(f'\\n=== Phase 2: Protenix ({len(protenix_queue)} targets) ===')\n        tids = list(protenix_queue.keys())\n        seqs = [protenix_queue[t][1] for t in tids]\n        n_samples = [protenix_queue[t][0] for t in tids]\n        protenix_results = run_protenix_batch(\n            tids, seqs, n_samples,\n            InferenceDataset, InferenceRunner, ptx_helpers\n        )\n    elif protenix_queue and (not use_protenix or not PROTENIX_AVAILABLE):\n        print(f'\\n=== Phase 2: Protenixスキップ ===')\n\n    # Phase 3: 統合 + TM-score\n    print(f'\\n=== Phase 3: 統合 + TM-score ===')\n    for tid in tqdm(tbm_preds_cache.keys(), desc='評価'):\n        seq = val_seqs_df[val_seqs_df['target_id'] == tid]['sequence'].iloc[0]\n        gt_conformations = ground_truth[tid]\n\n        combined = list(tbm_preds_cache[tid])\n\n        # Protenix追加\n        ptx = protenix_results.get(tid)\n        if ptx is not None and ptx.ndim == 3:\n            for j in range(ptx.shape[0]):\n                if len(combined) >= N_SAMPLE:\n                    break\n                combined.append(ptx[j])\n\n        # De-novoフォールバック\n        while len(combined) < N_SAMPLE:\n            dn = generate_rna_structure(seq, seed=hash(tid)+len(combined)*1000)\n            refined = adaptive_rna_constraints(dn, seq, confidence=0.2)\n            combined.append(refined)\n\n        # TM-score: max over predictions × max over conformations\n        best_tm = 0.0\n        for pred in combined[:N_SAMPLE]:\n            for gt_conf in gt_conformations:\n                score = tm_score(pred, gt_conf)\n                best_tm = max(best_tm, score)\n\n        n_tbm = len(tbm_preds_cache[tid])\n        n_ptx = min(N_SAMPLE - n_tbm, ptx.shape[0]) if ptx is not None and hasattr(ptx, 'shape') else 0\n        method = 'TBM' if n_tbm >= N_SAMPLE else ('TBM+Protenix' if n_ptx > 0 else 'de-novo')\n\n        results.append({\n            'target_id': tid,\n            'seq_len': len(seq),\n            'n_templates': len(find_similar_sequences(seq, train_seqs_df, train_coords)) if n_tbm > 0 else 0,\n            'n_tbm_used': n_tbm,\n            'n_protenix_used': n_ptx,\n            'method': method,\n            'best_tm_score': best_tm,\n        })\n\n        if verbose:\n            print(f'  {tid} (L={len(seq):4d}): TM={best_tm:.4f}  [{method}]')\n\n    if not results:\n        return 0.0, pd.DataFrame()\n\n    df = pd.DataFrame(results)\n    mean_tm = df['best_tm_score'].mean()\n    return mean_tm, df\n\n\n# 実行\nt_start = time.time()\nmean_tm, results_df = evaluate_pipeline(\n    val_seqs, ground_truth, train_seqs, train_coords,\n    use_protenix=USE_PROTENIX\n)\nelapsed = time.time() - t_start\n\nprint(f'\\n{\"=\"*50}')\nprint(f'平均TM-score: {mean_tm:.4f}')\nprint(f'ターゲット数: {len(results_df)}')\nprint(f'処理時間: {elapsed:.1f}s')\nprint(f'{\"=\"*50}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-08T05:24:31.388678Z","iopub.execute_input":"2026-03-08T05:24:31.389192Z","iopub.status.idle":"2026-03-08T06:26:06.987626Z","shell.execute_reply.started":"2026-03-08T05:24:31.389167Z","shell.execute_reply":"2026-03-08T06:26:06.987031Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 9: 結果の詳細分析\n# ============================================================\n\nif len(results_df) > 0:\n    print('--- 統計サマリ ---')\n    print(f'  TM-score 平均: {results_df[\"best_tm_score\"].mean():.4f}')\n    print(f'  TM-score 中央値: {results_df[\"best_tm_score\"].median():.4f}')\n    print(f'  TM-score 標準偏差: {results_df[\"best_tm_score\"].std():.4f}')\n    print(f'  TM > 0.5: {(results_df[\"best_tm_score\"] > 0.5).sum()}/{len(results_df)}')\n    print(f'  TM > 0.3: {(results_df[\"best_tm_score\"] > 0.3).sum()}/{len(results_df)}')\n    print()\n    print('--- 手法別内訳 ---')\n    for method, grp in results_df.groupby('method'):\n        print(f'  {method}: {len(grp)} targets, mean TM={grp[\"best_tm_score\"].mean():.4f}')\n    print()\n    print('--- ターゲット別 (TM降順) ---')\n    for _, r in results_df.sort_values('best_tm_score', ascending=False).iterrows():\n        print(f'  {r[\"target_id\"]:6s} L={r[\"seq_len\"]:4d}  TM={r[\"best_tm_score\"]:.4f}  [{r[\"method\"]}]')\n\n    # CSV保存\n    out_path = 'validation_results_full.csv'\n    results_df.to_csv(out_path, index=False)\n    print(f'\\n結果保存: {out_path}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-08T06:26:06.988547Z","iopub.execute_input":"2026-03-08T06:26:06.988838Z","iopub.status.idle":"2026-03-08T06:26:07.002195Z","shell.execute_reply.started":"2026-03-08T06:26:06.988804Z","shell.execute_reply":"2026-03-08T06:26:07.001568Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 10: パラメータスイープ (オプション)\n# ============================================================\n# ★★★ RUN_SWEEP = True にして実行 ★★★\n\nRUN_SWEEP = False\n\nif RUN_SWEEP:\n    sweep_configs = []\n\n    # スイープする変数を定義\n    for v in [10, 12, 13]:\n        sweep_configs.append(('version', v, {'VERSION': v}))\n\n    for pct in [30, 40, 50, 60, 70]:\n        sweep_configs.append(('min_pct_id', pct, {'MIN_PERCENT_IDENTITY': pct}))\n\n    for cov in [0.3, 0.4, 0.5, 0.6, 0.7]:\n        sweep_configs.append(('min_coverage', cov, {'MIN_COVERAGE': cov}))\n\n    for ns in [0.1, 0.15, 0.2, 0.25, 0.3]:\n        sweep_configs.append(('norm_score_cutoff', ns, {'NORM_SCORE_CUTOFF': ns}))\n\n    for bs in [0.05, 0.10, 0.15, 0.22, 0.30]:\n        sweep_configs.append(('bond_strength', bs, {'CONSTRAINT_BOND_STRENGTH': bs}))\n\n    for cp in [0, 1, 2, 3, 5]:\n        sweep_configs.append(('constraint_passes', cp, {'CONSTRAINT_PASSES': cp}))\n\n    print(f'パラメータスイープ: {len(sweep_configs)} 設定')\n    print('='*70)\n\n    sweep_results = []\n    # 元の値を保存\n    original_globals = {\n        'VERSION': VERSION, 'MIN_PERCENT_IDENTITY': MIN_PERCENT_IDENTITY,\n        'MIN_COVERAGE': MIN_COVERAGE, 'NORM_SCORE_CUTOFF': NORM_SCORE_CUTOFF,\n        'CONSTRAINT_BOND_STRENGTH': CONSTRAINT_BOND_STRENGTH,\n        'CONSTRAINT_PASSES': CONSTRAINT_PASSES,\n    }\n\n    for param_name, param_val, overrides in sweep_configs:\n        # グローバル変数を一時変更\n        for k, v in overrides.items():\n            globals()[k] = v\n\n        # アライナー再作成 (VERSIONが変わった場合)\n        if 'VERSION' in overrides:\n            globals()['_aligner'] = _make_aligner()\n\n        t0 = time.time()\n        mean_tm_sw, _ = evaluate_pipeline(\n            val_seqs, ground_truth, train_seqs, train_coords,\n            use_protenix=False, verbose=False\n        )\n        elapsed_sw = time.time() - t0\n\n        sweep_results.append({\n            'param': param_name, 'value': param_val,\n            'mean_tm': mean_tm_sw, 'elapsed_s': elapsed_sw\n        })\n        print(f'  {param_name:22s} = {str(param_val):8s} → TM = {mean_tm_sw:.4f}  ({elapsed_sw:.1f}s)')\n\n        # 元に戻す\n        for k, v in original_globals.items():\n            globals()[k] = v\n        globals()['_aligner'] = _make_aligner()\n\n    # サマリ表示\n    sw_df = pd.DataFrame(sweep_results)\n    print(f'\\n{\"=\"*70}')\n    print('スイープ結果サマリ:')\n    for param in sw_df['param'].unique():\n        sub = sw_df[sw_df['param']==param].sort_values('mean_tm', ascending=False)\n        print(f'\\n--- {param} ---')\n        for _, r in sub.iterrows():\n            marker = ' ★' if r['mean_tm'] == sub['mean_tm'].max() else ''\n            print(f'  {str(r[\"value\"]):8s} → {r[\"mean_tm\"]:.4f}{marker}')\n\n    sw_df.to_csv('sweep_results.csv', index=False)\n    print(f'\\nスイープ結果保存: sweep_results.csv')\nelse:\n    print('スイープ無効。Cell上部の RUN_SWEEP = True で実行可能。')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-08T06:28:12.964941Z","iopub.execute_input":"2026-03-08T06:28:12.965766Z","iopub.status.idle":"2026-03-08T07:13:07.927338Z","shell.execute_reply.started":"2026-03-08T06:28:12.965734Z","shell.execute_reply":"2026-03-08T07:13:07.926276Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Cell 11: 可視化 (オプション)\n# ============================================================\ntry:\n    import matplotlib.pyplot as plt\n\n    if len(results_df) > 0:\n        fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n\n        # 1. TM-score分布\n        axes[0].hist(results_df['best_tm_score'], bins=20, edgecolor='black', alpha=0.7)\n        axes[0].axvline(x=0.5, color='r', linestyle='--', label='TM=0.5 (correct fold)')\n        axes[0].axvline(x=results_df['best_tm_score'].mean(), color='g', linestyle='--', label=f'Mean={results_df[\"best_tm_score\"].mean():.3f}')\n        axes[0].set_xlabel('TM-score')\n        axes[0].set_ylabel('Count')\n        axes[0].set_title('TM-score Distribution')\n        axes[0].legend()\n\n        # 2. 配列長 vs TM-score\n        colors = {'TBM': 'blue', 'TBM+Protenix': 'green', 'de-novo': 'red'}\n        for method in results_df['method'].unique():\n            sub = results_df[results_df['method'] == method]\n            axes[1].scatter(sub['seq_len'], sub['best_tm_score'],\n                           label=method, color=colors.get(method, 'gray'), s=50, alpha=0.7)\n        axes[1].set_xlabel('Sequence Length')\n        axes[1].set_ylabel('TM-score')\n        axes[1].set_title('Length vs TM-score')\n        axes[1].legend()\n\n        # 3. 手法別スコア\n        method_stats = results_df.groupby('method')['best_tm_score'].agg(['mean', 'count'])\n        bars = axes[2].bar(method_stats.index, method_stats['mean'],\n                          color=[colors.get(m, 'gray') for m in method_stats.index])\n        for bar, (_, row) in zip(bars, method_stats.iterrows()):\n            axes[2].text(bar.get_x() + bar.get_width()/2, bar.get_height(),\n                        f'n={int(row[\"count\"])}', ha='center', va='bottom')\n        axes[2].set_ylabel('Mean TM-score')\n        axes[2].set_title('Score by Method')\n\n        plt.tight_layout()\n        plt.savefig('validation_analysis.png', dpi=150, bbox_inches='tight')\n        plt.show()\n        print('保存: validation_analysis.png')\nexcept ImportError:\n    print('matplotlib未インストール。可視化スキップ。')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-08T06:26:07.018116Z","iopub.execute_input":"2026-03-08T06:26:07.01833Z","iopub.status.idle":"2026-03-08T06:26:07.87661Z","shell.execute_reply.started":"2026-03-08T06:26:07.018311Z","shell.execute_reply":"2026-03-08T06:26:07.875285Z"}},"outputs":[],"execution_count":null}]}