{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":14774,"databundleVersionId":875431,"sourceType":"competition"},{"sourceId":12742344,"sourceType":"datasetVersion","datasetId":8054848},{"sourceId":12743169,"sourceType":"datasetVersion","datasetId":8055457},{"sourceId":518149,"sourceType":"modelInstanceVersion","modelInstanceId":408300,"modelId":426151},{"sourceId":518226,"sourceType":"modelInstanceVersion","modelInstanceId":408341,"modelId":426191}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport os\nimport pandas as pd\nimport random\nimport timm\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom albumentations import Compose, Normalize, HorizontalFlip, Rotate, ColorJitter\nfrom albumentations.pytorch import ToTensorV2\nfrom sklearn.metrics import cohen_kappa_score\nfrom sklearn.model_selection import train_test_split\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\nfrom torch.utils.data import DataLoader, Dataset\nfrom tqdm import tqdm\nfrom scipy.optimize import minimize\n\n# ==============================================================================\n# 0. Global Settings\n# ==============================================================================\ndef set_seed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed_all(seed)\n        torch.backends.cudnn.deterministic = True\n        torch.backends.cudnn.benchmark = False\nset_seed(42)\n\n# ==============================================================================\n# 1. Preprocessing Functions\n# ==============================================================================\ndef circle_crop(image):\n    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n    _, thresh = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY)\n    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n    if not contours:\n        return image\n    cnt = max(contours, key=cv2.contourArea)\n    x, y, w, h = cv2.boundingRect(cnt)\n    cropped_image = image[y:y + h, x:x + w]\n    return cropped_image\n\ndef apply_ben_graham_preprocessing(image, sigmaX=30):\n    blurred_image = cv2.GaussianBlur(image, (0, 0), sigmaX)\n    processed_image = cv2.addWeighted(image, 4, blurred_image, -4, 128)\n    return processed_image\n\ndef preprocess_directory(input_dir, output_dir, image_size):\n    if not os.path.exists(output_dir):\n        os.makedirs(output_dir)\n        print(f\"Creating directory: {output_dir}\")\n\n    image_files = [f for f in os.listdir(input_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg'))]\n    \n    print(f\"\\nStarting to process directory: {input_dir}\")\n    for filename in tqdm(image_files, desc=f\"Processing {os.path.basename(input_dir)}\"):\n        input_path = os.path.join(input_dir, filename)\n        output_path = os.path.join(output_dir, filename)\n\n        image = cv2.imread(input_path)\n        if image is None:\n            continue\n\n        processed_image = circle_crop(image)\n        processed_image = cv2.resize(processed_image, (image_size, image_size))\n        processed_image = apply_ben_graham_preprocessing(processed_image)\n        cv2.imwrite(output_path, processed_image)\n\n    print(f\"Processing complete! Processed images saved to: {output_dir}\")\n\n\n# ==============================================================================\n# 2. PyTorch Model and Data Classes\n# ==============================================================================\ndef replace_batchnorm_with_groupnorm(module, num_groups=32):\n    for name, child in module.named_children():\n        if isinstance(child, nn.BatchNorm2d):\n            num_channels = child.num_features\n            if num_channels % num_groups == 0:\n                setattr(module, name, nn.GroupNorm(num_groups=num_groups, num_channels=num_channels))\n        else:\n            replace_batchnorm_with_groupnorm(child, num_groups)\n\nclass EfficientNetModel(nn.Module):\n    def __init__(self, model_name, pretrained_model_path=None):\n        super().__init__()\n        self.model = timm.create_model(model_name, pretrained=False)\n        replace_batchnorm_with_groupnorm(self.model)\n        \n        in_features = self.model.classifier.in_features\n        self.model.classifier = nn.Sequential(\n            nn.Linear(in_features, 1)\n        )\n        if pretrained_model_path and os.path.exists(pretrained_model_path):\n            self.model.load_state_dict(torch.load(pretrained_model_path), strict=False)\n        elif pretrained_model_path:\n            print(f\"Warning: Pretrained model path not found: {pretrained_model_path}\")\n\n    def forward(self, x):\n        return self.model(x)\n\nclass CustomImageDataset(Dataset):\n    def __init__(self, img_dir, labels_df, transform=None):\n        self.img_dir = img_dir\n        self.labels_df = labels_df\n        self.transform = transform\n    def __len__(self):\n        return len(self.labels_df)\n    def __getitem__(self, idx):\n        img_id = self.labels_df.iloc[idx, 0]\n        label = float(self.labels_df.iloc[idx, 1])\n        img_path = os.path.join(self.img_dir, img_id + '.png')\n        try:\n            img = cv2.imread(img_path)\n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n            if self.transform:\n                img = self.transform(image=img)[\"image\"]\n            return img, label\n        except Exception as e:\n            return self.__getitem__((idx + 1) % len(self))\n\ndef get_transforms(data_type='train'):\n    if data_type == 'train':\n        return Compose([\n            Rotate(limit=40, p=0.5),\n            ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.1, p=0.3),\n            HorizontalFlip(p=0.5),\n            Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n            ToTensorV2(),\n        ])\n    else:\n        return Compose([\n            Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n            ToTensorV2(),\n        ])\n\n# ==============================================================================\n# 3. Training and Prediction Functions\n# ==============================================================================\ndef train(model, train_loader, valid_loader, device, num_epochs, lr, image_size):\n    criterion = nn.MSELoss()\n    optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-5)\n    scheduler = CosineAnnealingLR(optimizer, T_max=num_epochs, eta_min=1e-6)\n\n    best_valid_kappa = 0.0\n    epochs_no_improve = 0\n    patience = 5\n    save_path = os.path.join(os.getcwd(), f'best_model_kappa_{image_size}.pth')\n\n    for epoch in range(num_epochs):\n        model.train()\n        running_loss = 0.0\n        \n        for images, labels in tqdm(train_loader, desc=\"Training\"):\n            images = images.to(device)\n            labels = labels.to(device).float().view(-1, 1)\n            optimizer.zero_grad()\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            loss.backward()\n            optimizer.step()\n            running_loss += loss.item()\n\n        train_loss = running_loss / len(train_loader)\n        \n        model.eval()\n        all_valid_labels = []\n        all_valid_preds = []\n        with torch.no_grad():\n            for images, labels in tqdm(valid_loader, desc=\"Validating\"):\n                images = images.to(device)\n                outputs = model(images).squeeze()\n                all_valid_labels.extend(labels.cpu().numpy())\n                preds_np = outputs.cpu().numpy()\n                all_valid_preds.extend(np.atleast_1d(preds_np))\n        \n        rounded_preds = np.round(all_valid_preds).astype(int)\n        valid_kappa = cohen_kappa_score(all_valid_labels, rounded_preds, weights='quadratic')\n\n        print(f\"\\nEpoch {epoch + 1}/{num_epochs}\")\n        print(f\"  Training Loss: {train_loss:.4f}\")\n        print(f\"  Validation Kappa (rounded): {valid_kappa:.4f}\")\n        print(f\"  Current Learning Rate: {optimizer.param_groups[0]['lr']:.6f}\")\n\n        scheduler.step()\n\n        if valid_kappa > best_valid_kappa:\n            best_valid_kappa = valid_kappa\n            epochs_no_improve = 0\n            torch.save(model.state_dict(), save_path)\n            print(f\"  -> Saved new best model! Validation Kappa: {best_valid_kappa:.4f}\")\n        else:\n            epochs_no_improve += 1\n        \n        if epochs_no_improve >= patience:\n            print(f\"\\nEarly stopping triggered after {patience} epochs with no improvement.\")\n            break\n\nclass OptimizedRounder:\n    def __init__(self):\n        self.coef_ = 0\n    def _kappa_loss(self, coef, X, y):\n        X_p = self.predict(X, coef)\n        ll = cohen_kappa_score(y, X_p, weights='quadratic')\n        return -ll\n    def fit(self, X, y):\n        loss_partial = lambda coef: self._kappa_loss(coef, X, y)\n        initial_coef = [0.5, 1.5, 2.5, 3.5]\n        self.coef_ = minimize(loss_partial, initial_coef, method='nelder-mead')['x']\n        print(f\"Optimized thresholds: {self.coef_}\")\n    def predict(self, X, coef):\n        X_p = np.copy(X)\n        for i, pred in enumerate(X_p):\n            if pred < coef[0]: X_p[i] = 0\n            elif pred >= coef[0] and pred < coef[1]: X_p[i] = 1\n            elif pred >= coef[1] and pred < coef[2]: X_p[i] = 2\n            elif pred >= coef[2] and pred < coef[3]: X_p[i] = 3\n            else: X_p[i] = 4\n        return X_p.astype(int)\n\ndef get_validation_predictions(loader, model, device):\n    model.eval()\n    all_labels = []\n    all_preds = []\n    with torch.no_grad():\n        for images, labels in tqdm(loader, desc=\"Getting validation predictions\"):\n            images = images.to(device)\n            outputs = model(images).squeeze()\n            all_labels.extend(labels.cpu().numpy())\n            preds_np = outputs.cpu().numpy()\n            all_preds.extend(np.atleast_1d(preds_np))\n    return np.array(all_preds), np.array(all_labels)\n\ndef load_and_predict_test_data(test_csv_path, img_dir, model, device, coefficients):\n    test_df = pd.read_csv(test_csv_path)\n    predictions = []\n    test_transform = get_transforms('test')\n    rounder = OptimizedRounder()\n    model.eval()\n    for img_id in tqdm(test_df['id_code'], desc=\"Predicting on test set\"):\n        img_path = os.path.join(img_dir, img_id + '.png')\n        if not os.path.exists(img_path):\n            predictions.append(-1) \n            continue\n        img = cv2.imread(img_path)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        image_tensor = test_transform(image=img)[\"image\"].unsqueeze(0).to(device)\n        with torch.no_grad():\n            pred = model(image_tensor).squeeze().cpu().numpy()\n        predictions.append(pred)\n    if -1 in predictions:\n        valid_preds = [p for p in predictions if p != -1]\n        rounded_valid_preds = np.round(valid_preds).astype(int)\n        if len(rounded_valid_preds) > 0: mode_val = pd.Series(rounded_valid_preds).mode()[0]\n        else: mode_val = 0\n        predictions = [mode_val if p == -1 else p for p in predictions]\n    test_preds_rounded = rounder.predict(np.array(predictions), coefficients)\n    test_df['diagnosis'] = test_preds_rounded\n    submission_file = os.path.join(os.getcwd(), 'submission.csv')\n    test_df.to_csv(submission_file, index=False)\n    print(f\"\\nPredictions saved to {submission_file}\")\n\n# ==============================================================================\n# 4. Main Execution Logic\n# ==============================================================================\nif __name__ == \"__main__\":\n    # --- Step 1: Define Paths and Parameters ---\n    print(\"--- Step 1: Initializing paths and parameters ---\")\n    MODEL_NAME = 'efficientnet_b6'\n    IMAGE_SIZE = 512\n    BATCH_SIZE = 4\n    NUM_EPOCHS = 30\n    LEARNING_RATE = 1e-4\n    NUM_WORKERS = 2\n    \n    # --- Path Definitions ---\n    COMPETITION_DATA_DIR = '/kaggle/input/aptos2019-blindness-detection'\n    source_train_dir = os.path.join(COMPETITION_DATA_DIR, 'train_images')\n    source_test_dir = os.path.join(COMPETITION_DATA_DIR, 'test_images')\n    train_label_file = os.path.join(COMPETITION_DATA_DIR, 'train.csv')\n    test_label_file = os.path.join(COMPETITION_DATA_DIR, 'test.csv')\n    \n    PREPROCESSED_DATA_INPUT_DIR = '/kaggle/input/images-preprocessed-512'\n    \n    PREPROCESSED_DATA_WORKING_DIR = '/kaggle/working'\n\n    PRETRAINED_MODEL_PATH = '/kaggle/input/efficientnetb6/pytorch/default/1/pytorch_model_effb6.bin'\n    \n    model_path = os.path.join(os.getcwd(), f'best_model_kappa_{IMAGE_SIZE}.pth')\n    \n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    print(f\"Device being used: {device}\")\n\n    # --- Step 2: Smartly Decide Which Image Dataset to Use ---\n    print(\"\\n--- Step 2: Checking preprocessed data ---\")\n    \n    final_train_image_dir = os.path.join(PREPROCESSED_DATA_INPUT_DIR, f'train_images_preprocessed_{IMAGE_SIZE}')\n    final_test_image_dir = os.path.join(PREPROCESSED_DATA_INPUT_DIR, f'test_images_preprocessed_{IMAGE_SIZE}')\n\n    if os.path.exists(final_train_image_dir) and os.path.exists(final_test_image_dir):\n        print(f\"Successfully found uploaded preprocessed data, using path: {PREPROCESSED_DATA_INPUT_DIR}\")\n    else:\n        print(f\"Preprocessed data not found at '{PREPROCESSED_DATA_INPUT_DIR}'.\")\n        print(\"Will perform real-time preprocessing and save images to /kaggle/working/ directory.\")\n        final_train_image_dir = os.path.join(PREPROCESSED_DATA_WORKING_DIR, f'train_images_preprocessed_{IMAGE_SIZE}')\n        final_test_image_dir = os.path.join(PREPROCESSED_DATA_WORKING_DIR, f'test_images_preprocessed_{IMAGE_SIZE}')\n        \n        preprocess_directory(source_train_dir, final_train_image_dir, IMAGE_SIZE)\n        preprocess_directory(source_test_dir, final_test_image_dir, IMAGE_SIZE)\n\n    # --- Step 3: Run Model Training (if needed) ---\n    print(\"\\n--- Step 3: Checking for existing model ---\")\n    run_training = True\n    if os.path.exists(model_path):\n        print(f\"Model file found at {model_path}. Skipping training.\")\n        run_training = False\n\n    if run_training:\n        print(\"Starting model training...\")\n        all_labels_df = pd.read_csv(train_label_file)\n        train_df, valid_df = train_test_split(\n            all_labels_df, test_size=0.2, random_state=42, stratify=all_labels_df['diagnosis']\n        )\n        train_dataset = CustomImageDataset(final_train_image_dir, train_df, transform=get_transforms('train'))\n        valid_dataset = CustomImageDataset(final_train_image_dir, valid_df, transform=get_transforms('valid'))\n        \n        train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS, pin_memory=True)\n        valid_loader = DataLoader(valid_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, pin_memory=True)\n        \n        model = EfficientNetModel(MODEL_NAME, pretrained_model_path=PRETRAINED_MODEL_PATH).to(device)\n        train(model, train_loader, valid_loader, device, NUM_EPOCHS, LEARNING_RATE, IMAGE_SIZE)\n    \n    # --- Step 4: Optimize Rounding Thresholds ---\n    print(\"\\n--- Step 4: Optimizing rounding thresholds ---\")\n    if not os.path.exists(model_path):\n         print(\"Error: Model file not found. Cannot perform threshold optimization.\")\n    else:\n        all_labels_df = pd.read_csv(train_label_file)\n        _, valid_df_for_opt = train_test_split(\n            all_labels_df, test_size=0.2, random_state=42, stratify=all_labels_df['diagnosis']\n        )\n        valid_dataset_for_opt = CustomImageDataset(final_train_image_dir, valid_df_for_opt, transform=get_transforms('valid'))\n        valid_loader_for_opt = DataLoader(valid_dataset_for_opt, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\n        \n        model_for_opt = EfficientNetModel(MODEL_NAME, pretrained_model_path=None).to(device)\n        model_for_opt.load_state_dict(torch.load(model_path, map_location=device))\n        \n        val_preds, val_labels = get_validation_predictions(valid_loader_for_opt, model_for_opt, device)\n        opt_rounder = OptimizedRounder()\n        opt_rounder.fit(val_preds, val_labels)\n        optimized_coefficients = opt_rounder.coef_\n        \n        optimized_preds = opt_rounder.predict(val_preds, optimized_coefficients)\n        final_val_kappa = cohen_kappa_score(val_labels, optimized_preds, weights='quadratic')\n        print(f\"Final optimized validation Kappa: {final_val_kappa:.4f}\")\n    \n        # --- Step 5: Load Best Model and Predict ---\n        print(\"\\n--- Step 5: Loading best model and predicting on test set ---\")\n        model_for_pred = EfficientNetModel(MODEL_NAME, pretrained_model_path=None).to(device)\n        model_for_pred.load_state_dict(torch.load(model_path, map_location=device))\n        load_and_predict_test_data(test_label_file, final_test_image_dir, model_for_pred, device, optimized_coefficients)\n\n    print(\"\\n--- Script execution complete ---\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-12T20:47:24.651424Z","iopub.execute_input":"2026-01-12T20:47:24.652174Z","iopub.status.idle":"2026-01-12T21:32:33.371907Z","shell.execute_reply.started":"2026-01-12T20:47:24.652146Z","shell.execute_reply":"2026-01-12T21:32:33.371140Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Install if needed\n!pip install pytorch-grad-cam","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T21:32:33.373467Z","iopub.execute_input":"2026-01-12T21:32:33.373767Z","iopub.status.idle":"2026-01-12T21:32:35.877535Z","shell.execute_reply.started":"2026-01-12T21:32:33.373744Z","shell.execute_reply":"2026-01-12T21:32:35.876791Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.nn.functional as F\nimport matplotlib.pyplot as plt\n\nclass SimpleGradCAM:\n    def __init__(self, model, target_layer):\n        self.model = model\n        self.target_layer = target_layer\n        self.gradients = None\n        self.activations = None\n        \n        target_layer.register_forward_hook(self.save_activation)\n        target_layer.register_full_backward_hook(self.save_gradient)\n    \n    def save_activation(self, module, input, output):\n        self.activations = output.detach()\n    \n    def save_gradient(self, module, grad_input, grad_output):\n        self.gradients = grad_output[0].detach()\n    \n    def generate(self, input_tensor):\n        self.model.eval()\n        output = self.model(input_tensor)\n        \n        self.model.zero_grad()\n        output.backward()\n        \n        weights = self.gradients.mean(dim=(2, 3), keepdim=True)\n        cam = (weights * self.activations).sum(dim=1, keepdim=True)\n        cam = F.relu(cam)\n        cam = F.interpolate(cam, size=(512, 512), mode='bilinear', align_corners=False)\n        cam = cam - cam.min()\n        cam = cam / (cam.max() + 1e-8)\n        \n        return cam.squeeze().cpu().numpy()\n\n# Load model\nmodel = EfficientNetModel(MODEL_NAME, pretrained_model_path=None).to(device)\nmodel.load_state_dict(torch.load(model_path, map_location=device))\n\n# Initialize GradCAM\ntarget_layer = model.model.conv_head\ncam = SimpleGradCAM(model, target_layer)\n\n# Prepare data\nall_labels_df = pd.read_csv(train_label_file)\n_, valid_df = train_test_split(all_labels_df, test_size=0.2, random_state=42, stratify=all_labels_df['diagnosis'])\nvalid_df = valid_df.reset_index(drop=True)\nvalid_dataset = CustomImageDataset(final_train_image_dir, valid_df, transform=get_transforms('valid'))\n\n# Generate visualizations\nfig, axes = plt.subplots(5, 4, figsize=(12, 15))\n\nfor grade in range(5):\n    grade_mask = valid_df['diagnosis'] == grade\n    grade_indices = valid_df[grade_mask].index.tolist()[:2]\n    \n    for i, idx in enumerate(grade_indices):\n        img_tensor, label = valid_dataset[idx]\n        input_tensor = img_tensor.unsqueeze(0).to(device).requires_grad_(True)\n        \n        grayscale_cam = cam.generate(input_tensor)\n        \n        img_np = img_tensor.permute(1, 2, 0).cpu().numpy()\n        img_np = img_np * np.array([0.229, 0.224, 0.225]) + np.array([0.485, 0.456, 0.406])\n        img_np = np.clip(img_np, 0, 1)\n        \n        heatmap = plt.cm.jet(grayscale_cam)[:, :, :3]\n        overlay = 0.5 * img_np + 0.5 * heatmap\n        overlay = np.clip(overlay, 0, 1)\n        \n        axes[grade, i*2].imshow(img_np)\n        axes[grade, i*2].set_title(f'Grade {grade}')\n        axes[grade, i*2].axis('off')\n        \n        axes[grade, i*2 + 1].imshow(overlay)\n        axes[grade, i*2 + 1].set_title(f'Grade {grade} - GradCAM')\n        axes[grade, i*2 + 1].axis('off')\n\nplt.suptitle('Diabetic Retinopathy Grading: Model Attention by Severity', fontsize=14)\nplt.tight_layout()\nplt.savefig('gradcam_by_grade.png', dpi=150, bbox_inches='tight')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T21:32:35.878530Z","iopub.execute_input":"2026-01-12T21:32:35.878749Z","iopub.status.idle":"2026-01-12T21:32:45.714496Z","shell.execute_reply.started":"2026-01-12T21:32:35.878725Z","shell.execute_reply":"2026-01-12T21:32:45.713446Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Save model and export to ONNX\nmodel.eval()\nmodel_cpu = model.cpu()\n\n# Save PyTorch weights (backup)\ntorch.save(model_cpu.state_dict(), '/kaggle/working/best_model_kappa_512.pth')\n\n# Export to ONNX\ndummy_input = torch.randn(1, 3, 512, 512)\ntorch.onnx.export(\n    model_cpu,\n    dummy_input,\n    '/kaggle/working/dr_model.onnx',\n    input_names=['image'],\n    output_names=['severity_score'],\n    dynamic_axes={'image': {0: 'batch'}, 'severity_score': {0: 'batch'}},\n    opset_version=17\n)\n\nprint(\"Saved dr_model.pth and dr_model.onnx\")\nprint(\"Download both from the Output tab on the right -->\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T22:02:57.452717Z","iopub.execute_input":"2026-01-12T22:02:57.453348Z","iopub.status.idle":"2026-01-12T22:03:02.802974Z","shell.execute_reply.started":"2026-01-12T22:02:57.453321Z","shell.execute_reply":"2026-01-12T22:03:02.802128Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}