{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":87793,"databundleVersionId":11403143,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":10984389,"sourceType":"datasetVersion","datasetId":6836394}],"dockerImageVersionId":30918,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader, ConcatDataset\nfrom torch.nn.utils.rnn import pad_sequence\n\n#####################################\n# 1. Carga de archivos de la competencia\n#####################################\nbase_path = '/kaggle/input/stanford-rna-3d-folding'\ntrain_sequences_file = os.path.join(base_path, 'train_sequences.csv')\ntrain_labels_file = os.path.join(base_path, 'train_labels.csv')\nvalidation_sequences_file = os.path.join(base_path, 'validation_sequences.csv')\nvalidation_labels_file = os.path.join(base_path, 'validation_labels.csv')\ntest_sequences_file = os.path.join(base_path, 'test_sequences.csv')\n\ntrain_sequences_df = pd.read_csv(train_sequences_file)\ntrain_labels_df = pd.read_csv(train_labels_file)\nvalidation_sequences_df = pd.read_csv(validation_sequences_file)\nvalidation_labels_df = pd.read_csv(validation_labels_file)\ntest_sequences_df = pd.read_csv(test_sequences_file)\n\nprint(\"Train sequences:\", train_sequences_df.shape)\nprint(\"Validation sequences:\", validation_sequences_df.shape)\nprint(\"Test sequences:\", test_sequences_df.shape)\n\n#####################################\n# 2. Dataset experimental\n#####################################\nclass RNADataset(Dataset):\n    def __init__(self, seq_csv, label_csv=None, max_length=512):\n        self.seq_df = pd.read_csv(seq_csv)\n        self.label_csv = label_csv\n        if label_csv is not None:\n            self.label_df = pd.read_csv(label_csv)\n        else:\n            self.label_df = None\n        self.max_length = max_length\n        self.token_dict = {'A': 0, 'C': 1, 'G': 2, 'U': 3}\n        self.unk_token = 4\n\n        # Filtrar secuencias que tienen coordenadas válidas (si se cuenta con etiquetas)\n        if self.label_df is not None:\n            valid_indices = []\n            for idx, row in self.seq_df.iterrows():\n                target_id = row['target_id']\n                seq = row['sequence'].strip()\n                tokens = self.tokenize(seq)\n                L = len(tokens)\n                coords = self.get_coordinates(target_id, L)\n                if coords is not None:\n                    valid_indices.append(idx)\n                else:\n                    print(f\"Filtrando target {target_id} sin coordenadas válidas.\")\n            self.seq_df = self.seq_df.loc[valid_indices].reset_index(drop=True)\n            print(f\"Dataset experimental filtrado: {len(self.seq_df)} muestras válidas.\")\n\n    def tokenize(self, seq):\n        tokens = [self.token_dict.get(ch, self.unk_token) for ch in seq][:self.max_length]\n        return torch.tensor(tokens, dtype=torch.long)\n    \n    def get_coordinates(self, target_id, L):\n        if self.label_df is None:\n            return None\n        df_target = self.label_df[self.label_df['ID'].str.startswith(target_id)]\n        if df_target.empty:\n            return None\n        coords_flat = df_target.iloc[0, 3:].values.astype(float)\n        if np.isnan(coords_flat).all():\n            print(f\"Warning: todas las coordenadas son NaN para target {target_id}\")\n            return None\n        try:\n            coords = torch.tensor(coords_flat, dtype=torch.float).view(-1, 3)\n        except Exception as e:\n            print(f\"Error en reshape para target {target_id}: {e}\")\n            return None\n        if coords.shape[0] > L:\n            coords = coords[:L]\n        elif coords.shape[0] < L:\n            pad = torch.zeros((L - coords.shape[0], 3), dtype=torch.float)\n            coords = torch.cat([coords, pad], dim=0)\n        coords = torch.where(torch.isnan(coords), torch.zeros_like(coords), coords)\n        return coords\n\n    def __len__(self):\n        return len(self.seq_df)\n    \n    def __getitem__(self, idx):\n        row = self.seq_df.iloc[idx]\n        target_id = row['target_id']\n        seq = row['sequence'].strip()\n        tokens = self.tokenize(seq)\n        sample = {\n            'tokens': tokens,\n            'target_id': target_id,\n            'length': len(tokens)  # Longitud real sin padding\n        }\n        if self.label_df is not None:\n            coords = self.get_coordinates(target_id, len(tokens))\n            if coords is not None:\n                sample['coords'] = coords\n        return sample\n\n#####################################\n# 3. Dataset sintético\n#####################################\nclass SyntheticRNADataset(Dataset):\n    def __init__(self, base_folder, max_length=512):\n        self.max_length = max_length\n        self.pdb_files = []\n        for root, dirs, files in os.walk(base_folder):\n            for file in files:\n                if file.endswith('.pdb'):\n                    self.pdb_files.append(os.path.join(root, file))\n        self.samples = []\n        for pdb_file in self.pdb_files:\n            sample = self.parse_pdb(pdb_file)\n            if sample is not None:\n                self.samples.append(sample)\n        print(f\"Dataset sintético: {len(self.samples)} muestras extraídas de PDB.\")\n\n    def __len__(self):\n        return len(self.samples)\n\n    def __getitem__(self, idx):\n        return self.samples[idx]\n\n    def parse_pdb(self, pdb_file):\n        target_id = os.path.splitext(os.path.basename(pdb_file))[0]\n        try:\n            with open(pdb_file, 'r') as f:\n                lines = f.readlines()\n        except Exception as e:\n            print(f\"Error al leer {pdb_file}: {e}\")\n            return None\n        residues = {}\n        for line in lines:\n            if line.startswith(\"ATOM\"):\n                atom_name = line[12:16].strip()\n                resname = line[17:20].strip()\n                try:\n                    resSeq = int(line[22:26].strip())\n                    x = float(line[30:38].strip())\n                    y = float(line[38:46].strip())\n                    z = float(line[46:54].strip())\n                except Exception:\n                    continue\n                # Prioridad: C1' o C1*; si no, P\n                if atom_name in [\"C1'\", \"C1*\"]:\n                    residues[resSeq] = (resname, np.array([x, y, z]))\n                elif atom_name == \"P\" and resSeq not in residues:\n                    residues[resSeq] = (resname, np.array([x, y, z]))\n        if len(residues) == 0:\n            return None\n        sorted_keys = sorted(residues.keys())\n        # Convertir cada nombre de residuo a una sola letra (usando el primer carácter)\n        sequence = \"\".join([residues[k][0][0] for k in sorted_keys])\n        coords = np.stack([residues[k][1] for k in sorted_keys], axis=0)\n        if len(sequence) > self.max_length:\n            sequence = sequence[:self.max_length]\n            coords = coords[:self.max_length, :]\n        sample = {\n            \"target_id\": target_id,\n            \"sequence\": sequence,\n            \"coords\": torch.tensor(coords, dtype=torch.float),\n            \"length\": min(len(sequence), self.max_length)\n        }\n        return sample\n\n#####################################\n# 4. Función de collate combinada para ambos datasets\n#####################################\ndef combined_collate_fn(batch):\n    token_dict = {'A': 0, 'C': 1, 'G': 2, 'U': 3}\n    unk_token = 4\n    tokens_list = []\n    lengths = []\n    coords_list = []\n    target_ids = []\n    for item in batch:\n        # Si ya existe \"tokens\", la usamos; si no, generamos a partir de \"sequence\"\n        if 'tokens' in item:\n            tokens = item['tokens']\n        elif 'sequence' in item:\n            seq = item['sequence']\n            tokens = torch.tensor([token_dict.get(ch, unk_token) for ch in seq], dtype=torch.long)\n        else:\n            raise KeyError(\"La muestra no contiene ni 'tokens' ni 'sequence'.\")\n        tokens_list.append(tokens)\n        lengths.append(len(tokens))  # Tomar la longitud original sin padding\n        target_ids.append(item['target_id'])\n        if 'coords' in item:\n            coords_list.append(item['coords'])\n        else:\n            coords_list.append(torch.tensor([]))\n    padded_tokens = pad_sequence(tokens_list, batch_first=True, padding_value=0)\n    if all(c.numel() > 0 for c in coords_list):\n        padded_coords = pad_sequence(coords_list, batch_first=True, padding_value=0.0)\n    else:\n        padded_coords = None\n    batch_dict = {'tokens': padded_tokens, 'target_ids': target_ids, 'lengths': lengths}\n    if padded_coords is not None:\n        batch_dict['coords'] = padded_coords\n    return batch_dict\n\n#####################################\n# 5. Modelo predictivo: Transformer para predecir matriz de distancias\n#####################################\nclass MiniLLM(nn.Module):\n    def __init__(self, vocab_size=5, embed_dim=256, n_heads=8, num_layers=6, hidden_dim=512, max_length=512):\n        super(MiniLLM, self).__init__()\n        self.max_length = max_length\n        self.embedding = nn.Embedding(vocab_size, embed_dim)\n        self.pos_embedding = nn.Parameter(torch.randn(1, max_length, embed_dim))\n        encoder_layer = nn.TransformerEncoderLayer(\n            d_model=embed_dim,\n            nhead=n_heads,\n            dim_feedforward=hidden_dim,\n            dropout=0.1,\n            batch_first=True\n        )\n        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)\n        # Cabeza para predecir la distancia entre pares de residuos\n        self.distance_head = nn.Sequential(\n            nn.Linear(2 * embed_dim, hidden_dim),\n            nn.ReLU(),\n            nn.Linear(hidden_dim, 1)\n        )\n        \n    def forward(self, tokens, mask=None):\n        # tokens: [B, L]\n        x = self.embedding(tokens)  # [B, L, embed_dim]\n        pos_emb = self.pos_embedding[:, :tokens.size(1), :]\n        x = x + pos_emb\n        x = self.transformer(x, src_key_padding_mask=mask)  # [B, L, embed_dim]\n        B, L, _ = x.size()\n        x_i = x.unsqueeze(2).expand(B, L, L, x.size(-1))\n        x_j = x.unsqueeze(1).expand(B, L, L, x.size(-1))\n        pair_features = torch.cat([x_i, x_j], dim=-1)  # [B, L, L, 2*embed_dim]\n        dist_pred = self.distance_head(pair_features).squeeze(-1)  # [B, L, L]\n        return torch.relu(dist_pred)\n\n#####################################\n# 6. Reconstrucción 3D mediante MDS\n#####################################\ndef mds_reconstruction(distance_matrix):\n    L = distance_matrix.shape[0]\n    J = np.eye(L) - np.ones((L, L)) / L\n    B = -0.5 * J @ (distance_matrix ** 2) @ J\n    eigvals, eigvecs = np.linalg.eigh(B)\n    idx = np.argsort(eigvals)[::-1][:3]\n    coords = eigvecs[:, idx] * np.sqrt(np.maximum(eigvals[idx], 0))\n    return coords\n\n#####################################\n# 7. Refinamiento y escalado\n#####################################\ndef refine_structure(init_coords, predicted_dist, num_steps=100, lr=1e-4, bond_length=5.0, bond_weight=0.1):\n    coords = init_coords.clone().detach().requires_grad_(True)\n    optimizer_coords = optim.Adam([coords], lr=lr)\n    for step in range(num_steps):\n        diff = coords.unsqueeze(0) - coords.unsqueeze(1)  # (L, L, 3)\n        dists = torch.sqrt(torch.sum(diff ** 2, dim=-1) + 1e-6)\n        loss_dist = ((dists - predicted_dist) ** 2).mean()\n        # Penalización para mantener la longitud de enlace entre residuos consecutivos\n        consecutive = torch.sqrt(torch.sum((coords[1:] - coords[:-1]) ** 2, dim=-1) + 1e-6)\n        loss_bond = ((consecutive - bond_length) ** 2).mean()\n        loss = loss_dist + bond_weight * loss_bond\n        optimizer_coords.zero_grad()\n        loss.backward()\n        torch.nn.utils.clip_grad_norm_([coords], max_norm=1.0)\n        optimizer_coords.step()\n        if step % 25 == 0:\n            print(f\"Refinamiento step {step}: loss = {loss.item():.4f}\")\n    return coords.detach().cpu().numpy()\n\ndef scale_coordinates(coords, target_bond_length=5.0):\n    diffs = coords[1:] - coords[:-1]\n    bond_lengths = np.linalg.norm(diffs, axis=1)\n    mean_bond = np.mean(bond_lengths)\n    if mean_bond < 1e-3:\n        print(\"Mean bond length muy bajo, la estructura puede estar degenerada.\")\n        return coords\n    scale = target_bond_length / mean_bond\n    return coords * scale\n\n#####################################\n# 8. Función de entrenamiento (usando longitudes válidas)\n#####################################\ndef train_model(model, dataloader, optimizer, device):\n    model.train()\n    scaler = torch.cuda.amp.GradScaler()  # para entrenamiento FP16\n    running_loss = 0.0\n    total_valid = 0.0\n\n    for batch in tqdm(dataloader, desc=\"Entrenando\"):\n        tokens = batch['tokens'].to(device)   # [B, L_pad]\n        coords = batch['coords'].to(device)     # [B, L_pad, 3]\n        lengths = batch['lengths']              # lista de longitudes reales\n        B, L_pad, _ = coords.shape\n        \n        optimizer.zero_grad()\n        \n        with torch.cuda.amp.autocast():  # habilitar FP16\n            pred_dists = model(tokens)  # [B, L_pad, L_pad]\n            # Calcular la matriz de distancias verdadera en batch (incluyendo padding)\n            diff = coords.unsqueeze(2) - coords.unsqueeze(1)  # [B, L_pad, L_pad, 3]\n            true_dists = torch.sqrt(torch.sum(diff ** 2, dim=-1) + 1e-6)  # [B, L_pad, L_pad]\n            \n            # Crear máscara para posiciones válidas según cada longitud\n            mask = torch.zeros(B, L_pad, dtype=torch.float32, device=device)\n            for i, l in enumerate(lengths):\n                mask[i, :l] = 1.0\n            mask2 = mask.unsqueeze(2) * mask.unsqueeze(1)  # [B, L_pad, L_pad]\n            \n            # Aplicar la máscara\n            valid_pred = pred_dists * mask2\n            valid_true = true_dists * mask2\n            \n            # Calcular el número de entradas válidas\n            valid_count = mask2.sum()\n            # Calcular la pérdida MSE solo sobre las posiciones válidas\n            loss = torch.nn.functional.mse_loss(valid_pred, valid_true, reduction='sum') / (valid_count + 1e-6)\n        \n        scaler.scale(loss).backward()\n        scaler.unscale_(optimizer)\n        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n        scaler.step(optimizer)\n        scaler.update()\n        \n        running_loss += loss.item() * valid_count.item()\n        total_valid += valid_count.item()\n    \n    avg_loss = running_loss / total_valid if total_valid > 0 else 0\n    print(f\"Loss de entrenamiento promedio: {avg_loss:.4f}\")\n\n#####################################\n# 9. Función de inferencia: generación de 5 predicciones\n#####################################\ndef generate_predictions(model, tokens, device, num_predictions=5, refine=True):\n    \"\"\"\n    Función mejorada para generar predicciones más robustas.\n    \"\"\"\n    model.eval()\n    tokens = tokens.to(device)\n    seq_length = tokens.size(1)  # Longitud de la secuencia\n    \n    with torch.no_grad():\n        try:\n            pred_dists = model(tokens)  # [1, L, L]\n            dist_matrix = pred_dists[0].cpu().numpy()\n        except Exception as e:\n            print(f\"Error en modelo de predicción: {e}\")\n            # Crear una matriz de distancias fallback\n            dist_matrix = np.ones((seq_length, seq_length)) * 5.0\n            np.fill_diagonal(dist_matrix, 0.0)\n    \n    # Limpiar la matriz de distancias\n    if np.isnan(dist_matrix).any() or np.isinf(dist_matrix).any():\n        print(\"Advertencia: matriz de distancias contiene NaN o Inf, se reemplazan por valores razonables.\")\n        mask = np.isnan(dist_matrix) | np.isinf(dist_matrix)\n        dist_matrix[mask] = 5.0  # Valor razonable para distancias desconocidas\n    \n    # Asegurar que la matriz sea simétrica\n    dist_matrix = 0.5 * (dist_matrix + dist_matrix.T)\n    \n    # Reconstrucción mediante MDS con manejo de errores\n    try:\n        init_coords = mds_reconstruction(dist_matrix)\n    except Exception as e:\n        print(f\"Error en MDS: {e}\")\n        # Crear coordenadas en línea recta como fallback\n        init_coords = np.zeros((seq_length, 3))\n        for i in range(seq_length):\n            init_coords[i, 0] = i * 5.0  # Separación de 5.0 en el eje X\n    \n    # Normalizar y escalar\n    try:\n        init_coords = init_coords - init_coords[0]  # Fijar primer residuo en (0,0,0)\n        init_coords = scale_coordinates(init_coords, target_bond_length=5.0)\n    except Exception as e:\n        print(f\"Error en normalización: {e}\")\n    \n    # Refinamiento con manejo de errores\n    if refine:\n        try:\n            init_coords_tensor = torch.from_numpy(init_coords).float().to(device)\n            predicted_dist_tensor = torch.from_numpy(dist_matrix).float().to(device)\n            refined = refine_structure(init_coords_tensor, predicted_dist_tensor,\n                                      num_steps=100, lr=1e-4, bond_length=5.0, bond_weight=0.1)\n            refined = scale_coordinates(refined, target_bond_length=5.0)\n        except Exception as e:\n            print(f\"Error en refinamiento: {e}\")\n            refined = init_coords\n    else:\n        refined = init_coords\n    \n    # Final limpieza y normalización\n    # refined = refined - refined[0]\n    refined = np.round(refined, 12)\n    \n    # Limpiar NaN o Inf residuales\n    if np.isnan(refined).any() or np.isinf(refined).any():\n        print(\"Advertencia: coordenadas refinadas contienen NaN o Inf, se reemplazan por ceros.\")\n        refined = np.nan_to_num(refined, nan=0.0, posinf=0.0, neginf=0.0)\n    \n    # Primera predicción\n    predictions = [refined]\n    \n    # Generar variantes\n    for _ in range(num_predictions - 1):\n        try:\n            noise = np.random.normal(scale=0.2, size=refined.shape)\n            variant = refined + noise\n            variant = scale_coordinates(variant, target_bond_length=5.0)\n            variant = variant - variant[0]\n            \n            # Limpiar posibles NaN o Inf\n            if np.isnan(variant).any() or np.isinf(variant).any():\n                variant = np.nan_to_num(variant, nan=0.0, posinf=0.0, neginf=0.0)\n                \n            variant = np.round(variant, 12)\n            predictions.append(variant)\n        except Exception as e:\n            print(f\"Error al generar variante: {e}\")\n            # Si hay error, duplicar la predicción base\n            predictions.append(refined.copy())\n    \n    # Asegurar que todas las predicciones sean válidas\n    for i in range(len(predictions)):\n        if not isinstance(predictions[i], np.ndarray) or predictions[i].shape != (seq_length, 3):\n            print(f\"Corrigiendo forma de predicción {i+1}\")\n            new_pred = np.zeros((seq_length, 3))\n            if isinstance(predictions[i], np.ndarray):\n                min_len = min(seq_length, predictions[i].shape[0])\n                if predictions[i].ndim >= 2 and predictions[i].shape[1] >= 3:\n                    new_pred[:min_len] = predictions[i][:min_len, :3]\n            predictions[i] = new_pred\n    \n    return predictions\n  \n#####################################\n# 10. Función para guardar submission en el formato requerido\n#####################################\ndef save_submission_per_residue(test_df, predictions_dict, output_file='submission.csv', max_length=512):\n    \"\"\"\n    Función corregida para crear el archivo de presentación con el formato correcto.\n    Garantiza que todas las secuencias en test_df tengan entradas en el archivo final.\n    \"\"\"\n    rows = []\n    \n    # Recorre cada secuencia en el conjunto de prueba\n    for _, row in test_df.iterrows():\n        target_id = row['target_id']\n        seq_full = row['sequence'].strip()\n        seq_trunc = seq_full[:max_length]\n        seq_len = len(seq_trunc)\n        \n        # Verifica si hay predicciones para este target_id\n        if target_id not in predictions_dict:\n            print(f\"Advertencia: no se encontraron predicciones para {target_id}, generando coordenadas nulas.\")\n            # En lugar de omitir, genera coordenadas nulas para todas las posiciones\n            for i in range(seq_len):\n                resid = i + 1\n                resname = seq_trunc[i]\n                rid = f\"{target_id}_{resid}\"\n                # Coordenadas nulas para las 5 predicciones (15 valores)\n                coords_flat = [0.0] * 15\n                rows.append([rid, resname, resid] + coords_flat)\n            continue\n            \n        # Obtiene las predicciones para este target_id\n        preds = predictions_dict[target_id]\n        \n        # Asegura que tengamos exactamente 5 predicciones\n        while len(preds) < 5:\n            if len(preds) > 0:\n                # Duplica la última predicción si hay al menos una\n                preds.append(preds[-1].copy())\n            else:\n                # Si no hay predicciones, crea una matriz nula\n                preds.append(np.zeros((seq_len, 3)))\n        \n        # Limita a 5 predicciones\n        preds = preds[:5]\n        \n        # Procesa cada residuo en la secuencia\n        for i in range(seq_len):\n            resid = i + 1\n            resname = seq_trunc[i]\n            rid = f\"{target_id}_{resid}\"\n            coords_flat = []\n            \n            # Recoge las coordenadas de cada predicción\n            for pred_idx, pred in enumerate(preds):\n                # Asegura que la predicción tenga el tamaño adecuado\n                if not isinstance(pred, np.ndarray) or i >= pred.shape[0] or pred.shape[1] != 3:\n                    print(f\"Error en predicción {pred_idx+1} para {target_id} residuo {i+1}, usando coordenadas nulas\")\n                    coords_flat.extend([0.0, 0.0, 0.0])\n                else:\n                    # Extrae las coordenadas y maneja posibles NaN o inf\n                    coord = pred[i]\n                    clean_coords = []\n                    for c in coord:\n                        if np.isnan(c) or np.isinf(c):\n                            clean_coords.append(0.0)\n                        else:\n                            clean_coords.append(float(c))\n                    coords_flat.extend(clean_coords)\n            \n            # Asegura que haya exactamente 15 valores (5 predicciones x 3 dimensiones)\n            if len(coords_flat) < 15:\n                print(f\"Faltan coordenadas para {rid}, completando con ceros\")\n                coords_flat.extend([0.0] * (15 - len(coords_flat)))\n            elif len(coords_flat) > 15:\n                print(f\"Demasiadas coordenadas para {rid}, truncando\")\n                coords_flat = coords_flat[:15]\n                \n            # Redondea a 12 decimales para evitar problemas de precisión\n            coords_flat = [round(float(c), 12) for c in coords_flat]\n            \n            # Agrega la fila al conjunto de resultados\n            rows.append([rid, resname, resid] + coords_flat)\n    \n    # Define las columnas según lo esperado por la competencia\n    columns = [\"ID\", \"resname\", \"resid\"] + [f\"{dim}_{i}\" for i in range(1, 6) for dim in [\"x\", \"y\", \"z\"]]\n    \n    # Crea el DataFrame\n    submission_df = pd.DataFrame(rows, columns=columns)\n    \n    # Asegura que no haya valores NaN\n    submission_df = submission_df.fillna(0.0)\n    \n    # Convierte explícitamente las columnas de coordenadas a float\n    for col in columns[3:]:\n        submission_df[col] = submission_df[col].astype(float)\n    \n    # Guarda el archivo\n    submission_df.to_csv(output_file, index=False, float_format='%.12f')\n    print(f\"Submission guardado en {output_file} con {len(submission_df)} filas\")\n    print(\"Vista previa de los primeros 5 registros:\")\n    print(submission_df.head())\n    \n    return submission_df\n\ndef verify_submission(submission_file, test_df=None):\n    \"\"\"\n    Función mejorada para verificar el archivo de presentación.\n    Ahora verifica más aspectos y puede validar contra el DataFrame de prueba original.\n    \"\"\"\n    df = pd.read_csv(submission_file)\n    \n    # Verificaciones básicas de formato\n    if len(df.columns) != 18:\n        print(f\"Error: El archivo de envío tiene {len(df.columns)} columnas, se esperaban 18.\")\n        return False\n        \n    if df.isnull().any().any():\n        print(\"Error: El archivo de envío contiene valores NaN.\")\n        null_counts = df.isnull().sum()\n        print(\"Columnas con valores NaN:\", null_counts[null_counts > 0])\n        return False\n    \n    # Verificar tipos de datos\n    for col in df.columns[3:]:\n        if not np.issubdtype(df[col].dtype, np.floating):\n            print(f\"Error: La columna {col} no contiene valores flotantes.\")\n            return False\n    \n    # Verificar valores extremos o incorrectos\n    for col in df.columns[3:]:\n        if df[col].isin([np.inf, -np.inf]).any():\n            print(f\"Error: La columna {col} contiene valores infinitos.\")\n            return False\n        \n        if (df[col].abs() > 1e6).any():\n            print(f\"Advertencia: La columna {col} contiene valores muy grandes (>1e6).\")\n    \n    # Si se proporciona el DataFrame de prueba, verificar la correspondencia completa\n    if test_df is not None:\n        expected_ids = set()\n        for _, row in test_df.iterrows():\n            target_id = row['target_id']\n            seq = row['sequence'].strip()[:512]  # Truncar a max_length\n            for i in range(len(seq)):\n                expected_ids.add(f\"{target_id}_{i+1}\")\n        \n        submission_ids = set(df['ID'])\n        \n        missing_ids = expected_ids - submission_ids\n        if missing_ids:\n            print(f\"Error: Faltan {len(missing_ids)} IDs en el archivo de envío.\")\n            print(\"Ejemplos de IDs faltantes:\", list(missing_ids)[:5])\n            return False\n        \n        extra_ids = submission_ids - expected_ids\n        if extra_ids:\n            print(f\"Error: Hay {len(extra_ids)} IDs adicionales en el archivo de envío.\")\n            print(\"Ejemplos de IDs adicionales:\", list(extra_ids)[:5])\n            return False\n    \n    print(\"El archivo de envío cumple con el formato esperado.\")\n    return True\n\n#####################################\n# 11. Bloque principal\n#####################################\nif __name__ == \"__main__\":\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    print(\"Usando dispositivo:\", device)\n    \n    # Crear datasets: experimental y sintético\n    exp_dataset = RNADataset(train_sequences_file, label_csv=train_labels_file, max_length=512)\n    synthetic_folder = '/kaggle/input/uw-synthetic-rna-final/compile_all/'\n    synth_dataset = SyntheticRNADataset(synthetic_folder, max_length=512)\n    \n    # Combinar ambos datasets para pre-entrenamiento\n    combined_dataset = ConcatDataset([synth_dataset, exp_dataset])\n    train_loader = DataLoader(combined_dataset, batch_size=8, shuffle=True, collate_fn=combined_collate_fn)\n    \n    # Dataset de test (usamos el dataset experimental sin etiquetas)\n    test_dataset = RNADataset(test_sequences_file, label_csv=None, max_length=512)\n    test_loader = DataLoader(test_dataset, batch_size=1, shuffle=False, collate_fn=combined_collate_fn)\n    \n    # Instanciar y entrenar el modelo predictivo\n    model = MiniLLM(vocab_size=5, embed_dim=256, n_heads=8, num_layers=6, hidden_dim=512, max_length=512)\n    model.to(device)\n    \n    optimizer = optim.Adam(model.parameters(), lr=1e-5)\n    epochs = 3\n    for epoch in range(epochs):\n        print(f\"\\n=== Epoch {epoch+1}/{epochs} ===\")\n        train_model(model, train_loader, optimizer, device)\n    \n    # Inferencia en test: para cada secuencia se generan 5 predicciones\n    model.eval()\n    predictions_dict = {}\n    \n    # Crear un conjunto de todos los target_ids en el conjunto de prueba\n    all_test_targets = set(test_sequences_df['target_id'])\n    \n    # Realizar inferencia con manejo de errores\n    for batch in tqdm(test_loader, desc=\"Inferencia en test\"):\n        try:\n            tokens = batch['tokens']  # [1, L]\n            target_ids = batch['target_ids']\n            tid = target_ids[0]\n            \n            preds = generate_predictions(model, tokens, device, num_predictions=5, refine=True)\n            predictions_dict[tid] = preds\n            \n            # Remover este target_id del conjunto de todos los targets\n            if tid in all_test_targets:\n                all_test_targets.remove(tid)\n        except Exception as e:\n            print(f\"Error en predicción para {tid}: {e}\")\n            # Crear predicciones nulas para este target\n            seq_length = tokens.size(1)\n            null_preds = [np.zeros((seq_length, 3)) for _ in range(5)]\n            predictions_dict[tid] = null_preds\n    \n    # Generar predicciones para cualquier target_id que no se procesó\n    for tid in all_test_targets:\n        print(f\"Generando predicciones nulas para target no procesado: {tid}\")\n        row = test_sequences_df[test_sequences_df['target_id'] == tid].iloc[0]\n        seq = row['sequence'].strip()[:512]\n        seq_length = len(seq)\n        null_preds = [np.zeros((seq_length, 3)) for _ in range(5)]\n        predictions_dict[tid] = null_preds\n    \n    # Guardar submission en el formato requerido con verificación adicional\n    submission_df = save_submission_per_residue(test_sequences_df, predictions_dict, output_file='submission.csv', max_length=512)\n    verify_submission('submission.csv', test_df=test_sequences_df)\n    \n    # Verificar la integridad de la presentación una vez más\n    print(\"\\nVerificación final del archivo de envío:\")\n    verify_submission('submission.csv')\n    \n    # Contar el número total de filas esperadas\n    total_expected_rows = 0\n    for _, row in test_sequences_df.iterrows():\n        seq = row['sequence'].strip()[:512]\n        total_expected_rows += len(seq)\n    \n    # Confirmar el número de filas\n    actual_rows = len(pd.read_csv('submission.csv'))\n    print(f\"\\nTotal de filas esperadas: {total_expected_rows}\")\n    print(f\"Total de filas en el archivo: {actual_rows}\")\n    \n    if total_expected_rows != actual_rows:\n        print(f\"ADVERTENCIA: Discrepancia en el número de filas. Faltan {total_expected_rows - actual_rows} filas.\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-03-14T14:40:31.727381Z","iopub.execute_input":"2025-03-14T14:40:31.727744Z","execution_failed":"2025-03-14T22:45:49.128Z"}},"outputs":[],"execution_count":null}]}