{"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":268942,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":230141,"modelId":251887},{"sourceId":663314,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":471723,"modelId":487624},{"sourceId":714180,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":540408,"modelId":553604}],"dockerImageVersionId":31259,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"from __future__ import annotations\n\nimport os\nimport gc\nimport shutil\nimport glob\nimport json\nimport argparse\nimport math\nimport re\nfrom dataclasses import dataclass, field\nfrom typing import List, Tuple, Optional, Dict\n\nimport numpy as np\nimport pandas as pd\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\nfrom PIL import Image\n\n# Optional deps (used by CSIRO v4 + DINOv3)\nimport cv2\n\n# ============================================================\n# Global toggles\n# ============================================================\n\n# Kaggle submission runs are often fully offline. Force Hugging Face + Transformers to never\n# make HTTP calls (and fail fast if something isn't already present in /kaggle/input).\nos.environ.setdefault(\"HF_HUB_OFFLINE\", \"1\")\nos.environ.setdefault(\"TRANSFORMERS_OFFLINE\", \"1\")\nos.environ.setdefault(\"HF_HOME\", \"/kaggle/working/hf\")\nos.environ.setdefault(\"HF_HUB_CACHE\", \"/kaggle/working/hf/hub\")\nos.environ.setdefault(\"TOKENIZERS_PARALLELISM\", \"false\")\ntorch.backends.cudnn.benchmark = True\n\n# ============================================================\n# RAM / VRAM cleanup helpers\n# ============================================================\n\ndef hard_cleanup() -> None:\n    \"\"\"Aggressively release Python + CUDA memory.\"\"\"\n    gc.collect()\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n        try:\n            torch.cuda.ipc_collect()\n        except Exception:\n            pass\n\n# ============================================================\n# Dataset helpers (long format)\n# ============================================================\n\ndef _resolve_image_path(img_dir: str, image_id: str) -> str:\n    \"\"\"Find the first file in img_dir that starts with image_id (any extension).\"\"\"\n    direct = os.path.join(img_dir, image_id)\n    if os.path.exists(direct):\n        return direct\n\n    exts = [\".jpg\", \".jpeg\", \".png\", \".tif\", \".tiff\", \".webp\", \".JPG\", \".JPEG\", \".PNG\", \".TIF\", \".TIFF\"]\n    for ext in exts:\n        p = os.path.join(img_dir, f\"{image_id}{ext}\")\n        if os.path.exists(p):\n            return p\n\n    hits = glob.glob(os.path.join(img_dir, f\"{image_id}*\"))\n    if hits:\n        return hits[0]\n\n    raise FileNotFoundError(f\"Could not resolve image path for image_id={image_id} in {img_dir}\")\n\n\ndef _split_image_id(sample_id: str) -> str:\n    \"\"\"Row id is like 'ID123__Dry_Green_g' -> base image id 'ID123'.\"\"\"\n    return sample_id.split(\"__\", 1)[0] if \"__\" in str(sample_id) else str(sample_id)\n\n\ndef _load_long_data(data_dir: str) -> Tuple[pd.DataFrame, pd.DataFrame, str]:\n    \"\"\"Return train_long, test_long, id_col. Long format columns:\n      - id_col (usually sample_id)\n      - image_id\n      - target_type\n      - target (train only)\n    \"\"\"\n    train_csv = os.path.join(data_dir, \"train.csv\")\n    test_csv = os.path.join(data_dir, \"test.csv\")\n    sub_csv = os.path.join(data_dir, \"sample_submission.csv\")\n\n    train = pd.read_csv(train_csv)\n    test = pd.read_csv(test_csv) if os.path.exists(test_csv) else None\n    sub = pd.read_csv(sub_csv)\n\n    # Identify id column\n    id_candidates = [c for c in [\"sample_id\", \"id\", \"row_id\"] if (c in train.columns) or (c in sub.columns)]\n    id_col = id_candidates[0] if id_candidates else sub.columns[0]\n\n    # Train: long or wide\n    if (\"target\" in train.columns) and ((\"target_type\" in train.columns) or (\"target_name\" in train.columns)):\n        ttype_col = \"target_type\" if \"target_type\" in train.columns else \"target_name\"\n        train_long = train[[id_col, ttype_col, \"target\"]].copy().rename(columns={ttype_col: \"target_type\"})\n    else:\n        known = [\"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\", \"Dry_Total_g\", \"GDM_g\"]\n        target_cols = [c for c in known if c in train.columns]\n        if not target_cols:\n            non_id = [c for c in train.columns if c != id_col]\n            target_cols = [c for c in non_id if pd.api.types.is_numeric_dtype(train[c])]\n        base_id = id_col\n        train_long = train.melt(id_vars=[base_id], value_vars=target_cols, var_name=\"target_type\", value_name=\"target\")\n        train_long[id_col] = train_long[base_id].astype(str) + \"__\" + train_long[\"target_type\"].astype(str)\n\n    # Test: from test.csv if it has target_type, else from sample_submission ids\n    if test is not None and ((\"target_type\" in test.columns) or (\"target_name\" in test.columns)):\n        ttype_col = \"target_type\" if \"target_type\" in test.columns else \"target_name\"\n        test_long = test[[id_col, ttype_col]].copy().rename(columns={ttype_col: \"target_type\"})\n        if \"image_path\" in test.columns:\n            test_long[\"image_path\"] = test[\"image_path\"].astype(str)\n    else:\n        test_long = sub[[id_col]].copy()\n        test_long[\"target_type\"] = test_long[id_col].astype(str).str.split(\"__\").str[-1]\n\n    train_long[\"image_id\"] = train_long[id_col].astype(str).map(_split_image_id)\n    test_long[\"image_id\"] = test_long[id_col].astype(str).map(_split_image_id)\n\n    return train_long, test_long, id_col\n\n\ndef enforce_mass_balance_long(pred_long: pd.DataFrame, id_col: str) -> pd.DataFrame:\n    \"\"\"Project predictions onto the mass-balance constraint subspace.\n\n    Constraints per image:\n      (1) Dry_Green_g + Dry_Clover_g - GDM_g = 0\n      (2) GDM_g + Dry_Dead_g - Dry_Total_g = 0\n\n    This is an orthogonal projection (least-squares closest point) rather than\n    hard overwriting some targets from others.\n\n    Expects columns: [id_col, image_id, target_type, target]\n    \"\"\"\n    needed = [\"Dry_Green_g\", \"Dry_Clover_g\", \"Dry_Dead_g\", \"GDM_g\", \"Dry_Total_g\"]\n    if not set(needed).issubset(set(pred_long[\"target_type\"].unique())):\n        return pred_long\n\n    wide = pred_long.pivot(index=\"image_id\", columns=\"target_type\", values=\"target\").reindex(columns=needed).copy().astype(np.float64)\n    X = wide.values\n\n    A = np.array(\n        [\n            [1.0, 1.0, 0.0, -1.0, 0.0],\n            [0.0, 0.0, 1.0, 1.0, -1.0],\n        ],\n        dtype=np.float64,\n    )\n    ATA_inv = np.linalg.inv(A @ A.T)  # 2x2\n\n    # Project: x' = x - A^T (A A^T)^{-1} A x\n    X_proj = X - (A.T @ (ATA_inv @ (A @ X.T))).T\n    X_proj = np.clip(X_proj, 0.0, None)\n\n    wide.iloc[:, :] = X_proj.astype(np.float64)\n\n    fixed = wide.reset_index().melt(id_vars=[\"image_id\"], var_name=\"target_type\", value_name=\"target\")\n    fixed = fixed.merge(pred_long[[id_col, \"image_id\", \"target_type\"]], on=[\"image_id\", \"target_type\"], how=\"right\")\n    return fixed[[id_col, \"image_id\", \"target_type\", \"target\"]]\n\n\n# ============================================================\n# SigLIP embeddings + Ridge\n# ============================================================\n\ndef _pick_siglip_dir() -> str:\n    # Kaggle Models sometimes mount under \".../transformers/default/1\"\n    candidates = [\n        \"/kaggle/input/google-siglip-so400m-patch14-384/transformers/default/1\",\n        \"/kaggle/input/google-siglip-so400m-patch14-384\",\n    ]\n    for p in candidates:\n        if os.path.exists(p):\n            return p\n    return candidates[0]\n\n\ndef generate_siglip_submission(\n    data_dir: str,\n    siglip_dir: str,\n    out_csv: str,\n    device: str = \"cuda\",\n    batch_size: int = 4,\n    ridge_alpha: float = 3.0,\n    cache_prefix: str = \"/kaggle/working/siglip_cache\",\n) -> str:\n    \"\"\"Generate a submission using SigLIP vision embeddings + per-target Ridge regression.\"\"\"\n    from sklearn.linear_model import Ridge\n    from sklearn.pipeline import make_pipeline\n    from sklearn.preprocessing import StandardScaler\n    from transformers import AutoProcessor, SiglipVisionModel\n\n    train_long, test_long, id_col = _load_long_data(data_dir)\n    train_img_dir = os.path.join(data_dir, \"train\")\n    test_img_dir = os.path.join(data_dir, \"test\")\n    target_types = sorted(train_long[\"target_type\"].unique().tolist())\n\n    # Wide labels: one row per image\n    y_wide = train_long.pivot(index=\"image_id\", columns=\"target_type\", values=\"target\").reindex(columns=target_types)\n    train_images = y_wide.index.tolist()\n    test_images = sorted(test_long[\"image_id\"].unique().tolist())\n\n    # Optional cache (nice for iterative notebook runs)\n    tr_cache = f\"{cache_prefix}_train.npy\"\n    te_cache = f\"{cache_prefix}_test.npy\"\n    meta_cache = f\"{cache_prefix}_meta.json\"\n    X_train = X_test = None\n\n    if os.path.exists(tr_cache) and os.path.exists(te_cache) and os.path.exists(meta_cache):\n        try:\n            meta = json.load(open(meta_cache, \"r\"))\n            if meta.get(\"train_images\") == train_images and meta.get(\"test_images\") == test_images and meta.get(\"siglip_dir\") == siglip_dir:\n                X_train = np.load(tr_cache)\n                X_test = np.load(te_cache)\n                print(\"[siglip] loaded cached embeddings.\")\n        except Exception:\n            X_train = X_test = None\n\n    def load_image(image_id: str, split: str) -> Image.Image:\n        img_dir = train_img_dir if split == \"train\" else test_img_dir\n        p = _resolve_image_path(img_dir, image_id)\n        return Image.open(p).convert(\"RGB\")\n\n    if X_train is None or X_test is None:\n        processor = AutoProcessor.from_pretrained(siglip_dir, local_files_only=True)\n        vision = SiglipVisionModel.from_pretrained(siglip_dir, local_files_only=True).to(device).eval()\n\n        @torch.no_grad()\n        def embed_ids(image_ids: List[str], split: str) -> np.ndarray:\n            embs = []\n            for i in range(0, len(image_ids), batch_size):\n                chunk = image_ids[i : i + batch_size]\n                imgs = [load_image(x, split) for x in chunk]\n                inputs = processor(images=imgs, return_tensors=\"pt\")\n                pixel_values = inputs[\"pixel_values\"].to(device)\n                amp_device = \"cuda\" if str(device).startswith(\"cuda\") else \"cpu\"\n                with torch.autocast(device_type=amp_device, dtype=torch.float16, enabled=(amp_device == \"cuda\")):\n                    out = vision(pixel_values=pixel_values)\n                    pooled = out.pooler_output\n                embs.append(pooled.float().cpu().numpy())\n                del pixel_values, out, pooled, inputs, imgs\n            return np.concatenate(embs, axis=0)\n\n        print(f\"[siglip] embedding train ({len(train_images)})...\")\n        X_train = embed_ids(train_images, \"train\")\n        print(f\"[siglip] embedding test ({len(test_images)})...\")\n        X_test = embed_ids(test_images, \"test\")\n\n        try:\n            np.save(tr_cache, X_train)\n            np.save(te_cache, X_test)\n            json.dump({\"train_images\": train_images, \"test_images\": test_images, \"siglip_dir\": siglip_dir}, open(meta_cache, \"w\"))\n        except Exception:\n            pass\n\n        del vision, processor\n        hard_cleanup()\n\n    preds_wide = np.zeros((len(test_images), len(target_types)), dtype=np.float32)\n    for j, tt in enumerate(target_types):\n        y = y_wide[tt].values.astype(np.float32)\n        y_tr = np.log1p(np.clip(y, 0.0, None))\n        model = make_pipeline(StandardScaler(with_mean=True), Ridge(alpha=ridge_alpha))\n        model.fit(X_train, y_tr)\n        y_pred = model.predict(X_test)\n        preds_wide[:, j] = np.expm1(y_pred).astype(np.float32)\n\n    pred_wide_df = pd.DataFrame(preds_wide, index=test_images, columns=target_types).reset_index().rename(columns={\"index\": \"image_id\"})\n    pred_long = pred_wide_df.melt(id_vars=[\"image_id\"], var_name=\"target_type\", value_name=\"target\")\n    pred_long = pred_long.merge(test_long[[id_col, \"image_id\", \"target_type\"]], on=[\"image_id\", \"target_type\"], how=\"right\")\n    pred_long = pred_long[[id_col, \"image_id\", \"target_type\", \"target\"]]\n\n    pred_long = enforce_mass_balance_long(pred_long, id_col)\n    pred_long[[id_col, \"target\"]].to_csv(out_csv, index=False, float_format=\"%.6f\")\n\n    del X_train, X_test, preds_wide\n    hard_cleanup()\n    return out_csv\n\n\n# ============================================================\n# DINOv3 baseline (baseline-dinov3_aux fold checkpoints)\n# ============================================================\n\ndef _pick_dinov3_ckpts() -> List[str]:\n    # Prefer baseline-dinov3_aux checkpoints from Kaggle Models\n    candidates = sorted(glob.glob(\"/kaggle/input/baseline-dinov3/**/best_model_fold*.pth\", recursive=True))\n    aux = [p for p in candidates if \"baseline-dinov3_aux\" in p]\n    picks = aux if aux else candidates\n\n    # sort by fold number\n    def fold_num(p: str) -> int:\n        m = re.search(r\"fold(\\d+)\", os.path.basename(p))\n        return int(m.group(1)) if m else 999\n\n    picks = sorted(picks, key=fold_num)\n    return picks\n\n\ndef _pick_dinov3_model_name() -> str:\n    import timm\n\n    names = timm.list_models(\"*dinov3*\")\n    preferred = [\n        \"vit_huge_plus_patch16_224.dinov3.lvd1689m\",\n        \"vit_huge_plus_patch16_224.dinov3\",\n        \"vit_huge_patch16_224.dinov3.lvd1689m\",\n        \"vit_huge_patch16_224.dinov3\",\n    ]\n    for p in preferred:\n        if p in names:\n            return p\n    # fallback: any huge dinov3\n    for p in names:\n        if \"huge\" in p and \"patch16\" in p:\n            return p\n    # last resort\n    return names[0] if names else \"vit_huge_patch16_224\"\n\n\nclass DinoV3BiomassDataset(Dataset):\n    def __init__(self, image_ids: List[str], img_dir: str, img_size: Tuple[int, int], flip: Optional[str] = None):\n        self.image_ids = image_ids\n        self.img_dir = img_dir\n        self.img_size = img_size\n        self.flip = flip\n\n        from torchvision import transforms\n\n        self.tf = transforms.Compose([\n            transforms.Resize(img_size, interpolation=transforms.InterpolationMode.BILINEAR),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]),\n        ])\n\n    def __len__(self) -> int:\n        return len(self.image_ids)\n\n    def __getitem__(self, idx: int):\n        image_id = self.image_ids[idx]\n        path = _resolve_image_path(self.img_dir, image_id)\n        img = Image.open(path).convert(\"RGB\")\n        if self.flip == \"h\":\n            if hasattr(Image, \"Transpose\"):\n                img = img.transpose(Image.Transpose.FLIP_LEFT_RIGHT)\n            else:\n                img = img.transpose(Image.FLIP_LEFT_RIGHT)\n        elif self.flip == \"v\":\n            if hasattr(Image, \"Transpose\"):\n                img = img.transpose(Image.Transpose.FLIP_TOP_BOTTOM)\n            else:\n                img = img.transpose(Image.FLIP_TOP_BOTTOM)\n\n        w, h = img.size\n        half = w // 2\n        left = img.crop((0, 0, half, h))\n        right = img.crop((half, 0, w, h))\n\n        return self.tf(left), self.tf(right), image_id\n\n\nclass CSIROModelRegressor(nn.Module):\n    \"\"\"\n    From public SigLIP/DINOv3 solution: takes left + right views, FiLM-modulates each side,\n    then predicts (green, clover, dead). Remaining targets can be derived by mass balance.\n\n    NOTE:\n      - Different public checkpoints use different MLP hidden sizes (e.g., 512 vs 640).\n      - We therefore allow `head_dim` to be passed (and we infer it from the checkpoint at load time).\n    \"\"\"\n\n    def __init__(\n        self,\n        model_name: str,\n        dropout: float = 0.2,\n        pretrained: bool = False,\n        dropout_backbone: float = 0.0,\n        features_dim: Optional[int] = None,\n        head_dim: int = 640,\n    ):\n        super().__init__()\n        import timm\n\n        self.backbone = timm.create_model(\n            model_name,\n            pretrained=pretrained,\n            num_classes=0,\n            global_pool=\"avg\",\n            drop_rate=dropout_backbone,\n            drop_path_rate=dropout_backbone,\n        )\n        features_dim = features_dim or getattr(self.backbone, \"num_features\", None) or 1280\n\n        self.film_alpha = nn.Linear(features_dim, features_dim)\n        self.film_beta = nn.Linear(features_dim, features_dim)\n\n        # Some public checkpoints do NOT include FiLM weights. We'll detect that at load time\n        # and set this flag accordingly so random FiLM params never affect predictions.\n        self.apply_film: bool = True\n\n        self.head_green = nn.Sequential(\n            nn.Linear(features_dim, head_dim),\n            nn.GELU(),\n            nn.Dropout(dropout),\n            nn.Linear(head_dim, 1),\n        )\n        self.head_clover = nn.Sequential(\n            nn.Linear(features_dim, head_dim),\n            nn.GELU(),\n            nn.Dropout(dropout),\n            nn.Linear(head_dim, 1),\n        )\n        self.head_dead = nn.Sequential(\n            nn.Linear(features_dim, head_dim),\n            nn.GELU(),\n            nn.Dropout(dropout),\n            nn.Linear(head_dim, 1),\n        )\n\n    def forward(self, x_left: torch.Tensor, x_right: torch.Tensor) -> torch.Tensor:\n        z_left = self.backbone(x_left)\n        z_right = self.backbone(x_right)\n        if getattr(self, \"apply_film\", True):\n            alpha = self.film_alpha(z_left)\n            beta = self.film_beta(z_left)\n            z_right = alpha * z_right + beta\n\n        green = self.head_green(z_right)\n        clover = self.head_clover(z_right)\n        dead = self.head_dead(z_right)\n        return torch.cat([green, clover, dead], dim=1)  # (B,3)\n\n\ndef _unwrap_state_dict(ckpt: object) -> Dict[str, torch.Tensor]:\n    \"\"\"Best-effort extraction of a model state_dict from common checkpoint formats.\"\"\"\n    if isinstance(ckpt, dict):\n        # Common wrappers\n        for k in [\"state_dict\", \"model_state\", \"model_state_dict\", \"model\", \"net\", \"ema\", \"model_ema\"]:\n            v = ckpt.get(k, None)\n            if isinstance(v, dict) and v and all(isinstance(x, torch.Tensor) for x in v.values()):\n                return v\n        # Sometimes the checkpoint itself is already a state dict\n        if ckpt and all(isinstance(x, torch.Tensor) for x in ckpt.values()):\n            return ckpt  # type: ignore[return-value]\n    raise ValueError(\"Could not extract a state_dict from checkpoint.\")\n\n\n@torch.no_grad()\ndef _predict_dinov3_fold(\n    ckpt_path: str,\n    model_name: str,\n    test_ids: List[str],\n    test_img_dir: str,\n    img_size: Tuple[int, int] = (512, 512),\n    batch_size: int = 8,\n    num_workers: int = 2,\n    device: str = \"cuda\",\n    tta: bool = True,\n) -> Tuple[np.ndarray, List[str]]:\n    raw = torch.load(ckpt_path, map_location=\"cpu\")\n    state = _unwrap_state_dict(raw)\n\n    # Strip DataParallel prefix if present\n    if any(k.startswith(\"module.\") for k in state.keys()):\n        state = {k[len(\"module.\"):]: v for k, v in state.items()}\n\n    # Some training code nests under \"model.\" prefix\n    if any(k.startswith(\"model.\") for k in state.keys()):\n        state = {k[len(\"model.\"):]: v for k, v in state.items()}\n\n    # Infer the MLP hidden dim from checkpoint (some use 512, others use 640)\n    head_dim = None\n    for k in (\"head_green.0.weight\", \"head_clover.0.weight\", \"head_dead.0.weight\"):\n        if isinstance(state, dict) and k in state:\n            head_dim = int(state[k].shape[0])\n            break\n    if head_dim is None:\n        head_dim = 640\n\n    # If the checkpoint doesn't contain FiLM params, we must NOT apply FiLM at inference\n    # (otherwise random init FiLM will destroy predictions).\n    apply_film = False\n    apply_film = any((\"film_alpha.\" in k) or (\"film_beta.\" in k) for k in state.keys())\n\n    model = CSIROModelRegressor(model_name=model_name, pretrained=False, head_dim=head_dim).to(device).eval()\n    model.apply_film = apply_film\n    missing, unexpected = model.load_state_dict(state, strict=False)\n    if missing or unexpected:\n        print(f\"[dinov3] {os.path.basename(ckpt_path)}: missing={len(missing)}, unexpected={len(unexpected)}\")\n\n    def make_loader(flip: Optional[str]) -> DataLoader:\n        ds = DinoV3BiomassDataset(test_ids, test_img_dir, img_size=img_size, flip=flip)\n        return DataLoader(ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True, drop_last=False)\n\n    loaders = [make_loader(None)]\n    if tta:\n        loaders += [make_loader(\"h\"), make_loader(\"v\")]\n\n    all_preds = None\n    for loader in loaders:\n        preds = []\n        ids = []\n        for xL, xR, image_id in loader:\n            xL = xL.to(device, non_blocking=True)\n            xR = xR.to(device, non_blocking=True)\n            amp_device = \"cuda\" if str(device).startswith(\"cuda\") else \"cpu\"\n            with torch.autocast(device_type=amp_device, dtype=torch.float16, enabled=(amp_device == \"cuda\")):\n                out = model(xL, xR)\n            preds.append(out.float().cpu().numpy())\n            ids.extend(list(image_id))\n        preds = np.concatenate(preds, axis=0)\n        if all_preds is None:\n            all_preds = preds\n        else:\n            all_preds += preds\n\n    all_preds = all_preds / len(loaders)\n\n    del model, state\n    hard_cleanup()\n    return all_preds, test_ids\n\n\ndef generate_dinov3_submission(\n    data_dir: str,\n    out_csv: str,\n    img_size: Tuple[int, int] = (512, 512),\n    batch_size: int = 8,\n    num_workers: int = 2,\n    tta: bool = True,\n) -> str:\n    _train_long, test_long, id_col = _load_long_data(data_dir)\n    test_img_dir = os.path.join(data_dir, \"test\")\n\n    test_ids = sorted(test_long[\"image_id\"].unique().tolist())\n\n    ckpts = _pick_dinov3_ckpts()\n    if not ckpts:\n        raise FileNotFoundError(\"Could not find baseline-dinov3 checkpoints under /kaggle/input/baseline-dinov3\")\n\n    model_name = _pick_dinov3_model_name()\n    print(f\"[dinov3] Using model_name={model_name}\")\n    print(\"[dinov3] Found ckpts:\")\n    for p in ckpts[:10]:\n        print(\" \", p)\n\n    device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\n    fold_sum = None\n    for k, ckpt in enumerate(ckpts):\n        print(f\"[dinov3] Fold {k} -> {os.path.basename(ckpt)}\")\n        preds3, _ = _predict_dinov3_fold(\n            ckpt_path=ckpt,\n            model_name=model_name,\n            test_ids=test_ids,\n            test_img_dir=test_img_dir,\n            img_size=img_size,\n            batch_size=batch_size,\n            num_workers=num_workers,\n            device=device,\n            tta=tta,\n        )\n        if fold_sum is None:\n            fold_sum = preds3\n        else:\n            fold_sum += preds3\n\n    preds3 = fold_sum / len(ckpts)\n    preds3 = np.clip(preds3, 0.0, None)\n\n    # Build 5 targets by mass balance\n    green = preds3[:, 0]\n    clover = preds3[:, 1]\n    dead = preds3[:, 2]\n    gdm = green + clover\n    total = gdm + dead\n\n    pred_wide = pd.DataFrame({\n        \"image_id\": test_ids,\n        \"Dry_Green_g\": green,\n        \"Dry_Clover_g\": clover,\n        \"Dry_Dead_g\": dead,\n        \"GDM_g\": gdm,\n        \"Dry_Total_g\": total,\n    })\n\n    pred_long = pred_wide.melt(id_vars=[\"image_id\"], var_name=\"target_type\", value_name=\"target\")\n    pred_long = pred_long.merge(test_long[[id_col, \"image_id\", \"target_type\"]], on=[\"image_id\", \"target_type\"], how=\"right\")\n    pred_long = pred_long[[id_col, \"image_id\", \"target_type\", \"target\"]]\n\n    pred_long = enforce_mass_balance_long(pred_long, id_col)\n    pred_long[[id_col, \"target\"]].to_csv(out_csv, index=False, float_format=\"%.6f\")\n    hard_cleanup()\n    return out_csv\n\n\n# ============================================================\n# CSIRO v4 model (fold checkpoints under /kaggle/input/csiro)\n# The block below is copied (with minimal edits) from a public notebook.\n# ============================================================\n\n# NOTE: Albumentations does a network version-check on import in some environments.\n# We delay-import it inside the CSIRO v4 block when/if you restore/use that code.\nA = None  # type: ignore\nToTensorV2 = None  # type: ignore\n\n\n@dataclass\nclass Config:\n    seed: int = 42\n\n    # training\n    experiment_dir_a: str = \"/kaggle/input/csiro/pytorch/default/12\"  # PublicModel root\n    ckpt_pattern_fold_x_a: str = \"/kaggle/input/csiro/pytorch/default/12/fold_{fold}/checkpoints/best_wr2.pt\"  # newer naming\n    ckpt_pattern_foldx_a: str = \"/kaggle/input/csiro/pytorch/default/12/fold{fold}/checkpoints/best_wr2.pt\"  # older naming\n    n_folds_a: int = 5\n\n    # inference\n    device: str = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    batch_size: int = 4\n    num_workers: int = 2\n    use_tta: bool = True\n\n    # data\n    test_image_size: Tuple[int, int] = (768, 768)\n\n    # model defaults\n    model_name_a: str = \"CrossPVT_T2T_MambaDINO\"\n    dropout: float = 0.0\n    hidden_ratio: float = 0.5\n\n    # heads\n    in_chans: int = 6\n    num_classes: int = 5\n    all_target_cols: Tuple[str, ...] = (\"Dry_Green_g\", \"Dry_Clover_g\", \"Dry_Dead_g\", \"GDM_g\", \"Dry_Total_g\")\n\n    # architecture\n    t2t_depth: int = 1\n    cross_depth: int = 2\n    pyramid_dims: Tuple[int, int, int] = (128, 256, 512)\n    num_heads: Tuple[int, int, int] = (2, 4, 8)\n\n\nCFG = Config()\n\n# ---- CSIRO v4 model code (unchanged) ----\n\nclass LayerScale(nn.Module):\n    def __init__(self, dim, init_values=1e-5, inplace=False):\n        super().__init__()\n        self.inplace = inplace\n        self.gamma = nn.Parameter(init_values * torch.ones(dim))\n\n    def forward(self, x):\n        return x.mul_(self.gamma) if self.inplace else x * self.gamma\n\n# NOTE: full CSIRO v4 architecture and run_inference_v4 were present in the previous canvas.\n# If you want that exact full block reinserted (it is ~1000 lines), say \"restore csiro block\".\n\n# ============================================================\n# Blending (CSV-only)\n# ============================================================\n\ndef blend_submissions(\n    sub_paths: List[str],\n    weights: Optional[List[float]],\n    out_csv: str,\n    data_dir: str,\n    per_target_weights: Optional[Dict[str, List[float]]] = None,\n) -> str:\n    \"\"\"Blend submissions by id column (first col of sample_submission.csv).\"\"\"\n    sub_base = pd.read_csv(os.path.join(data_dir, \"sample_submission.csv\"))\n    id_col = sub_base.columns[0]\n\n    if weights is None:\n        weights = [1.0] * len(sub_paths)\n    if len(sub_paths) == 0:\n        raise ValueError(\"No submissions to blend.\")\n    if len(sub_paths) != len(weights):\n        raise ValueError(\"weights length must match sub_paths length\")\n\n    dfs = []\n    for i, p in enumerate(sub_paths):\n        df = pd.read_csv(p)\n        if id_col not in df.columns or \"target\" not in df.columns:\n            raise ValueError(f\"{p} must have columns [{id_col}, target]\")\n        dfs.append(df[[id_col, \"target\"]].rename(columns={\"target\": f\"target_{i}\"}))\n\n    merged = sub_base[[id_col]].copy()\n    for df in dfs:\n        merged = merged.merge(df, on=id_col, how=\"left\")\n\n    w_default = np.array(weights, dtype=np.float32)\n    w_default = w_default / (w_default.sum() + 1e-12)\n\n    cols = [c for c in merged.columns if c.startswith(\"target_\")]\n    mat = merged[cols].values.astype(np.float32)\n\n    # Default blend\n    merged[\"target\"] = (mat * w_default.reshape(1, -1)).sum(axis=1)\n\n    # Optional per-target override weights (e.g., use DINO-only for Dry_Clover_g)\n    if per_target_weights:\n        target_type = merged[id_col].astype(str).str.split(\"__\").str[-1]\n        for tt, w_tt in per_target_weights.items():\n            if len(w_tt) != len(sub_paths):\n                raise ValueError(f\"per_target_weights['{tt}'] must have length {len(sub_paths)}\")\n            mask = (target_type == tt).values\n            if not mask.any():\n                continue\n            w_t = np.array(w_tt, dtype=np.float32)\n            w_t = w_t / (w_t.sum() + 1e-12)\n            merged.loc[mask, \"target\"] = (mat[mask] * w_t.reshape(1, -1)).sum(axis=1)\n\n    # Mass-balance projection on the final blended targets\n    tmp = merged[[id_col, \"target\"]].copy()\n    tmp[\"image_id\"] = tmp[id_col].astype(str).map(_split_image_id)\n    tmp[\"target_type\"] = tmp[id_col].astype(str).str.split(\"__\").str[-1]\n    tmp = enforce_mass_balance_long(tmp[[id_col, \"image_id\", \"target_type\", \"target\"]], id_col)\n    merged = merged.drop(columns=[\"target\"]).merge(tmp[[id_col, \"target\"]], on=id_col, how=\"left\")\n\n    merged[[id_col, \"target\"]].to_csv(out_csv, index=False, float_format=\"%.6f\")\n    return out_csv\n\n\n# ============================================================\n# Notebook-friendly default runner\n# ============================================================\n\ndef _running_in_notebook() -> bool:\n    import sys\n\n    return \"ipykernel\" in sys.modules\n\n\ndef run_default() -> None:\n    \"\"\"Default behavior when you click Run/Run All in Kaggle.\n\n    1) Builds SigLIP submission -> /kaggle/working/submission_siglip.csv\n    2) Builds DINOv3 submission (if checkpoints exist)\n    3) Builds CSIRO v4 submission (if present)\n    4) Blends whatever succeeded -> /kaggle/working/submission.csv\n    \"\"\"\n    data_dir = \"/kaggle/input/csiro-biomass\"\n\n    outputs: List[str] = []\n    weights: List[float] = []\n\n    # 1) SigLIP ridge baseline\n    siglip_out = \"/kaggle/working/submission_siglip.csv\"\n    device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    run_type = str(os.environ.get(\"KAGGLE_KERNEL_RUN_TYPE\", \"\"))\n    is_submission = \"submission\" in run_type.lower()\n    print(f\"[env] run_type={run_type}  submission={is_submission}  cuda={torch.cuda.is_available()}  device={device}\")\n\n    print(\"[run_default] SigLIP ->\", siglip_out)\n    generate_siglip_submission(\n        data_dir=data_dir,\n        siglip_dir=_pick_siglip_dir(),\n        out_csv=siglip_out,\n        device=device,\n        batch_size=(8 if is_submission else 4),\n        ridge_alpha=3.0,\n    )\n    outputs.append(siglip_out)\n    weights.append(0.35)\n\n    # 2) DINOv3 folds (if present)\n    try:\n        dino_out = \"/kaggle/working/submission_dinov3.csv\"\n        print(\"[run_default] DINOv3 ->\", dino_out)\n        generate_dinov3_submission(\n            data_dir=data_dir,\n            out_csv=dino_out,\n            img_size=(512, 512),\n            batch_size=(4 if (is_submission and device == \"cpu\") else 8),\n            num_workers=0,\n            tta=(False if is_submission else True),\n        )\n        outputs.append(dino_out)\n        weights.append(0.65)\n    except Exception as e:\n        print(\"[run_default] Skipping DINOv3 (reason):\", repr(e))\n\n    final_out = \"/kaggle/working/submission.csv\"\n    if len(outputs) == 1:\n        print(\"[run_default] Only one model; writing final ->\", final_out)\n        # IMPORTANT: in the submission run, the real test set can be huge.\n        # Don't read the entire CSV into RAM just to copy it.\n        shutil.copyfile(outputs[0], final_out)\n    else:\n        print(\"[run_default] Blending:\")\n        for w, p in zip(weights, outputs):\n            print(f\"  w={w:.3f}  {p}\")\n        per_tt = None\n        if len(outputs) == 2:\n            # Public baseline: Clover tends to be better from DINO; blend others.\n            per_tt = {\"Dry_Clover_g\": [0.0, 1.0]}\n        blend_submissions(outputs, weights, final_out, data_dir=data_dir, per_target_weights=per_tt)\n\n    print(\"[run_default] Done. Preview:\")\n    try:\n        print(pd.read_csv(final_out, nrows=5))\n    except Exception as e:\n        print(\"[run_default] Preview skipped:\", repr(e))\n\n\ndef main() -> None:\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--data_dir\", default=\"/kaggle/input/csiro-biomass\")\n    parser.add_argument(\"--out\", default=\"/kaggle/working/submission.csv\")\n\n    # Jupyter injects extra args like \"-f <kernel.json>\" into sys.argv.\n    _args, _unknown = parser.parse_known_args()\n\n    run_default()\n\n\nif __name__ == \"__main__\":\n    if _running_in_notebook():\n        run_default()\n    else:\n        main()\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}