{"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":14524447,"sourceType":"datasetVersion","datasetId":9276500},{"sourceId":4534,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":3326,"modelId":986}],"dockerImageVersionId":31260,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CSIRO Image2Biomass - DINOv2 Inference Notebook\n## Load trained models and generate submission\n\nThis notebook loads pre-trained DINOv2 models and generates predictions for submission.\n\n**Requirements:**\n- DINOv2 base model from Kaggle Models: `dinov2/pytorch/base/1`\n- Trained model weights uploaded as a Kaggle dataset\n- Competition data (`csiro-biomass`)\n\n**Note:** No internet access required - uses `local_files_only=True`","metadata":{}},{"cell_type":"code","source":"import os\nimport gc\nimport random\nimport math\nfrom pathlib import Path\nfrom dataclasses import dataclass\nfrom typing import Tuple, Dict, List\n\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom transformers import AutoImageProcessor, AutoModel\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm.auto import tqdm\nimport warnings\nwarnings.filterwarnings('ignore')\n\nprint(f\"PyTorch version: {torch.__version__}\")\nprint(f\"CUDA available: {torch.cuda.is_available()}\")\nif torch.cuda.is_available():\n    print(f\"GPU: {torch.cuda.get_device_name(0)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:36.073647Z","iopub.execute_input":"2026-01-17T07:06:36.074248Z","iopub.status.idle":"2026-01-17T07:06:36.081247Z","shell.execute_reply.started":"2026-01-17T07:06:36.074216Z","shell.execute_reply":"2026-01-17T07:06:36.080328Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@dataclass\nclass InferenceConfig:\n    \"\"\"Inference configuration\"\"\"\n    # Paths\n    DATA_PATH: Path = \"/kaggle/input/csiro-biomass\"\n    DINO_PATH: Path = \"/kaggle/input/dinov2/pytorch/base/1\"      # Pre-downloaded DINOv2 model\n    WEIGHTS_PATH: Path = \"/kaggle/input/csiro-model/dinov2_fold4.pth\"    # Trained regression head weights\n    OUTPUT_PATH: Path = \"./\"\n    \n    # Model\n    IMG_SIZE: int = 518  # DINOv2 optimal (multiple of 14)\n    HEAD_HIDDEN_DIM: int = 512\n    DROPOUT: float = 0.2\n    \n    # Inference\n    N_FOLDS: int = 5\n    BATCH_SIZE: int = 4\n    NUM_WORKERS: int = 0\n    USE_TTA: bool = True\n    TTA_MODES: Tuple[str, ...] = ('original', 'hflip', 'vflip')\n    \n    # Target columns (order matters for predictions)\n    TARGET_COLS: Tuple[str, ...] = ('Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g')\n    ALL_TARGET_COLS: Tuple[str, ...] = ('Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g')\n    \n    # Misc\n    SEED: int = 42\n    DEVICE: str = 'cuda' if torch.cuda.is_available() else 'cpu'\n    \n    def __post_init__(self):\n        if os.path.exists('/kaggle/input/csiro-biomass'):\n            # Kaggle environment\n            self.DATA_PATH = Path('/kaggle/input/csiro-biomass')\n            self.DINO_PATH = Path('/kaggle/input/dinov2/pytorch/base/1')  # Kaggle Models\n            self.WEIGHTS_PATH = Path('/kaggle/input/csiro-model')  # Your uploaded weights\n            self.OUTPUT_PATH = Path('/kaggle/working')\n            print(\"Running on Kaggle\")\n        else:\n            # Local environment\n            self.DATA_PATH = Path('data')\n            self.DINO_PATH = Path('models/dinov2-base')  # Local path\n            self.WEIGHTS_PATH = Path('models')\n            self.OUTPUT_PATH = Path('.')\n            print(\"Running locally\")\n        \n        self.OUTPUT_PATH.mkdir(parents=True, exist_ok=True)\n\n\nCFG = InferenceConfig()\nprint(f\"Device: {CFG.DEVICE}\")\nprint(f\"Data path: {CFG.DATA_PATH}\")\nprint(f\"DINOv2 path: {CFG.DINO_PATH}\")\nprint(f\"Weights path: {CFG.WEIGHTS_PATH}\")\nprint(f\"Output path: {CFG.OUTPUT_PATH}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:36.082349Z","iopub.execute_input":"2026-01-17T07:06:36.082997Z","iopub.status.idle":"2026-01-17T07:06:36.100053Z","shell.execute_reply.started":"2026-01-17T07:06:36.082974Z","shell.execute_reply":"2026-01-17T07:06:36.099407Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def seed_everything(seed: int) -> None:\n    \"\"\"Set seed for reproducibility\"\"\"\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed(seed)\n        torch.cuda.manual_seed_all(seed)\n        torch.backends.cudnn.deterministic = True\n\n\ndef flush() -> None:\n    \"\"\"Free GPU memory\"\"\"\n    gc.collect()\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n\n\ndef clean_values(arr: np.ndarray) -> np.ndarray:\n    \"\"\"Clean NaN, inf values and ensure non-negative\"\"\"\n    arr = np.nan_to_num(arr, nan=0.0, posinf=0.0, neginf=0.0)\n    arr = np.maximum(arr, 0.0)\n    return arr\n\n\nseed_everything(CFG.SEED)\nprint(f\"Seed: {CFG.SEED}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:36.101192Z","iopub.execute_input":"2026-01-17T07:06:36.101475Z","iopub.status.idle":"2026-01-17T07:06:36.117391Z","shell.execute_reply.started":"2026-01-17T07:06:36.101454Z","shell.execute_reply":"2026-01-17T07:06:36.116632Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. Load Data","metadata":{}},{"cell_type":"code","source":"def load_test_data(data_path: Path) -> Tuple[pd.DataFrame, pd.DataFrame]:\n    \"\"\"\n    Load test data in both long and wide format.\n    \n    Returns:\n        test_long: Original format (for sample_id mapping)\n        test_wide: Wide format (one row per image, for inference)\n    \"\"\"\n    test_long = pd.read_csv(data_path / 'test.csv')\n    \n    test_wide = test_long.copy()\n    test_wide['target'] = 0\n    test_wide = pd.pivot_table(\n        test_wide, \n        values='target', \n        index='image_path',\n        columns='target_name', \n        aggfunc='mean'\n    ).reset_index()\n    test_wide['full_path'] = test_wide['image_path'].apply(lambda p: str(data_path / p))\n    \n    return test_long, test_wide\n\n\ntest_long, test_df = load_test_data(CFG.DATA_PATH)\nprint(f\"Test samples (long format): {len(test_long)}\")\nprint(f\"Test samples (wide format - unique images): {len(test_df)}\")\nprint(f\"\\nSample data:\")\nprint(test_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:36.191744Z","iopub.execute_input":"2026-01-17T07:06:36.191974Z","iopub.status.idle":"2026-01-17T07:06:36.214608Z","shell.execute_reply.started":"2026-01-17T07:06:36.191955Z","shell.execute_reply":"2026-01-17T07:06:36.213894Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Image Preprocessing","metadata":{}},{"cell_type":"code","source":"def clean_image(img: np.ndarray) -> np.ndarray:\n    \"\"\"Remove bottom 10% (datestamp) and clean orange markers\"\"\"\n    h, w = img.shape[:2]\n    img = img[0:int(h * 0.90), :]  # Remove bottom 10%\n    \n    # Remove orange markers\n    hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)\n    lower = np.array([5, 150, 150])\n    upper = np.array([25, 255, 255])\n    mask = cv2.inRange(hsv, lower, upper)\n    mask = cv2.dilate(mask, np.ones((3, 3), np.uint8), iterations=2)\n    if np.sum(mask) > 0:\n        img = cv2.inpaint(img, mask, 3, cv2.INPAINT_TELEA)\n    \n    return img\n\n\ndef get_tta_transforms(img_size: int, mode: str) -> A.Compose:\n    \"\"\"Get transforms for TTA modes\"\"\"\n    transforms_list = [A.Resize(img_size, img_size)]\n    \n    if mode == 'hflip':\n        transforms_list.append(A.HorizontalFlip(p=1.0))\n    elif mode == 'vflip':\n        transforms_list.append(A.VerticalFlip(p=1.0))\n    elif mode == 'both':\n        transforms_list.append(A.HorizontalFlip(p=1.0))\n        transforms_list.append(A.VerticalFlip(p=1.0))\n    \n    # Note: We don't normalize here - the processor handles it\n    transforms_list.append(ToTensorV2())\n    \n    return A.Compose(transforms_list)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:36.216141Z","iopub.execute_input":"2026-01-17T07:06:36.216336Z","iopub.status.idle":"2026-01-17T07:06:36.223262Z","shell.execute_reply.started":"2026-01-17T07:06:36.216319Z","shell.execute_reply":"2026-01-17T07:06:36.222681Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class TestDataset(Dataset):\n    \"\"\"Test dataset for inference\"\"\"\n    \n    def __init__(self, df: pd.DataFrame, img_size: int, tta_mode: str = 'original'):\n        self.df = df.reset_index(drop=True)\n        self.img_size = img_size\n        self.tta_mode = tta_mode\n        self.transform = get_tta_transforms(img_size, tta_mode)\n    \n    def __len__(self) -> int:\n        return len(self.df)\n    \n    def __getitem__(self, idx: int) -> Dict:\n        row = self.df.iloc[idx]\n        \n        # Load image\n        img = cv2.imread(row['full_path'])\n        if img is None:\n            print(f\"Warning: Could not read image {row['full_path']}\")\n            img = np.zeros((1000, 2000, 3), dtype=np.uint8)\n        \n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = clean_image(img)\n        \n        # Split into left and right halves\n        h, w = img.shape[:2]\n        mid = w // 2\n        left = img[:, :mid]\n        right = img[:, mid:]\n        \n        # Apply transforms (resize + TTA flip)\n        left_t = self.transform(image=left)['image']  # [C, H, W], uint8\n        right_t = self.transform(image=right)['image']\n        \n        return {\n            'left': left_t,\n            'right': right_t,\n            'image_path': row['image_path']\n        }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:36.224067Z","iopub.execute_input":"2026-01-17T07:06:36.224809Z","iopub.status.idle":"2026-01-17T07:06:36.241019Z","shell.execute_reply.started":"2026-01-17T07:06:36.224786Z","shell.execute_reply":"2026-01-17T07:06:36.240311Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Model Architecture\n\nUsing `transformers` library with `local_files_only=True` to load DINOv2 without internet.","metadata":{}},{"cell_type":"code","source":"class BiomassModel(nn.Module):\n    \"\"\"\n    DINOv2 backbone (from transformers) + regression heads for biomass prediction.\n    \n    Uses local_files_only=True for offline inference on Kaggle.\n    \"\"\"\n    \n    def __init__(\n        self, \n        dino_path: str,\n        head_hidden_dim: int = 512,\n        dropout: float = 0.2\n    ):\n        super().__init__()\n        \n        # Load DINOv2 from local path (no internet needed)\n        self.processor = AutoImageProcessor.from_pretrained(dino_path, local_files_only=True)\n        self.encoder = AutoModel.from_pretrained(dino_path, local_files_only=True)\n        \n        # Freeze encoder\n        for param in self.encoder.parameters():\n            param.requires_grad = False\n        self.encoder.eval()\n        \n        # Get feature dimension from encoder config\n        self.feat_dim = self.encoder.config.hidden_size  # 768 for base\n        \n        # Projection layer (left + right features concatenated)\n        self.proj = nn.Sequential(\n            nn.Linear(self.feat_dim * 2, head_hidden_dim),\n            nn.LayerNorm(head_hidden_dim),\n            nn.GELU(),\n            nn.Dropout(dropout)\n        )\n        \n        # Regression heads\n        self.head_green = nn.Sequential(\n            nn.Linear(head_hidden_dim, head_hidden_dim // 2),\n            nn.GELU(),\n            nn.Dropout(dropout),\n            nn.Linear(head_hidden_dim // 2, 1),\n            nn.Softplus()\n        )\n        \n        self.head_dead = nn.Sequential(\n            nn.Linear(head_hidden_dim, head_hidden_dim // 2),\n            nn.GELU(),\n            nn.Dropout(dropout),\n            nn.Linear(head_hidden_dim // 2, 1),\n            nn.Softplus()\n        )\n        \n        self.head_clover = nn.Sequential(\n            nn.Linear(head_hidden_dim, head_hidden_dim // 2),\n            nn.GELU(),\n            nn.Dropout(dropout),\n            nn.Linear(head_hidden_dim // 2, 1),\n            nn.Softplus()\n        )\n    \n    def extract_features(self, x: torch.Tensor) -> torch.Tensor:\n        \"\"\"\n        Extract features using DINOv2 encoder.\n        \n        Args:\n            x: Image tensor [B, C, H, W] in range [0, 255] uint8 or [0, 1] float\n            \n        Returns:\n            Pooled features [B, feat_dim]\n        \"\"\"\n        # Convert to format expected by processor\n        if x.dtype == torch.uint8:\n            # Already uint8, convert to numpy for processor\n            imgs = x.permute(0, 2, 3, 1).cpu().numpy()  # [B, H, W, C]\n        else:\n            # Float tensor, convert to uint8\n            imgs = (x * 255).clamp(0, 255).byte().permute(0, 2, 3, 1).cpu().numpy()\n        \n        # Process images\n        inputs = self.processor(images=list(imgs), return_tensors=\"pt\")\n        inputs = {k: v.to(x.device) for k, v in inputs.items()}\n        \n        # Extract features\n        with torch.no_grad():\n            outputs = self.encoder(**inputs)\n            # Use CLS token as pooled features\n            features = outputs.last_hidden_state[:, 0, :]  # [B, feat_dim]\n        \n        return features\n    \n    def forward(self, left: torch.Tensor, right: torch.Tensor) -> Dict[str, torch.Tensor]:\n        \"\"\"\n        Forward pass.\n        \n        Args:\n            left: Left half of image [B, C, H, W]\n            right: Right half of image [B, C, H, W]\n            \n        Returns:\n            Dictionary with all 5 target predictions\n        \"\"\"\n        # Extract features from both halves\n        feat_left = self.extract_features(left)\n        feat_right = self.extract_features(right)\n        \n        # Concatenate and project\n        feat_cat = torch.cat([feat_left, feat_right], dim=1)  # [B, feat_dim*2]\n        feat = self.proj(feat_cat)  # [B, head_hidden_dim]\n        \n        # Predict base targets\n        green = self.head_green(feat).squeeze(-1)\n        dead = self.head_dead(feat).squeeze(-1)\n        clover = self.head_clover(feat).squeeze(-1)\n        \n        # Derive other targets\n        gdm = green + clover\n        total = gdm + dead\n        \n        return {\n            'Dry_Green_g': green,\n            'Dry_Dead_g': dead,\n            'Dry_Clover_g': clover,\n            'GDM_g': gdm,\n            'Dry_Total_g': total\n        }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:36.241979Z","iopub.execute_input":"2026-01-17T07:06:36.242245Z","iopub.status.idle":"2026-01-17T07:06:36.257823Z","shell.execute_reply.started":"2026-01-17T07:06:36.242217Z","shell.execute_reply":"2026-01-17T07:06:36.257229Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Test model loading\nprint(\"Loading DINOv2 model...\")\nmodel = BiomassModel(\n    dino_path=str(CFG.DINO_PATH),\n    head_hidden_dim=CFG.HEAD_HIDDEN_DIM,\n    dropout=CFG.DROPOUT\n).to(CFG.DEVICE)\n\nprint(f\"Feature dimension: {model.feat_dim}\")\nprint(f\"Model loaded successfully!\")\n\n# Count parameters\ntotal_params = sum(p.numel() for p in model.parameters())\ntrainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\nprint(f\"Total parameters: {total_params:,}\")\nprint(f\"Trainable parameters: {trainable_params:,}\")\n\ndel model\nflush()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:36.259666Z","iopub.execute_input":"2026-01-17T07:06:36.260191Z","iopub.status.idle":"2026-01-17T07:06:47.276114Z","shell.execute_reply.started":"2026-01-17T07:06:36.260170Z","shell.execute_reply":"2026-01-17T07:06:47.275322Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Inference Functions","metadata":{}},{"cell_type":"code","source":"def load_model_weights(model: BiomassModel, weights_path: Path) -> BiomassModel:\n    \"\"\"\n    Load trained regression head weights into model.\n    \n    Note: Only loads the projection and head weights, not the encoder.\n    \"\"\"\n    state_dict = torch.load(weights_path, map_location=CFG.DEVICE, weights_only=True)\n    \n    # Filter to only load proj and head weights (not encoder)\n    head_state_dict = {k: v for k, v in state_dict.items() \n                       if k.startswith('proj') or k.startswith('head')}\n    \n    model.load_state_dict(head_state_dict, strict=False)\n    return model\n\n\n@torch.no_grad()\ndef predict_fold(\n    model: BiomassModel,\n    dataloader: DataLoader,\n    cfg: InferenceConfig\n) -> np.ndarray:\n    \"\"\"Run prediction for a single fold\"\"\"\n    model.eval()\n    all_preds = []\n    \n    for batch in tqdm(dataloader, desc='Predicting', leave=False):\n        left = batch['left'].to(cfg.DEVICE, non_blocking=True)\n        right = batch['right'].to(cfg.DEVICE, non_blocking=True)\n        \n        preds = model(left, right)\n        \n        # Stack predictions in target order\n        pred_arr = np.stack([\n            preds['Dry_Clover_g'].cpu().numpy(),\n            preds['Dry_Dead_g'].cpu().numpy(),\n            preds['Dry_Green_g'].cpu().numpy(),\n            preds['Dry_Total_g'].cpu().numpy(),\n            preds['GDM_g'].cpu().numpy()\n        ], axis=1)\n        all_preds.append(pred_arr)\n    \n    result = np.vstack(all_preds)\n    result = clean_values(result)\n    return result","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:47.277332Z","iopub.execute_input":"2026-01-17T07:06:47.277642Z","iopub.status.idle":"2026-01-17T07:06:47.285608Z","shell.execute_reply.started":"2026-01-17T07:06:47.277615Z","shell.execute_reply":"2026-01-17T07:06:47.284916Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_inference(\n    test_df: pd.DataFrame,\n    cfg: InferenceConfig\n) -> np.ndarray:\n    \"\"\"\n    Run full inference with TTA and fold averaging.\n    \n    Returns:\n        Predictions array of shape (n_samples, 5)\n    \"\"\"\n    all_fold_preds = []\n    \n    # Create base model (encoder loaded once)\n    print(\"Loading DINOv2 encoder...\")\n    base_model = BiomassModel(\n        dino_path=str(cfg.DINO_PATH),\n        head_hidden_dim=cfg.HEAD_HIDDEN_DIM,\n        dropout=cfg.DROPOUT\n    ).to(cfg.DEVICE)\n    \n    for fold in range(cfg.N_FOLDS):\n        fold_path = cfg.WEIGHTS_PATH / f'dinov2_fold{fold}.pth'\n        \n        if not fold_path.exists():\n            print(f\"Warning: {fold_path} not found, skipping fold {fold}\")\n            continue\n        \n        print(f\"\\n===== Fold {fold} =====\")\n        \n        # Load fold weights\n        model = load_model_weights(base_model, fold_path)\n        model.eval()\n        \n        # TTA\n        tta_preds = []\n        tta_modes = cfg.TTA_MODES if cfg.USE_TTA else ('original',)\n        \n        for tta_mode in tta_modes:\n            print(f\"  TTA: {tta_mode}\")\n            \n            dataset = TestDataset(test_df, cfg.IMG_SIZE, tta_mode=tta_mode)\n            dataloader = DataLoader(\n                dataset,\n                batch_size=cfg.BATCH_SIZE,\n                shuffle=False,\n                num_workers=cfg.NUM_WORKERS,\n                pin_memory=True\n            )\n            \n            preds = predict_fold(model, dataloader, cfg)\n            tta_preds.append(preds)\n        \n        # Average TTA predictions\n        fold_pred = np.mean(tta_preds, axis=0)\n        fold_pred = clean_values(fold_pred)\n        all_fold_preds.append(fold_pred)\n    \n    del base_model\n    flush()\n    \n    if not all_fold_preds:\n        raise ValueError(\"No fold models found!\")\n    \n    # Average across folds\n    final_preds = np.mean(all_fold_preds, axis=0)\n    final_preds = clean_values(final_preds)\n    \n    return final_preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:47.286426Z","iopub.execute_input":"2026-01-17T07:06:47.286673Z","iopub.status.idle":"2026-01-17T07:06:47.305310Z","shell.execute_reply.started":"2026-01-17T07:06:47.286642Z","shell.execute_reply":"2026-01-17T07:06:47.304663Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Run Inference","metadata":{}},{"cell_type":"code","source":"print(\"Running inference...\")\nprint(f\"TTA enabled: {CFG.USE_TTA}\")\nprint(f\"TTA modes: {CFG.TTA_MODES}\")\n\npredictions = run_inference(test_df, CFG)\n\nprint(f\"\\nPredictions shape: {predictions.shape}\")\nprint(f\"Predictions columns: {CFG.ALL_TARGET_COLS}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:47.306147Z","iopub.execute_input":"2026-01-17T07:06:47.306424Z","iopub.status.idle":"2026-01-17T07:06:56.665344Z","shell.execute_reply.started":"2026-01-17T07:06:47.306396Z","shell.execute_reply":"2026-01-17T07:06:56.664682Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Display prediction statistics\nprint(\"\\n===== Prediction Statistics =====\")\nfor i, target in enumerate(CFG.ALL_TARGET_COLS):\n    vals = predictions[:, i]\n    print(f\"{target}: Mean={vals.mean():.4f}, Std={vals.std():.4f}, Min={vals.min():.4f}, Max={vals.max():.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:56.684130Z","iopub.execute_input":"2026-01-17T07:06:56.684362Z","iopub.status.idle":"2026-01-17T07:06:56.689198Z","shell.execute_reply.started":"2026-01-17T07:06:56.684342Z","shell.execute_reply":"2026-01-17T07:06:56.688454Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Post-Processing","metadata":{}},{"cell_type":"code","source":"def post_process_biomass(predictions: np.ndarray, target_order: Tuple[str, ...]) -> np.ndarray:\n    \"\"\"\n    Adjust predictions to satisfy physical constraints using projection.\n    \n    Constraints:\n    - GDM = Green + Clover\n    - Total = GDM + Dead\n    \"\"\"\n    idx_green = target_order.index('Dry_Green_g')\n    idx_dead = target_order.index('Dry_Dead_g')\n    idx_clover = target_order.index('Dry_Clover_g')\n    idx_gdm = target_order.index('GDM_g')\n    idx_total = target_order.index('Dry_Total_g')\n    \n    # Reorder to [Green, Clover, Dead, GDM, Total]\n    reorder = [idx_green, idx_clover, idx_dead, idx_gdm, idx_total]\n    Y = predictions[:, reorder].T\n    \n    # Constraint matrix\n    C = np.array([\n        [1, 1, 0, -1, 0],   # Green + Clover - GDM = 0\n        [0, 0, 1, 1, -1]    # Dead + GDM - Total = 0\n    ])\n    \n    # Projection matrix\n    P = np.eye(5) - C.T @ np.linalg.inv(C @ C.T) @ C\n    \n    # Project and clip\n    Y_reconciled = (P @ Y).T.clip(min=0)\n    \n    # Reorder back\n    ordered_cols = ['Dry_Green_g', 'Dry_Clover_g', 'Dry_Dead_g', 'GDM_g', 'Dry_Total_g']\n    reorder_back = [ordered_cols.index(col) for col in target_order]\n    result = Y_reconciled[:, reorder_back]\n    \n    return clean_values(result)\n\n\n# Apply post-processing\nfinal_predictions = post_process_biomass(predictions, CFG.ALL_TARGET_COLS)\n\n# Verify constraints\nidx_green = CFG.ALL_TARGET_COLS.index('Dry_Green_g')\nidx_dead = CFG.ALL_TARGET_COLS.index('Dry_Dead_g')\nidx_clover = CFG.ALL_TARGET_COLS.index('Dry_Clover_g')\nidx_gdm = CFG.ALL_TARGET_COLS.index('GDM_g')\nidx_total = CFG.ALL_TARGET_COLS.index('Dry_Total_g')\n\ngdm_check = final_predictions[:, idx_green] + final_predictions[:, idx_clover] - final_predictions[:, idx_gdm]\ntotal_check = final_predictions[:, idx_gdm] + final_predictions[:, idx_dead] - final_predictions[:, idx_total]\n\nprint(f\"GDM constraint violation (max): {np.abs(gdm_check).max():.6f}\")\nprint(f\"Total constraint violation (max): {np.abs(total_check).max():.6f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:56.689989Z","iopub.execute_input":"2026-01-17T07:06:56.690286Z","iopub.status.idle":"2026-01-17T07:06:56.723569Z","shell.execute_reply.started":"2026-01-17T07:06:56.690266Z","shell.execute_reply":"2026-01-17T07:06:56.722894Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Create Submission","metadata":{}},{"cell_type":"code","source":"def create_submission(\n    predictions: np.ndarray,\n    test_long: pd.DataFrame,\n    test_wide: pd.DataFrame,\n    target_order: Tuple[str, ...],\n    output_path: Path\n) -> pd.DataFrame:\n    \"\"\"Create submission CSV file.\"\"\"\n    # Create wide format predictions\n    pred_dict = {'image_path': test_wide['image_path'].values}\n    for i, col in enumerate(target_order):\n        pred_dict[col] = clean_values(predictions[:, i])\n    \n    wide_df = pd.DataFrame(pred_dict)\n    \n    # Melt to long format\n    long_preds = wide_df.melt(\n        id_vars=['image_path'],\n        value_vars=list(target_order),\n        var_name='target_name',\n        value_name='target'\n    )\n    \n    # Merge with original test_long to get sample_id\n    submission = pd.merge(\n        test_long[['sample_id', 'image_path', 'target_name']],\n        long_preds,\n        on=['image_path', 'target_name'],\n        how='left'\n    )[['sample_id', 'target']]\n    \n    # Final cleanup\n    submission['target'] = submission['target'].fillna(0.0)\n    submission['target'] = np.nan_to_num(submission['target'].values, nan=0.0, posinf=0.0, neginf=0.0)\n    submission['target'] = np.maximum(submission['target'].values, 0.0)\n    \n    # Save\n    submission_path = output_path / 'submission.csv'\n    submission.to_csv(submission_path, index=False)\n    print(f\"Saved submission to {submission_path}\")\n    \n    return submission","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:56.724338Z","iopub.execute_input":"2026-01-17T07:06:56.724693Z","iopub.status.idle":"2026-01-17T07:06:56.741971Z","shell.execute_reply.started":"2026-01-17T07:06:56.724665Z","shell.execute_reply":"2026-01-17T07:06:56.741511Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create submission\nsubmission = create_submission(\n    predictions=final_predictions,\n    test_long=test_long,\n    test_wide=test_df,\n    target_order=CFG.ALL_TARGET_COLS,\n    output_path=CFG.OUTPUT_PATH\n)\n\nprint(f\"\\nSubmission shape: {submission.shape}\")\nprint(f\"Expected rows: {len(test_long)}\")\nprint(f\"Match: {len(submission) == len(test_long)}\")\nprint(f\"\\nNaN count: {submission['target'].isna().sum()}\")\nprint(f\"Negative count: {(submission['target'] < 0).sum()}\")\nprint(f\"\\nFirst 10 rows:\")\nprint(submission.head(10))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:56.742663Z","iopub.execute_input":"2026-01-17T07:06:56.742921Z","iopub.status.idle":"2026-01-17T07:06:56.776132Z","shell.execute_reply.started":"2026-01-17T07:06:56.742896Z","shell.execute_reply":"2026-01-17T07:06:56.775633Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8. Summary","metadata":{}},{"cell_type":"code","source":"print(\"\\n\" + \"=\"*50)\nprint(\"INFERENCE SUMMARY\")\nprint(\"=\"*50)\nprint(f\"DINOv2 path: {CFG.DINO_PATH}\")\nprint(f\"Weights path: {CFG.WEIGHTS_PATH}\")\nprint(f\"Folds used: {CFG.N_FOLDS}\")\nprint(f\"TTA enabled: {CFG.USE_TTA}\")\nprint(f\"TTA modes: {CFG.TTA_MODES}\")\nprint(f\"\\nSubmission rows: {len(submission)}\")\nprint(f\"Unique images: {len(test_df)}\")\nprint(\"\\n\" + \"=\"*50)\nprint(\"Inference complete!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T07:06:56.777022Z","iopub.execute_input":"2026-01-17T07:06:56.777231Z","iopub.status.idle":"2026-01-17T07:06:56.782309Z","shell.execute_reply.started":"2026-01-17T07:06:56.777213Z","shell.execute_reply":"2026-01-17T07:06:56.781876Z"}},"outputs":[],"execution_count":null}]}