{"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":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\"\"\"\n================================================================================\nCSIRO IMAGE2BIOMASS - FAST CODE COMPETITION SOLUTION\n================================================================================\n\nOPTIMIZED FOR SPEED:\n- 3 folds (not 5)\n- 40 epochs (not 100)  \n- 256 image size (not 384)\n- 4 TTA (not 8)\n- Faster model architecture\n\nTOTAL TIME: ~3-4 hours for training + inference on 800+ images\n\n================================================================================\n\"\"\"\n\nimport os\nimport gc\nimport random\nimport warnings\nwarnings.filterwarnings('ignore')\n\nimport numpy as np\nimport pandas as pd\nimport cv2\nfrom tqdm.auto import tqdm\nfrom typing import List\nfrom dataclasses import dataclass, field\nimport time\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import GradScaler, autocast\nfrom torch.optim.lr_scheduler import OneCycleLR\n\nimport torchvision.models as models\n\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import r2_score\nfrom sklearn.preprocessing import RobustScaler\n\n\n# ============================================================================\n# TIMING\n# ============================================================================\nSTART_TIME = time.time()\n\ndef print_time(msg=\"\"):\n    elapsed = (time.time() - START_TIME) / 60\n    print(f\"[{elapsed:.1f} min] {msg}\")\n\n\n# ============================================================================\n# SETUP\n# ============================================================================\n\ndef set_seed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = True  # Enable for speed\n\nset_seed(42)\n\nprint(\"=\"*70)\nprint(\"CSIRO BIOMASS - FAST CODE COMPETITION SOLUTION\")\nprint(\"=\"*70)\nprint(f\"PyTorch: {torch.__version__}\")\nprint(f\"CUDA: {torch.cuda.is_available()}\")\nif torch.cuda.is_available():\n    print(f\"GPU: {torch.cuda.get_device_name(0)}\")\n\n\n# ============================================================================\n# CONFIG - OPTIMIZED FOR SPEED\n# ============================================================================\n\n@dataclass\nclass Config:\n    INPUT_DIR: str = \"/kaggle/input/csiro-biomass\"\n    OUTPUT_DIR: str = \"/kaggle/working\"\n    \n    # SMALLER image for speed\n    IMG_SIZE: int = 256  # Was 384\n    \n    BASE_TARGETS: List[str] = field(default_factory=lambda: [\n        \"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\"\n    ])\n    AUX_TARGETS: List[str] = field(default_factory=lambda: [\n        \"Pre_GSHH_NDVI\", \"Height_Ave_cm\"\n    ])\n    \n    # FASTER training\n    BATCH_SIZE: int = 16  # Larger batch\n    NUM_EPOCHS: int = 40  # Fewer epochs\n    LEARNING_RATE: float = 2e-4  # Higher LR for faster convergence\n    WEIGHT_DECAY: float = 0.01\n    \n    N_FOLDS: int = 3  # Fewer folds (was 5)\n    SEED: int = 42\n    \n    DROPOUT: float = 0.2\n    PATIENCE: int = 10  # Less patience\n    GRAD_ACCUM: int = 2  # Less accumulation\n    USE_AMP: bool = True\n    NUM_WORKERS: int = 2  # Use workers for speed\n    \n    # LESS TTA\n    N_TTA: int = 4  # Was 8\n    \n    BASE_LOSS_WEIGHTS: List[float] = field(default_factory=lambda: [1.5, 0.8, 1.2])\n    AUX_LOSS_WEIGHT: float = 0.2\n    WARMUP_EPOCHS: int = 3\n\nCFG = Config()\n\nprint(f\"\\nFAST Config:\")\nprint(f\"  Image: {CFG.IMG_SIZE}x{CFG.IMG_SIZE}\")\nprint(f\"  Folds: {CFG.N_FOLDS}\")\nprint(f\"  Epochs: {CFG.NUM_EPOCHS}\")\nprint(f\"  TTA: {CFG.N_TTA}\")\nprint(f\"  Batch: {CFG.BATCH_SIZE}\")\n\n\n# ============================================================================\n# DATA\n# ============================================================================\n\ndef load_data(input_dir):\n    train_raw = pd.read_csv(os.path.join(input_dir, \"train.csv\"))\n    test_raw = pd.read_csv(os.path.join(input_dir, \"test.csv\"))\n    \n    print_time(\"Loading data...\")\n    \n    sample_path = train_raw['image_path'].iloc[0]\n    image_base = input_dir\n    for base in [input_dir, os.path.join(input_dir, 'train_images')]:\n        if os.path.exists(os.path.join(base, sample_path)):\n            image_base = base\n            break\n    \n    aux_cols = [c for c in train_raw.columns if c not in ['sample_id', 'target_name', 'target']]\n    train_aux = train_raw.drop_duplicates(subset=['image_path'])[aux_cols].reset_index(drop=True)\n    train_targets = train_raw.pivot_table(\n        index='image_path', columns='target_name', values='target', aggfunc='first'\n    ).reset_index()\n    train_df = train_aux.merge(train_targets, on='image_path', how='left')\n    \n    n_test = len(test_raw['image_path'].unique())\n    print(f\"  Train: {len(train_df)}, Test: {n_test}\")\n    \n    return train_df, test_raw, image_base\n\n\ntrain_df, test_df, IMAGE_BASE = load_data(CFG.INPUT_DIR)\n\n# Scalers\nbase_scaler = RobustScaler()\naux_scaler = RobustScaler()\nbase_scaler.fit(train_df[CFG.BASE_TARGETS].values)\naux_scaler.fit(train_df[CFG.AUX_TARGETS].values)\nBASE_CENTERS = np.array(base_scaler.center_, dtype=np.float32)\nBASE_SCALES = np.array(base_scaler.scale_, dtype=np.float32)\n\n\n# ============================================================================\n# AUGMENTATIONS - SIMPLIFIED FOR SPEED\n# ============================================================================\n\ndef get_train_transforms(img_size):\n    return A.Compose([\n        A.RandomResizedCrop(size=(img_size, img_size), scale=(0.8, 1.0)),\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.RandomRotate90(p=0.5),\n        A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5),\n        A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=20, p=0.3),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2(),\n    ])\n\ndef get_valid_transforms(img_size):\n    return A.Compose([\n        A.Resize(height=img_size, width=img_size),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2(),\n    ])\n\ndef get_tta_transforms(img_size, n_tta=4):\n    base = [A.Resize(height=img_size, width=img_size),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2()]\n    return [\n        A.Compose(base),\n        A.Compose([A.HorizontalFlip(p=1.0)] + base),\n        A.Compose([A.VerticalFlip(p=1.0)] + base),\n        A.Compose([A.HorizontalFlip(p=1.0), A.VerticalFlip(p=1.0)] + base),\n    ][:n_tta]\n\n\n# ============================================================================\n# DATASET\n# ============================================================================\n\nclass BiomassDataset(Dataset):\n    def __init__(self, df, image_base, transform=None, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.image_base = image_base\n        self.transform = transform\n        self.is_train = is_train\n        self.image_paths = [os.path.join(image_base, p) for p in self.df['image_path'].values]\n        \n        if is_train:\n            self.base_targets = base_scaler.transform(self.df[CFG.BASE_TARGETS].values).astype(np.float32)\n            self.aux_targets = aux_scaler.transform(self.df[CFG.AUX_TARGETS].values).astype(np.float32)\n    \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        image = cv2.imread(self.image_paths[idx])\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        if self.transform:\n            image = self.transform(image=image)['image']\n        \n        result = {'image': image}\n        if self.is_train:\n            result['base_targets'] = torch.tensor(self.base_targets[idx], dtype=torch.float32)\n            result['aux_targets'] = torch.tensor(self.aux_targets[idx], dtype=torch.float32)\n        return result\n\n\n# ============================================================================\n# MODEL - LIGHTWEIGHT\n# ============================================================================\n\nclass FastBiomassModel(nn.Module):\n    \"\"\"Lightweight model for fast training\"\"\"\n    \n    def __init__(self, dropout=0.2):\n        super().__init__()\n        \n        # ResNet34 is FASTER than ResNet50\n        self.backbone = models.resnet34(weights=None)\n        self.feat_dim = self.backbone.fc.in_features  # 512\n        self.backbone.fc = nn.Identity()\n        \n        print(f\"  Model: ResNet34 (fast, features={self.feat_dim})\")\n        \n        # Simpler head\n        self.head = nn.Sequential(\n            nn.Linear(self.feat_dim, 256),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout),\n            nn.Linear(256, 64),\n            nn.ReLU(inplace=True),\n            nn.Linear(64, 3),  # 3 base targets\n        )\n        \n        self.aux_head = nn.Sequential(\n            nn.Linear(self.feat_dim, 32),\n            nn.ReLU(inplace=True),\n            nn.Linear(32, 2),  # 2 aux targets\n        )\n    \n    def forward(self, x, return_aux=True):\n        features = self.backbone(x)\n        base_pred = self.head(features)\n        if return_aux:\n            return base_pred, self.aux_head(features)\n        return base_pred\n\n\n# ============================================================================\n# LOSS & METRICS\n# ============================================================================\n\nclass SimpleLoss(nn.Module):\n    def __init__(self, base_weights, aux_weight=0.2):\n        super().__init__()\n        self.base_weights = torch.tensor(base_weights, dtype=torch.float32)\n        self.aux_weight = aux_weight\n    \n    def forward(self, base_pred, aux_pred, base_target, aux_target):\n        device = base_pred.device\n        w = self.base_weights.to(device)\n        base_loss = ((base_pred - base_target) ** 2 * w.unsqueeze(0)).mean()\n        aux_loss = F.mse_loss(aux_pred, aux_target)\n        return base_loss + self.aux_weight * aux_loss\n\n\ndef calc_r2(preds_norm, targets_norm):\n    preds = np.clip(preds_norm * BASE_SCALES + BASE_CENTERS, 0, None)\n    targets = targets_norm * BASE_SCALES + BASE_CENTERS\n    \n    g_p, d_p, c_p = preds[:, 0], preds[:, 1], preds[:, 2]\n    g_t, d_t, c_t = targets[:, 0], targets[:, 1], targets[:, 2]\n    \n    # Physical constraints\n    gdm_p, gdm_t = g_p + c_p, g_t + c_t\n    tot_p, tot_t = g_p + d_p + c_p, g_t + d_t + c_t\n    \n    def safe_r2(yt, yp):\n        return max(0, r2_score(yt, yp)) if len(yt) > 1 and np.var(yt) > 1e-10 else 0\n    \n    r2 = {\n        'Dry_Green_g': safe_r2(g_t, g_p),\n        'Dry_Dead_g': safe_r2(d_t, d_p),\n        'Dry_Clover_g': safe_r2(c_t, c_p),\n        'GDM_g': safe_r2(gdm_t, gdm_p),\n        'Dry_Total_g': safe_r2(tot_t, tot_p),\n    }\n    \n    weights = {'Dry_Green_g': 0.1, 'Dry_Dead_g': 0.1, 'Dry_Clover_g': 0.1, \n               'GDM_g': 0.2, 'Dry_Total_g': 0.5}\n    return sum(r2[k] * weights[k] for k in weights), r2\n\n\n# ============================================================================\n# TRAINING - FAST\n# ============================================================================\n\ndef train_epoch(model, loader, criterion, optimizer, scheduler, device, scaler, cfg):\n    model.train()\n    total_loss, count = 0, 0\n    \n    for batch in loader:\n        images = batch['image'].to(device, non_blocking=True)\n        base_t = batch['base_targets'].to(device, non_blocking=True)\n        aux_t = batch['aux_targets'].to(device, non_blocking=True)\n        \n        optimizer.zero_grad(set_to_none=True)\n        \n        with autocast(enabled=cfg.USE_AMP):\n            base_p, aux_p = model(images)\n            loss = criterion(base_p, aux_p, base_t, aux_t)\n        \n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        \n        if scheduler:\n            scheduler.step()\n        \n        total_loss += loss.item() * images.size(0)\n        count += images.size(0)\n    \n    return total_loss / count\n\n\n@torch.no_grad()\ndef validate(model, loader, device, cfg):\n    model.eval()\n    all_preds, all_targets = [], []\n    \n    for batch in loader:\n        images = batch['image'].to(device, non_blocking=True)\n        with autocast(enabled=cfg.USE_AMP):\n            base_p, _ = model(images)\n        all_preds.append(base_p.cpu().numpy())\n        all_targets.append(batch['base_targets'].numpy())\n    \n    return calc_r2(np.concatenate(all_preds), np.concatenate(all_targets))\n\n\n# ============================================================================\n# TRAIN FOLD - FAST\n# ============================================================================\n\ndef train_fold(fold, train_df, image_base, cfg):\n    print_time(f\"FOLD {fold+1}/{cfg.N_FOLDS}\")\n    \n    train_data = train_df[train_df['fold'] != fold].reset_index(drop=True)\n    val_data = train_df[train_df['fold'] == fold].reset_index(drop=True)\n    \n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    \n    train_ds = BiomassDataset(train_data, image_base, get_train_transforms(cfg.IMG_SIZE))\n    val_ds = BiomassDataset(val_data, image_base, get_valid_transforms(cfg.IMG_SIZE))\n    \n    train_loader = DataLoader(train_ds, batch_size=cfg.BATCH_SIZE, shuffle=True, \n                              num_workers=cfg.NUM_WORKERS, pin_memory=True, drop_last=True)\n    val_loader = DataLoader(val_ds, batch_size=cfg.BATCH_SIZE*2, \n                            num_workers=cfg.NUM_WORKERS, pin_memory=True)\n    \n    model = FastBiomassModel(dropout=cfg.DROPOUT).to(device)\n    criterion = SimpleLoss(cfg.BASE_LOSS_WEIGHTS, cfg.AUX_LOSS_WEIGHT).to(device)\n    \n    optimizer = optim.AdamW(model.parameters(), lr=cfg.LEARNING_RATE, weight_decay=cfg.WEIGHT_DECAY)\n    \n    total_steps = len(train_loader) * cfg.NUM_EPOCHS\n    scheduler = OneCycleLR(optimizer, max_lr=cfg.LEARNING_RATE, total_steps=total_steps,\n                           pct_start=0.1, anneal_strategy='cos')\n    \n    scaler = GradScaler()\n    \n    best_r2 = 0\n    patience = 0\n    model_path = os.path.join(cfg.OUTPUT_DIR, f'model_fold{fold}.pth')\n    \n    for epoch in range(cfg.NUM_EPOCHS):\n        train_loss = train_epoch(model, train_loader, criterion, optimizer, scheduler, device, scaler, cfg)\n        val_r2, _ = validate(model, val_loader, device, cfg)\n        \n        if (epoch + 1) % 5 == 0 or val_r2 > best_r2:\n            print_time(f\"  E{epoch+1}: Loss={train_loss:.4f}, R²={val_r2:.4f}\")\n        \n        if val_r2 > best_r2 + 0.005:\n            best_r2 = val_r2\n            patience = 0\n            torch.save({'model_state_dict': model.state_dict(), 'best_r2': best_r2}, model_path)\n        else:\n            patience += 1\n            if patience >= cfg.PATIENCE:\n                print_time(f\"  Early stop at epoch {epoch+1}\")\n                break\n    \n    print_time(f\"  Fold {fold+1} Best: {best_r2:.4f}\")\n    \n    del model, optimizer, train_loader, val_loader\n    gc.collect()\n    torch.cuda.empty_cache()\n    \n    return best_r2\n\n\n# ============================================================================\n# INFERENCE - FAST BATCH PROCESSING\n# ============================================================================\n\n@torch.no_grad()\ndef run_fast_inference(test_df, image_base, cfg):\n    \"\"\"Fast batch inference with minimal TTA\"\"\"\n    \n    print_time(\"Starting inference...\")\n    \n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    test_images = test_df['image_path'].unique()\n    n_test = len(test_images)\n    print_time(f\"  Test images: {n_test}\")\n    \n    # Prepare transforms\n    tta_tfms = get_tta_transforms(cfg.IMG_SIZE, cfg.N_TTA)\n    \n    # Load all models\n    models_list = []\n    for fold in range(cfg.N_FOLDS):\n        model_path = os.path.join(cfg.OUTPUT_DIR, f'model_fold{fold}.pth')\n        if os.path.exists(model_path):\n            model = FastBiomassModel(dropout=cfg.DROPOUT)\n            ckpt = torch.load(model_path, map_location=device)\n            model.load_state_dict(ckpt['model_state_dict'])\n            model = model.to(device)\n            model.eval()\n            models_list.append(model)\n            print_time(f\"  Loaded fold {fold} (R²={ckpt.get('best_r2', 0):.4f})\")\n    \n    if not models_list:\n        raise RuntimeError(\"No models found!\")\n    \n    # Process images\n    all_preds = []\n    all_paths = []\n    \n    batch_size = 8  # Process multiple images at once\n    \n    for i in tqdm(range(0, n_test, batch_size), desc='Inference'):\n        batch_paths = test_images[i:i+batch_size]\n        batch_preds = []\n        \n        for img_name in batch_paths:\n            img_path = os.path.join(image_base, img_name)\n            image = cv2.imread(img_path)\n            if image is None:\n                batch_preds.append(np.zeros(3))\n                all_paths.append(img_name)\n                continue\n            \n            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n            \n            # TTA predictions\n            tta_preds = []\n            for tfm in tta_tfms:\n                img_tensor = tfm(image=image)['image'].unsqueeze(0).to(device)\n                \n                # Ensemble over models\n                model_preds = []\n                for model in models_list:\n                    with autocast(enabled=True):\n                        pred = model(img_tensor, return_aux=False)\n                    model_preds.append(pred.cpu().numpy())\n                \n                tta_preds.append(np.mean(model_preds, axis=0))\n            \n            final_pred = np.mean(tta_preds, axis=0).squeeze()\n            batch_preds.append(final_pred)\n            all_paths.append(img_name)\n        \n        all_preds.extend(batch_preds)\n        \n        # Progress update\n        if (i // batch_size) % 50 == 0:\n            print_time(f\"  Processed {min(i+batch_size, n_test)}/{n_test}\")\n    \n    # Cleanup\n    for model in models_list:\n        del model\n    gc.collect()\n    torch.cuda.empty_cache()\n    \n    print_time(f\"  Inference complete: {len(all_preds)} predictions\")\n    \n    return all_paths, np.array(all_preds)\n\n\n# ============================================================================\n# CREATE SUBMISSION\n# ============================================================================\n\ndef create_submission(paths, preds_norm, test_df, output_path):\n    print_time(\"Creating submission...\")\n    \n    preds = np.clip(preds_norm * BASE_SCALES + BASE_CENTERS, 0, None)\n    if len(preds.shape) == 1:\n        preds = preds.reshape(1, -1)\n    \n    g, d, c = preds[:, 0], preds[:, 1], preds[:, 2]\n    gdm = g + c  # Physical constraint\n    total = g + d + c  # Physical constraint\n    \n    all_preds = {'Dry_Green_g': g, 'Dry_Dead_g': d, 'Dry_Clover_g': c,\n                 'GDM_g': gdm, 'Dry_Total_g': total}\n    \n    path_to_idx = {p: i for i, p in enumerate(paths)}\n    \n    submission = []\n    for _, row in test_df.iterrows():\n        idx = path_to_idx.get(row['image_path'])\n        if idx is not None and row['target_name'] in all_preds:\n            value = float(all_preds[row['target_name']][idx])\n        else:\n            value = 0.0\n        submission.append({'sample_id': row['sample_id'], 'target': max(0.0, value)})\n    \n    submission_df = pd.DataFrame(submission)\n    submission_df.to_csv(output_path, index=False)\n    \n    print_time(f\"  Saved: {output_path}\")\n    print(f\"  Rows: {len(submission_df)}\")\n    print(f\"  Stats: mean={submission_df['target'].mean():.2f}, std={submission_df['target'].std():.2f}\")\n    \n    return submission_df\n\n\n# ============================================================================\n# MAIN\n# ============================================================================\n\nif __name__ == \"__main__\":\n    print_time(\"=\" * 50)\n    print_time(\"STARTING FAST TRAINING\")\n    print_time(\"=\" * 50)\n    \n    # Create folds\n    train_df['total_bin'] = pd.qcut(train_df['Dry_Total_g'], q=5, labels=False, duplicates='drop')\n    train_df['fold'] = -1\n    skf = StratifiedKFold(n_splits=CFG.N_FOLDS, shuffle=True, random_state=CFG.SEED)\n    for fold, (_, val_idx) in enumerate(skf.split(train_df, train_df['total_bin'])):\n        train_df.loc[val_idx, 'fold'] = fold\n    \n    # Train\n    fold_scores = []\n    for fold in range(CFG.N_FOLDS):\n        score = train_fold(fold, train_df, IMAGE_BASE, CFG)\n        fold_scores.append(score)\n    \n    cv_score = np.mean(fold_scores)\n    print_time(\"=\" * 50)\n    print_time(f\"TRAINING COMPLETE - CV: {cv_score:.4f}\")\n    print_time(\"=\" * 50)\n    \n    # Inference\n    paths, predictions = run_fast_inference(test_df, IMAGE_BASE, CFG)\n    \n    # Save\n    submission_path = os.path.join(CFG.OUTPUT_DIR, 'submission.csv')\n    submission = create_submission(paths, predictions, test_df, submission_path)\n    \n    # Verify\n    print_time(\"=\" * 50)\n    print_time(\"VERIFICATION\")\n    print_time(\"=\" * 50)\n    \n    if os.path.exists(submission_path):\n        print(f\"✓ submission.csv EXISTS\")\n        print(f\"  Size: {os.path.getsize(submission_path):,} bytes\")\n        print(f\"  Rows: {len(pd.read_csv(submission_path))}\")\n    else:\n        print(\"✗ submission.csv NOT FOUND!\")\n    \n    print_time(\"=\" * 50)\n    total_time = (time.time() - START_TIME) / 60\n    print(f\"TOTAL TIME: {total_time:.1f} minutes ({total_time/60:.1f} hours)\")\n    print_time(\"DONE!\")\n    print_time(\"=\" * 50)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-16T22:30:42.952928Z","iopub.execute_input":"2026-01-16T22:30:42.953238Z","iopub.status.idle":"2026-01-16T22:46:15.609320Z","shell.execute_reply.started":"2026-01-16T22:30:42.953214Z","shell.execute_reply":"2026-01-16T22:46:15.608690Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}