{"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,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":13900620,"sourceType":"datasetVersion","datasetId":8856212},{"sourceId":663314,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":471723,"modelId":487624},{"sourceId":716648,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":544792,"modelId":557864}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"\"\"\"\nCSIRO Biomass Prediction Inference\n\"\"\"\n\nimport warnings\nfrom pathlib import Path\nfrom typing import List, Tuple, Dict, Any\n\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom PIL import Image\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms as T\nfrom tqdm.auto import tqdm\nimport timm\nfrom tqdm.auto import tqdm\ntqdm.pandas()\n\nwarnings.filterwarnings(\"ignore\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T17:06:58.146796Z","iopub.execute_input":"2026-01-18T17:06:58.147096Z","iopub.status.idle":"2026-01-18T17:06:58.152825Z","shell.execute_reply.started":"2026-01-18T17:06:58.147072Z","shell.execute_reply":"2026-01-18T17:06:58.152249Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─── Configuration ──────────────────────────────────────────────────────────────\nclass CFG:\n    \"\"\"Global configuration\"\"\"\n    DATA_ROOT = Path(\"/kaggle/input/csiro-biomass/\")\n    TEST_CSV = DATA_ROOT / \"test.csv\"\n    \n    MODEL_NAME = \"vit_large_patch16_dinov3_qkvb\"\n    MODELS_DIR = Path(\"/kaggle/input/csiro-vit-large-dinov3/pytorch/default/1/csiro-dinov3-models\")\n    \n    IMG_SIZE = (512, 512)\n    BATCH_SIZE = 32\n    DEVICE = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n    \n    TARGET_COLUMNS = [\"Dry_Green_g\", \"Dry_Clover_g\", \"Dry_Dead_g\"]\n    OUTPUT_COLUMNS = TARGET_COLUMNS + [\"GDM_g\", \"Dry_Total_g\"]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T17:06:58.211429Z","iopub.execute_input":"2026-01-18T17:06:58.212039Z","iopub.status.idle":"2026-01-18T17:06:58.216520Z","shell.execute_reply.started":"2026-01-18T17:06:58.212013Z","shell.execute_reply":"2026-01-18T17:06:58.215832Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─── Dataset ────────────────────────────────────────────────────────────────────\nclass PairedBiomassDataset(Dataset):\n    \"\"\"Dataset that splits each image into left/right halves\"\"\"\n    \n    def __init__(self, df: pd.DataFrame, transform: T.Compose = None):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n\n    def __len__(self) -> int:\n        return len(self.df)\n\n    def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor, List[float]]:\n        row = self.df.iloc[idx]\n        \n        img = Image.open(CFG.DATA_ROOT / row[\"image_path\"]).convert(\"RGB\")\n        w, h = img.size\n        mid = w // 2\n        \n        left = img.crop((0, 0, mid, h))\n        right = img.crop((mid, 0, w, h))\n        \n        if self.transform is not None:\n            left = self.transform(left)\n            right = self.transform(right)\n            \n        # We don't really need targets in inference, but keeping structure\n        targets = [0.0] * len(CFG.TARGET_COLUMNS)\n        \n        return left, right, targets","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T17:06:58.251068Z","iopub.execute_input":"2026-01-18T17:06:58.251411Z","iopub.status.idle":"2026-01-18T17:06:58.257818Z","shell.execute_reply.started":"2026-01-18T17:06:58.251388Z","shell.execute_reply":"2026-01-18T17:06:58.257284Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_test_transforms(image_size: Tuple[int, int]) -> List[T.Compose]:\n    normalize = T.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n\n    return [\n        T.Compose([\n            T.Resize(image_size),\n            T.ToTensor(),\n            normalize,\n        ]),\n        T.Compose([\n            T.Resize(image_size),\n            T.RandomHorizontalFlip(p=1.0),\n            T.ToTensor(),\n            normalize,\n        ]),\n        T.Compose([\n            T.Resize(image_size),\n            T.RandomVerticalFlip(p=1.0),\n            T.ToTensor(),\n            normalize,\n        ]),\n    ]\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T17:06:58.295422Z","iopub.execute_input":"2026-01-18T17:06:58.295618Z","iopub.status.idle":"2026-01-18T17:06:58.300169Z","shell.execute_reply.started":"2026-01-18T17:06:58.295602Z","shell.execute_reply":"2026-01-18T17:06:58.299516Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_test_dataloaders(df: pd.DataFrame, batch_size: int = 32) -> List[DataLoader]:\n    transforms_list = create_test_transforms(CFG.IMG_SIZE)\n\n    loaders = []\n    for transform in transforms_list:\n        dataset = PairedBiomassDataset(df, transform=transform)\n        loader = DataLoader(\n            dataset,\n            batch_size=batch_size,\n            shuffle=False,\n            num_workers=0,\n            pin_memory=True,\n            drop_last=False\n        )\n        loaders.append(loader)\n    return loaders\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T17:06:58.376256Z","iopub.execute_input":"2026-01-18T17:06:58.376844Z","iopub.status.idle":"2026-01-18T17:06:58.381128Z","shell.execute_reply.started":"2026-01-18T17:06:58.376820Z","shell.execute_reply":"2026-01-18T17:06:58.380459Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─── Model ──────────────────────────────────────────────────────────────────────\nclass FiLM(nn.Module):\n    \"\"\"Feature-wise Linear Modulation\"\"\"\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.ReLU(inplace=True),\n            nn.Linear(feat_dim // 2, feat_dim * 2)\n        )\n\n    def forward(self, context: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:\n        gamma_beta = self.mlp(context)\n        return torch.chunk(gamma_beta, 2, dim=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T17:06:58.880494Z","iopub.execute_input":"2026-01-18T17:06:58.881129Z","iopub.status.idle":"2026-01-18T17:06:58.885826Z","shell.execute_reply.started":"2026-01-18T17:06:58.881105Z","shell.execute_reply":"2026-01-18T17:06:58.885184Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CSIROBiomassRegressor(nn.Module):\n    \"\"\"Main model with FiLM conditioning between left & right image features\"\"\"\n    \n    def __init__(\n        self,\n        model_name: str = CFG.MODEL_NAME,\n        pretrained: bool = False,\n        dropout: float = 0.0,\n    ):\n        super().__init__()\n        \n        self.backbone = timm.create_model(\n            model_name,\n            pretrained=pretrained,\n            num_classes=0,\n            global_pool=\"avg\"\n        )\n        \n        feat_dim = self.backbone.num_features\n        \n        self.film = FiLM(feat_dim)\n        self.dropout = nn.Dropout(dropout)\n        \n        def regression_head():\n            return nn.Sequential(\n                nn.Linear(feat_dim * 2, 8),\n                nn.ReLU(inplace=True),\n                nn.Dropout(dropout),\n                nn.Linear(8, 1),\n            )\n\n        self.head_green = regression_head()\n        self.head_clover = regression_head()\n        self.head_dead = regression_head()\n\n        self.softplus = nn.Softplus(beta=1.0)\n\n    def forward(self, left: torch.Tensor, right: torch.Tensor) -> torch.Tensor:\n        f_left = self.backbone(left)\n        f_right = self.backbone(right)\n\n        # Average features as context for FiLM\n        context = (f_left + f_right) / 2\n        gamma, beta = self.film(context)\n\n        # Modulate both feature vectors\n        f_left_mod = f_left * (1 + gamma) + beta\n        f_right_mod = f_right * (1 + gamma) + beta\n\n        # Concatenate modulated features\n        combined = torch.cat([f_left_mod, f_right_mod], dim=1)\n\n        # Predictions\n        green = self.softplus(self.head_green(combined))\n        clover = self.softplus(self.head_clover(combined))\n        dead = self.softplus(self.head_dead(combined))\n\n        return torch.cat([green, clover, dead], dim=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T17:06:59.972062Z","iopub.execute_input":"2026-01-18T17:06:59.972925Z","iopub.status.idle":"2026-01-18T17:06:59.980505Z","shell.execute_reply.started":"2026-01-18T17:06:59.972889Z","shell.execute_reply":"2026-01-18T17:06:59.979875Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─── Inference Utils ────────────────────────────────────────────────────────────\n@torch.inference_mode()\ndef predict_one_loader(model: nn.Module, loader: DataLoader) -> np.ndarray:\n    model.eval()\n    preds = []\n    \n    for left, right, _ in tqdm(loader, desc=\"Predicting\", leave=False):\n        left = left.to(CFG.DEVICE)\n        right = right.to(CFG.DEVICE)\n        \n        out = model(left, right)\n        preds.append(out.detach().cpu().numpy())\n        \n    return np.concatenate(preds)\n\n\ndef predict_tta(model: nn.Module, loaders: List[DataLoader]) -> np.ndarray:\n    \"\"\"Test-time augmentation averaging\"\"\"\n    all_preds = []\n    for loader in loaders:\n        fold_preds = predict_one_loader(model, loader)\n        all_preds.append(fold_preds)\n    return np.mean(all_preds, axis=0)\n\n\ndef ensemble_predict(models_dir: Path, loaders: List[DataLoader]) -> np.ndarray:\n    \"\"\"Load all models and average their TTA predictions\"\"\"\n    all_model_preds = []\n    \n    for ckpt_path in tqdm(list(models_dir.glob(\"*.pth\")), desc=\"Models\"):\n        model = CSIROBiomassRegressor().to(CFG.DEVICE)\n        state_dict = torch.load(ckpt_path, map_location=CFG.DEVICE)\n        model.load_state_dict(state_dict)\n        \n        tta_pred = predict_tta(model, loaders)\n        all_model_preds.append(tta_pred)\n        \n        del model\n        torch.cuda.empty_cache()\n    \n    return np.mean(all_model_preds, axis=0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T17:07:00.133064Z","iopub.execute_input":"2026-01-18T17:07:00.133364Z","iopub.status.idle":"2026-01-18T17:07:00.140583Z","shell.execute_reply.started":"2026-01-18T17:07:00.133340Z","shell.execute_reply":"2026-01-18T17:07:00.139895Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─── Main Execution ─────────────────────────────────────────────────────────────\ndef main():\n    print(\"=== CSIRO Biomass Inference ===\")\n    print(f\"Device       : {CFG.DEVICE}\")\n    print(f\"Model        : {CFG.MODEL_NAME}\")\n    print(f\"Models count : {len(list(CFG.MODELS_DIR.glob('*.pth')))}\")\n    print()\n\n    # 1. Load & prepare test data\n    print(\"Loading test data...\")\n    test_df = pd.read_csv(CFG.TEST_CSV)\n    \n    # Create image dataframe (one row per image)\n    test_df[['prefix', 'suffix']] = test_df['sample_id'].str.split('__', expand=True)\n    image_df = test_df.groupby(['prefix', 'image_path']).size().reset_index(name='count')\n    image_df = image_df[['prefix', 'image_path']].rename(columns={'prefix': 'sample_id_prefix'})\n\n    image_df['image_path'] = image_df['image_path'].progress_apply(\n        lambda x: str(CFG.DATA_ROOT / x)\n    )\n\n    # 2. Create dataloaders\n    print(\"Creating TTA dataloaders...\")\n    test_loaders = get_test_dataloaders(image_df, CFG.BATCH_SIZE)\n\n    # 3. Run ensemble inference\n    print(\"Running ensemble prediction...\")\n    ensemble_predictions = ensemble_predict(CFG.MODELS_DIR, test_loaders)\n\n    # 4. Assign predictions\n    image_df[CFG.TARGET_COLUMNS] = ensemble_predictions\n    \n    image_df[\"GDM_g\"] = image_df[\"Dry_Green_g\"] + image_df[\"Dry_Clover_g\"]\n    image_df[\"Dry_Total_g\"] = image_df[\"GDM_g\"] + image_df[\"Dry_Dead_g\"]\n\n    # 5. Create submission format\n    print(\"Preparing submission...\")\n    submission = (\n        image_df.set_index(\"sample_id_prefix\")[CFG.OUTPUT_COLUMNS]\n        .stack()\n        .reset_index()\n        .rename(columns={\"level_1\": \"target_name\", 0: \"target\"})\n    )\n    \n    submission[\"sample_id\"] = (\n        submission[\"sample_id_prefix\"] + \"__\" + submission[\"target_name\"]\n    )\n\n    # Final columns\n    submission = submission[[\"sample_id\", \"target\"]].sort_values(\"sample_id\")\n\n    # Save\n    submission.to_csv(\"submission.csv\", index=False)\n    print(\"\\nFirst few rows of submission:\")\n    print(submission.head(12))\n    print(f\"\\nSubmission shape: {submission.shape}\")\n    print(\"Done!\")\n\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T17:07:00.141943Z","iopub.execute_input":"2026-01-18T17:07:00.142155Z","iopub.status.idle":"2026-01-18T17:07:30.802265Z","shell.execute_reply.started":"2026-01-18T17:07:00.142137Z","shell.execute_reply":"2026-01-18T17:07:30.801509Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}