{"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"},{"sourceId":14463363,"sourceType":"datasetVersion","datasetId":9237012}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ======================================================\n# Imports\n# ======================================================\nimport os\nimport cv2\nimport math\nimport timm\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nimport pandas as pd\nimport argparse\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm import tqdm\n\n# ======================================================\n# Configuration\n# ======================================================\nclass Args:\n    # -------------------------------------------------------\n    # USER CONFIGURATION\n    # -------------------------------------------------------\n    # Path where you uploaded your model weights and train.csv\n    dataset_path = \"/kaggle/input/mamba-huber-raw-experiment-2-derive-dead\" \n    \n    # Path to competition data\n    test_csv = \"/kaggle/input/csiro-biomass/test.csv\"\n    image_root = \"/kaggle/input/csiro-biomass\"\n    \n    # -------------------------------------------------------\n    # MODEL SETTINGS (Must match training)\n    # -------------------------------------------------------\n    model_filename = \"best_model.pt\"\n    train_csv_filename = \"train.csv\"\n    \n    img_size = 518\n    backbone = \"vit_large_patch14_dinov2\"\n    batch_size = 16\n    num_workers = 2\n    device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    \n    out_path = \"submission.csv\"\n\ndef get_args():\n    return Args()\n\n# ======================================================\n# Model Architecture (EXACT COPY FROM TRAINING)\n# ======================================================\nclass LocalMambaBlock(nn.Module):\n    \"\"\" PyTorch-native Mamba-style Gated Block for token mixing \"\"\"\n    def __init__(self, dim, kernel_size=5, expand_ratio=2, dropout=0.0):\n        super().__init__()\n        self.norm = nn.LayerNorm(dim)\n        hidden_dim = int(dim * expand_ratio)\n        self.in_proj = nn.Linear(dim, hidden_dim * 2)\n        self.conv = nn.Conv1d(hidden_dim, hidden_dim, kernel_size=kernel_size, \n                              padding=kernel_size//2, groups=hidden_dim)\n        self.act = nn.SiLU()\n        self.out_proj = nn.Linear(hidden_dim, dim)\n        self.drop = nn.Dropout(dropout)\n\n    def forward(self, x):\n        # x: [B, L, C]\n        B, L, C = x.shape\n        residual = x\n        x = self.norm(x)\n        x_proj = self.in_proj(x)\n        x1, x2 = x_proj.chunk(2, dim=-1)\n        x1 = x1.transpose(1, 2)\n        x1 = self.conv(x1)\n        x1 = x1.transpose(1, 2)\n        x1 = self.act(x1)\n        out = x1 * F.silu(x2) # Gating\n        out = self.out_proj(out)\n        out = self.drop(out)\n        return residual + out\n\nclass MambaFusionRegressor(nn.Module):\n    def __init__(self, backbone_name=\"vit_large_patch14_dinov2\", pretrained=False, meta_dim=4):\n        super().__init__()\n        \n        # 1. Visual Backbone \n        self.backbone = timm.create_model(backbone_name, pretrained=pretrained, num_classes=0, global_pool='', dynamic_img_size=True)\n        \n        nf = self.backbone.num_features\n\n        # 2. Mamba Fusion \n        self.fusion = nn.Sequential(\n            LocalMambaBlock(nf, kernel_size=5, dropout=0.1),\n            LocalMambaBlock(nf, kernel_size=21, dropout=0.1) \n        )\n        \n        # 3. Pooling & Heads\n        self.pool = nn.AdaptiveAvgPool1d(1)\n        \n        self.meta_encoder = nn.Sequential(\n            nn.Linear(meta_dim, 64),\n            nn.SiLU(),\n            nn.Linear(64, 64)\n        )\n\n        # Update Heads to accept (Visual_Features + Meta_Features)\n        in_dim = self.backbone.num_features + 64 \n        \n        self.head_green = nn.Sequential(nn.Linear(in_dim, in_dim//2), \n                                        nn.GELU(), nn.Dropout(0.2), \n                                        nn.Linear(in_dim//2, 1))\n        self.head_clover = nn.Sequential(nn.Linear(in_dim, in_dim//2), \n                                         nn.GELU(), nn.Dropout(0.2), \n                                         nn.Linear(in_dim//2, 1))\n        self.head_dead = nn.Sequential(nn.Linear(in_dim, in_dim//2), \n                                       nn.GELU(), nn.Dropout(0.2), \n                                       nn.Linear(in_dim//2, 1))\n\n    def forward(self, left, right, meta=None):\n        # 1. Extract features (Frozen DINO)\n        l_feat = self.backbone(left)\n        r_feat = self.backbone(right)\n        \n        # 2. Concatenate Sequence: [B, 2*N, C]\n        x = torch.cat([l_feat, r_feat], dim=1)\n        \n        # 3. Mamba Fusion\n        x = self.fusion(x)\n        \n        # 4. Pooling: [B, C]\n        x = x.transpose(1, 2)\n        x = self.pool(x).flatten(1)\n\n        if meta is not None:\n            m_emb = self.meta_encoder(meta) # [B, 64]\n            x = torch.cat([x, m_emb], dim=1)\n        \n        # 5. Raw Regression\n        green = self.head_green(x)\n        clover = self.head_clover(x)\n        dead = self.head_dead(x)\n\n        green = F.relu(green)\n        clover = F.relu(clover)\n        dead = F.relu(dead)\n        \n        # 6. Physical Sums\n        gdm = green + clover\n        total = gdm + dead\n        \n        # ORDER: [green, clover, dead, gdm, total]\n        return torch.cat([green, clover, dead, gdm, total], dim=1)\n\n# ======================================================\n# Inference Dataset\n# ======================================================\ndef clean_image(img):\n    return img\n\nclass InferenceDataset(Dataset):\n    def __init__(self, df, image_root, img_size=518, stats=None):\n        self.df = df.reset_index(drop=True)\n        self.image_root = image_root\n        self.img_size = img_size\n        \n        # Fallback stats if None\n        if stats is None:\n            stats = {'ndvi_mean': 0.5, 'ndvi_std': 0.2, 'h_mean': 10.0, 'h_std': 10.0}\n        self.stats = stats\n\n        # Check available columns\n        self.has_ndvi = \"Pre_GSHH_NDVI\" in self.df.columns\n        self.has_height = \"Height_Ave_cm\" in self.df.columns\n        self.has_date = \"Sampling_Date\" in self.df.columns\n        \n        if self.has_date:\n            self.df[\"Sampling_Date\"] = pd.to_datetime(self.df[\"Sampling_Date\"], errors='coerce')\n\n        # Standard ImageNet normalization\n        norm_stats = {'mean': [0.485, 0.456, 0.406], 'std': [0.229, 0.224, 0.225]}\n        \n        self.tfms = A.Compose([\n            A.Resize(img_size, img_size), \n            A.Normalize(**norm_stats), \n            ToTensorV2()\n        ])\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        path = os.path.join(self.image_root, row[\"image_path\"])\n        \n        # 1. Image Loading\n        img = None\n        if os.path.exists(path):\n            img = cv2.imread(path)\n            \n        if img is None:\n            # Create black image if missing\n            img = np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n        else:\n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n            img = clean_image(img) \n\n        # 2. Split Image (Left / Right)\n        h, w, _ = img.shape\n        mid = w // 2\n        \n        # Handle edge case where image is too narrow\n        if w < 2: \n            left_img = np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n            right_img = np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n        else:\n            left_img = img[:, :mid]\n            right_img = img[:, mid:]\n\n        left = self.tfms(image=left_img)[\"image\"]\n        right = self.tfms(image=right_img)[\"image\"]\n\n        # 3. Meta Features (Standardized using Train Stats)\n        if self.has_ndvi and pd.notnull(row[\"Pre_GSHH_NDVI\"]):\n            ndvi = (float(row[\"Pre_GSHH_NDVI\"]) - self.stats['ndvi_mean']) / (self.stats['ndvi_std'] + 1e-6)\n        else: \n            ndvi = 0.0\n\n        if self.has_height and pd.notnull(row[\"Height_Ave_cm\"]):\n            height = (float(row[\"Height_Ave_cm\"]) - self.stats['h_mean']) / (self.stats['h_std'] + 1e-6)\n        else: \n            height = 0.0\n\n        if self.has_date and pd.notnull(row[\"Sampling_Date\"]):\n            doy = row[\"Sampling_Date\"].dayofyear\n            doy_sin = math.sin(2 * math.pi * doy / 365)\n            doy_cos = math.cos(2 * math.pi * doy / 365)\n        else: \n            doy_sin, doy_cos = 0.0, 0.0\n        \n        meta = torch.tensor([ndvi, height, doy_sin, doy_cos], dtype=torch.float32)\n        \n        return left, right, meta, row[\"image_path\"]\n\n# ======================================================\n# Test Time Augmentation (TTA)\n# ======================================================\ndef predict_tta(model, left, right, meta):\n    \"\"\"\n    4x TTA: Original, HFlip, VFlip, H+V Flip\n    \"\"\"\n    # 1. Prepare Stack\n    # [B, C, H, W] -> [B*4, C, H, W]\n    \n    # Left Augs\n    l_h = torch.flip(left, [3])\n    l_v = torch.flip(left, [2])\n    l_hv = torch.flip(left, [2, 3])\n    l_stack = torch.cat([left, l_h, l_v, l_hv], dim=0)\n\n    # Right Augs\n    r_h = torch.flip(right, [3])\n    r_v = torch.flip(right, [2])\n    r_hv = torch.flip(right, [2, 3])\n    r_stack = torch.cat([right, r_h, r_v, r_hv], dim=0)\n    \n    # Meta Augs (Repeat 4 times)\n    m_stack = meta.repeat(4, 1)\n\n    # 2. Inference\n    with torch.no_grad():\n        # Output: [B*4, 5] -> [green, clover, dead, gdm, total]\n        preds = model(l_stack, r_stack, m_stack) \n    \n    # 3. Aggregation\n    batch_size = left.shape[0]\n    # Reshape to [4, B, 5] and mean across views\n    preds = preds.view(4, batch_size, -1).mean(dim=0)\n    \n    return preds\n\n# ======================================================\n# Main\n# ======================================================\ndef long_to_wide_keep_meta(df):\n    \"\"\" Convert Test DF (Long) to Wide for per-image inference \"\"\"\n    meta_cols = [\"image_path\", \"Sampling_Date\", \"Pre_GSHH_NDVI\", \"Height_Ave_cm\"]\n    existing_meta_cols = [c for c in meta_cols if c in df.columns]\n    \n    # Drop duplicates to get one row per image\n    meta = df[existing_meta_cols].drop_duplicates(\"image_path\")\n    return meta\n\ndef main():\n    args = get_args()\n    device = torch.device(args.device)\n    \n    model_path = os.path.join(args.dataset_path, args.model_filename)\n    train_csv_path = os.path.join(args.dataset_path, args.train_csv_filename)\n\n    # --- 1. Load Stats from Train CSV ---\n    print(f\"📊 Calculating stats from: {train_csv_path}\")\n    if os.path.exists(train_csv_path):\n        train_df = pd.read_csv(train_csv_path)\n        stats = {\n            \"ndvi_mean\": train_df[\"Pre_GSHH_NDVI\"].mean(),\n            \"ndvi_std\": train_df[\"Pre_GSHH_NDVI\"].std(),\n            \"h_mean\": train_df[\"Height_Ave_cm\"].mean(),\n            \"h_std\": train_df[\"Height_Ave_cm\"].std(),\n        }\n        print(f\"   Stats loaded: NDVI Mean={stats['ndvi_mean']:.3f}, H Mean={stats['h_mean']:.3f}\")\n    else:\n        print(\"⚠️ Warning: Train CSV not found in dataset_path. Using default fallbacks.\")\n        stats = {'ndvi_mean': 0.65, 'ndvi_std': 0.15, 'h_mean': 7.6, 'h_std': 10.2}\n\n    # --- 2. Prepare Data ---\n    if not os.path.exists(args.test_csv):\n        print(f\"❌ Error: Test CSV not found at {args.test_csv}\")\n        return\n\n    test_df = pd.read_csv(args.test_csv)\n    \n    # Ensure sample_id exists for submission\n    if \"sample_id\" not in test_df.columns:\n        if \"id\" in test_df.columns:\n            test_df[\"sample_id\"] = test_df[\"id\"]\n        else:\n            # Create synthetic ID if missing\n            test_df[\"sample_id\"] = test_df[\"image_path\"] + \"_\" + test_df[\"target_name\"]\n\n    # Convert to one-row-per-image for efficient inference\n    unique_df = long_to_wide_keep_meta(test_df)\n    \n    ds = InferenceDataset(unique_df, args.image_root, img_size=args.img_size, stats=stats)\n    loader = DataLoader(ds, batch_size=args.batch_size, shuffle=False, \n                        num_workers=args.num_workers, pin_memory=True)\n\n    # --- 3. Load Model ---\n    print(f\"🤖 Loading Model from: {model_path}\")\n    model = MambaFusionRegressor(backbone_name=args.backbone, pretrained=False).to(device)\n    \n    if os.path.exists(model_path):\n        # Load weights\n        state_dict = torch.load(model_path, map_location=device)\n        model.load_state_dict(state_dict, strict=True)\n        print(\"   Weights loaded successfully.\")\n    else:\n        print(f\"❌ Error: Model weights not found at {model_path}\")\n        return\n        \n    model.eval()\n\n    # --- 4. Inference Loop ---\n    print(\"🚀 Running Inference with 4x TTA...\")\n    results = {} \n    \n    with torch.no_grad():\n        for left, right, meta, paths in tqdm(loader):\n            left, right, meta = left.to(device), right.to(device), meta.to(device)\n            \n            # Run TTA\n            # returns [B, 5] -> [green, clover, dead, gdm, total]\n            preds_batch = predict_tta(model, left, right, meta)\n            preds_batch = preds_batch.cpu().numpy()\n\n            for i, path in enumerate(paths):\n                p = preds_batch[i]\n                \n                # Training code order: [green, clover, dead, gdm, total]\n                val_green  = max(0.0, float(p[0]))\n                val_clover = max(0.0, float(p[1]))\n                val_dead   = max(0.0, float(p[2]))\n                val_gdm    = max(0.0, float(p[3]))\n                val_total  = max(0.0, float(p[4]))\n\n                results[path] = {\n                    \"Dry_Green_g\":  val_green,\n                    \"Dry_Clover_g\": val_clover,\n                    \"Dry_Dead_g\":   val_dead,\n                    \"GDM_g\":        val_gdm,\n                    \"Dry_Total_g\":  val_total\n                }\n\n    # --- 5. Generate Submission ---\n    print(\"💾 Generating submission file...\")\n    submission_rows = []\n    \n    # Iterate through original test_df to map predictions to specific target rows\n    for _, row in tqdm(test_df.iterrows(), total=len(test_df)):\n        img_path = row[\"image_path\"]\n        target_name = row[\"target_name\"]\n        s_id = row[\"sample_id\"]\n\n        pred_val = 0.0\n        if img_path in results and target_name in results[img_path]:\n            pred_val = results[img_path][target_name]\n        \n        submission_rows.append({\"sample_id\": s_id, \"target\": pred_val})\n\n    sub_df = pd.DataFrame(submission_rows)\n    sub_df.to_csv(args.out_path, index=False)\n    print(f\"✅ Submission saved to {args.out_path}\")\n    print(sub_df.head())\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-11T11:41:55.436108Z","iopub.execute_input":"2026-01-11T11:41:55.436793Z","iopub.status.idle":"2026-01-11T11:42:10.020541Z","shell.execute_reply.started":"2026-01-11T11:41:55.436759Z","shell.execute_reply":"2026-01-11T11:42:10.019866Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # ======================================================\n# # Imports\n# # ======================================================\n# import sys\n# import os\n# import cv2\n# import math\n# import timm\n# import torch\n# import torch.nn as nn\n# import torch.nn.functional as F\n# import numpy as np\n# import pandas as pd\n# from torch.utils.data import Dataset, DataLoader\n# import albumentations as A\n# from albumentations.pytorch import ToTensorV2\n# from tqdm import tqdm\n\n# # ======================================================\n# # Configuration\n# # ======================================================\n# class Args:\n#     # --- PATHS (Adjust these for your environment) ---\n#     test_csv = \"/kaggle/input/csiro-biomass/test.csv\"\n#     image_root = \"/kaggle/input/csiro-biomass\"\n    \n#     # Directory where you saved your training artifacts\n#     # Make sure this folder contains 'best_model.pt' and 'train.csv'\n#     model_dir = \"/kaggle/input/mamba-huber-raw-experiment-2-derive-dead\"\n    \n#     model_path = f\"{model_dir}/best_model.pt\"\n#     train_csv_path = f\"{model_dir}/train.csv\" \n    \n#     out_path = \"submission.csv\"\n    \n#     # --- SETTINGS ---\n#     batch_size = 16\n#     num_workers = 4\n#     device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    \n#     # Must match training script exactly\n#     img_size = 518   \n#     backbone = \"vit_large_patch14_dinov2\"\n\n# def get_args():\n#     return Args()\n\n# # ======================================================\n# # Model Architecture (MUST MATCH TRAINING CODE)\n# # ======================================================\n# class LocalMambaBlock(nn.Module):\n#     \"\"\" PyTorch-native Mamba-style Gated Block for token mixing \"\"\"\n#     def __init__(self, dim, kernel_size=5, expand_ratio=2, dropout=0.0):\n#         super().__init__()\n#         self.norm = nn.LayerNorm(dim)\n#         hidden_dim = int(dim * expand_ratio)\n#         self.in_proj = nn.Linear(dim, hidden_dim * 2)\n#         self.conv = nn.Conv1d(hidden_dim, hidden_dim, kernel_size=kernel_size, \n#                               padding=kernel_size//2, groups=hidden_dim)\n#         self.act = nn.SiLU()\n#         self.out_proj = nn.Linear(hidden_dim, dim)\n#         self.drop = nn.Dropout(dropout)\n\n#     def forward(self, x):\n#         # x: [B, L, C]\n#         B, L, C = x.shape\n#         residual = x\n#         x = self.norm(x)\n#         x_proj = self.in_proj(x)\n#         x1, x2 = x_proj.chunk(2, dim=-1)\n#         x1 = x1.transpose(1, 2)\n#         x1 = self.conv(x1)\n#         x1 = x1.transpose(1, 2)\n#         x1 = self.act(x1)\n#         out = x1 * F.silu(x2) # Gating\n#         out = self.out_proj(out)\n#         out = self.drop(out)\n#         return residual + out\n\n# class MambaFusionRegressor(nn.Module):\n#     def __init__(self, backbone_name=\"vit_large_patch14_dinov2\", pretrained=False):\n#         super().__init__()\n        \n#         # 1. Visual Backbone \n#         self.backbone = timm.create_model(backbone_name, pretrained=pretrained, num_classes=0, global_pool='', dynamic_img_size=True)\n        \n#         # We don't need gradients for inference\n#         for param in self.backbone.parameters():\n#             param.requires_grad = False\n            \n#         if hasattr(self.backbone, 'set_grad_checkpointing'):\n#             self.backbone.set_grad_checkpointing(True)\n\n#         nf = self.backbone.num_features\n\n#         # 2. Mamba Fusion \n#         self.fusion = nn.Sequential(\n#             LocalMambaBlock(nf, kernel_size=5, dropout=0.1),\n#             LocalMambaBlock(nf, kernel_size=21, dropout=0.1) # Matches training kernel size\n#         )\n        \n#         # 3. Pooling & Heads\n#         self.pool = nn.AdaptiveAvgPool1d(1)\n        \n#         # Prediction Heads (Raw Regression)\n#         # Note: Training code had green, clover, dead heads.\n#         self.head_green = nn.Sequential(nn.Linear(nf, nf//2), nn.GELU(), nn.Dropout(0.2), nn.Linear(nf//2, 1))\n#         self.head_clover = nn.Sequential(nn.Linear(nf, nf//2), nn.GELU(), nn.Dropout(0.2), nn.Linear(nf//2, 1))\n#         self.head_dead = nn.Sequential(nn.Linear(nf, nf//2), nn.GELU(), nn.Dropout(0.2), nn.Linear(nf//2, 1))\n\n#     def forward(self, left, right, meta=None):\n#         # 1. Extract features\n#         l_feat = self.backbone(left)\n#         r_feat = self.backbone(right)\n        \n#         # 2. Concatenate Sequence\n#         x = torch.cat([l_feat, r_feat], dim=1)\n        \n#         # 3. Mamba Fusion\n#         x = self.fusion(x)\n        \n#         # 4. Pooling\n#         x = x.transpose(1, 2)\n#         x = self.pool(x).flatten(1)\n        \n#         # 5. Raw Regression\n#         green = self.head_green(x)\n#         clover = self.head_clover(x)\n#         dead = self.head_dead(x)\n        \n#         # 6. Physical Sums\n#         gdm = green + clover\n#         total = gdm + dead\n        \n#         # Returns: [green, clover, dead, gdm, total]\n#         return torch.cat([green, clover, dead, gdm, total], dim=1)\n\n# # ======================================================\n# # Data Processing\n# # ======================================================\n# def clean_image(img):\n#     return img\n\n# class InferenceDataset(Dataset):\n#     def __init__(self, df, image_root, img_size=518, stats=None):\n#         self.df = df.reset_index(drop=True)\n#         self.image_root = image_root\n#         # Default fallback stats if none provided, though code should load them\n#         self.stats = stats if stats else {'ndvi_mean':0.6, 'ndvi_std':0.15, 'h_mean':10.0, 'h_std':10.0}\n#         self.img_size = img_size\n        \n#         self.has_ndvi = \"Pre_GSHH_NDVI\" in self.df.columns\n#         self.has_height = \"Height_Ave_cm\" in self.df.columns\n#         self.has_date = \"Sampling_Date\" in self.df.columns\n#         if self.has_date:\n#             self.df[\"Sampling_Date\"] = pd.to_datetime(self.df[\"Sampling_Date\"], errors='coerce')\n\n#         # Matches training normalization\n#         norm_stats = {'mean': [0.485, 0.456, 0.406], 'std': [0.229, 0.224, 0.225]}\n        \n#         self.tfms = A.Compose([\n#             A.Resize(img_size, img_size), \n#             A.Normalize(**norm_stats), \n#             ToTensorV2()\n#         ])\n\n#     def __len__(self):\n#         return len(self.df)\n\n#     def __getitem__(self, idx):\n#         row = self.df.iloc[idx]\n#         path = os.path.join(self.image_root, row[\"image_path\"])\n        \n#         # Safe Image Loading\n#         if not os.path.exists(path):\n#             img = np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n#         else:\n#             img = cv2.imread(path)\n#             if img is None:\n#                 img = np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n#             else:\n#                 img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n#                 img = clean_image(img) \n\n#         h, w, _ = img.shape\n#         mid = w // 2\n        \n#         # Handle edge cases where image might be corrupt or 0 width\n#         if w < 2: \n#             left_img = np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n#             right_img = np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n#         else:\n#             left_img = img[:, :mid]\n#             right_img = img[:, mid:]\n\n#         left = self.tfms(image=left_img)[\"image\"]\n#         right = self.tfms(image=right_img)[\"image\"]\n\n#         # Meta Features Normalization\n#         if self.has_ndvi and pd.notnull(row[\"Pre_GSHH_NDVI\"]):\n#             ndvi = (float(row[\"Pre_GSHH_NDVI\"]) - self.stats['ndvi_mean']) / (self.stats['ndvi_std'] + 1e-6)\n#         else: ndvi = 0.0\n\n#         if self.has_height and pd.notnull(row[\"Height_Ave_cm\"]):\n#             height = (float(row[\"Height_Ave_cm\"]) - self.stats['h_mean']) / (self.stats['h_std'] + 1e-6)\n#         else: height = 0.0\n\n#         if self.has_date and pd.notnull(row[\"Sampling_Date\"]):\n#             doy = row[\"Sampling_Date\"].dayofyear\n#             doy_sin = math.sin(2 * math.pi * doy / 365)\n#             doy_cos = math.cos(2 * math.pi * doy / 365)\n#         else: doy_sin, doy_cos = 0.0, 0.0\n        \n#         meta = torch.tensor([ndvi, height, doy_sin, doy_cos], dtype=torch.float32)\n#         return left, right, meta, row[\"image_path\"]\n\n# # ======================================================\n# # TTA (Test Time Augmentation)\n# # ======================================================\n# def predict_tta(model, left, right, meta):\n#     \"\"\"\n#     Performs 4x TTA: Original, Flip Horizontal, Flip Vertical, Rotate 180 (Flip H+V)\n#     \"\"\"\n#     # 1. Prepare Augmentations\n#     # Stack consists of: [Original, H-Flip, V-Flip, Rotate-180]\n    \n#     # Left Augmentations\n#     l_orig = left\n#     l_hflip = torch.flip(left, [3])      # Horizontal Flip (width dim)\n#     l_vflip = torch.flip(left, [2])      # Vertical Flip (height dim)\n#     l_hvflip = torch.flip(left, [2, 3])  # Rotate 180 (Both flips)\n    \n#     # Right Augmentations\n#     r_orig = right\n#     r_hflip = torch.flip(right, [3])\n#     r_vflip = torch.flip(right, [2])\n#     r_hvflip = torch.flip(right, [2, 3])\n    \n#     # Stack into a larger batch: Shape [B*4, C, H, W]\n#     l_stack = torch.cat([l_orig, l_hflip, l_vflip, l_hvflip], dim=0)\n#     r_stack = torch.cat([r_orig, r_hflip, r_vflip, r_hvflip], dim=0)\n    \n#     # Repeat Meta: Shape [B*4, Meta_Dim]\n#     meta_stack = meta.repeat(4, 1)\n    \n#     # 2. Inference\n#     with torch.no_grad():\n#         # [B*4, 5] -> [Green, Clover, Dead, GDM, Total]\n#         preds = model(l_stack, r_stack, meta_stack) \n        \n#     # 3. Average Results\n#     # Reshape to [4, B, 5] and take mean across dim 0\n#     batch_size = left.shape[0]\n#     preds = preds.view(4, batch_size, -1).mean(dim=0)\n    \n#     return preds\n\n# # ======================================================\n# # Main\n# # ======================================================\n# def long_to_wide_keep_meta(df):\n#     \"\"\"\n#     Converts the submission format (long) to image-level format (wide)\n#     so we only process each image once.\n#     \"\"\"\n#     meta_cols = [\"image_path\", \"Sampling_Date\", \"Pre_GSHH_NDVI\", \"Height_Ave_cm\"]\n#     existing_meta_cols = [c for c in meta_cols if c in df.columns]\n#     meta = df[existing_meta_cols].drop_duplicates(\"image_path\")\n#     return meta\n\n# def main():\n#     args = get_args()\n#     device = torch.device(args.device)\n    \n#     print(f\"📂 Model Directory: {args.model_dir}\")\n    \n#     # --- 1. Compute Stats from Training CSV ---\n#     print(f\"📊 Calculating stats from: {args.train_csv_path}\")\n#     if os.path.exists(args.train_csv_path):\n#         train_df = pd.read_csv(args.train_csv_path)\n#         stats = {\n#             \"ndvi_mean\": train_df[\"Pre_GSHH_NDVI\"].mean(),\n#             \"ndvi_std\": train_df[\"Pre_GSHH_NDVI\"].std(),\n#             \"h_mean\": train_df[\"Height_Ave_cm\"].mean(),\n#             \"h_std\": train_df[\"Height_Ave_cm\"].std(),\n#         }\n#         print(f\"   -> NDVI Mean: {stats['ndvi_mean']:.4f}, Std: {stats['ndvi_std']:.4f}\")\n#     else:\n#         print(\"⚠️ Warning: Train CSV not found. Using fallback stats. (This is risky!)\")\n#         stats = {'ndvi_mean': 0.65, 'ndvi_std': 0.15, 'h_mean': 7.6, 'h_std': 10.2}\n\n#     # --- 2. Load Test Data ---\n#     if not os.path.exists(args.test_csv):\n#         print(f\"❌ Error: Test CSV not found at {args.test_csv}\")\n#         return\n\n#     test_df = pd.read_csv(args.test_csv)\n    \n#     # Ensure sample_id exists\n#     if \"sample_id\" not in test_df.columns:\n#         if \"id\" in test_df.columns:\n#             test_df[\"sample_id\"] = test_df[\"id\"]\n#         else:\n#             # Fallback construction of ID\n#             test_df[\"sample_id\"] = test_df[\"image_path\"] + \"_\" + test_df[\"target_name\"]\n\n#     # Convert to one row per image for efficient inference\n#     unique_df = long_to_wide_keep_meta(test_df)\n#     print(f\"🖼️  Unique Images to process: {len(unique_df)}\")\n    \n#     ds = InferenceDataset(unique_df, args.image_root, img_size=args.img_size, stats=stats)\n#     loader = DataLoader(ds, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True)\n\n#     # --- 3. Load Model ---\n#     print(f\"🤖 Loading Model Weights: {args.model_path}\")\n#     if not os.path.exists(args.model_path):\n#         print(f\"❌ Error: Model file not found at {args.model_path}\")\n#         return\n        \n#     model = MambaFusionRegressor(backbone_name=args.backbone, pretrained=False).to(device)\n    \n#     # Strict loading ensures keys match exactly\n#     state_dict = torch.load(args.model_path, map_location=device)\n#     model.load_state_dict(state_dict, strict=True)\n#     model.eval()\n\n#     # --- 4. Inference with TTA ---\n#     print(\"🚀 Running Inference with 4x TTA...\")\n#     results = {} \n    \n#     with torch.no_grad():\n#         for left, right, meta, paths in tqdm(loader):\n#             left, right, meta = left.to(device), right.to(device), meta.to(device)\n            \n#             # Use TTA Function here\n#             preds_main = predict_tta(model, left, right, meta)\n#             preds_main = preds_main.cpu().numpy()\n\n#             for i, path in enumerate(paths):\n#                 p_main = preds_main[i]\n                \n#                 # Training Output Order: [green, clover, dead, gdm, total]\n#                 # Indices: 0, 1, 2, 3, 4\n                \n#                 val_green  = max(0.0, float(p_main[0]))\n#                 val_clover = max(0.0, float(p_main[1]))\n#                 val_dead   = max(0.0, float(p_main[2]))\n#                 val_gdm    = max(0.0, float(p_main[3]))\n#                 val_total  = max(0.0, float(p_main[4]))\n\n#                 results[path] = {\n#                     \"Dry_Green_g\":  val_green,\n#                     \"Dry_Clover_g\": val_clover,\n#                     \"Dry_Dead_g\":   val_dead,\n#                     \"GDM_g\":        val_gdm,\n#                     \"Dry_Total_g\":  val_total,\n#                 }\n\n#     # --- 5. Generate Submission ---\n#     print(\"💾 Generating submission file...\")\n#     submission_rows = []\n    \n#     # Iterate through original Long format CSV to ensure correct order/IDs\n#     for _, row in tqdm(test_df.iterrows(), total=len(test_df)):\n#         img_path = row[\"image_path\"]\n#         target_name = row[\"target_name\"]\n#         s_id = row[\"sample_id\"]\n\n#         pred_val = 0.0\n#         if img_path in results and target_name in results[img_path]:\n#             pred_val = results[img_path][target_name]\n        \n#         submission_rows.append({\"sample_id\": s_id, \"target\": pred_val})\n\n#     sub_df = pd.DataFrame(submission_rows)\n#     sub_df.to_csv(args.out_path, index=False)\n#     print(f\"✅ Submission saved to {args.out_path}\")\n#     print(sub_df.head())\n\n# if __name__ == \"__main__\":\n#     main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-11T09:22:38.318633Z","iopub.execute_input":"2026-01-11T09:22:38.318955Z","iopub.status.idle":"2026-01-11T09:22:52.191769Z","shell.execute_reply.started":"2026-01-11T09:22:38.318931Z","shell.execute_reply":"2026-01-11T09:22:52.1909Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # ======================================================\n# # Imports\n# # ======================================================\n# import sys\n# import os\n# import cv2\n# import math\n# import timm\n# import torch\n# import torch.nn as nn\n# import torch.nn.functional as F\n# import numpy as np\n# import pandas as pd\n# from torch.utils.data import Dataset, DataLoader\n# import albumentations as A\n# from albumentations.pytorch import ToTensorV2\n# from tqdm import tqdm\n\n# # ======================================================\n# # Configuration\n# # ======================================================\n# class Args:\n#     # --- PATHS ---\n#     test_csv = \"/kaggle/input/csiro-biomass/test.csv\"\n#     image_root = \"/kaggle/input/csiro-biomass\"\n    \n#     # Model & Artifact Paths\n#     model_dir = \"/kaggle/input/mamba-huber-raw-experiment-2-derive-dead\"\n#     model_path = f\"{model_dir}/best_model.pt\"\n#     train_csv_path = f\"{model_dir}/train.csv\" \n    \n#     out_path = \"submission.csv\"\n    \n#     # --- SETTINGS ---\n#     batch_size = 16\n#     num_workers = 2\n#     device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    \n#     # Must match training script\n#     img_size = 518   \n#     backbone = \"vit_large_patch14_dinov2\"\n\n# def get_args():\n#     return Args()\n\n# # ======================================================\n# # Model Architecture\n# # ======================================================\n\n# class LocalMambaBlock(nn.Module):\n#     def __init__(self, dim, kernel_size=5, expand_ratio=2, dropout=0.0):\n#         super().__init__()\n#         self.norm = nn.LayerNorm(dim)\n#         hidden_dim = int(dim * expand_ratio)\n#         self.in_proj = nn.Linear(dim, hidden_dim * 2)\n#         self.conv = nn.Conv1d(hidden_dim, hidden_dim, kernel_size=kernel_size, \n#                               padding=kernel_size//2, groups=hidden_dim)\n#         self.act = nn.SiLU()\n#         self.out_proj = nn.Linear(hidden_dim, dim)\n#         self.drop = nn.Dropout(dropout)\n\n#     def forward(self, x):\n#         B, L, C = x.shape\n#         residual = x\n#         x = self.norm(x)\n#         x_proj = self.in_proj(x)\n#         x1, x2 = x_proj.chunk(2, dim=-1)\n#         x1 = x1.transpose(1, 2)\n#         x1 = self.conv(x1)\n#         x1 = x1.transpose(1, 2)\n#         x1 = self.act(x1)\n#         out = x1 * F.silu(x2)\n#         out = self.out_proj(out)\n#         out = self.drop(out)\n#         return residual + out\n\n# class MambaFusionRegressor(nn.Module):\n#     def __init__(self, backbone_name=\"vit_large_patch14_dinov2\", pretrained=False):\n#         super().__init__()\n#         self.backbone = timm.create_model(backbone_name, pretrained=pretrained, num_classes=0, global_pool='', dynamic_img_size=True)\n#         for param in self.backbone.parameters():\n#             param.requires_grad = False\n#         if hasattr(self.backbone, 'set_grad_checkpointing'):\n#             self.backbone.set_grad_checkpointing(True)\n#         nf = self.backbone.num_features\n\n#         self.fusion = nn.Sequential(\n#             LocalMambaBlock(nf, kernel_size=5, dropout=0.1),\n#             LocalMambaBlock(nf, kernel_size=7, dropout=0.1) \n#         )\n#         self.pool = nn.AdaptiveAvgPool1d(1)\n        \n#         self.head_green = nn.Sequential(nn.Linear(nf, nf//2), nn.GELU(), nn.Dropout(0.2), nn.Linear(nf//2, 1))\n#         self.head_clover = nn.Sequential(nn.Linear(nf, nf//2), nn.GELU(), nn.Dropout(0.2), nn.Linear(nf//2, 1))\n#         self.head_total = nn.Sequential(nn.Linear(nf, nf//2), nn.GELU(), nn.Dropout(0.2), nn.Linear(nf//2, 1))\n\n#     def forward(self, left, right, meta=None):\n#         with torch.no_grad():\n#             l_feat = self.backbone(left)\n#             r_feat = self.backbone(right)\n        \n#         x = torch.cat([l_feat, r_feat], dim=1)\n#         x = self.fusion(x)\n#         x = x.transpose(1, 2)\n#         x = self.pool(x).flatten(1)\n        \n#         green = F.relu(self.head_green(x))\n#         clover = F.relu(self.head_clover(x))\n#         total = F.relu(self.head_total(x))\n        \n#         gdm = green + clover\n#         dead = F.relu(total - gdm)\n        \n#         return torch.cat([green, clover, total, gdm, dead], dim=1)\n\n# # ======================================================\n# # Data Processing\n# # ======================================================\n\n# def clean_image(img):\n#     return img\n\n# class InferenceDataset(Dataset):\n#     def __init__(self, df, image_root, img_size=518, stats=None):\n#         self.df = df.reset_index(drop=True)\n#         self.image_root = image_root\n#         self.stats = stats if stats else {'ndvi_mean':0, 'ndvi_std':1, 'h_mean':0, 'h_std':1}\n#         self.img_size = img_size\n        \n#         self.has_ndvi = \"Pre_GSHH_NDVI\" in self.df.columns\n#         self.has_height = \"Height_Ave_cm\" in self.df.columns\n#         self.has_date = \"Sampling_Date\" in self.df.columns\n#         if self.has_date:\n#             self.df[\"Sampling_Date\"] = pd.to_datetime(self.df[\"Sampling_Date\"], errors='coerce')\n\n#         norm_stats = {'mean': [0.485, 0.456, 0.406], 'std': [0.229, 0.224, 0.225]}\n#         self.tfms = A.Compose([\n#             A.Resize(img_size, img_size), \n#             A.Normalize(**norm_stats), \n#             ToTensorV2()\n#         ])\n\n#     def __len__(self):\n#         return len(self.df)\n\n#     def __getitem__(self, idx):\n#         row = self.df.iloc[idx]\n#         path = os.path.join(self.image_root, row[\"image_path\"])\n        \n#         if not os.path.exists(path):\n#             img = np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n#         else:\n#             img = cv2.imread(path)\n#             if img is None:\n#                 img = np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n#             else:\n#                 img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n#                 img = clean_image(img) \n\n#         h, w, _ = img.shape\n#         mid = w // 2\n#         if w < 2: \n#             left_img = np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n#             right_img = np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n#         else:\n#             left_img = img[:, :mid]\n#             right_img = img[:, mid:]\n\n#         left = self.tfms(image=left_img)[\"image\"]\n#         right = self.tfms(image=right_img)[\"image\"]\n\n#         if self.has_ndvi and pd.notnull(row[\"Pre_GSHH_NDVI\"]):\n#             ndvi = (float(row[\"Pre_GSHH_NDVI\"]) - self.stats['ndvi_mean']) / (self.stats['ndvi_std'] + 1e-6)\n#         else: ndvi = 0.0\n\n#         if self.has_height and pd.notnull(row[\"Height_Ave_cm\"]):\n#             height = (float(row[\"Height_Ave_cm\"]) - self.stats['h_mean']) / (self.stats['h_std'] + 1e-6)\n#         else: height = 0.0\n\n#         if self.has_date and pd.notnull(row[\"Sampling_Date\"]):\n#             doy = row[\"Sampling_Date\"].dayofyear\n#             doy_sin = math.sin(2 * math.pi * doy / 365)\n#             doy_cos = math.cos(2 * math.pi * doy / 365)\n#         else: doy_sin, doy_cos = 0.0, 0.0\n        \n#         meta = torch.tensor([ndvi, height, doy_sin, doy_cos], dtype=torch.float32)\n#         return left, right, meta, row[\"image_path\"]\n\n# # ======================================================\n# # TTA (Test Time Augmentation)\n# # ======================================================\n# def predict_tta(model, left, right, meta):\n#     \"\"\"\n#     Performs 4x TTA: Original, Flip Horizontal, Flip Vertical, Rotate 180\n#     Matches training augmentations (HFlip, VFlip, Rotate90 variants)\n#     \"\"\"\n#     # 1. Prepare Augmentations\n#     # Stack consists of: [Original, H-Flip, V-Flip, Rotate-180 (Flip H+V)]\n    \n#     # Left Augmentations\n#     l_orig = left\n#     l_hflip = torch.flip(left, [3])\n#     l_vflip = torch.flip(left, [2])\n#     l_hvflip = torch.flip(left, [2, 3])\n    \n#     # Right Augmentations\n#     r_orig = right\n#     r_hflip = torch.flip(right, [3])\n#     r_vflip = torch.flip(right, [2])\n#     r_hvflip = torch.flip(right, [2, 3])\n    \n#     # Stack into a larger batch: Shape [B*4, C, H, W]\n#     l_stack = torch.cat([l_orig, l_hflip, l_vflip, l_hvflip], dim=0)\n#     r_stack = torch.cat([r_orig, r_hflip, r_vflip, r_hvflip], dim=0)\n    \n#     # Repeat Meta: Shape [B*4, Meta_Dim]\n#     meta_stack = meta.repeat(4, 1)\n    \n#     # 2. Inference\n#     with torch.no_grad():\n#         preds = model(l_stack, r_stack, meta_stack) # [B*4, 5]\n        \n#     # 3. Average Results\n#     # Reshape to [4, B, 5] and take mean across dim 0\n#     batch_size = left.shape[0]\n#     preds = preds.view(4, batch_size, -1).mean(dim=0)\n    \n#     return preds\n\n# # ======================================================\n# # Main\n# # ======================================================\n\n# def long_to_wide_keep_meta(df):\n#     meta_cols = [\"image_path\", \"Sampling_Date\", \"Pre_GSHH_NDVI\", \"Height_Ave_cm\"]\n#     existing_meta_cols = [c for c in meta_cols if c in df.columns]\n#     meta = df[existing_meta_cols].drop_duplicates(\"image_path\")\n#     return meta\n\n# def main():\n#     args = get_args()\n#     device = torch.device(args.device)\n    \n#     # --- 1. Compute Stats ---\n#     print(f\"📊 Calculating stats from: {args.train_csv_path}\")\n#     if os.path.exists(args.train_csv_path):\n#         train_df = pd.read_csv(args.train_csv_path)\n#         stats = {\n#             \"ndvi_mean\": train_df[\"Pre_GSHH_NDVI\"].mean(),\n#             \"ndvi_std\": train_df[\"Pre_GSHH_NDVI\"].std(),\n#             \"h_mean\": train_df[\"Height_Ave_cm\"].mean(),\n#             \"h_std\": train_df[\"Height_Ave_cm\"].std(),\n#         }\n#     else:\n#         print(\"⚠️ Warning: Train CSV not found. Using fallback stats.\")\n#         stats = {'ndvi_mean': 0.65, 'ndvi_std': 0.15, 'h_mean': 7.6, 'h_std': 10.2}\n\n#     # --- 2. Load Data ---\n#     if not os.path.exists(args.test_csv):\n#         print(\"Error: Test CSV not found.\")\n#         return\n\n#     test_df = pd.read_csv(args.test_csv)\n#     if \"sample_id\" not in test_df.columns:\n#         if \"id\" in test_df.columns:\n#             test_df[\"sample_id\"] = test_df[\"id\"]\n#         else:\n#             test_df[\"sample_id\"] = test_df[\"image_path\"] + \"_\" + test_df[\"target_name\"]\n\n#     unique_df = long_to_wide_keep_meta(test_df)\n#     ds = InferenceDataset(unique_df, args.image_root, img_size=args.img_size, stats=stats)\n#     loader = DataLoader(ds, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True)\n\n#     # --- 3. Load Model ---\n#     print(f\"🤖 Loading Model: {args.model_path}\")\n#     model = MambaFusionRegressor(backbone_name=args.backbone, pretrained=False).to(device)\n    \n#     if os.path.exists(args.model_path):\n#         model.load_state_dict(torch.load(args.model_path, map_location=device), strict=True)\n#     else:\n#         print(f\"❌ Error: Model weights not found at {args.model_path}\")\n#         return\n        \n#     model.eval()\n\n#     # --- 4. Inference with TTA ---\n#     print(\"🚀 Running Inference with 4x TTA...\")\n#     results = {} \n    \n#     with torch.no_grad():\n#         for left, right, meta, paths in tqdm(loader):\n#             left, right, meta = left.to(device), right.to(device), meta.to(device)\n            \n#             # Use TTA Function here\n#             preds_main = predict_tta(model, left, right, meta)\n#             preds_main = preds_main.cpu().numpy()\n\n#             for i, path in enumerate(paths):\n#                 p_main = preds_main[i]\n                \n#                 # [Green, Clover, Total, GDM, Dead]\n#                 val_green  = max(0.0, float(p_main[0]))\n#                 val_clover = max(0.0, float(p_main[1]))\n#                 val_total  = max(0.0, float(p_main[2]))\n#                 val_gdm    = max(0.0, float(p_main[3]))\n#                 val_dead   = max(0.0, float(p_main[4]))\n\n#                 results[path] = {\n#                     \"Dry_Green_g\":  val_green,\n#                     \"Dry_Clover_g\": val_clover,\n#                     \"Dry_Total_g\":  val_total,\n#                     \"GDM_g\":        val_gdm,\n#                     \"Dry_Dead_g\":   val_dead\n#                 }\n\n#     # --- 5. Submission ---\n#     print(\"💾 Generating submission file...\")\n#     submission_rows = []\n    \n#     for _, row in tqdm(test_df.iterrows(), total=len(test_df)):\n#         img_path = row[\"image_path\"]\n#         target_name = row[\"target_name\"]\n#         s_id = row[\"sample_id\"]\n\n#         pred_val = 0.0\n#         if img_path in results and target_name in results[img_path]:\n#             pred_val = results[img_path][target_name]\n        \n#         submission_rows.append({\"sample_id\": s_id, \"target\": pred_val})\n\n#     sub_df = pd.DataFrame(submission_rows)\n#     sub_df.to_csv(args.out_path, index=False)\n#     print(f\"✅ Submission saved to {args.out_path}\")\n#     print(sub_df.head())\n\n# if __name__ == \"__main__\":\n#     main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-11T09:22:06.572327Z","iopub.execute_input":"2026-01-11T09:22:06.572653Z","iopub.status.idle":"2026-01-11T09:22:06.584454Z","shell.execute_reply.started":"2026-01-11T09:22:06.572624Z","shell.execute_reply":"2026-01-11T09:22:06.583474Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # ======================================================\n# # Imports\n# # ======================================================\n# import sys\n# import os\n# import cv2\n# import math\n# import timm\n# import torch\n# import torch.nn as nn\n# import torch.nn.functional as F\n# import numpy as np\n# import pandas as pd\n# from torch.utils.data import Dataset, DataLoader\n# import albumentations as A\n# from albumentations.pytorch import ToTensorV2\n# from tqdm import tqdm\n\n# # ======================================================\n# # Configuration\n# # ======================================================\n# class Args:\n#     # --- PATHS ---\n#     # Standard Kaggle Competition Paths\n#     test_csv = \"/kaggle/input/csiro-biomass/test.csv\"\n#     image_root = \"/kaggle/input/csiro-biomass\"\n    \n#     model_dir = \"/kaggle/input/mamba-huber-raw-experiment-2-derive-dead\"\n#     model_path = f\"{model_dir}/best_model.pt\"\n#     train_csv_path = f\"{model_dir}/train.csv\" \n    \n#     out_path = \"submission.csv\"\n    \n#     # --- SETTINGS ---\n#     batch_size = 16\n#     num_workers = 2\n#     device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    \n#     # Must match training script\n#     img_size = 518   \n#     backbone = \"vit_large_patch14_dinov2\"\n\n# def get_args():\n#     return Args()\n\n# # ======================================================\n# # Model Architecture (Must match Training Code)\n# # ======================================================\n\n# class LocalMambaBlock(nn.Module):\n#     \"\"\" PyTorch-native Mamba-style Gated Block for token mixing \"\"\"\n#     def __init__(self, dim, kernel_size=5, expand_ratio=2, dropout=0.0):\n#         super().__init__()\n#         self.norm = nn.LayerNorm(dim)\n#         hidden_dim = int(dim * expand_ratio)\n#         self.in_proj = nn.Linear(dim, hidden_dim * 2)\n#         self.conv = nn.Conv1d(hidden_dim, hidden_dim, kernel_size=kernel_size, \n#                               padding=kernel_size//2, groups=hidden_dim)\n#         self.act = nn.SiLU()\n#         self.out_proj = nn.Linear(hidden_dim, dim)\n#         self.drop = nn.Dropout(dropout)\n\n#     def forward(self, x):\n#         # x: [B, L, C]\n#         B, L, C = x.shape\n#         residual = x\n#         x = self.norm(x)\n#         x_proj = self.in_proj(x)\n#         x1, x2 = x_proj.chunk(2, dim=-1)\n#         x1 = x1.transpose(1, 2)\n#         x1 = self.conv(x1)\n#         x1 = x1.transpose(1, 2)\n#         x1 = self.act(x1)\n#         out = x1 * F.silu(x2) # Gating\n#         out = self.out_proj(out)\n#         out = self.drop(out)\n#         return residual + out\n\n# class MambaFusionRegressor(nn.Module):\n#     def __init__(self, backbone_name=\"vit_large_patch14_dinov2\", pretrained=False):\n#         super().__init__()\n        \n#         # 1. Visual Backbone \n#         self.backbone = timm.create_model(backbone_name, pretrained=pretrained, num_classes=0, global_pool='', dynamic_img_size=True)\n        \n#         # Freeze backbone logic (ensure consistency)\n#         for param in self.backbone.parameters():\n#             param.requires_grad = False\n\n#         if hasattr(self.backbone, 'set_grad_checkpointing'):\n#             self.backbone.set_grad_checkpointing(True)\n\n#         nf = self.backbone.num_features\n\n#         # 2. Mamba Fusion \n#         self.fusion = nn.Sequential(\n#             LocalMambaBlock(nf, kernel_size=5, dropout=0.1),\n#             LocalMambaBlock(nf, kernel_size=7, dropout=0.1) \n#         )\n        \n#         # 3. Pooling & Heads\n#         self.pool = nn.AdaptiveAvgPool1d(1)\n        \n#         # Prediction Heads (Raw Regression)\n#         self.head_green = nn.Sequential(nn.Linear(nf, nf//2), nn.GELU(), nn.Dropout(0.2), nn.Linear(nf//2, 1))\n#         self.head_clover = nn.Sequential(nn.Linear(nf, nf//2), nn.GELU(), nn.Dropout(0.2), nn.Linear(nf//2, 1))\n#         self.head_total = nn.Sequential(nn.Linear(nf, nf//2), nn.GELU(), nn.Dropout(0.2), nn.Linear(nf//2, 1))\n\n#     def forward(self, left, right, meta=None):\n#         with torch.no_grad():\n#             # 1. Extract features (Frozen DINO)\n#             l_feat = self.backbone(left)\n#             r_feat = self.backbone(right)\n        \n#         # 2. Concatenate Sequence: [B, 2*N, C]\n#         x = torch.cat([l_feat, r_feat], dim=1)\n        \n#         # 3. Mamba Fusion\n#         x = self.fusion(x)\n        \n#         # 4. Pooling: [B, C]\n#         x = x.transpose(1, 2)\n#         x = self.pool(x).flatten(1)\n        \n#         # 5. Raw Regression\n#         green = F.relu(self.head_green(x))\n#         clover = F.relu(self.head_clover(x))\n#         total = F.relu(self.head_total(x))\n        \n#         # 6. Physical Sums\n#         gdm = green + clover\n#         dead = F.relu(total - gdm)\n        \n#         # Returns: [Green, Clover, Total, GDM, Dead]\n#         return torch.cat([green, clover, total, gdm, dead], dim=1)\n\n# # ======================================================\n# # Data Processing\n# # ======================================================\n\n# def clean_image(img):\n#     return img\n\n# class InferenceDataset(Dataset):\n#     def __init__(self, df, image_root, img_size=518, stats=None):\n#         self.df = df.reset_index(drop=True)\n#         self.image_root = image_root\n        \n#         # Stats from training distribution\n#         self.stats = stats if stats else {'ndvi_mean':0, 'ndvi_std':1, 'h_mean':0, 'h_std':1}\n#         self.img_size = img_size\n        \n#         self.has_ndvi = \"Pre_GSHH_NDVI\" in self.df.columns\n#         self.has_height = \"Height_Ave_cm\" in self.df.columns\n#         self.has_date = \"Sampling_Date\" in self.df.columns\n        \n#         if self.has_date:\n#             self.df[\"Sampling_Date\"] = pd.to_datetime(self.df[\"Sampling_Date\"], errors='coerce')\n\n#         # Validation Transforms\n#         norm_stats = {'mean': [0.485, 0.456, 0.406], 'std': [0.229, 0.224, 0.225]}\n#         self.tfms = A.Compose([\n#             A.Resize(img_size, img_size), \n#             A.Normalize(**norm_stats), \n#             ToTensorV2()\n#         ])\n\n#     def __len__(self):\n#         return len(self.df)\n\n#     def __getitem__(self, idx):\n#         row = self.df.iloc[idx]\n#         path = os.path.join(self.image_root, row[\"image_path\"])\n        \n#         if not os.path.exists(path):\n#             img = np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n#         else:\n#             img = cv2.imread(path)\n#             if img is None:\n#                 img = np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n#             else:\n#                 img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n#                 img = clean_image(img) \n\n#         h, w, _ = img.shape\n#         mid = w // 2\n#         # Handle potential empty or corrupt images\n#         if w < 2: \n#             left_img = np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n#             right_img = np.zeros((self.img_size, self.img_size, 3), dtype=np.uint8)\n#         else:\n#             left_img = img[:, :mid]\n#             right_img = img[:, mid:]\n\n#         left = self.tfms(image=left_img)[\"image\"]\n#         right = self.tfms(image=right_img)[\"image\"]\n\n#         # --- Meta Features ---\n#         if self.has_ndvi and pd.notnull(row[\"Pre_GSHH_NDVI\"]):\n#             ndvi = (float(row[\"Pre_GSHH_NDVI\"]) - self.stats['ndvi_mean']) / (self.stats['ndvi_std'] + 1e-6)\n#         else: ndvi = 0.0\n\n#         if self.has_height and pd.notnull(row[\"Height_Ave_cm\"]):\n#             height = (float(row[\"Height_Ave_cm\"]) - self.stats['h_mean']) / (self.stats['h_std'] + 1e-6)\n#         else: height = 0.0\n\n#         if self.has_date and pd.notnull(row[\"Sampling_Date\"]):\n#             doy = row[\"Sampling_Date\"].dayofyear\n#             doy_sin = math.sin(2 * math.pi * doy / 365)\n#             doy_cos = math.cos(2 * math.pi * doy / 365)\n#         else: doy_sin, doy_cos = 0.0, 0.0\n        \n#         # Tensor: [ndvi, height, sin, cos] (Matches training code)\n#         meta = torch.tensor([ndvi, height, doy_sin, doy_cos], dtype=torch.float32)\n        \n#         return left, right, meta, row[\"image_path\"]\n\n# # ======================================================\n# # Helpers\n# # ======================================================\n\n# def long_to_wide_keep_meta(df):\n#     \"\"\"\n#     Transforms submission format (long) to image-level format (wide)\n#     so we can run inference once per image.\n#     \"\"\"\n#     meta_cols = [\"image_path\", \"Sampling_Date\", \"Pre_GSHH_NDVI\", \"Height_Ave_cm\"]\n#     existing_meta_cols = [c for c in meta_cols if c in df.columns]\n    \n#     # Drop duplicates to get unique images\n#     meta = df[existing_meta_cols].drop_duplicates(\"image_path\")\n#     return meta\n\n# def main():\n#     args = get_args()\n#     device = torch.device(args.device)\n    \n#     # --- 1. Compute Stats from Train CSV ---\n#     print(f\"📊 Calculating stats from: {args.train_csv_path}\")\n#     if os.path.exists(args.train_csv_path):\n#         train_df = pd.read_csv(args.train_csv_path)\n#         stats = {\n#             \"ndvi_mean\": train_df[\"Pre_GSHH_NDVI\"].mean(),\n#             \"ndvi_std\": train_df[\"Pre_GSHH_NDVI\"].std(),\n#             \"h_mean\": train_df[\"Height_Ave_cm\"].mean(),\n#             \"h_std\": train_df[\"Height_Ave_cm\"].std(),\n#         }\n#         print(f\"   Stats found: {stats}\")\n#     else:\n#         print(\"⚠️ Warning: Train CSV not found in model dir. Using fallback stats.\")\n#         stats = {'ndvi_mean': 0.65, 'ndvi_std': 0.15, 'h_mean': 7.6, 'h_std': 10.2}\n\n#     # --- 2. Load Test Data ---\n#     if not os.path.exists(args.test_csv):\n#         print(\"Error: Test CSV not found.\")\n#         return\n\n#     test_df = pd.read_csv(args.test_csv)\n    \n#     # Ensure sample_id exists\n#     if \"sample_id\" not in test_df.columns:\n#         if \"id\" in test_df.columns:\n#             test_df[\"sample_id\"] = test_df[\"id\"]\n#         else:\n#             test_df[\"sample_id\"] = test_df[\"image_path\"] + \"_\" + test_df[\"target_name\"]\n\n#     # Convert to unique images for batch inference\n#     unique_df = long_to_wide_keep_meta(test_df)\n#     print(f\"🖼️  Unique images to process: {len(unique_df)}\")\n    \n#     ds = InferenceDataset(unique_df, args.image_root, img_size=args.img_size, stats=stats)\n#     loader = DataLoader(ds, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True)\n\n#     # --- 3. Load Model ---\n#     print(f\"🤖 Loading Model: {args.model_path}\")\n#     model = MambaFusionRegressor(backbone_name=args.backbone, pretrained=False).to(device)\n    \n#     if os.path.exists(args.model_path):\n#         state_dict = torch.load(args.model_path, map_location=device)\n#         model.load_state_dict(state_dict, strict=True)\n#         print(\"✅ Weights loaded successfully.\")\n#     else:\n#         print(f\"❌ Error: Model weights not found at {args.model_path}\")\n#         return\n        \n#     model.eval()\n\n#     # --- 4. Inference ---\n#     print(\"🚀 Running Inference...\")\n#     results = {} \n    \n#     with torch.no_grad():\n#         for left, right, meta, paths in tqdm(loader):\n#             left, right, meta = left.to(device), right.to(device), meta.to(device)\n            \n#             # Predict\n#             # Model returns: [Green, Clover, Total, GDM, Dead]\n#             preds_main = model(left, right, meta)\n#             preds_main = preds_main.cpu().numpy()\n\n#             for i, path in enumerate(paths):\n#                 p_main = preds_main[i]\n                \n#                 # Clip negative predictions (ReLU in model handles most, but safe to clamp)\n#                 val_green  = max(0.0, float(p_main[0]))\n#                 val_clover = max(0.0, float(p_main[1]))\n#                 val_total  = max(0.0, float(p_main[2]))\n#                 val_gdm    = max(0.0, float(p_main[3]))\n#                 val_dead   = max(0.0, float(p_main[4]))\n\n#                 results[path] = {\n#                     \"Dry_Green_g\":  val_green,\n#                     \"Dry_Clover_g\": val_clover,\n#                     \"Dry_Total_g\":  val_total,\n#                     \"GDM_g\":        val_gdm,\n#                     \"Dry_Dead_g\":   val_dead\n#                 }\n\n#     # --- 5. Generate Submission ---\n#     print(\"💾 Generating submission file...\")\n#     submission_rows = []\n    \n#     for _, row in tqdm(test_df.iterrows(), total=len(test_df)):\n#         img_path = row[\"image_path\"]\n#         target_name = row[\"target_name\"]\n#         s_id = row[\"sample_id\"]\n\n#         pred_val = 0.0\n#         if img_path in results and target_name in results[img_path]:\n#             pred_val = results[img_path][target_name]\n        \n#         submission_rows.append({\"sample_id\": s_id, \"target\": pred_val})\n\n#     sub_df = pd.DataFrame(submission_rows)\n#     sub_df.to_csv(args.out_path, index=False)\n#     print(f\"✅ Submission saved to {args.out_path}\")\n#     print(sub_df.head())\n\n# if __name__ == \"__main__\":\n#     main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-11T09:22:06.585535Z","iopub.execute_input":"2026-01-11T09:22:06.5858Z","iopub.status.idle":"2026-01-11T09:22:06.6334Z","shell.execute_reply.started":"2026-01-11T09:22:06.585775Z","shell.execute_reply":"2026-01-11T09:22:06.632567Z"}},"outputs":[],"execution_count":null}]}