{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":14544659,"sourceType":"datasetVersion","datasetId":9289599},{"sourceId":14625009,"sourceType":"datasetVersion","datasetId":9341855},{"sourceId":282128585,"sourceType":"kernelVersion"}],"dockerImageVersionId":31260,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":88.966464,"end_time":"2026-01-18T05:26:21.285385","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-01-18T05:24:52.318921","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CSIRO Biomass Prediction - DINOv3 + Mamba Inference\n\n**Inference notebook for DINOv3 + Mamba Fusion models**\n\n## Model Architecture:\n- **DINOv3 ViT-Huge backbone** (vit_huge_plus_patch16_dinov3.lvd1689m)\n- **LocalMamba fusion blocks** for spatial token mixing  \n- **FiLM conditioning** for cross-stream modulation\n- **Target-specific heads** with Softplus activation\n\n## Ensemble Strategy:\n- Uses Folds 0-3 (excludes Fold 4 due to negative R²)\n- R²-weighted ensemble (better folds get higher weight)\n- Post-processing: Clover scaling, conditional Dead scaling\n\n## Expected CV Score: ~0.734 (mean of Folds 0-3)","metadata":{"papermill":{"duration":0.003929,"end_time":"2026-01-18T05:24:55.908289","exception":false,"start_time":"2026-01-18T05:24:55.904360","status":"completed"},"tags":[]}},{"cell_type":"code","source":"!pip uninstall -y timm\n!pip install -q --no-deps /kaggle/input/wheels-csiro/timm-1.0.22-py3-none-any.whl","metadata":{"execution":{"iopub.status.busy":"2026-01-26T11:57:45.302444Z","iopub.execute_input":"2026-01-26T11:57:45.302748Z","iopub.status.idle":"2026-01-26T11:57:50.325799Z","shell.execute_reply.started":"2026-01-26T11:57:45.302711Z","shell.execute_reply":"2026-01-26T11:57:50.324736Z"},"papermill":{"duration":4.815436,"end_time":"2026-01-18T05:25:00.726789","exception":false,"start_time":"2026-01-18T05:24:55.911353","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nfrom pathlib import Path\nfrom typing import List, Tuple, Dict, Optional\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nimport timm\nfrom tqdm import tqdm\nfrom sklearn.preprocessing import LabelEncoder, StandardScaler\nimport gc\n\nSEED = 42\ndef seed_everything(seed=SEED):\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\nseed_everything()\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Current Device:\", device)","metadata":{"execution":{"iopub.status.busy":"2026-01-26T11:57:50.327913Z","iopub.execute_input":"2026-01-26T11:57:50.328257Z","iopub.status.idle":"2026-01-26T11:58:01.411356Z","shell.execute_reply.started":"2026-01-26T11:57:50.328217Z","shell.execute_reply":"2026-01-26T11:58:01.410604Z"},"papermill":{"duration":14.421094,"end_time":"2026-01-18T05:25:15.151219","exception":false,"start_time":"2026-01-18T05:25:00.730125","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CFG:\n    # Target order for submission: [Clover, Dead, Green, Total, GDM]\n    ALL_TARGETS = [\"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\", \"Dry_Total_g\", \"GDM_g\"]\n    \n    # Paths\n    DATA_DIR = Path(\"/kaggle/input/csiro-biomass\")\n    MODEL_DIR = Path(\"/kaggle/input/new-dinov3-mamba\")  # Update to your model directory\n    \n    # Model\n    IMG_SIZE = 512\n    BACKBONE = \"vit_huge_plus_patch16_dinov3.lvd1689m\"\n    BATCH_SIZE = 2\n    \n    # Fold configuration - exclude Fold 5 (negative R²)\n    # Fold R² scores from training: [0.8159, 0.6905, 0.7561, 0.6725, -1.5983]\n    # FOLD_R2_SCORES = {\n    #     0: 0.8159,\n    #     1: 0.6905,\n    #     2: 0.7561,\n    #     3: 0.6725,\n    #     # Fold 4 excluded (R² = -1.5983)\n    # }\n    # new maba\n    # Fold 1: R² = 0.8072\n    # Fold 2: R² = -1.9415\n    # Fold 3: R² = 0.7671\n    # Fold 4: R² = 0.7107\n    # Fold 5: R² = 0.7847\n    FOLD_R2_SCORES = {\n        # 0: 0.8072,\n        # 1: -1.9415,\n        2: 0.7671,\n        3: 0.7107,\n        4: 0.7847,\n    }\n    \n    # Model files - update these to match your saved model names\n    MODEL_FILES = {\n        # 0: \"model_fold_0.pth\",\n        # 1: \"model_fold_1.pth\",\n        2: \"model_fold_2.pth\",\n        3: \"model_fold_3.pth\",\n        4: \"model_fold_4.pth\",\n    }\n    \n    # TTA configuration\n    USE_TTA = False\n\nprint(f\"Config loaded:\")\nprint(f\"  Folds to use: {list(CFG.FOLD_R2_SCORES.keys())}\")\nprint(f\"  R² scores: {list(CFG.FOLD_R2_SCORES.values())}\")\nprint(f\"  Mean R² (excluding Fold 5): {np.mean(list(CFG.FOLD_R2_SCORES.values())):.4f}\")","metadata":{"execution":{"iopub.status.busy":"2026-01-26T11:58:01.412493Z","iopub.execute_input":"2026-01-26T11:58:01.412986Z","iopub.status.idle":"2026-01-26T11:58:01.419495Z","shell.execute_reply.started":"2026-01-26T11:58:01.412960Z","shell.execute_reply":"2026-01-26T11:58:01.418756Z"},"papermill":{"duration":0.009204,"end_time":"2026-01-18T05:25:15.163783","exception":false,"start_time":"2026-01-18T05:25:15.154579","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load data\ntrain_df = pd.read_csv(CFG.DATA_DIR / \"train.csv\")\ntest_df = pd.read_csv(CFG.DATA_DIR / \"test.csv\")\n\nprint(f\"Train rows: {len(train_df)}\")\nprint(f\"Test rows: {len(test_df)}\")\nprint(\"\\nTest data preview:\")\ntest_df.head()","metadata":{"execution":{"iopub.status.busy":"2026-01-26T11:58:01.421004Z","iopub.execute_input":"2026-01-26T11:58:01.421226Z","iopub.status.idle":"2026-01-26T11:58:01.483585Z","shell.execute_reply.started":"2026-01-26T11:58:01.421205Z","shell.execute_reply":"2026-01-26T11:58:01.482862Z"},"papermill":{"duration":0.046883,"end_time":"2026-01-18T05:25:15.213671","exception":false,"start_time":"2026-01-18T05:25:15.166788","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Prepare Test Data with Metadata","metadata":{"papermill":{"duration":0.003436,"end_time":"2026-01-18T05:25:15.220864","exception":false,"start_time":"2026-01-18T05:25:15.217428","status":"completed"},"tags":[]}},{"cell_type":"code","source":"test_wide = test_df[[\"image_path\"]].drop_duplicates().reset_index(drop=True)\n\nprint(f\"Test data prepared: {len(test_wide)} samples\")\ntest_wide.head()","metadata":{"execution":{"iopub.status.busy":"2026-01-26T11:58:01.485208Z","iopub.execute_input":"2026-01-26T11:58:01.485693Z","iopub.status.idle":"2026-01-26T11:58:01.499772Z","shell.execute_reply.started":"2026-01-26T11:58:01.485670Z","shell.execute_reply":"2026-01-26T11:58:01.499211Z"},"papermill":{"duration":0.019485,"end_time":"2026-01-18T05:25:15.243739","exception":false,"start_time":"2026-01-18T05:25:15.224254","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Define Model Architecture","metadata":{"papermill":{"duration":0.003254,"end_time":"2026-01-18T05:25:15.250337","exception":false,"start_time":"2026-01-18T05:25:15.247083","status":"completed"},"tags":[]}},{"cell_type":"code","source":"class LocalMambaBlock(nn.Module):\n    \"\"\"\n    Lightweight Mamba-like block for spatial token mixing.\n    \n    Features:\n    - LayerNorm for stability\n    - Depthwise Conv1D for local spatial mixing\n    - Sigmoid gating mechanism\n    - Residual connection\n    \"\"\"\n    def __init__(self, dim: int, kernel_size: int = 5, dropout: float = 0.1):\n        super().__init__()\n        self.norm = nn.LayerNorm(dim)\n        self.dwconv = nn.Conv1d(\n            dim, dim, \n            kernel_size=kernel_size,\n            padding=kernel_size // 2, \n            groups=dim  # Depthwise\n        )\n        self.gate = nn.Linear(dim, dim)\n        self.proj = nn.Linear(dim, dim)\n        self.drop = nn.Dropout(dropout)\n    \n    def forward(self, x: torch.Tensor) -> torch.Tensor:\n        \"\"\"\n        Args:\n            x: (B, N, D) - Batch, Tokens, Dimension\n        Returns:\n            (B, N, D)\n        \"\"\"\n        shortcut = x\n        x = self.norm(x)\n        \n        # Gating mechanism\n        g = torch.sigmoid(self.gate(x))\n        x = x * g\n        \n        # Depthwise conv (transpose for Conv1d)\n        x = x.transpose(1, 2)  # (B, D, N)\n        x = self.dwconv(x)\n        x = x.transpose(1, 2)  # (B, N, D)\n        \n        # Project and dropout\n        x = self.proj(x)\n        x = self.drop(x)\n        \n        return shortcut + x\n\n\nclass FiLM(nn.Module):\n    \"\"\"\n    Feature-wise Linear Modulation.\n    Generates scale (gamma) and shift (beta) from context.\n    \"\"\"\n    def __init__(self, feat_dim: int):\n        super().__init__()\n        self.mlp = nn.Sequential(\n            nn.Linear(feat_dim, feat_dim // 2),\n            nn.GELU(),\n            nn.Linear(feat_dim // 2, feat_dim * 2)\n        )\n    \n    def forward(self, context: torch.Tensor):\n        \"\"\"\n        Args:\n            context: (B, D)\n        Returns:\n            gamma, beta: each (B, D)\n        \"\"\"\n        gamma_beta = self.mlp(context)\n        return torch.chunk(gamma_beta, 2, dim=-1)\n\n\nclass BiomassModelDINOv3Mamba(nn.Module):\n    \"\"\"\n    DINOv3/v2 + Mamba Fusion + FiLM + Multi-Head Regression\n    \n    Architecture:\n    1. DINOv3/v2 backbone extracts tokens from left/right images\n    2. Concatenate tokens from both streams\n    3. LocalMamba blocks fuse spatial information\n    4. FiLM modulates features based on context\n    5. Target-specific heads predict 3 independent targets\n    6. Derive Dry_Total_g and GDM_g from predictions\n    \"\"\"\n    def __init__(\n        self, \n        model_name: str,\n        pretrained: bool = True,\n        freeze_backbone: bool = True,\n        n_mamba_blocks: int = 2,\n        dropout: float = 0.2\n    ):\n        super().__init__()\n        self.model_name = model_name\n        \n        # 1. Backbone - DINOv3/v2 ViT\n        self.backbone = timm.create_model(\n            model_name,\n            pretrained=pretrained,\n            num_classes=0,\n            global_pool=''  # Return all tokens\n        )\n        nf = self.backbone.num_features\n        print(f\"Backbone features: {nf}\")\n        \n        # 2. Mamba Fusion Blocks\n        self.fusion = nn.Sequential(*[\n            LocalMambaBlock(nf, kernel_size=5, dropout=0.1)\n            for _ in range(n_mamba_blocks)\n        ])\n        \n        # 3. FiLM Conditioning\n        self.film = FiLM(nf)\n        \n        # 4. Pooling\n        self.pool = nn.AdaptiveAvgPool1d(1)\n        \n        # 5. Target-Specific Heads with Softplus (ensures positive outputs)\n        # Predicts: [Dry_Green_g, Dry_Clover_g, Dry_Dead_g]\n        def make_head():\n            return nn.Sequential(\n                nn.Linear(nf, nf // 2),\n                nn.GELU(),\n                nn.Dropout(dropout),\n                nn.Linear(nf // 2, 1),\n                nn.Softplus()  # Ensures positive output\n            )\n        \n        self.head_green = make_head()\n        self.head_clover = make_head()\n        self.head_dead = make_head()\n        \n        # 6. Freeze backbone if specified\n        if freeze_backbone:\n            self._freeze_backbone()\n    \n    def _freeze_backbone(self):\n        \"\"\"Freeze all backbone parameters.\"\"\"\n        for param in self.backbone.parameters():\n            param.requires_grad = False\n        print(\"Backbone frozen\")\n    \n    def forward(self, left: torch.Tensor, right: torch.Tensor):\n        \"\"\"\n        Args:\n            left: (B, 3, H, W) - Left image crop\n            right: (B, 3, H, W) - Right image crop\n        \n        Returns:\n            preds_3: (B, 3) - [Green, Clover, Dead] predictions\n        \"\"\"\n        # Extract tokens: (B, N, D)\n        x_l = self.backbone(left)\n        x_r = self.backbone(right)\n        \n        # Concatenate tokens from both streams: (B, 2N, D)\n        x_cat = torch.cat([x_l, x_r], dim=1)\n        \n        # Mamba fusion\n        x_fused = self.fusion(x_cat)\n        \n        # FiLM conditioning using mean of fused features\n        context = x_fused.mean(dim=1)  # (B, D)\n        gamma, beta = self.film(context)\n        \n        # Apply FiLM modulation\n        x_modulated = gamma.unsqueeze(1) * x_fused + beta.unsqueeze(1)\n        \n        # Pool to single vector: (B, D)\n        x_pool = self.pool(x_modulated.transpose(1, 2)).flatten(1)\n        \n        # Predict each target\n        green = self.head_green(x_pool)    # (B, 1)\n        clover = self.head_clover(x_pool)  # (B, 1)\n        dead = self.head_dead(x_pool)      # (B, 1)\n        \n        # Stack predictions: (B, 3) in order [Green, Clover, Dead]\n        preds_3 = torch.cat([green, clover, dead], dim=1)\n        \n        return preds_3\n    \n    def predict_all_5(self, left: torch.Tensor, right: torch.Tensor):\n        \"\"\"\n        Predict all 5 targets including derived ones.\n        \n        Returns:\n            preds_5: (B, 5) - [Clover, Dead, Green, Total, GDM]\n        \"\"\"\n        preds_3 = self.forward(left, right)  # [Green, Clover, Dead]\n        \n        green = preds_3[:, 0:1]\n        clover = preds_3[:, 1:2]\n        dead = preds_3[:, 2:3]\n        \n        # Derive GDM and Total\n        gdm = green + clover\n        total = gdm + dead\n        \n        # Return in order: [Clover, Dead, Green, Total, GDM]\n        preds_5 = torch.cat([clover, dead, green, total, gdm], dim=1)\n        \n        return preds_5\n\n\nprint(\"Model architecture defined (matching training notebook)\")","metadata":{"execution":{"iopub.status.busy":"2026-01-26T11:58:01.500661Z","iopub.execute_input":"2026-01-26T11:58:01.500935Z","iopub.status.idle":"2026-01-26T11:58:01.518842Z","shell.execute_reply.started":"2026-01-26T11:58:01.500905Z","shell.execute_reply":"2026-01-26T11:58:01.518145Z"},"papermill":{"duration":0.015018,"end_time":"2026-01-18T05:25:15.268951","exception":false,"start_time":"2026-01-18T05:25:15.253933","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Define Test Dataset and DataLoader","metadata":{"papermill":{"duration":0.003294,"end_time":"2026-01-18T05:25:15.275497","exception":false,"start_time":"2026-01-18T05:25:15.272203","status":"completed"},"tags":[]}},{"cell_type":"code","source":"class PastureImageTestDataset(Dataset):\n    \"\"\"Dataset for test images with metadata\"\"\"\n    def __init__(self, df, image_root, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.image_root = image_root\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        \n        # Load image\n        img_path = self.image_root / row[\"image_path\"]\n        img = Image.open(img_path).convert(\"RGB\")\n        w, h = img.size  # expect (2000, 1000)\n\n        # Split in the middle (deterministic for test)\n        left = img.crop((0, 0, h, h))        # (0, 0, 1000, 1000)\n        right = img.crop((w - h, 0, w, h))   # (1000, 0, 2000, 1000)\n\n        if self.transform:\n            img1 = self.transform(left)\n            img2 = self.transform(right)\n\n        # Return images and row info\n        return (img1, img2), row.to_dict()\n\n\n# Define transforms\ntest_tfms = T.Compose([\n    T.Resize((CFG.IMG_SIZE, CFG.IMG_SIZE)),\n    T.ToTensor(),\n    T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n])\n\n\ndef get_tta_transforms():\n    \"\"\"Returns a list of transform pipelines for TTA during inference.\"\"\"\n    base_transforms = [\n        T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n    ]\n\n    # View 1: Original\n    original_view = T.Compose([\n        T.Resize((CFG.IMG_SIZE, CFG.IMG_SIZE)),\n        T.ToTensor(),\n        *base_transforms\n    ])\n\n    # View 2: Horizontal Flip\n    hflip_view = T.Compose([\n        T.Resize((CFG.IMG_SIZE, CFG.IMG_SIZE)),\n        T.RandomHorizontalFlip(p=1.0),\n        T.ToTensor(),\n        *base_transforms\n    ])\n\n    # View 3: Vertical Flip\n    vflip_view = T.Compose([\n        T.Resize((CFG.IMG_SIZE, CFG.IMG_SIZE)),\n        T.RandomVerticalFlip(p=1.0),\n        T.ToTensor(),\n        *base_transforms\n    ])\n\n    # View 4: Rotate90\n    vflip_view = T.Compose([\n        T.Resize((CFG.IMG_SIZE, CFG.IMG_SIZE)),\n        T.Lambda(lambda img: T.functional.rotate(img, angle=90)),\n        T.ToTensor(),\n        *base_transforms\n    ])\n\n    return [original_view, hflip_view, vflip_view]\n\n\ndef collate_fn_test(batch):\n    \"\"\"Custom collate function for test data\"\"\"\n    imgs1, imgs2 = [], []\n    row_infos = []\n\n    for (img1, img2), row_info in batch:\n        imgs1.append(img1)\n        imgs2.append(img2)\n        row_infos.append(row_info)\n\n    imgs1 = torch.stack(imgs1)\n    imgs2 = torch.stack(imgs2)\n\n    return (imgs1, imgs2), row_infos\n\n\nprint(\"Test dataset and transforms defined\")","metadata":{"execution":{"iopub.status.busy":"2026-01-26T11:58:01.519777Z","iopub.execute_input":"2026-01-26T11:58:01.520118Z","iopub.status.idle":"2026-01-26T11:58:01.534060Z","shell.execute_reply.started":"2026-01-26T11:58:01.520091Z","shell.execute_reply":"2026-01-26T11:58:01.533510Z"},"papermill":{"duration":0.014234,"end_time":"2026-01-18T05:25:15.292926","exception":false,"start_time":"2026-01-18T05:25:15.278692","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Inference Function","metadata":{"papermill":{"duration":0.003348,"end_time":"2026-01-18T05:25:15.299618","exception":false,"start_time":"2026-01-18T05:25:15.296270","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def predict_with_model(model, loader):\n    \"\"\"\n    Make predictions with a single model.\n    \n    Args:\n        model: Trained BiomassModelDINOv3Mamba model\n        loader: DataLoader for test data\n    \n    Returns:\n        predictions: numpy array of shape (n_samples, 3) with [Green, Clover, Dead]\n    \"\"\"\n    model.eval()\n    preds_all = []\n    \n    with torch.no_grad():\n        for (imgs1, imgs2), _ in tqdm(loader, desc=\"Inference\"):\n            imgs1, imgs2 = imgs1.to(device), imgs2.to(device)\n            with torch.amp.autocast('cuda'):\n                # Model returns [Green, Clover, Dead]\n                pred = model(imgs1, imgs2)\n            preds_all.append(pred.cpu().numpy())\n    \n    return np.vstack(preds_all)\n\n\ndef expand_predictions(preds_3: np.ndarray) -> np.ndarray:\n    \"\"\"\n    Expand 3 predictions to 5 targets.\n    \n    Input order: [Green, Clover, Dead]\n    Output order: [Clover, Dead, Green, Total, GDM]\n    \"\"\"\n    green = np.clip(preds_3[:, 0], 0, None)\n    clover = np.clip(preds_3[:, 1], 0, None)\n    dead = np.clip(preds_3[:, 2], 0, None)\n    \n    gdm = green + clover\n    total = gdm + dead\n    \n    return np.stack([clover, dead, green, total, gdm], axis=1)\n\n\nprint(\"Inference and expand functions defined\")","metadata":{"execution":{"iopub.status.busy":"2026-01-26T11:58:01.534771Z","iopub.execute_input":"2026-01-26T11:58:01.534956Z","iopub.status.idle":"2026-01-26T11:58:01.548737Z","shell.execute_reply.started":"2026-01-26T11:58:01.534939Z","shell.execute_reply":"2026-01-26T11:58:01.547994Z"},"papermill":{"duration":0.009771,"end_time":"2026-01-18T05:25:15.312585","exception":false,"start_time":"2026-01-18T05:25:15.302814","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Run Inference with Model Ensemble\n\nLoad one model at a time, make predictions, and ensemble the results.","metadata":{"papermill":{"duration":0.003301,"end_time":"2026-01-18T05:25:15.319339","exception":false,"start_time":"2026-01-18T05:25:15.316038","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Create test dataset and dataloader\ntest_dataset = PastureImageTestDataset(\n    test_wide,\n    CFG.DATA_DIR,\n    test_tfms\n)\n\ntest_loader = DataLoader(\n    test_dataset,\n    batch_size=CFG.BATCH_SIZE,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True,\n    collate_fn=collate_fn_test\n)\n\nprint(f\"Test loader created with {len(test_dataset)} samples\")\n\n# Compute R²-based weights for ensemble (only positive R² folds)\nr2_scores = np.array(list(CFG.FOLD_R2_SCORES.values()))\nfold_weights = r2_scores / r2_scores.sum()  # Normalize to sum to 1\n\nprint(f\"\\nFold weights based on R² scores:\")\nfor fold, weight in zip(CFG.FOLD_R2_SCORES.keys(), fold_weights):\n    print(f\"  Fold {fold}: R²={CFG.FOLD_R2_SCORES[fold]:.4f}, weight={weight:.4f}\")\n\n# Storage for ensemble predictions (3 targets: Green, Clover, Dead)\nall_fold_predictions = []\n\n# Loop through each fold\nfor fold, r2_score in CFG.FOLD_R2_SCORES.items():\n    print(f\"\\n{'='*60}\")\n    print(f\"Processing Fold {fold} (R²: {r2_score:.4f})\")\n    print(f\"{'='*60}\")\n    \n    model_file = CFG.MODEL_FILES.get(fold)\n    if model_file is None:\n        print(f\"No model file configured for fold {fold}, skipping...\")\n        continue\n        \n    model_path = CFG.MODEL_DIR / model_file\n    if not os.path.exists(model_path):\n        print(f\"Model file not found: {model_path}, skipping...\")\n        continue\n        \n    print(f\"Loading model from {model_path}...\")\n    \n    # Create model with matching architecture\n    model = BiomassModelDINOv3Mamba(\n        model_name=CFG.BACKBONE,\n        pretrained=False,\n        freeze_backbone=True\n    )\n    \n    # Load weights\n    state_dict = torch.load(model_path, map_location='cpu')\n    \n    # Handle DataParallel state dict if needed\n    if list(state_dict.keys())[0].startswith('module.'):\n        state_dict = {k.replace('module.', ''): v for k, v in state_dict.items()}\n    \n    model.load_state_dict(state_dict)\n    model = model.to(device)\n    \n    # Make predictions (returns 3 targets: Green, Clover, Dead)\n    fold_predictions = predict_with_model(model, test_loader)\n    \n    # Get weight for this fold\n    fold_idx = list(CFG.FOLD_R2_SCORES.keys()).index(fold)\n    weight = fold_weights[fold_idx]\n    \n    all_fold_predictions.append(weight * fold_predictions)\n    \n    print(f\"\\nFold {fold} predictions shape: {fold_predictions.shape}\")\n    print(f\"Predictions stats: min={fold_predictions.min():.4f}, max={fold_predictions.max():.4f}, mean={fold_predictions.mean():.4f}\")\n    \n    # Clean up to free memory\n    del model, state_dict\n    torch.cuda.empty_cache()\n    gc.collect()\n    print(f\"Memory cleaned after fold {fold}\")\n\nprint(f\"\\n{'='*60}\")\nprint(f\"All folds processed: {len(all_fold_predictions)} models\")\nprint(f\"{'='*60}\")","metadata":{"execution":{"iopub.status.busy":"2026-01-26T11:58:01.549584Z","iopub.execute_input":"2026-01-26T11:58:01.549803Z","iopub.status.idle":"2026-01-26T12:00:19.321844Z","shell.execute_reply.started":"2026-01-26T11:58:01.549783Z","shell.execute_reply":"2026-01-26T12:00:19.321195Z"},"papermill":{"duration":62.21848,"end_time":"2026-01-18T05:26:17.541528","exception":false,"start_time":"2026-01-18T05:25:15.323048","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Ensemble Predictions","metadata":{"papermill":{"duration":0.003983,"end_time":"2026-01-18T05:26:17.549743","exception":false,"start_time":"2026-01-18T05:26:17.545760","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Ensemble predictions (weighted average)\nif len(all_fold_predictions) == 0:\n    raise ValueError(\"No model predictions were generated. Check if model files exist.\")\n\n# Sum weighted predictions (already weighted, so just sum)\nensemble_preds_3 = np.sum(all_fold_predictions, axis=0)\n\nprint(f\"Ensemble predictions (3 targets) shape: {ensemble_preds_3.shape}\")\nprint(f\"  [Green, Clover, Dead]\")\n\n# Expand to 5 targets: [Clover, Dead, Green, Total, GDM]\nensemble_preds_5 = expand_predictions(ensemble_preds_3)\n\nprint(f\"\\nExpanded predictions (5 targets) shape: {ensemble_preds_5.shape}\")\nprint(f\"  [Clover, Dead, Green, Total, GDM]\")\n\n# Show predictions for first sample\nprint(\"\\nFirst sample predictions (before post-processing):\")\nfor i, target in enumerate(CFG.ALL_TARGETS):\n    print(f\"  {target}: {ensemble_preds_5[0, i]:.4f}\")","metadata":{"execution":{"iopub.status.busy":"2026-01-26T12:00:19.322832Z","iopub.execute_input":"2026-01-26T12:00:19.323067Z","iopub.status.idle":"2026-01-26T12:00:19.329120Z","shell.execute_reply.started":"2026-01-26T12:00:19.323042Z","shell.execute_reply":"2026-01-26T12:00:19.328553Z"},"papermill":{"duration":0.011433,"end_time":"2026-01-18T05:26:17.565133","exception":false,"start_time":"2026-01-18T05:26:17.553700","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Create Submission File","metadata":{"papermill":{"duration":0.00389,"end_time":"2026-01-18T05:26:17.572978","exception":false,"start_time":"2026-01-18T05:26:17.569088","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Apply post-processing\n# Target order: [Clover, Dead, Green, Total, GDM]\n#                  0       1      2      3      4\n\n# Make a copy for post-processing\nfinal_preds = ensemble_preds_5.copy()\n\n# Clover (index 0): scale down\nfinal_preds[:, 0] *= 0.8\n\n# Dead (index 1): conditional scaling\ndead = final_preds[:, 1]\nfinal_preds[:, 1] = np.where(\n    dead > 20, dead * 1.1,\n    np.where(dead < 10, dead * 0.9, dead)\n)\n\n# Recompute derived targets after post-processing\n# GDM = Green + Clover\nfinal_preds[:, 4] = final_preds[:, 2] + final_preds[:, 0]\n# Total = GDM + Dead\nfinal_preds[:, 3] = final_preds[:, 4] + final_preds[:, 1]\n\nprint(\"Post-processing applied:\")\nprint(\"  - Clover: scaled by 0.8\")\nprint(\"  - Dead: scaled by 1.1 if >20, by 0.9 if <10\")\nprint(\"  - GDM and Total recomputed\")\n\n# Create submission dataframe\n# Target order: [Clover, Dead, Green, Total, GDM]\nsubmission_rows = []\n\nfor idx, row in test_wide.iterrows():\n    image_id = row['image_path'].split('/')[-1].replace('.jpg', '')\n    \n    for target_idx, target_name in enumerate(CFG.ALL_TARGETS):\n        sample_id = f\"{image_id}__{target_name}\"\n        prediction = max(0, final_preds[idx, target_idx])  # Ensure non-negative\n        \n        submission_rows.append({\n            'sample_id': sample_id,\n            'target': prediction\n        })\n\nsubmission_df = pd.DataFrame(submission_rows)\nsubmission_path = \"submission.csv\"\nsubmission_df.to_csv(submission_path, index=False)\nprint(f\"\\nSubmission saved to {submission_path}\")","metadata":{"execution":{"iopub.status.busy":"2026-01-26T12:00:19.329864Z","iopub.execute_input":"2026-01-26T12:00:19.330085Z","iopub.status.idle":"2026-01-26T12:00:19.349811Z","shell.execute_reply.started":"2026-01-26T12:00:19.330066Z","shell.execute_reply":"2026-01-26T12:00:19.349208Z"},"papermill":{"duration":0.017785,"end_time":"2026-01-18T05:26:17.594694","exception":false,"start_time":"2026-01-18T05:26:17.576909","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Display statistics\nprint(\"\\nSubmission statistics by target:\")\nfor target in CFG.ALL_TARGETS:\n    target_rows = submission_df[submission_df['sample_id'].str.contains(target)]\n    if len(target_rows) > 0:\n        print(f\"  {target}:\")\n        print(f\"    Count: {len(target_rows)}\")\n        print(f\"    Min: {target_rows['target'].min():.4f}\")\n        print(f\"    Max: {target_rows['target'].max():.4f}\")\n        print(f\"    Mean: {target_rows['target'].mean():.4f}\")\n        print(f\"    Median: {target_rows['target'].median():.4f}\")\n\nprint(\"\\nSubmission preview:\")\nsubmission_df.head(10)","metadata":{"execution":{"iopub.status.busy":"2026-01-26T12:00:19.350718Z","iopub.execute_input":"2026-01-26T12:00:19.350935Z","iopub.status.idle":"2026-01-26T12:00:19.368203Z","shell.execute_reply.started":"2026-01-26T12:00:19.350915Z","shell.execute_reply":"2026-01-26T12:00:19.367731Z"},"papermill":{"duration":0.021263,"end_time":"2026-01-18T05:26:17.619973","exception":false,"start_time":"2026-01-18T05:26:17.598710","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"papermill":{"duration":0.004773,"end_time":"2026-01-18T05:26:17.629040","exception":false,"start_time":"2026-01-18T05:26:17.624267","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null}]}