{"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":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":126777,"databundleVersionId":15314950,"isSourceIdPinned":false},{"sourceType":"datasetVersion","sourceId":15105462,"datasetId":9671519,"databundleVersionId":15991414}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Imports & Setup","metadata":{}},{"cell_type":"code","source":"# 1. Install necessary library\n!pip install -qU timm\n\n# 2. Imports\nimport os\nimport gc\nimport math\nimport random\nimport warnings\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom tqdm.auto import tqdm\nfrom PIL import Image\nfrom sklearn.model_selection import StratifiedKFold\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as transforms\nimport timm\n\n# Suppress warnings for cleaner output\nwarnings.filterwarnings(\"ignore\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-03-09T15:29:52.038514Z","iopub.execute_input":"2026-03-09T15:29:52.038818Z","iopub.status.idle":"2026-03-09T15:30:00.561827Z","shell.execute_reply.started":"2026-03-09T15:29:52.038775Z","shell.execute_reply":"2026-03-09T15:30:00.561222Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Configuration & Seeding","metadata":{}},{"cell_type":"code","source":"class Config:\n    seed = 42\n    # EVA-02 Large: A powerful transformer for fine-grained recognition\n    model_name = \"eva02_large_patch14_448.mim_m38m_ft_in22k_in1k\"\n    \n    img_size = 448\n    embedding_dim = 1024\n    num_classes = 31  # Adjust based on training set unique IDs if needed\n    \n    # Training Hyperparameters\n    num_epochs = 25        # Increased slightly for better convergence\n    batch_size = 4           # Keep small for large resolution\n    grad_accum = 4           # Effective batch size = 16\n    \n    # Learning Rates (LLRD specific)\n    encoder_lr = 1e-5        # Slower for the backbone\n    head_lr = 1e-3           # Faster for the classifier head\n    weight_decay = 0.05      # Standard for ViT\n    \n    # ArcFace Hyperparameters\n    arcface_s = 30.0\n    arcface_m = 0.50\n    \n    # Advanced Options\n    train_full_data = True  # Set TRUE for final submission, FALSE for validation\n    n_folds = 5\n    target_fold = 0          # Which fold to train on if validation is active\n    \n    use_tta = True           # Test Time Augmentation\n    use_qe = True            # Query Expansion\n    use_rerank = True        # K-Reciprocal Re-ranking\n\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\ndef seed_everything(seed):\n    random.seed(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n\nseed_everything(Config.seed)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-09T15:30:00.563265Z","iopub.execute_input":"2026-03-09T15:30:00.563542Z","iopub.status.idle":"2026-03-09T15:30:00.817229Z","shell.execute_reply.started":"2026-03-09T15:30:00.563516Z","shell.execute_reply":"2026-03-09T15:30:00.816642Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Dataset & Advanced Transforms","metadata":{}},{"cell_type":"code","source":"# Stronger augmentations for the training set\ntrain_transform = transforms.Compose([\n    transforms.Resize((Config.img_size, Config.img_size)),\n    transforms.RandomHorizontalFlip(p=0.5),\n    # TrivialAugmentWide: State-of-the-art auto-augmentation for small datasets\n    transforms.TrivialAugmentWide(interpolation=transforms.InterpolationMode.BICUBIC),\n    transforms.ToTensor(),\n    transforms.Normalize([0.481, 0.457, 0.408], [0.268, 0.261, 0.275]),\n    transforms.RandomErasing(p=0.25),\n])\n\n# Clean transform for validation/testing\ntest_transform = transforms.Compose([\n    transforms.Resize((Config.img_size, Config.img_size)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.481, 0.457, 0.408], [0.268, 0.261, 0.275]),\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-09T15:30:00.818368Z","iopub.execute_input":"2026-03-09T15:30:00.818673Z","iopub.status.idle":"2026-03-09T15:30:00.833775Z","shell.execute_reply.started":"2026-03-09T15:30:00.818649Z","shell.execute_reply":"2026-03-09T15:30:00.833129Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nclass JaguarDataset(Dataset):\n    def __init__(self, df, img_dir, transform=None, is_test=False):\n        self.df = df\n        self.img_dir = Path(img_dir)\n        self.transform = transform\n        self.is_test = is_test\n        \n        # Mapping labels to 0-N integers for ArcFace\n        if not is_test:\n            self.unique_ids = sorted(df[\"ground_truth\"].unique())\n            self.label_map = {name: i for i, name in enumerate(self.unique_ids)}\n            self.df[\"label\"] = self.df[\"ground_truth\"].map(self.label_map)\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_name = row[\"filename\"]\n        img_path = self.img_dir / img_name\n        \n        try:\n            img = Image.open(img_path).convert(\"RGB\")\n        except:\n            # Fallback for corrupted images (rare)\n            img = Image.new(\"RGB\", (Config.img_size, Config.img_size))\n\n        if self.transform:\n            img = self.transform(img)\n            \n        if self.is_test:\n            return img, img_name\n        \n        return img, torch.tensor(row[\"label\"], dtype=torch.long)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-09T15:30:00.834608Z","iopub.execute_input":"2026-03-09T15:30:00.834835Z","iopub.status.idle":"2026-03-09T15:30:00.848177Z","shell.execute_reply.started":"2026-03-09T15:30:00.834817Z","shell.execute_reply":"2026-03-09T15:30:00.847559Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model (EVA-02 + ArcFace + Trainable GeM)","metadata":{}},{"cell_type":"code","source":"class GeM(nn.Module):\n    def __init__(self, p=3, eps=1e-6):\n        super(GeM, self).__init__()\n        # p is now a learnable parameter\n        self.p = nn.Parameter(torch.ones(1) * p)\n        self.eps = eps\n\n    def forward(self, x):\n        return F.avg_pool2d(\n            x.clamp(min=self.eps).pow(self.p), (x.size(-2), x.size(-1))\n        ).pow(1.0 / self.p)\n\nclass ArcFaceLayer(nn.Module):\n    def __init__(self, in_features, out_features, s=30.0, m=0.5):\n        super().__init__()\n        self.s = s\n        self.m = m\n        self.weight = nn.Parameter(torch.FloatTensor(out_features, in_features))\n        nn.init.xavier_uniform_(self.weight)\n\n    def forward(self, input, label=None):\n        cosine = F.linear(F.normalize(input), F.normalize(self.weight))\n        if label is None:\n            return cosine\n        phi = cosine - self.m\n        one_hot = torch.zeros_like(cosine)\n        one_hot.scatter_(1, label.view(-1, 1), 1)\n        output = (one_hot * phi) + ((1.0 - one_hot) * cosine)\n        return output * self.s\n\nclass EVABoss(nn.Module):\n    def __init__(self, num_classes=Config.num_classes):\n        super().__init__()\n        self.backbone = timm.create_model(\n            Config.model_name, pretrained=True, num_classes=0\n        )\n        self.feat_dim = self.backbone.num_features\n        self.gem = GeM()\n        self.bn = nn.BatchNorm1d(self.feat_dim)\n        self.head = ArcFaceLayer(\n            self.feat_dim, num_classes, s=Config.arcface_s, m=Config.arcface_m\n        )\n\n    def forward(self, x, label=None):\n        features = self.backbone.forward_features(x)\n        \n        # Handle unpooled features (B, N, C) -> (B, C, H, W)\n        if features.dim() == 3:\n            B, N, C = features.shape\n            H = W = int(math.sqrt(N))\n            if H * W != N: # Handle CLS token if present\n                features = features[:, -H*W:, :]\n            features = features.permute(0, 2, 1).reshape(B, C, H, W)\n\n        emb = self.gem(features).flatten(1)\n        emb = self.bn(emb)\n        \n        if label is not None:\n            return self.head(emb, label)\n        return emb","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-09T15:30:00.849954Z","iopub.execute_input":"2026-03-09T15:30:00.850273Z","iopub.status.idle":"2026-03-09T15:30:00.861018Z","shell.execute_reply.started":"2026-03-09T15:30:00.850252Z","shell.execute_reply":"2026-03-09T15:30:00.860451Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Layer-wise Learning Rate Decay Optimizer\ndef get_optimizer_params(model, encoder_lr, head_lr, weight_decay=0.0):\n    param_optimizer = list(model.named_parameters())\n    no_decay = [\"bias\", \"LayerNorm.bias\", \"LayerNorm.weight\"]\n    optimizer_parameters = []\n    \n    # Simple LLRD implementation\n    layer_decay = 0.9\n    num_layers = 24 # Approximate for Large models, or calculate dynamically\n    \n    for name, p in model.named_parameters():\n        if not p.requires_grad:\n            continue\n        \n        lr = encoder_lr\n        if \"head\" in name:\n            lr = head_lr\n        elif \"blocks\" in name:\n            try:\n                # Attempt to extract block index to scale LR\n                layer_id = int(name.split(\"blocks.\")[1].split(\".\")[0])\n                lr = encoder_lr * (layer_decay ** (num_layers - layer_id))\n            except:\n                pass\n                \n        if any(nd in name for nd in no_decay):\n            optimizer_parameters.append({\"params\": [p], \"weight_decay\": 0.0, \"lr\": lr})\n        else:\n            optimizer_parameters.append({\"params\": [p], \"weight_decay\": weight_decay, \"lr\": lr})\n            \n    return optimizer_parameters","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-09T15:30:00.861888Z","iopub.execute_input":"2026-03-09T15:30:00.862146Z","iopub.status.idle":"2026-03-09T15:30:00.875626Z","shell.execute_reply.started":"2026-03-09T15:30:00.862127Z","shell.execute_reply":"2026-03-09T15:30:00.875026Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Post-Processing Utils (QE & Re-ranking)","metadata":{}},{"cell_type":"code","source":"def query_expansion(emb, top_k=3):\n    \"\"\"\n    Expands the query by averaging it with its top_k nearest neighbors\n    from the gallery (or itself in unsupervised settings).\n    \"\"\"\n    print(f\"Applying Query Expansion (k={top_k})...\")\n    # Cosine similarity matrix\n    sims = emb @ emb.T\n    # Get top k indices\n    indices = np.argsort(-sims, axis=1)[:, :top_k]\n    new_emb = np.zeros_like(emb)\n    for i in range(len(emb)):\n        new_emb[i] = np.mean(emb[indices[i]], axis=0)\n    # Re-normalize\n    return new_emb / np.linalg.norm(new_emb, axis=1, keepdims=True)\n\ndef k_reciprocal_rerank(prob, k1=20, k2=6, lambda_value=0.3):\n    \"\"\"\n    Re-ranking using k-reciprocal encoding.\n    \"\"\"\n    print(\"Applying K-Reciprocal Re-ranking...\")\n    q_g_dist = 1 - prob\n    original_dist = q_g_dist.copy()\n    initial_rank = np.argsort(original_dist, axis=1)\n    \n    nn_k1 = []\n    for i in range(prob.shape[0]):\n        forward_k1 = initial_rank[i, :k1 + 1]\n        backward_k1 = initial_rank[forward_k1, :k1 + 1]\n        fi = np.where(backward_k1 == i)[0]\n        nn_k1.append(forward_k1[fi])\n        \n    jaccard_dist = np.zeros_like(original_dist)\n    for i in range(prob.shape[0]):\n        ind_non_zero = np.where(original_dist[i, :] < 0.6)[0]\n        ind_images = [inv for inv in ind_non_zero if len(np.intersect1d(nn_k1[i], nn_k1[inv])) > 0]\n        for j in ind_images:\n            intersection = len(np.intersect1d(nn_k1[i], nn_k1[j]))\n            union = len(np.union1d(nn_k1[i], nn_k1[j]))\n            jaccard_dist[i, j] = 1 - intersection / union\n            \n    return 1 - (jaccard_dist * lambda_value + original_dist * (1 - lambda_value))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-09T15:30:00.876332Z","iopub.execute_input":"2026-03-09T15:30:00.876551Z","iopub.status.idle":"2026-03-09T15:30:00.887923Z","shell.execute_reply.started":"2026-03-09T15:30:00.876532Z","shell.execute_reply":"2026-03-09T15:30:00.887337Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training & Inference Engine","metadata":{}},{"cell_type":"code","source":"def train_epoch(model, loader, optimizer, criterion, scaler):\n    model.train()\n    loss_meter = 0\n    \n    for i, (imgs, labels) in enumerate(tqdm(loader, desc=\"Training\", leave=False)):\n        imgs, labels = imgs.to(Config.device), labels.to(Config.device)\n        \n        with torch.amp.autocast('cuda'):\n            logits = model(imgs, labels)\n            loss = criterion(logits, labels)\n            loss = loss / Config.grad_accum\n            \n        scaler.scale(loss).backward()\n        \n        if (i + 1) % Config.grad_accum == 0:\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            \n        loss_meter += loss.item() * Config.grad_accum\n        \n    return loss_meter / len(loader)\n\n@torch.no_grad()\ndef extract_features(model, loader):\n    model.eval()\n    feats, names = [], []\n    for imgs, fnames in tqdm(loader, desc=\"Inference\"):\n        imgs = imgs.to(Config.device)\n        \n        # Original forward pass\n        f1 = model(imgs)\n        \n        # Test Time Augmentation (Horizontal Flip)\n        if Config.use_tta:\n            f2 = model(torch.flip(imgs, [3]))\n            f1 = (f1 + f2) / 2\n            \n        feats.append(F.normalize(f1, dim=1).cpu())\n        names.extend(fnames)\n    return torch.cat(feats, dim=0).numpy(), names","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-09T15:30:00.888815Z","iopub.execute_input":"2026-03-09T15:30:00.889024Z","iopub.status.idle":"2026-03-09T15:30:00.901539Z","shell.execute_reply.started":"2026-03-09T15:30:00.889007Z","shell.execute_reply":"2026-03-09T15:30:00.900768Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Execution (Main Loop)","metadata":{}},{"cell_type":"code","source":"# --- PATHS ---\nTRAIN_CSV = \"/kaggle/input/jaguar-re-id/train.csv\"\nTEST_CSV = \"/kaggle/input/jaguar-re-id/test.csv\"\nTRAIN_DIR = \"/kaggle/input/jaguar-re-id/train/train\"\nTEST_DIR = \"/kaggle/input/jaguar-re-id/test/test\"\n\n# --- DATA LOADING ---\nfull_train_df = pd.read_csv(TRAIN_CSV)\ntest_df = pd.read_csv(TEST_CSV)\n\n# --- SPLIT SETUP (STRATIFIED K-FOLD) ---\nskf = StratifiedKFold(n_splits=Config.n_folds, shuffle=True, random_state=Config.seed)\n# Create a dummy fold column\nfull_train_df[\"fold\"] = -1\nfor fold, (_, val_idx) in enumerate(skf.split(full_train_df, full_train_df[\"ground_truth\"])):\n    full_train_df.loc[val_idx, \"fold\"] = fold\n\n# --- SELECT DATA FOR TRAINING ---\nif Config.train_full_data:\n    print(\"🚀 Training on FULL dataset for Submission\")\n    train_df = full_train_df\nelse:\n    print(f\"🔬 Training on FOLD {Config.target_fold} (Validation Mode)\")\n    train_df = full_train_df[full_train_df[\"fold\"] != Config.target_fold].reset_index(drop=True)\n    val_df = full_train_df[full_train_df[\"fold\"] == Config.target_fold].reset_index(drop=True)\n\n# Update Config with actual number of classes in training set\nConfig.num_classes = train_df[\"ground_truth\"].nunique()\n\ntrain_loader = DataLoader(\n    JaguarDataset(train_df, TRAIN_DIR, train_transform),\n    batch_size=Config.batch_size,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True,\n    drop_last=True\n)\n\n# --- MODEL SETUP ---\nmodel = EVABoss(num_classes=Config.num_classes).to(Config.device)\n\n# Advanced Optimizer Setup with LLRD\noptimizer_params = get_optimizer_params(\n    model, \n    encoder_lr=Config.encoder_lr, \n    head_lr=Config.head_lr, \n    weight_decay=Config.weight_decay\n)\noptimizer = torch.optim.AdamW(optimizer_params)\nscaler = torch.amp.GradScaler('cuda')\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(\n    optimizer, T_max=Config.num_epochs, eta_min=1e-6\n)\n\ncriterion = nn.CrossEntropyLoss()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-09T15:30:00.902467Z","iopub.execute_input":"2026-03-09T15:30:00.902720Z","iopub.status.idle":"2026-03-09T15:30:06.297474Z","shell.execute_reply.started":"2026-03-09T15:30:00.902692Z","shell.execute_reply":"2026-03-09T15:30:06.296878Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- TRAINING LOOP ---\nprint(f\"🔥 Starting Training: EVA-02 Large | {Config.num_epochs} Epochs\")\n\nfor epoch in range(Config.num_epochs):\n    loss = train_epoch(model, train_loader, optimizer, criterion, scaler)\n    scheduler.step()\n    current_lr = optimizer.param_groups[0]['lr']\n    print(f\"Epoch {epoch+1}/{Config.num_epochs} | Loss: {loss:.4f} | LR: {current_lr:.2e}\")\n\n# --- INFERENCE ---\nprint(\"\\n🔮 Starting Inference...\")\nunique_test_imgs = sorted(set(test_df[\"query_image\"]) | set(test_df[\"gallery_image\"]))\ntest_loader = DataLoader(\n    JaguarDataset(pd.DataFrame({\"filename\": unique_test_imgs}), TEST_DIR, test_transform, is_test=True),\n    batch_size=Config.batch_size * 2,\n    shuffle=False,\n    num_workers=2\n)\n\n# Extract Features\nemb, names = extract_features(model, test_loader)\nimg_map = {n: i for i, n in enumerate(names)}\n\n# --- POST PROCESSING ---\nif Config.use_qe:\n    emb = query_expansion(emb, top_k=3)\n\n# Calculate similarity matrix (Cosine)\nsim_matrix = emb @ emb.T\n\nif Config.use_rerank:\n    sim_matrix = k_reciprocal_rerank(sim_matrix, k1=20, k2=6, lambda_value=0.3)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-09T15:30:06.298379Z","iopub.execute_input":"2026-03-09T15:30:06.298625Z","iopub.status.idle":"2026-03-09T15:49:51.868759Z","shell.execute_reply.started":"2026-03-09T15:30:06.298605Z","shell.execute_reply":"2026-03-09T15:49:51.867860Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- GENERATE SUBMISSION ---\npreds = []\nfor _, row in tqdm(test_df.iterrows(), total=len(test_df), desc=\"Mapping Predictions\"):\n    idx_q = img_map[row[\"query_image\"]]\n    idx_g = img_map[row[\"gallery_image\"]]\n    \n    score = sim_matrix[idx_q, idx_g]\n    preds.append(max(0.0, min(1.0, score))) # Clip to valid range\n\nsub = pd.DataFrame({\"row_id\": test_df[\"row_id\"], \"similarity\": preds})\nsub.to_csv(\"submission.csv\", index=False)\n\nprint(f\"✅ Submission Saved! Mean Similarity: {np.mean(preds):.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-09T15:49:51.870749Z","iopub.execute_input":"2026-03-09T15:49:51.871085Z","iopub.status.idle":"2026-03-09T15:49:57.690664Z","shell.execute_reply.started":"2026-03-09T15:49:51.871039Z","shell.execute_reply":"2026-03-09T15:49:57.690005Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Simple blending with top score","metadata":{}},{"cell_type":"code","source":"import pandas as pd\n\n# Define the blending function\ndef blend_submissions(weight_dict, output_path):\n    # Initialize list to store loaded DataFrames\n    dataframes = []\n\n    # Load each submission with its weight\n    for path, weight in weight_dict.items():\n        # Read the CSV file\n        df = pd.read_csv(path)\n\n        # Add a weighted prediction column\n        df[\"weighted_pred\"] = df[\"similarity\"] * weight\n\n        # Append to list\n        dataframes.append(df[[\"row_id\", \"weighted_pred\"]])\n\n    # Merge all submissions on 'id'\n    merged = dataframes[0]\n    for df in dataframes[1:]:\n        # Merge on id\n        merged = merged.merge(df, on=\"row_id\", how=\"inner\", suffixes=(\"\", \"_dup\"))\n\n        # Combine duplicate weighted_pred columns if any\n        if \"weighted_pred_dup\" in merged.columns:\n            merged[\"weighted_pred\"] += merged[\"weighted_pred_dup\"]\n            merged.drop(columns=[\"weighted_pred_dup\"], inplace=True)\n\n    # Compute total weight\n    total_weight = sum(weight_dict.values())\n\n    # Compute blended prediction\n    merged[\"similarity\"] = merged[\"weighted_pred\"] / total_weight\n\n    # Prepare final DataFrame\n    blended = merged[[\"row_id\", \"similarity\"]]\n\n    # Save blended submission\n    blended.to_csv(output_path, index=False)\n\n    # Print confirmation\n    print(f\"✅ Blended submission saved to {output_path}\")\n# Define the main function\ndef main():\n    # Define file paths and their respective weights\n    weight_dict = {\n        \"/kaggle/input/datasets/yusufmurtaza01/best-public/submission.csv\": 2.8,\n        \"/kaggle/working/submission.csv\": 0.2,\n    }\n    # Call blend function\n    blend_submissions(weight_dict, output_path=\"submission_t2.csv\")\n\n# Call the main function\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-09T15:49:57.691647Z","iopub.execute_input":"2026-03-09T15:49:57.691896Z","iopub.status.idle":"2026-03-09T15:49:58.135571Z","shell.execute_reply.started":"2026-03-09T15:49:57.691875Z","shell.execute_reply":"2026-03-09T15:49:58.134682Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}