{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":99552,"databundleVersionId":13851420,"sourceType":"competition"},{"sourceId":13694916,"sourceType":"datasetVersion","datasetId":8710901},{"sourceId":13710418,"sourceType":"datasetVersion","datasetId":8722102}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install nnunet","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T11:32:44.865226Z","iopub.execute_input":"2025-11-13T11:32:44.865378Z","iopub.status.idle":"2025-11-13T11:34:34.059035Z","shell.execute_reply.started":"2025-11-13T11:32:44.865363Z","shell.execute_reply":"2025-11-13T11:34:34.058248Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport random\nimport warnings\nimport numpy as np\nimport pandas as pd\nimport cv2\nimport functools\nfrom pathlib import Path\nfrom tqdm import tqdm\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom typing import List, Tuple, Optional\nimport gc\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.optim import AdamW\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\n\nimport timm\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom sklearn.model_selection import StratifiedKFold, GroupKFold\nfrom sklearn.metrics import roc_auc_score\nimport pydicom\n\nwarnings.filterwarnings('ignore')\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T11:35:52.237014Z","iopub.execute_input":"2025-11-13T11:35:52.237277Z","iopub.status.idle":"2025-11-13T11:35:52.242636Z","shell.execute_reply.started":"2025-11-13T11:35:52.237248Z","shell.execute_reply":"2025-11-13T11:35:52.242067Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class Config:\n    DATA_DIR = \"/kaggle/input/another1\"\n    CVT_PNG_DIR = os.path.join(DATA_DIR, \"cvt_png\")\n    SERIES_MAPPING_PATH = os.path.join(DATA_DIR, \"series_index_mapping.csv\")\n    LOCALIZERS_PATH = os.path.join(DATA_DIR, \"train_localizers_with_relative.csv\")\n    TRAIN_CSV_PATH = \"/kaggle/input/rsna-intracranial-aneurysm-detection/train.csv\"\n    NUM_FRAMES = 8\n    IMAGE_SIZE = 224\n    NUM_CLASSES = 14\n    BATCH_SIZE = 6\n    NUM_EPOCHS = 50\n    LEARNING_RATE = 5e-5\n    MODEL_NAME_BACKBONE = \"tf_efficientnetv2_s.in1k\"\n    USE_METADATA = True\n    USE_WINDOWING = True\n    USE_3CHANNEL_INPUT = True\n    USE_IMPROVED_LOSS = True\n    USE_CLAHE = True\n    USE_STRONG_AUGMENTATION = True\n    NUM_WORKERS = 2\n    PIN_MEMORY = True\n    PREFETCH_FACTOR = 2\n    PERSISTENT_WORKERS = True\n    NUM_FOLDS = 5\n    FOLD = 0\n    ACCUMULATION_STEPS = 5\n    EARLY_STOPPING_PATIENCE = 3\n    USE_GROUP_CV = True\n    CACHE_SIZE = 100\n    OUTPUT_DIR = \"/kaggle/working\"\n    MODEL_NAME = \"eightframe_efficientnetv2s\"\n\nconfig = Config()\n\n# Print config summary\nprint(\"=== Configuration Summary ===\")\nprint(f\"Model Backbone: {config.MODEL_NAME_BACKBONE}\")\nprint(f\"Number of Frames: {config.NUM_FRAMES}\")\nprint(f\"Batch Size: {config.BATCH_SIZE}\")\nprint(f\"Accumulation Steps: {config.ACCUMULATION_STEPS}\")\nprint(f\"Effective Batch Size: {config.BATCH_SIZE * config.ACCUMULATION_STEPS}\")\nprint(f\"CLAHE Enabled: {config.USE_CLAHE}\")\nprint(f\"Strong Augmentation: {config.USE_STRONG_AUGMENTATION}\")\nprint(f\"Group Cross-Validation: {config.USE_GROUP_CV}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T11:35:52.243449Z","iopub.execute_input":"2025-11-13T11:35:52.243697Z","iopub.status.idle":"2025-11-13T11:35:52.266079Z","shell.execute_reply.started":"2025-11-13T11:35:52.243673Z","shell.execute_reply":"2025-11-13T11:35:52.265533Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def set_seed(seed: int = 42, deterministic: bool = False):\n    \"\"\"Set all random seeds for reproducibility. deterministic=True => cudnn.benchmark=False.\"\"\"\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    if deterministic:e\n    else:\n        torch.backends.cudnn.deterministic = False\n        torch.backends.cudnn.benchmark = True\n\n        torch.backends.cudnn.deterministic = True\n        torch.backends.cudnn.benchmark = Fals\nset_seed(42, deterministic=False)\n\n# Device\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {device}\")\nif torch.cuda.is_available():\n    try:\n        print(f\"GPU: {torch.cuda.get_device_name(0)}\")\n        print(f\"Memory: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB\")\n        print(f\"CUDA version: {torch.version.cuda}\")\n    except Exception:\n        pass\n    torch.cuda.empty_cache()\nelse:\n    raise RuntimeError(\"CUDA is not available! This code requires GPU.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T11:35:52.267664Z","iopub.execute_input":"2025-11-13T11:35:52.26791Z","iopub.status.idle":"2025-11-13T11:35:52.416319Z","shell.execute_reply.started":"2025-11-13T11:35:52.267894Z","shell.execute_reply":"2025-11-13T11:35:52.415537Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TARGET_COLS = [\n    'Left Infraclinoid Internal Carotid Artery',\n    'Right Infraclinoid Internal Carotid Artery', \n    'Left Supraclinoid Internal Carotid Artery',\n    'Right Supraclinoid Internal Carotid Artery',\n    'Left Middle Cerebral Artery',\n    'Right Middle Cerebral Artery',\n    'Anterior Communicating Artery',\n    'Left Anterior Cerebral Artery',\n    'Right Anterior Cerebral Artery', \n    'Left Posterior Communicating Artery',\n    'Right Posterior Communicating Artery',\n    'Basilar Tip',\n    'Other Posterior Circulation',\n    'Aneurysm Present'\n]\n\n# -------------------------\n# Read CSVs (assume present)\n# -------------------------\nprint(\"Loading data...\")\ntrain_df = pd.read_csv(config.TRAIN_CSV_PATH)\nseries_mapping_df = pd.read_csv(config.SERIES_MAPPING_PATH)\nlocalizers_df = pd.read_csv(config.LOCALIZERS_PATH)\n\nprint(f\"Train data shape: {train_df.shape}\")\nprint(f\"Series mapping shape: {series_mapping_df.shape}\")\nprint(f\"Localizers shape: {localizers_df.shape}\")\nprint(f\"Target columns: {len(TARGET_COLS)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T11:35:52.417119Z","iopub.execute_input":"2025-11-13T11:35:52.417308Z","iopub.status.idle":"2025-11-13T11:36:00.010854Z","shell.execute_reply.started":"2025-11-13T11:35:52.417293Z","shell.execute_reply":"2025-11-13T11:36:00.010106Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Image preprocessing helpers \n# -------------------------\ndef get_windowing_params(modality: str) -> Tuple[float, float]:\n    windows = {\n        'CT': (40, 80),\n        'CTA': (50, 350), \n        'MRA': (600, 1200),\n        'MRI': (40, 80),\n        'MR': (40, 80)\n    }\n    return windows.get(modality, (40, 80))\n\ndef apply_dicom_windowing(img: np.ndarray, window_center: float, window_width: float) -> np.ndarray:\n    img_min = window_center - window_width // 2\n    img_max = window_center + window_width // 2\n    img = np.clip(img, img_min, img_max)\n    img = (img - img_min) / (img_max - img_min + 1e-7)\n    return (img * 255).astype(np.uint8)\n\ndef apply_clahe_normalization(img: np.ndarray, modality: str) -> np.ndarray:\n    if not config.USE_CLAHE:\n        return img\n        \n    if modality in ['CTA', 'MRA']:\n        clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))\n        img_clahe = clahe.apply(img.astype(np.uint8))\n        img_clahe = cv2.convertScaleAbs(img_clahe, alpha=1.1, beta=5)\n    elif modality in ['MRI', 'MR']:\n        clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))\n        img_clahe = clahe.apply(img.astype(np.uint8))\n        img_clahe = np.power(img_clahe / 255.0, 0.9) * 255\n        img_clahe = img_clahe.astype(np.uint8)\n    else:\n        clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8, 8))\n        img_clahe = clahe.apply(img.astype(np.uint8))\n    \n    return img_clahe\n\ndef robust_normalization(volume: np.ndarray) -> np.ndarray:\n    p1, p99 = np.percentile(volume.flatten(), [1, 99])\n    volume_norm = np.clip(volume, p1, p99)\n    \n    if p99 > p1:\n        volume_norm = (volume_norm - p1) / (p99 - p1 + 1e-7)\n    else:\n        volume_norm = np.zeros_like(volume_norm)\n        \n    return (volume_norm * 255).astype(np.uint8)\n\ndef create_3channel_input_8frame(volume: np.ndarray) -> np.ndarray:\n    if len(volume) == 0:\n        return np.zeros((config.IMAGE_SIZE, config.IMAGE_SIZE, 3), dtype=np.uint8)\n    middle_slice = volume[len(volume) // 2]\n    mip = np.max(volume, axis=0)\n    std_proj = np.std(volume, axis=0).astype(np.float32)\n    if std_proj.max() > std_proj.min():\n        p1, p99 = np.percentile(std_proj, [5, 95])\n        std_proj = np.clip(std_proj, p1, p99)\n        std_proj = ((std_proj - p1) / (p99 - p1 + 1e-7) * 255).astype(np.uint8)\n    else:\n        std_proj = np.zeros_like(std_proj, dtype=np.uint8)\n    return np.stack([middle_slice, mip, std_proj], axis=-1)\n\ndef smart_8_frame_sampling(volume_paths: List[str], series_uid: str = None) -> List[str]:\n    n = len(volume_paths)\n    if n <= 8:\n        result = volume_paths[:]\n        while len(result) < 8:\n            result.extend(volume_paths[:8-len(result)])\n        return result[:8]\n    start_idx = max(0, int(n * 0.1))\n    available_frames = n - start_idx\n    step = max(1, available_frames // 8)\n    indices = []\n    current_idx = start_idx\n    while len(indices) < 8 and current_idx < n:\n        indices.append(current_idx)\n        current_idx += step\n    while len(indices) < 8:\n        remaining = [i for i in range(n) if i not in indices]\n        if remaining:\n            indices.append(remaining[len(indices) % len(remaining)])\n        else:\n            indices.append(indices[-1])\n    return [volume_paths[i] for i in indices[:8]]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T11:36:00.011639Z","iopub.execute_input":"2025-11-13T11:36:00.011866Z","iopub.status.idle":"2025-11-13T11:36:00.025024Z","shell.execute_reply.started":"2025-11-13T11:36:00.01185Z","shell.execute_reply":"2025-11-13T11:36:00.024395Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Robust DICOM path resolver (place near top)\n# -------------------------\ndef resolve_dicom_path(dicom_entry, series_uid):\n    \"\"\"\n    Resolve dicom path given dicom entry and series id.\n    Raises FileNotFoundError if nothing found.\n    \"\"\"\n    # 1) explicit absolute or relative path\n    if dicom_entry:\n        try:\n            if os.path.isabs(dicom_entry) and os.path.exists(dicom_entry):\n                return dicom_entry\n            if os.path.exists(dicom_entry):\n                return os.path.abspath(dicom_entry)\n        except Exception:\n            pass\n        # maybe relative to series_dir\n        series_dir = f\"/kaggle/input/rsna-intracranial-aneurysm-detection/series/{series_uid}\"\n        possible = os.path.join(series_dir, dicom_entry) if dicom_entry else None\n        if possible and os.path.exists(possible):\n            return os.path.abspath(possible)\n\n    # 2) try standard series folder\n    series_dir = f\"/kaggle/input/rsna-intracranial-aneurysm-detection/series/{series_uid}\"\n    if os.path.exists(series_dir):\n        candidates = sorted(glob.glob(os.path.join(series_dir, \"*.dcm\")) + glob.glob(os.path.join(series_dir, \"*.DCM\")))\n        if candidates:\n            return candidates[0]\n\n    # 3) other possible folders\n    possible_dirs = [\n        os.path.join(config.CVT_PNG_DIR, series_uid),\n        os.path.join(config.DATA_DIR, 'series', series_uid),\n    ]\n    for d in possible_dirs:\n        if os.path.exists(d):\n            cand = sorted(glob.glob(os.path.join(d, \"*.dcm\")) + glob.glob(os.path.join(d, \"*.DCM\")))\n            if cand:\n                return cand[0]\n\n    raise FileNotFoundError(f\"No DICOM file found for series {series_uid}. Tried entry='{dicom_entry}' and series folder='{series_dir}'.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T11:36:00.025795Z","iopub.execute_input":"2025-11-13T11:36:00.026051Z","iopub.status.idle":"2025-11-13T11:36:00.062723Z","shell.execute_reply.started":"2025-11-13T11:36:00.026029Z","shell.execute_reply":"2025-11-13T11:36:00.062191Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Create structured frame paths dict\n# -------------------------\ndef create_frame_paths_8frame_structured():\n    \"\"\"Return dict: series_uid -> {'paths': [...], 'is_dummy': bool}\"\"\"\n    frame_paths = {}\n    print(\"Creating 8-frame optimized structured paths from series_index_mapping.csv...\")\n    for series_uid in tqdm(train_df['SeriesInstanceUID'].unique(), desc=\"Processing series\"):\n        series_data = series_mapping_df[series_mapping_df['SeriesInstanceUID'] == series_uid]\n        if len(series_data) == 0:\n            frame_paths[series_uid] = {'paths': [], 'is_dummy': True}\n            continue\n\n        rows = train_df[train_df['SeriesInstanceUID'] == series_uid]\n        train_row = rows.iloc[0] if len(rows) > 0 else None\n\n        found_paths = []\n        if train_row is not None:\n            for target_col in TARGET_COLS[:-1]:\n                if train_row.get(target_col, 0) == 1:\n                    location_clean = target_col.replace('/', '_')\n                    series_dir = os.path.join(config.CVT_PNG_DIR, location_clean, series_uid)\n                    if os.path.exists(series_dir):\n                        png_files = sorted(glob.glob(os.path.join(series_dir, \"*.png\")))\n                        if png_files:\n                            found_paths = png_files\n                            break\n\n        if not found_paths:\n            dicom_dir = f\"/kaggle/input/rsna-intracranial-aneurysm-detection/series/{series_uid}\"\n            if os.path.exists(dicom_dir):\n                num_frames = len(series_data)\n                found_paths = [f\"dummy_path_{i:04d}.png\" for i in range(num_frames)]\n\n        if found_paths:\n            sampled = smart_8_frame_sampling(found_paths, series_uid)\n            is_dummy = any(p.startswith('dummy_path') for p in sampled)\n            frame_paths[series_uid] = {'paths': sampled, 'is_dummy': is_dummy}\n        else:\n            frame_paths[series_uid] = {'paths': [], 'is_dummy': True}\n    return frame_paths","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T11:36:00.063409Z","iopub.execute_input":"2025-11-13T11:36:00.063617Z","iopub.status.idle":"2025-11-13T11:36:00.087414Z","shell.execute_reply.started":"2025-11-13T11:36:00.063593Z","shell.execute_reply":"2025-11-13T11:36:00.086921Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Build structured frame paths\nframe_paths_dict = create_frame_paths_8frame_structured()\nprint(f\"Created paths for {len(frame_paths_dict)} series\")\n\n# Filter train_df to valid series\nvalid_series = [uid for uid, entry in frame_paths_dict.items() if len(entry['paths']) > 0 or not entry['is_dummy']]\ntrain_df_filtered = train_df[train_df['SeriesInstanceUID'].isin(valid_series)].copy()\nprint(f\"Filtered train data shape: {train_df_filtered.shape}\")\naneurysm_dist_filtered = train_df_filtered['Aneurysm Present'].value_counts()\nprint(f\"Aneurysm Present distribution: {aneurysm_dist_filtered.to_dict()}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T11:36:00.088027Z","iopub.execute_input":"2025-11-13T11:36:00.088415Z","iopub.status.idle":"2025-11-13T11:41:38.231317Z","shell.execute_reply.started":"2025-11-13T11:36:00.088394Z","shell.execute_reply":"2025-11-13T11:41:38.230445Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Dataset classes (training & test) with robust dicom resolver usage\n# -------------------------\nclass EightFrameDataset(Dataset):\n    def __init__(self, df, frame_paths_dict, series_mapping_df, num_frames=8, transform=None, is_training=True):\n        self.df = df.reset_index(drop=True)\n        self.frame_paths_dict = frame_paths_dict\n        self.series_mapping_df = series_mapping_df\n        self.num_frames = num_frames\n        self.transform = transform\n        self.is_training = is_training\n        self._cache = {}\n        self._cache_keys = []\n        self._max_cache_size = config.CACHE_SIZE\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        if idx in self._cache:\n            return self._cache[idx]\n        row = self.df.iloc[idx]\n        series_uid = row['SeriesInstanceUID']\n        labels = torch.tensor(row[TARGET_COLS].values.astype(np.float32))\n        metadata = self._extract_metadata(row)\n        image = self._load_8frame_3channel_image(series_uid, row)\n        result = (image, labels, metadata)\n        self._update_cache(idx, result)\n        return result\n\n    def _update_cache(self, idx, data):\n        if len(self._cache) >= self._max_cache_size:\n            oldest_idx = self._cache_keys.pop(0)\n            del self._cache[oldest_idx]\n        self._cache[idx] = data\n        self._cache_keys.append(idx)\n\n    def _extract_metadata(self, row) -> torch.Tensor:\n        if not config.USE_METADATA:\n            return torch.tensor([0.0, 0.0], dtype=torch.float32)\n        age = row.get('PatientAge', 50)\n        if pd.isna(age):\n            age = 50\n        elif isinstance(age, str):\n            age = int(''.join(filter(str.isdigit, age[:3])) or '50')\n        age = min(float(age), 100.0) / 100.0\n        sex = row.get('PatientSex', 'M')\n        sex = 1.0 if sex == 'M' else 0.0\n        return torch.tensor([age, sex], dtype=torch.float32)\n\n    def _load_8frame_3channel_image(self, series_uid: str, row) -> torch.Tensor:\n        entry = self.frame_paths_dict.get(series_uid, {'paths': [], 'is_dummy': True})\n        paths = entry['paths']\n        is_dummy = entry.get('is_dummy', True)\n        try:\n            if len(paths) == 0 or is_dummy:\n                volume = self._load_volume_from_dicom_8frame(series_uid, row)\n            else:\n                volume = self._load_volume_from_png_8frame(paths)\n            volume = robust_normalization(volume)\n            image = create_3channel_input_8frame(volume)\n            if self.transform:\n                transformed = self.transform(image=image)\n                image = transformed['image']\n            return image\n        except Exception as e:\n            print(f\"Error loading {series_uid}: {e}\")\n            dummy_image = np.zeros((config.IMAGE_SIZE, config.IMAGE_SIZE, 3), dtype=np.uint8)\n            if self.transform:\n                transformed = self.transform(image=dummy_image)\n                return transformed['image']\n            return torch.zeros(3, config.IMAGE_SIZE, config.IMAGE_SIZE)\n\n    def _load_volume_from_png_8frame(self, paths: List[str]) -> np.ndarray:\n        volume = []\n        if len(paths) != 8:\n            paths = smart_8_frame_sampling(paths)\n        for path in paths:\n            try:\n                img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)\n                if img is not None:\n                    img = cv2.resize(img, (config.IMAGE_SIZE, config.IMAGE_SIZE), interpolation=cv2.INTER_AREA)\n                    volume.append(img)\n                else:\n                    volume.append(np.zeros((config.IMAGE_SIZE, config.IMAGE_SIZE), dtype=np.uint8))\n            except:\n                volume.append(np.zeros((config.IMAGE_SIZE, config.IMAGE_SIZE), dtype=np.uint8))\n        return np.array(volume) if volume else np.zeros((8, config.IMAGE_SIZE, config.IMAGE_SIZE), dtype=np.uint8)\n\n    def _load_volume_from_dicom_8frame(self, series_uid: str, row) -> np.ndarray:\n        series_data = self.series_mapping_df[self.series_mapping_df['SeriesInstanceUID'] == series_uid].sort_values('relative_index')\n        if len(series_data) == 0:\n            return np.zeros((8, config.IMAGE_SIZE, config.IMAGE_SIZE), dtype=np.uint8)\n        modality = row.get('Modality', 'CT') if isinstance(row, (dict, pd.Series)) else 'CT'\n        # sampling\n        if len(series_data) <= 8:\n            sampled_data = series_data\n        else:\n            all_indices = list(range(len(series_data)))\n            sampled_indices = smart_8_frame_sampling([str(i) for i in all_indices])\n            sampled_indices = [int(i) for i in sampled_indices]\n            sampled_data = series_data.iloc[sampled_indices]\n        volume = []\n        for _, dicom_row in sampled_data.iterrows():\n            dicom_entry = dicom_row.get('dicom_filename', None) if isinstance(dicom_row, (dict, pd.Series)) else None\n            try:\n                dicom_path = resolve_dicom_path(dicom_entry, series_uid)\n            except FileNotFoundError:\n                volume.append(np.zeros((config.IMAGE_SIZE, config.IMAGE_SIZE), dtype=np.uint8))\n                continue\n            try:\n                ds = pydicom.dcmread(dicom_path, force=True)\n                img = ds.pixel_array.astype(np.float32)\n                if hasattr(ds, 'RescaleSlope') and hasattr(ds, 'RescaleIntercept'):\n                    img = img * float(ds.RescaleSlope) + float(ds.RescaleIntercept)\n                if config.USE_WINDOWING:\n                    window_center, window_width = get_windowing_params(modality)\n                    img = apply_dicom_windowing(img, window_center, window_width)\n                else:\n                    img_min, img_max = img.min(), img.max()\n                    if img_max > img_min:\n                        img = ((img - img_min) / (img_max - img_min) * 255).astype(np.uint8)\n                    else:\n                        img = np.zeros_like(img, dtype=np.uint8)\n                img = apply_clahe_normalization(img, modality)\n                img = cv2.resize(img, (config.IMAGE_SIZE, config.IMAGE_SIZE), interpolation=cv2.INTER_AREA)\n                volume.append(img)\n            except Exception as e:\n                # append zeros on failure\n                volume.append(np.zeros((config.IMAGE_SIZE, config.IMAGE_SIZE), dtype=np.uint8))\n                continue\n        # ensure 8 frames\n        while len(volume) < 8:\n            if volume:\n                volume.append(volume[-1])\n            else:\n                volume.append(np.zeros((config.IMAGE_SIZE, config.IMAGE_SIZE), dtype=np.uint8))\n        return np.array(volume[:8])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T11:41:38.233399Z","iopub.execute_input":"2025-11-13T11:41:38.233832Z","iopub.status.idle":"2025-11-13T11:41:38.254734Z","shell.execute_reply.started":"2025-11-13T11:41:38.233813Z","shell.execute_reply":"2025-11-13T11:41:38.253908Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Test dataset for inference\nclass TestEightFrameDataset(Dataset):\n    def __init__(self, series_uids: List[str], frame_paths_dict, series_mapping_df, transform=None):\n        self.series_uids = series_uids\n        self.frame_paths_dict = frame_paths_dict\n        self.series_mapping_df = series_mapping_df\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.series_uids)\n\n    def __getitem__(self, idx):\n        series_uid = self.series_uids[idx]\n        entry = self.frame_paths_dict.get(series_uid, {'paths': [], 'is_dummy': True})\n        paths = entry.get('paths', [])\n        is_dummy = entry.get('is_dummy', True)\n        rows = self.series_mapping_df[self.series_mapping_df['SeriesInstanceUID'] == series_uid]\n        modality = rows['Modality'].iloc[0] if len(rows) > 0 and 'Modality' in rows.columns else 'CT'\n        try:\n            if len(paths) == 0 or is_dummy:\n                volume = self._load_volume_from_dicom_8frame(series_uid, modality)\n            else:\n                volume = self._load_volume_from_png_8frame(paths)\n            volume = robust_normalization(volume)\n            image = create_3channel_input_8frame(volume)\n            if self.transform:\n                transformed = self.transform(image=image)\n                image = transformed['image']\n            metadata = torch.tensor([0.0, 0.0], dtype=torch.float32)\n            return image, metadata, series_uid\n        except Exception:\n            dummy_image = np.zeros((config.IMAGE_SIZE, config.IMAGE_SIZE, 3), dtype=np.uint8)\n            if self.transform:\n                transformed = self.transform(image=dummy_image)\n                image = transformed['image']\n            return image, torch.tensor([0.0, 0.0], dtype=torch.float32), series_uid\n\n    def _load_volume_from_png_8frame(self, paths: List[str]) -> np.ndarray:\n        volume = []\n        if len(paths) != 8:\n            paths = smart_8_frame_sampling(paths)\n        for path in paths:\n            try:\n                img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)\n                if img is not None:\n                    img = cv2.resize(img, (config.IMAGE_SIZE, config.IMAGE_SIZE), interpolation=cv2.INTER_AREA)\n                    volume.append(img)\n                else:\n                    volume.append(np.zeros((config.IMAGE_SIZE, config.IMAGE_SIZE), dtype=np.uint8))\n            except:\n                volume.append(np.zeros((config.IMAGE_SIZE, config.IMAGE_SIZE), dtype=np.uint8))\n        return np.array(volume) if volume else np.zeros((8, config.IMAGE_SIZE, config.IMAGE_SIZE), dtype=np.uint8)\n\n    def _load_volume_from_dicom_8frame(self, series_uid: str, modality: str) -> np.ndarray:\n        series_data = self.series_mapping_df[self.series_mapping_df['SeriesInstanceUID'] == series_uid].sort_values('relative_index')\n        if len(series_data) == 0:\n            return np.zeros((8, config.IMAGE_SIZE, config.IMAGE_SIZE), dtype=np.uint8)\n        if len(series_data) <= 8:\n            sampled = series_data\n        else:\n            all_indices = list(range(len(series_data)))\n            sampled_idx = smart_8_frame_sampling([str(i) for i in all_indices])\n            sampled_idx = [int(i) for i in sampled_idx]\n            sampled = series_data.iloc[sampled_idx]\n        volume = []\n        for _, dicom_row in sampled.iterrows():\n            dicom_entry = dicom_row.get('dicom_filename', None)\n            try:\n                dicom_path = resolve_dicom_path(dicom_entry, series_uid)\n            except FileNotFoundError:\n                volume.append(np.zeros((config.IMAGE_SIZE, config.IMAGE_SIZE), dtype=np.uint8))\n                continue\n            try:\n                ds = pydicom.dcmread(dicom_path, force=True)\n                img = ds.pixel_array.astype(np.float32)\n                if hasattr(ds, 'RescaleSlope') and hasattr(ds, 'RescaleIntercept'):\n                    img = img * float(ds.RescaleSlope) + float(ds.RescaleIntercept)\n                if config.USE_WINDOWING:\n                    wc, ww = get_windowing_params(modality)\n                    img = apply_dicom_windowing(img, wc, ww)\n                else:\n                    img_min, img_max = img.min(), img.max()\n                    if img_max > img_min:\n                        img = ((img - img_min) / (img_max - img_min) * 255).astype(np.uint8)\n                    else:\n                        img = np.zeros_like(img, dtype=np.uint8)\n                img = apply_clahe_normalization(img, modality)\n                img = cv2.resize(img, (config.IMAGE_SIZE, config.IMAGE_SIZE), interpolation=cv2.INTER_AREA)\n                volume.append(img)\n            except Exception:\n                volume.append(np.zeros((config.IMAGE_SIZE, config.IMAGE_SIZE), dtype=np.uint8))\n        while len(volume) < 8:\n            if volume:\n                volume.append(volume[-1])\n            else:\n                volume.append(np.zeros((config.IMAGE_SIZE, config.IMAGE_SIZE), dtype=np.uint8))\n        return np.array(volume[:8])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T11:41:38.255652Z","iopub.execute_input":"2025-11-13T11:41:38.255945Z","iopub.status.idle":"2025-11-13T11:41:38.283082Z","shell.execute_reply.started":"2025-11-13T11:41:38.255922Z","shell.execute_reply":"2025-11-13T11:41:38.282443Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Transforms (unchanged)\n# -------------------------\nif config.USE_STRONG_AUGMENTATION:\n    print(\"Using strong augmentation for better generalization...\")\n    train_transform = A.Compose([\n        A.Rotate(limit=15, p=0.7),\n        A.HorizontalFlip(p=0.5),\n        A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=10, p=0.6),\n        A.ElasticTransform(alpha=50, sigma=5, p=0.3),\n        A.GridDistortion(num_steps=3, distort_limit=0.1, p=0.3),\n        A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.6),\n        A.CLAHE(clip_limit=2.0, tile_grid_size=(8,8), p=0.5),\n        A.RandomGamma(gamma_limit=(80, 120), p=0.4),\n        A.GaussNoise(var_limit=(10, 80), p=0.4),\n        A.ISONoise(color_shift=(0.01, 0.05), intensity=(0.1, 0.5), p=0.3),\n        A.Blur(blur_limit=3, p=0.2),\n        A.CoarseDropout(max_holes=8, max_height=32, max_width=32, p=0.3),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\nelse:\n    print(\"Using standard augmentation...\")\n    train_transform = A.Compose([\n        A.Rotate(limit=10, p=0.5),\n        A.HorizontalFlip(p=0.5),\n        A.RandomBrightnessContrast(brightness_limit=0.1, contrast_limit=0.1, p=0.3),\n        A.GaussNoise(var_limit=(10, 50), p=0.2),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\nval_transform = A.Compose([\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2()\n])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T11:41:38.283857Z","iopub.execute_input":"2025-11-13T11:41:38.28413Z","iopub.status.idle":"2025-11-13T11:41:38.313338Z","shell.execute_reply.started":"2025-11-13T11:41:38.284111Z","shell.execute_reply":"2025-11-13T11:41:38.312767Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Model definition (same as before)\n# -------------------------\nclass ImprovedMultiFrameModel(nn.Module):\n    def __init__(self, num_frames=8, num_classes=14, pretrained=True):\n        super(ImprovedMultiFrameModel, self).__init__()\n        self.num_frames = num_frames\n        self.num_classes = num_classes\n        self.use_3channel = config.USE_3CHANNEL_INPUT\n        self.use_metadata = config.USE_METADATA\n        print(f\"Loading backbone: {config.MODEL_NAME_BACKBONE}\")\n        self.backbone = timm.create_model(\n            config.MODEL_NAME_BACKBONE,\n            pretrained=pretrained,\n            num_classes=0,\n            global_pool='avg'\n        )\n        self.feature_dim = self.backbone.num_features\n        print(f\"Backbone {config.MODEL_NAME_BACKBONE}: {self.feature_dim} features\")\n        if self.use_metadata:\n            self.meta_fc = nn.Sequential(\n                nn.Linear(2, 16),\n                nn.ReLU(),\n                nn.Dropout(0.2),\n                nn.Linear(16, 32),\n                nn.ReLU()\n            )\n            classifier_input_dim = self.feature_dim + 32\n        else:\n            classifier_input_dim = self.feature_dim\n        self.classifier = nn.Sequential(\n            nn.Linear(classifier_input_dim, 512),\n            nn.BatchNorm1d(512),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(512, 256),\n            nn.BatchNorm1d(256),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, num_classes)\n        )\n\n    def forward(self, x, meta=None):\n        features = self.backbone(x)\n        if self.use_metadata and meta is not None:\n            meta_features = self.meta_fc(meta)\n            features = torch.cat([features, meta_features], dim=1)\n        output = self.classifier(features)\n        return output","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Loss functions & metric (unchanged)\n# -------------------------\nclass FocalLoss(nn.Module):\n    def __init__(self, alpha=1, gamma=2):\n        super(FocalLoss, self).__init__()\n        self.alpha = alpha\n        self.gamma = gamma\n    def forward(self, inputs, targets):\n        bce_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction='none')\n        pt = torch.exp(-bce_loss)\n        focal_loss = self.alpha * (1-pt)**self.gamma * bce_loss\n        return focal_loss.mean()\n\nclass WeightedMultiLabelLoss(nn.Module):\n    def __init__(self, aneurysm_weight=3.0):\n        super(WeightedMultiLabelLoss, self).__init__()\n        self.weights = torch.ones(config.NUM_CLASSES, device=device)\n        self.weights[-1] = aneurysm_weight\n    def forward(self, outputs, targets):\n        bce_loss = F.binary_cross_entropy_with_logits(outputs, targets, reduction='none')\n        weighted_loss = bce_loss * self.weights\n        return weighted_loss.mean()\n\nclass ImprovedLoss(nn.Module):\n    def __init__(self, aneurysm_weight=3.0, focal_weight=0.3):\n        super(ImprovedLoss, self).__init__()\n        self.aneurysm_weight = aneurysm_weight\n        self.focal_weight = focal_weight\n        self.weights = torch.ones(config.NUM_CLASSES, device=device)\n        self.weights[-1] = aneurysm_weight\n        self.focal_loss = FocalLoss(alpha=1, gamma=2)\n    def forward(self, outputs, targets):\n        bce_loss = F.binary_cross_entropy_with_logits(outputs, targets, reduction='none')\n        weighted_bce = (bce_loss * self.weights).mean()\n        focal_loss = self.focal_loss(outputs, targets)\n        return (1 - self.focal_weight) * weighted_bce + self.focal_weight * focal_loss\n\ndef get_loss_function():\n    if config.USE_IMPROVED_LOSS:\n        return ImprovedLoss(aneurysm_weight=3.0, focal_weight=0.3)\n    else:\n        return WeightedMultiLabelLoss(aneurysm_weight=3.0)\n\ndef calculate_competition_metric(y_true, y_pred):\n    individual_aucs = []\n    for i in range(13):\n        try:\n            if len(np.unique(y_true[:, i])) > 1:\n                auc = roc_auc_score(y_true[:, i], y_pred[:, i])\n            else:\n                auc = 0.5\n            individual_aucs.append(auc)\n        except:\n            individual_aucs.append(0.5)\n    try:\n        if len(np.unique(y_true[:, 13])) > 1:\n            aneurysm_present_auc = roc_auc_score(y_true[:, 13], y_pred[:, 13])\n        else:\n            aneurysm_present_auc = 0.5\n    except:\n        aneurysm_present_auc = 0.5\n    avg_individual = np.mean(individual_aucs)\n    final_score = (aneurysm_present_auc + avg_individual) / 2\n    return final_score, aneurysm_present_auc, avg_individual, individual_aucs","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Save checkpoint helper\n# -------------------------\ndef save_checkpoint(model, optimizer, scheduler, epoch, best_score, val_loss, aneurysm_auc, avg_individual, out_dir, model_name, fold):\n    os.makedirs(out_dir, exist_ok=True)\n    model_path = os.path.join(\n        out_dir,\n        f\"{model_name}_fold{fold}_epoch{epoch}_score{best_score:.6f}.pth\"\n    )\n    torch.save({\n        'epoch': epoch,\n        'model_state_dict': model.state_dict(),\n        'optimizer_state_dict': optimizer.state_dict(),\n        'scheduler_state_dict': scheduler.state_dict(),\n        'best_score': best_score,\n        'val_loss': val_loss,\n        'aneurysm_auc': aneurysm_auc,\n        'avg_individual_auc': avg_individual,\n    }, model_path)\n    return model_path\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Build datasets & loaders\n# -------------------------\n# Create CV splits function (same as earlier)\ndef create_robust_cv_split(train_df_local, n_splits=5):\n    print(\"Creating patient-separated cross-validation split...\")\n    patient_groups = []\n    for series_uid in tqdm(train_df_local['SeriesInstanceUID'], desc=\"Reading DICOM patient info\"):\n        patient_group = get_patient_group_cached(series_uid)\n        patient_groups.append(patient_group)\n    train_df_local = train_df_local.copy()\n    train_df_local['patient_id'] = patient_groups\n    n_groups = train_df_local['patient_id'].nunique()\n    print(f\"True patient groups found: {n_groups}\")\n    if n_groups < n_splits:\n        print(\"Not enough patient groups; falling back to StratifiedKFold.\")\n        skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42)\n        return list(skf.split(train_df_local, train_df_local['Aneurysm Present']))\n    train_df_local['stratify_key'] = train_df_local['Modality'].astype(str) + '_' + train_df_local['Aneurysm Present'].astype(str)\n    group_kfold = GroupKFold(n_splits=n_splits)\n    splits = []\n    for fold_idx, (train_idx, val_idx) in enumerate(group_kfold.split(train_df_local, groups=train_df_local['patient_id'])):\n        splits.append((train_idx, val_idx))\n    return splits\n\n# caching helper used earlier (lru cached patient group)\n@functools.lru_cache(maxsize=5000)\ndef extract_dicom_patient_info(series_uid: str) -> Tuple[str, str]:\n    try:\n        dicom_dir = f\"/kaggle/input/rsna-intracranial-aneurysm-detection/series/{series_uid}\"\n        if os.path.exists(dicom_dir):\n            dcm_files = [f for f in os.listdir(dicom_dir) if f.endswith('.dcm') or f.endswith('.DCM')]\n            if dcm_files:\n                ds = pydicom.dcmread(os.path.join(dicom_dir, dcm_files[0]), stop_before_pixels=True, force=True)\n                study_uid = getattr(ds, 'StudyInstanceUID', None)\n                patient_id = getattr(ds, 'PatientID', None)\n                return study_uid or f\"fallback_{series_uid[:32]}\", patient_id\n    except Exception:\n        pass\n    return f\"fallback_{series_uid[:32]}\", f\"fallback_{series_uid[:32]}\"\n\n@functools.lru_cache(maxsize=5000)\ndef get_patient_group_cached(series_uid: str) -> str:\n    study_uid, patient_id = extract_dicom_patient_info(series_uid)\n    return study_uid if study_uid and not study_uid.startswith('fallback_') else patient_id\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create CV splits\ncv_splits = create_robust_cv_split(train_df_filtered, config.NUM_FOLDS)\ntrain_indices, val_indices = cv_splits[config.FOLD]\ntrain_fold_df = train_df_filtered.iloc[train_indices]\nval_fold_df = train_df_filtered.iloc[val_indices]\nprint(f\"Train fold size: {len(train_fold_df)}; Val fold size: {len(val_fold_df)}\")\n\n# Datasets\nprint(\"Creating 8-frame datasets with CLAHE...\")\ntrain_dataset = EightFrameDataset(train_fold_df, frame_paths_dict, series_mapping_df, num_frames=config.NUM_FRAMES, transform=train_transform, is_training=True)\nval_dataset = EightFrameDataset(val_fold_df, frame_paths_dict, series_mapping_df, num_frames=config.NUM_FRAMES, transform=val_transform, is_training=False)\n\ntrain_loader = DataLoader(train_dataset, batch_size=config.BATCH_SIZE, shuffle=True, num_workers=config.NUM_WORKERS, pin_memory=config.PIN_MEMORY, drop_last=True, prefetch_factor=config.PREFETCH_FACTOR, persistent_workers=config.PERSISTENT_WORKERS)\nval_loader = DataLoader(val_dataset, batch_size=config.BATCH_SIZE, shuffle=False, num_workers=config.NUM_WORKERS, pin_memory=config.PIN_MEMORY, prefetch_factor=config.PREFETCH_FACTOR, persistent_workers=config.PERSISTENT_WORKERS)\nprint(f\"Train batches: {len(train_loader)}, Val batches: {len(val_loader)}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Initialize model, optimizer, etc.\n# -------------------------\nmodel = ImprovedMultiFrameModel(num_frames=config.NUM_FRAMES, num_classes=config.NUM_CLASSES, pretrained=True)\nmodel = model.to(device)\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 params: {total_params:,}; Trainable: {trainable_params:,}\")\n\ncriterion = get_loss_function()\noptimizer = AdamW(model.parameters(), lr=config.LEARNING_RATE, weight_decay=1e-4)\nscheduler = CosineAnnealingLR(optimizer, T_max=config.NUM_EPOCHS, eta_min=1e-6)\nscaler = torch.cuda.amp.GradScaler()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Training & validation loops (unchanged logic, but use save_checkpoint)\n# -------------------------\ndef train_epoch_optimized(model, train_loader, criterion, optimizer, scaler, device, accumulation_steps):\n    model.train()\n    running_loss = 0.0\n    optimizer.zero_grad()\n    for batch_idx, (images, targets, metadata) in enumerate(tqdm(train_loader, desc=\"Training 8-Frame\")):\n        images = images.to(device, non_blocking=True)\n        targets = targets.to(device, non_blocking=True)\n        metadata = metadata.to(device, non_blocking=True)\n        with torch.cuda.amp.autocast():\n            outputs = model(images, metadata)\n            loss = criterion(outputs, targets)\n            loss = loss / accumulation_steps\n        scaler.scale(loss).backward()\n        if (batch_idx + 1) % accumulation_steps == 0:\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n        running_loss += loss.item() * accumulation_steps\n    if len(train_loader) % accumulation_steps != 0:\n        scaler.step(optimizer)\n        scaler.update()\n        optimizer.zero_grad()\n    return running_loss / len(train_loader)\n\ndef validate_epoch_optimized(model, val_loader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    all_outputs = []\n    all_targets = []\n    with torch.no_grad():\n        for images, targets, metadata in tqdm(val_loader, desc=\"Validating 8-Frame\"):\n            images = images.to(device, non_blocking=True)\n            targets = targets.to(device, non_blocking=True)\n            metadata = metadata.to(device, non_blocking=True)\n            with torch.cuda.amp.autocast():\n                logits = model(images, metadata)\n                loss = criterion(logits, targets)\n            outputs = torch.sigmoid(logits)\n            running_loss += loss.item()\n            all_outputs.append(outputs.cpu().numpy())\n            all_targets.append(targets.cpu().numpy())\n    all_outputs = np.concatenate(all_outputs)\n    all_targets = np.concatenate(all_targets)\n    final_score, aneurysm_auc, avg_individual, individual_aucs = calculate_competition_metric(all_targets, all_outputs)\n    return running_loss / len(val_loader), final_score, aneurysm_auc, avg_individual\n\ndef check_gpu_utilization():\n    if torch.cuda.is_available():\n        allocated = torch.cuda.memory_allocated() / 1024**3\n        reserved = torch.cuda.memory_reserved() / 1024**3\n        max_memory = torch.cuda.get_device_properties(0).total_memory / 1024**3\n        print(f\"GPU Memory - Allocated: {allocated:.2f}GB, Reserved: {reserved:.2f}GB, Total: {max_memory:.2f}GB\")\n        utilization = (allocated/max_memory)*100\n        print(f\"GPU Utilization: {utilization:.1f}%\")\n        return utilization\n    return 0\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Training loop: will save per-fold checkpoint via save_checkpoint\n# -------------------------\nbest_score = 0.0\nbest_epoch = 0\npatience_counter = 0\ntrain_losses = []\nval_losses = []\nval_scores = []\n\nprint(\"Starting training...\")\nfor epoch in range(config.NUM_EPOCHS):\n    print(f\"\\nEpoch {epoch+1}/{config.NUM_EPOCHS}\")\n    train_loss = train_epoch_optimized(model, train_loader, criterion, optimizer, scaler, device, config.ACCUMULATION_STEPS)\n    val_loss, val_score, aneurysm_auc, avg_individual = validate_epoch_optimized(model, val_loader, criterion, device)\n    scheduler.step()\n    train_losses.append(train_loss)\n    val_losses.append(val_loss)\n    val_scores.append(val_score)\n    print(f\"Train Loss: {train_loss:.6f}, Val Loss: {val_loss:.6f}, Val Score: {val_score:.6f}\")\n    gpu_util = check_gpu_utilization()\n    if val_score > best_score:\n        best_score = val_score\n        best_epoch = epoch + 1\n        patience_counter = 0\n        ckpt_path = save_checkpoint(model, optimizer, scheduler, epoch+1, best_score, val_loss, aneurysm_auc, avg_individual, config.OUTPUT_DIR, config.MODEL_NAME, config.FOLD)\n        print(f\"Saved checkpoint: {ckpt_path}\")\n    else:\n        patience_counter += 1\n        print(f\"No improvement. Patience: {patience_counter}/{config.EARLY_STOPPING_PATIENCE}\")\n        if patience_counter >= config.EARLY_STOPPING_PATIENCE:\n            print(f\"Early stopping at epoch {epoch+1}\")\n            break\n    torch.cuda.empty_cache()\n\nprint(\"Training complete.\")\nprint(f\"Best Score: {best_score:.6f} at epoch {best_epoch}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -------------------------\n# Inference & submission helper\n# -------------------------\n# def run_inference_and_create_submission(ckpt_paths: List[str],\n#                                         sample_submission_csv: str,\n#                                         output_csv: str = 'submission.csv',\n#                                         tta: bool = False,\n#                                         batch_size: int = None):\n#     batch_size = batch_size or config.BATCH_SIZE\n#     sample = pd.read_csv(sample_submission_csv)\n#     uid_col = sample.columns[0]\n#     label_cols = sample.columns[1:].tolist()\n#     test_series = sample[uid_col].tolist()\n#     test_dataset = TestEightFrameDataset(test_series, frame_paths_dict, series_mapping_df, transform=val_transform)\n#     test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False,\n#                              num_workers=config.NUM_WORKERS, pin_memory=config.PIN_MEMORY)\n#     models = []\n#     for p in ckpt_paths:\n#         ck = torch.load(p, map_location='cpu')\n#         m = ImprovedMultiFrameModel(num_frames=config.NUM_FRAMES, num_classes=config.NUM_CLASSES, pretrained=False)\n#         m.load_state_dict(ck['model_state_dict'])\n#         m.to(device).eval()\n#         models.append(m)\n#         print(f\"Loaded checkpoint {p}\")\n#     all_preds = []\n#     uids = []\n#     with torch.no_grad():\n#         for images, metas, batch_uids in tqdm(test_loader, desc=\"Inference\"):\n#             images = images.to(device, non_blocking=True)\n#             metas = metas.to(device, non_blocking=True)\n#             ensemble_probs = None\n    #         for m in models:\n    #             logits = m(images, metas)\n    #             probs = torch.sigmoid(logits).cpu().numpy()\n    #             if tta:\n    #                 imgs_np = images.cpu().numpy()\n    #                 flipped_np = np.flip(imgs_np, axis=-1).copy()\n    #                 flipped = torch.tensor(flipped_np).to(device)\n    #                 logits_f = m(flipped, metas)\n    #                 probs_f = torch.sigmoid(logits_f).cpu().numpy()\n    #                 probs = (probs + probs_f) / 2.0\n    #             ensemble_probs = probs if ensemble_probs is None else ensemble_probs + probs\n    #         ensemble_probs = ensemble_probs / len(models)\n    #         all_preds.append(ensemble_probs)\n    #         uids.extend(batch_uids)\n    # all_preds = np.vstack(all_preds)\n    # submission_df = pd.DataFrame(all_preds, columns=label_cols)\n    # submission_df.insert(0, uid_col, uids)\n    # submission_df = submission_df.set_index(uid_col).reindex(sample[uid_col]).reset_index()\n    # submission_df.to_csv(output_csv, index=False)\n    # print(f\"Saved submission to {output_csv}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# example: replace these with your actual printed paths from step 1\nckpt_paths = [\n    \"/kaggle/input/effecientnet/eightframe_efficientnetv2s_fold0_epoch12_score0.865494.pth\"\n]\n\n# quick existence check\nimport os\nfor p in ckpt_paths:\n    print(p, \"exists?\", os.path.exists(p))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T11:42:34.953679Z","iopub.execute_input":"2025-11-13T11:42:34.953956Z","iopub.status.idle":"2025-11-13T11:42:34.9599Z","shell.execute_reply.started":"2025-11-13T11:42:34.953937Z","shell.execute_reply":"2025-11-13T11:42:34.959015Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====== Inference-only helpers & runner (paste into one cell) ======\nimport os, glob, time\nfrom typing import List, Optional, Tuple\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport pydicom\nimport cv2\nfrom tqdm import tqdm\n\n# --- CONFIG (edit if needed) ---\nUPLOADED_CKPT_ROOT = \"/kaggle/input/effecientnet\"   # where you uploaded the 5 .pth files\nSERIES_FOLDER = \"/kaggle/input/rsna-intracranial-aneurysm-detection/series\"  # competition series folder\nOUTPUT_SUBMISSION = \"/kaggle/working/submission.csv\"\nGENERATED_SAMPLE = \"/kaggle/working/generated_sample_submission.csv\"\nBATCH_SIZE = 6\nNUM_WORKERS = 2\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nTTA = True   # set False to run faster / use less memory\n\n# Target columns (competition order)\nTARGET_COLS = [\n    'Left Infraclinoid Internal Carotid Artery',\n    'Right Infraclinoid Internal Carotid Artery', \n    'Left Supraclinoid Internal Carotid Artery',\n    'Right Supraclinoid Internal Carotid Artery',\n    'Left Middle Cerebral Artery',\n    'Right Middle Cerebral Artery',\n    'Anterior Communicating Artery',\n    'Left Anterior Cerebral Artery',\n    'Right Anterior Cerebral Artery', \n    'Left Posterior Communicating Artery',\n    'Right Posterior Communicating Artery',\n    'Basilar Tip',\n    'Other Posterior Circulation',\n    'Aneurysm Present'\n]\n\n# --- Minimal image helpers used by dataset (same processing as training pipeline) ---\ndef robust_normalization(volume: np.ndarray) -> np.ndarray:\n    p1, p99 = np.percentile(volume.flatten(), [1, 99])\n    volume_norm = np.clip(volume, p1, p99)\n    if p99 > p1:\n        volume_norm = (volume_norm - p1) / (p99 - p1 + 1e-7)\n    else:\n        volume_norm = np.zeros_like(volume_norm)\n    return (volume_norm * 255).astype(np.uint8)\n\ndef create_3channel_input_8frame(volume: np.ndarray, image_size=224) -> np.ndarray:\n    if len(volume) == 0:\n        return np.zeros((image_size, image_size, 3), dtype=np.uint8)\n    middle_slice = volume[len(volume) // 2]\n    mip = np.max(volume, axis=0)\n    std_proj = np.std(volume, axis=0).astype(np.float32)\n    if std_proj.max() > std_proj.min():\n        p1, p99 = np.percentile(std_proj, [5, 95])\n        std_proj = np.clip(std_proj, p1, p99)\n        std_proj = ((std_proj - p1) / (p99 - p1 + 1e-7) * 255).astype(np.uint8)\n    else:\n        std_proj = np.zeros_like(std_proj, dtype=np.uint8)\n    return np.stack([middle_slice, mip, std_proj], axis=-1)\n\ndef apply_dicom_windowing(img: np.ndarray, window_center: float, window_width: float) -> np.ndarray:\n    img_min = window_center - window_width // 2\n    img_max = window_center + window_width // 2\n    img = np.clip(img, img_min, img_max)\n    img = (img - img_min) / (img_max - img_min + 1e-7)\n    return (img * 255).astype(np.uint8)\n\n# basic sampling: evenly pick frames across available list to make exactly 8\ndef sample_eight_from_list(paths: List[str]) -> List[str]:\n    n = len(paths)\n    if n == 0:\n        return []\n    if n <= 8:\n        out = paths[:]\n        while len(out) < 8:\n            out.extend(paths[:8-len(out)])\n        return out[:8]\n    # evenly spaced indices\n    idxs = np.linspace(0, n-1, 8).astype(int).tolist()\n    return [paths[i] for i in idxs]\n\n# --- Minimal dataset that reads DICOMs from series folder for inference ---\nclass InferenceEightFrameDataset(Dataset):\n    def __init__(self, series_list: List[str], series_root: str, image_size: int = 224):\n        self.series_list = series_list\n        self.series_root = series_root\n        self.image_size = image_size\n\n    def __len__(self):\n        return len(self.series_list)\n\n    def __getitem__(self, idx):\n        series_uid = self.series_list[idx]\n        series_dir = os.path.join(self.series_root, series_uid)\n        # collect DICOM files\n        dcm_files = sorted(glob.glob(os.path.join(series_dir, \"*.dcm\")) + glob.glob(os.path.join(series_dir, \"*.DCM\")))\n        if len(dcm_files) == 0:\n            # return zeros if no files\n            dummy = np.zeros((3, self.image_size, self.image_size), dtype=np.float32)\n            return torch.from_numpy(dummy).float(), torch.tensor([0.0,0.0], dtype=torch.float32), series_uid\n\n        selected = sample_eight_from_list(dcm_files)\n        volume = []\n        modality = 'CT'  # default; could parse from DICOM if needed\n        for dpath in selected:\n            try:\n                ds = pydicom.dcmread(dpath, force=True)\n                img = ds.pixel_array.astype(np.float32)\n                if hasattr(ds, 'RescaleSlope') and hasattr(ds, 'RescaleIntercept'):\n                    img = img * float(ds.RescaleSlope) + float(ds.RescaleIntercept)\n                # windowing default for CT\n                img = apply_dicom_windowing(img, 40, 80)\n                img = cv2.resize(img, (self.image_size, self.image_size), interpolation=cv2.INTER_AREA)\n                volume.append(img.astype(np.uint8))\n            except Exception:\n                volume.append(np.zeros((self.image_size, self.image_size), dtype=np.uint8))\n        while len(volume) < 8:\n            volume.append(volume[-1] if volume else np.zeros((self.image_size, self.image_size), dtype=np.uint8))\n        volume = np.array(volume[:8])\n        volume = robust_normalization(volume)\n        img = create_3channel_input_8frame(volume, image_size=self.image_size)  # H,W,3\n        # convert to tensor (channel-first) and normalize roughly\n        img_tensor = torch.from_numpy(img.transpose(2,0,1)).float() / 255.0\n        metadata = torch.tensor([0.0, 0.0], dtype=torch.float32)\n        return img_tensor, metadata, series_uid\n\n# --- helper: discover checkpoints in uploaded dataset root ---\ndef discover_checkpoints(root_dir: str) -> List[str]:\n    patterns = [\"**/*.pth\", \"**/*.pt\"]\n    found = []\n    for p in patterns:\n        found.extend(glob.glob(os.path.join(root_dir, p), recursive=True))\n    found = sorted(found)\n    return found\n\n# --- helper: load models; IMPORTANT: ImprovedMultiFrameModel class must be defined in kernel ---\ndef load_models_from_ckpts(ckpt_paths: List[str], device: torch.device) -> List[nn.Module]:\n    \"\"\"\n    Robust loader that handles different checkpoint formats and PyTorch safety changes.\n    - Tries safe 'weights-only' load where available.\n    - Falls back to `weights_only=False` if required (less safe).\n    - Supports checkpoints saved as full dict with keys like 'model_state_dict' or as raw state_dict.\n    \"\"\"\n    models = []\n    for p in ckpt_paths:\n        print(f\"\\n--- Loading checkpoint: {p} ---\")\n        if not os.path.exists(p):\n            raise FileNotFoundError(f\"Checkpoint path not found: {p}\")\n\n        # 1) Try a safe load first (PyTorch 2.6+ uses weights_only=True by default).\n        ck = None\n        load_failed = False\n        try:\n            # Try the \"safe\" load (weights-only). If the file contains only tensors/state_dict this will work.\n            ck = torch.load(p, map_location='cpu')  # try default behavior first\n            print(\"  -> torch.load succeeded (default).\")\n        except Exception as e_safe:\n            print(\"  -> torch.load (default) failed:\", repr(e_safe))\n            load_failed = True\n\n        # 2) If failed, try the less-restrictive load (weights_only=False). Trust file must be assured.\n        if ck is None:\n            try:\n                # This may re-enable arbitrary code execution during unpickling; only do for trusted checkpoints.\n                # Note: In older/newer PyTorch versions the arg name may vary; using kwargs is compatible.\n                ck = torch.load(p, map_location='cpu', weights_only=False)\n                print(\"  -> torch.load succeeded with weights_only=False (fallback).\")\n            except Exception as e_full:\n                # Final fallback: print helpful debug and raise\n                print(\"  -> Fallback torch.load with weights_only=False failed too.\")\n                print(\"     Exception:\", repr(e_full))\n                raise RuntimeError(f\"Failed to load checkpoint {p}. See above for errors.\") from e_full\n\n        # 3) Interpret ck contents\n        state_dict = None\n        if isinstance(ck, dict):\n            # Common formats:\n            # - {'model_state_dict': {...}, 'optimizer_state_dict': ...}\n            # - {'state_dict': {...}}\n            # - raw state_dict already (mapping of tensors)\n            if 'model_state_dict' in ck:\n                state_dict = ck['model_state_dict']\n                print(\"  -> Found key 'model_state_dict' in checkpoint.\")\n            elif 'state_dict' in ck:\n                state_dict = ck['state_dict']\n                print(\"  -> Found key 'state_dict' in checkpoint.\")\n            else:\n                # Might be that the whole dict *is* the state dict\n                # Heuristic: if values look like tensors, treat as state_dict\n                sample_vals = list(ck.values())[:5]\n                if len(sample_vals) > 0 and all(hasattr(v, 'shape') or torch.is_tensor(v) for v in sample_vals):\n                    state_dict = ck\n                    print(\"  -> Treating loaded dict as raw state_dict (heuristic).\")\n                else:\n                    # Not a recognized model state dict; maybe saved object with model inside\n                    # We'll attempt to find a nested model state dict\n                    nested = None\n                    for k, v in ck.items():\n                        if isinstance(v, dict):\n                            # check for tensor-like values inside\n                            vals = list(v.values())[:5]\n                            if vals and all(hasattr(x, 'shape') or torch.is_tensor(x) for x in vals):\n                                nested = v\n                                print(f\"  -> Found nested state-like dict at key '{k}'; using that.\")\n                                break\n                    if nested is not None:\n                        state_dict = nested\n                    else:\n                        # As last resort, error with informative message\n                        print(\"  -> Couldn't find a plausible model state dict inside the checkpoint.\")\n                        raise RuntimeError(f\"Unrecognized checkpoint format in {p}. Keys: {list(ck.keys())[:20]}\")\n        else:\n            # ck is not a dict (unexpected)\n            raise RuntimeError(f\"Loaded checkpoint object is not a dict for {p}. Type: {type(ck)}\")\n\n        # 4) Build model instance and load weights\n        try:\n            # IMPORTANT: ImprovedMultiFrameModel must be defined in the kernel (matching saved arch).\n            model = ImprovedMultiFrameModel(num_frames=config.NUM_FRAMES, num_classes=config.NUM_CLASSES, pretrained=False)\n        except Exception as e:\n            print(\"  -> Failed to instantiate ImprovedMultiFrameModel. Exception:\", repr(e))\n            raise\n\n        # Some state_dicts may have 'module.' prefixes if saved from DataParallel:\n        def clean_state_dict(sd):\n            if not isinstance(sd, dict):\n                return sd\n            new_sd = {}\n            for k, v in sd.items():\n                new_key = k\n                if k.startswith('module.'):\n                    new_key = k[len('module.'):]\n                new_sd[new_key] = v\n            return new_sd\n\n        state_dict_clean = clean_state_dict(state_dict)\n\n        # Try to load strictly first, otherwise try non-strict and print missing keys\n        try:\n            model.load_state_dict(state_dict_clean, strict=True)\n            print(\"  -> state_dict loaded with strict=True.\")\n        except RuntimeError as e_strict:\n            print(\"  -> strict load failed:\", repr(e_strict))\n            # Try non-strict and show summaries\n            load_res = model.load_state_dict(state_dict_clean, strict=False)\n            print(\"  -> Loaded with strict=False. Missing keys:\", load_res.missing_keys[:10])\n            print(\"               Unexpected keys:\", load_res.unexpected_keys[:10])\n\n        model.to(device)\n        model.eval()\n        models.append(model)\n        print(f\"  -> Model loaded and moved to {device}.\")\n\n    return models\n\n# --- inference across ensemble with optional simple TTA (horizontal flip) ---\ndef predict_ensemble(models: List[nn.Module], dataloader: DataLoader, device: torch.device, tta: bool = True) -> Tuple[List[str], np.ndarray]:\n    all_uids = []\n    all_preds = []\n    with torch.no_grad():\n        for imgs, metas, uids in tqdm(dataloader, desc=\"Inference\"):\n            imgs = imgs.to(device, non_blocking=True)\n            metas = metas.to(device, non_blocking=True)\n            batch_preds = None\n            for m in models:\n                logits = m(imgs, metas)  # shape (B, num_classes)\n                probs = torch.sigmoid(logits).cpu().numpy()\n                if tta:\n                    imgs_np = imgs.cpu().numpy()\n                    flipped = torch.tensor(np.flip(imgs_np, axis=-1).copy()).to(device)\n                    logits_f = m(flipped, metas)\n                    probs_f = torch.sigmoid(logits_f).cpu().numpy()\n                    probs = (probs + probs_f) / 2.0\n                batch_preds = probs if batch_preds is None else batch_preds + probs\n            batch_preds = batch_preds / len(models)\n            all_preds.append(batch_preds)\n            all_uids.extend(uids)\n    if len(all_preds) == 0:\n        return all_uids, np.zeros((0, len(TARGET_COLS)))\n    all_preds = np.vstack(all_preds)\n    return all_uids, all_preds\n\n# --- create submission dataframe, match sample ordering or generate sample if missing ---\ndef create_submission(uids: List[str], preds: np.ndarray, sample_submission_path: Optional[str], out_csv: str = OUTPUT_SUBMISSION) -> pd.DataFrame:\n    df = pd.DataFrame(preds, columns=TARGET_COLS)\n    df.insert(0, 'SeriesInstanceUID', uids)\n    if sample_submission_path and os.path.exists(sample_submission_path):\n        sample = pd.read_csv(sample_submission_path)\n        uid_col = sample.columns[0]\n        df = df.set_index('SeriesInstanceUID').reindex(sample[uid_col]).reset_index()\n    else:\n        # Save generated sample for inspection\n        sample_gen = df.copy()\n        sample_gen.iloc[:,1:] = 0.0\n        sample_gen.to_csv(GENERATED_SAMPLE, index=False)\n        print(\"Saved generated sample submission to:\", GENERATED_SAMPLE)\n    df.to_csv(out_csv, index=False)\n    print(\"Saved final submission to:\", out_csv)\n    return df\n\n# --- optional: compute RSNA competition metric given true labels & predicted probs ---\nfrom sklearn.metrics import roc_auc_score\ndef compute_competition_metric_if_labels(true_df: pd.DataFrame, pred_df: pd.DataFrame) -> Tuple[float, float, float]:\n    \"\"\"\n    true_df: dataframe with 'SeriesInstanceUID' + TARGET_COLS (ground truth)\n    pred_df: dataframe with 'SeriesInstanceUID' + TARGET_COLS (predictions)\n    returns: final_score, aneurysm_auc, avg_individual\n    \"\"\"\n    merged = pd.merge(true_df[['SeriesInstanceUID'] + TARGET_COLS], pred_df, on='SeriesInstanceUID', suffixes=('_true','_pred'))\n    y_true = merged[[c + '_true' for c in TARGET_COLS]].values\n    y_pred = merged[[c for c in TARGET_COLS]].values\n    individual_aucs = []\n    for i in range(13):\n        try:\n            if len(np.unique(y_true[:, i])) > 1:\n                auc = roc_auc_score(y_true[:, i], y_pred[:, i])\n            else:\n                auc = 0.5\n            individual_aucs.append(auc)\n        except:\n            individual_aucs.append(0.5)\n    try:\n        if len(np.unique(y_true[:, 13])) > 1:\n            aneurysm_auc = roc_auc_score(y_true[:, 13], y_pred[:, 13])\n        else:\n            aneurysm_auc = 0.5\n    except:\n        aneurysm_auc = 0.5\n    avg_individual = np.mean(individual_aucs)\n    final_score = (aneurysm_auc + avg_individual) / 2.0\n    return final_score, aneurysm_auc, avg_individual\n\n# --- top-level runner: auto-discover ckpts, run inference, produce submission ---\ndef run_full_inference_and_submit(\n        uploaded_ckpt_root: str = UPLOADED_CKPT_ROOT,\n        series_root: str = SERIES_FOLDER,\n        sample_submission_csv: Optional[str] = None,\n        out_submission: str = OUTPUT_SUBMISSION,\n        tta: bool = TTA,\n        batch_size: int = BATCH_SIZE,\n        num_workers: int = NUM_WORKERS,\n        device: torch.device = DEVICE\n    ) -> pd.DataFrame:\n\n    # discover ckpts\n    ckpts = discover_checkpoints(uploaded_ckpt_root)   # single assignment (fixed)\n    if len(ckpts) == 0:\n        raise FileNotFoundError(f\"No checkpoints found under {uploaded_ckpt_root}\")\n    # optionally restrict to 5 most recent or all found\n    print(f\"Found {len(ckpts)} checkpoints; using them all for ensemble.\")\n    # load models\n    models = load_models_from_ckpts(ckpts, device)\n    # build test series list\n    if not os.path.exists(series_root):\n        raise FileNotFoundError(f\"Series folder not found at {series_root}. Attach competition dataset or provide 'series_root'.\")\n    test_series = sorted([name for name in os.listdir(series_root) if os.path.isdir(os.path.join(series_root, name))])\n    print(f\"Test series count: {len(test_series)}\")\n    # dataset + loader\n    ds = InferenceEightFrameDataset(test_series, series_root, image_size=224)\n    loader = DataLoader(ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)\n    # predict\n    uids, preds = predict_ensemble(models, loader, device, tta=tta)\n    # create submission file\n    submission_df = create_submission(uids, preds, sample_submission_path=sample_submission_csv, out_csv=out_submission)\n    return submission_df\n# =====================================================================\n# HOW TO CALL:\n# 1) Make sure class ImprovedMultiFrameModel is defined in the kernel exactly as used to save .pth\n# 2) Then run this line (adjust paths if needed):\n# submission_df = run_full_inference_and_submit(uploaded_ckpt_root=\"/kaggle/input/effecientnet\",\n#                                               series_root=\"/kaggle/input/rsna-intracranial-aneurysm-detection/series\",\n#                                               sample_submission_csv=None, out_submission=\"/kaggle/working/submission.csv\",\n#                                               tta=True, batch_size=6, num_workers=2, device=DEVICE)\n#\n# 3) If you have a local ground-truth CSV for those series (val set), compute metric:\n# true_df = pd.read_csv('/kaggle/input/.../val_labels.csv')   # must contain SeriesInstanceUID + TARGET_COLS\n# score, aneur_auc, avg_indiv = compute_competition_metric_if_labels(true_df, submission_df)\n# print(\"Score:\", score, aneur_auc, avg_indiv)\n#\n# NOTE: I cannot run this here — paste & run in your Kaggle notebook. If you get errors, copy the exact traceback and paste it back and I'll walk you through fixes.\n# =====================================================================\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_df = run_full_inference_and_submit(uploaded_ckpt_root=\"/kaggle/input/effecientnet\",\n                                              series_root=\"/kaggle/input/rsna-intracranial-aneurysm-detection/series\",\n                                              sample_submission_csv=None, out_submission=\"/kaggle/working/submission.csv\",\n                                              tta=True, batch_size=6, num_workers=2, device=(\"cuda\"))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n#  RSNA ANEURYSM — FINAL INFERENCE & SUBMISSION PIPELINE\n#  Fully compatible with your ImprovedMultiFrameModel\n#  Uses your uploaded .pth files\n# ============================================================\n\n# import os, glob, traceback\n# import numpy as np\n# import pandas as pd\n# import cv2\n# import pydicom\n# from tqdm import tqdm\n# from typing import List, Optional\n# import torch\n# import torch.nn as nn\n# from torch.utils.data import Dataset, DataLoader\n\n\n# # ============================================================\n# # CONFIG — adapt only if needed\n# # ============================================================\n\n# TEST_SERIES_DIR = \"/kaggle/input/rsna-intracranial-aneurysm-detection/series\"\n# CKPT_DIR        = \"/kaggle/input/effecientnet\"   # your uploaded folder with 5 pth files\n# SUBMISSION_PATH = \"/kaggle/working/submission.csv\"\n# IMAGE_SIZE      = 224\n# NUM_FRAMES      = 8\n# NUM_CLASSES     = 14\n# DEVICE          = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# TARGET_COLS = [\n#     'Left Infraclinoid Internal Carotid Artery',\n#     'Right Infraclinoid Internal Carotid Artery',\n#     'Left Supraclinoid Internal Carotid Artery',\n#     'Right Supraclinoid Internal Carotid Artery',\n#     'Left Middle Cerebral Artery',\n#     'Right Middle Cerebral Artery',\n#     'Anterior Communicating Artery',\n#     'Left Anterior Cerebral Artery',\n#     'Right Anterior Cerebral Artery',\n#     'Left Posterior Communicating Artery',\n#     'Right Posterior Communicating Artery',\n#     'Basilar Tip',\n#     'Other Posterior Circulation',\n#     'Aneurysm Present'\n# ]\n\n\n# # ============================================================\n# # YOUR EXACT MODEL (copying exactly as you used)\n# # ============================================================\n\n# class ImprovedMultiFrameModel(nn.Module):\n#     def __init__(self, num_frames=8, num_classes=14, pretrained=True):\n#         super().__init__()\n#         self.num_frames = num_frames\n#         self.num_classes = num_classes\n#         self.use_metadata = config.USE_METADATA\n\n#         self.backbone = timm.create_model(\n#             config.MODEL_NAME_BACKBONE, \n#             pretrained=pretrained,\n#             num_classes=0,\n#             global_pool='avg'\n#         )\n#         self.feature_dim = self.backbone.num_features\n\n#         if self.use_metadata:\n#             self.meta_fc = nn.Sequential(\n#                 nn.Linear(2,16),\n#                 nn.ReLU(),\n#                 nn.Dropout(0.2),\n#                 nn.Linear(16,32),\n#                 nn.ReLU()\n#             )\n#             classifier_input_dim = self.feature_dim + 32\n#         else:\n#             classifier_input_dim = self.feature_dim\n\n#         self.classifier = nn.Sequential(\n#             nn.Linear(classifier_input_dim, 512),\n#             nn.BatchNorm1d(512),\n#             nn.ReLU(),\n#             nn.Dropout(0.3),\n#             nn.Linear(512,256),\n#             nn.BatchNorm1d(256),\n#             nn.ReLU(),\n#             nn.Dropout(0.3),\n#             nn.Linear(256, num_classes)\n#         )\n\n#     def forward(self, x, meta=None):\n#         feats = self.backbone(x)\n#         if self.use_metadata and meta is not None:\n#             meta_feats = self.meta_fc(meta)\n#             feats = torch.cat([feats, meta_feats], dim=1)\n#         out = self.classifier(feats)\n#         return out\n\n\n# # ============================================================\n# # PREPROCESSING HELPERS (IDENTICAL TO TRAINING)\n# # ============================================================\n\n# def robust_normalization(volume):\n#     p1, p99 = np.percentile(volume.flatten(), [1,99])\n#     vol = np.clip(volume, p1, p99)\n#     if p99 > p1:\n#         vol = (vol - p1) / (p99 - p1 + 1e-7)\n#     else:\n#         vol = np.zeros_like(vol)\n#     return (vol * 255).astype(np.uint8)\n\n# def create_3channel_input_8frame(volume):\n#     mid = volume[len(volume)//2]\n#     mip = np.max(volume, axis=0)\n#     std = np.std(volume, axis=0).astype(np.float32)\n#     if std.max() > std.min():\n#         p1, p99 = np.percentile(std, [5,95])\n#         std = np.clip(std, p1, p99)\n#         std = ((std-p1)/(p99-p1+1e-7)*255).astype(np.uint8)\n#     else:\n#         std = np.zeros_like(mid, dtype=np.uint8)\n#     return np.stack([mid, mip, std], axis=-1)\n\n# def window_ct(img):\n#     return apply_dicom_windowing(img, 40,80)\n\n# def apply_dicom_windowing(img, center, width):\n#     mn = center - width//2\n#     mx = center + width//2\n#     img = np.clip(img, mn, mx)\n#     img = (img - mn)/(mx-mn+1e-7)\n#     return (img*255).astype(np.uint8)\n\n# def sample_8(paths):\n#     n = len(paths)\n#     if n <= 8:\n#         out = paths.copy()\n#         while len(out)<8: out+=paths\n#         return out[:8]\n#     idxs = np.linspace(0, n-1, 8).astype(int)\n#     return [paths[i] for i in idxs]\n\n\n# # ============================================================\n# # INFERENCE DATASET (robust)\n# # ============================================================\n\n# class RSNAInferenceDataset(Dataset):\n#     def __init__(self, series_uids, series_dir):\n#         self.series_uids = series_uids\n#         self.series_dir = series_dir\n\n#     def __len__(self):\n#         return len(self.series_uids)\n\n#     def __getitem__(self, idx):\n#         uid = self.series_uids[idx]\n#         folder = os.path.join(self.series_dir, uid)\n#         dcm_files = sorted(glob.glob(folder+\"/*.dcm\") + glob.glob(folder+\"/*.DCM\"))\n\n#         if not dcm_files:\n#             # return blank\n#             img = torch.zeros(3, IMAGE_SIZE, IMAGE_SIZE)\n#             meta = torch.tensor([0.0,0.0])\n#             return img, meta, uid\n\n#         chosen = sample_8(dcm_files)\n#         vol = []\n\n#         for fp in chosen:\n#             try:\n#                 ds = pydicom.dcmread(fp, force=True)\n#                 img = ds.pixel_array.astype(np.float32)\n#                 if hasattr(ds,\"RescaleSlope\"):\n#                     img = img*float(ds.RescaleSlope) + float(ds.RescaleIntercept)\n#                 img = window_ct(img)\n#                 if img.ndim==3:\n#                     img = img[:,:,0]\n#                 img = cv2.resize(img,(IMAGE_SIZE,IMAGE_SIZE))\n#             except:\n#                 img = np.zeros((IMAGE_SIZE,IMAGE_SIZE),dtype=np.uint8)\n#             vol.append(img)\n\n#         vol = np.array(vol)\n#         vol = robust_normalization(vol)\n#         img3 = create_3channel_input_8frame(vol)\n\n#         img3 = img3.astype(np.uint8)\n#         img3 = torch.tensor(img3.transpose(2,0,1)).float()/255.0\n\n#         # metadata default (age/sex not available for test)\n#         meta = torch.tensor([0.0,0.0], dtype=torch.float32)\n\n#         return img3, meta, uid\n\n\n# # ============================================================\n# # LOAD CHECKPOINTS\n# # ============================================================\n\n# def load_all_models(ckpt_dir):\n#     ckpts = sorted(glob.glob(os.path.join(ckpt_dir,\"*.pth\")))\n#     if not ckpts:\n#         raise ValueError(\"No pth files found in \"+ckpt_dir)\n\n#     models = []\n#     for path in ckpts:\n#         print(\"Loading:\", path)\n#         ck = torch.load(path, map_location=\"cpu\", weights_only=False)\n\n#         if \"model_state_dict\" in ck:\n#             sd = ck[\"model_state_dict\"]\n#         else:\n#             sd = ck\n\n#         model = ImprovedMultiFrameModel(\n#             num_frames=NUM_FRAMES,\n#             num_classes=NUM_CLASSES,\n#             pretrained=False\n#         )\n#         model.load_state_dict(sd, strict=False)\n#         model.to(DEVICE).eval()\n#         models.append(model)\n\n#     print(f\"Loaded {len(models)} models.\")\n#     return models\n\n\n# # ============================================================\n# # ENSEMBLE PREDICTION\n# # ============================================================\n\n# def predict(models, loader, tta=True):\n#     all_uids = []\n#     all_preds = []\n\n#     with torch.no_grad():\n#         for imgs, meta, uids in tqdm(loader, desc=\"Predicting\"):\n#             imgs = imgs.to(DEVICE)\n#             meta = meta.to(DEVICE)\n\n#             outputs = 0\n#             for model in models:\n#                 logits = model(imgs, meta)\n#                 prob = torch.sigmoid(logits).cpu().numpy()\n\n#                 if tta:\n#                     flipped = torch.flip(imgs, [-1])\n#                     logits_f = model(flipped, meta)\n#                     prob_f = torch.sigmoid(logits_f).cpu().numpy()\n#                     prob = (prob + prob_f)/2\n\n#                 outputs += prob\n\n#             outputs /= len(models)\n#             all_preds.append(outputs)\n#             all_uids.extend(uids)\n\n#     return np.vstack(all_preds), all_uids\n\n\n# # ============================================================\n# # SUBMISSION BUILDER\n# # ============================================================\n\n# def build_submission(uids, preds, path=SUBMISSION_PATH):\n#     df = pd.DataFrame(preds, columns=TARGET_COLS)\n#     df.insert(0, \"SeriesInstanceUID\", uids)\n#     df.to_csv(path, index=False)\n#     print(\"Saved submission:\", path)\n#     return df\n\n\n# # ============================================================\n# # MAIN EXECUTION\n# # ============================================================\n\n# # 1) load all .pth models\n# models = load_all_models(CKPT_DIR)\n\n# # 2) read all test series\n# series_uids = sorted([d for d in os.listdir(TEST_SERIES_DIR) \n#                       if os.path.isdir(os.path.join(TEST_SERIES_DIR, d))])\n# print(\"Test series:\", len(series_uids))\n\n# # 3) dataset + loader\n# ds = RSNAInferenceDataset(series_uids, TEST_SERIES_DIR)\n# loader = DataLoader(ds, batch_size=4, shuffle=False, num_workers=2)\n\n# # 4) predict\n# preds, uids = predict(models, loader, tta=True)\n\n# # 5) make submission\n# submission_df = build_submission(uids, preds, SUBMISSION_PATH)\n\n# # OPTIONAL: Kaggle API auto-submit\n# # !kaggle competitions submit -c rsna-intracranial-aneurysm-detection -f /kaggle/working/submission.csv -m \"8frame effnetv2 ensemble TTA\"\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T11:43:04.625238Z","iopub.execute_input":"2025-11-13T11:43:04.625527Z","iopub.status.idle":"2025-11-13T12:27:28.048976Z","shell.execute_reply.started":"2025-11-13T11:43:04.625472Z","shell.execute_reply":"2025-11-13T12:27:28.04814Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd, os\npath = \"/kaggle/working/submission.csv\"\nprint(\"Exists:\", os.path.exists(path))\nsub = pd.read_csv(path)\nprint(\"Shape:\", sub.shape)\nprint(\"Columns:\", sub.columns.tolist())\nprint(\"First rows:\")\nprint(sub.head())\n# quick checks\nassert sub.columns[0] == \"SeriesInstanceUID\", \"First column name is wrong!\"\nassert len(sub.columns) == 15, \"There should be 1 UID + 14 target columns\"\nprint(\"All checks passed (basic).\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T12:40:55.22884Z","iopub.execute_input":"2025-11-13T12:40:55.229562Z","iopub.status.idle":"2025-11-13T12:40:55.264932Z","shell.execute_reply.started":"2025-11-13T12:40:55.22953Z","shell.execute_reply":"2025-11-13T12:40:55.264262Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!kaggle competitions submit -c rsna-intracranial-aneurysm-detection -f /kaggle/working/submission.csv -m \"8frame effnetv2 ensemble TTA\"\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T12:42:43.547241Z","iopub.execute_input":"2025-11-13T12:42:43.54776Z","iopub.status.idle":"2025-11-13T12:42:44.583782Z","shell.execute_reply.started":"2025-11-13T12:42:43.547735Z","shell.execute_reply":"2025-11-13T12:42:44.583053Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1. Sanity-check submission and align with sample_submission (if present)\nimport os, pandas as pd, numpy as np, glob, subprocess, json\n\nSUB_PATH = \"/kaggle/working/submission.csv\"   # your generated submission\nSAMPLE_PATHS = [\n    \"/kaggle/input/rsna-intracranial-aneurysm-detection/sample_submission.csv\",\n    \"/kaggle/input/sample_submission.csv\"  # alternate possible mount\n]\n\nTARGET_COLS = [\n    'Left Infraclinoid Internal Carotid Artery',\n    'Right Infraclinoid Internal Carotid Artery',\n    'Left Supraclinoid Internal Carotid Artery',\n    'Right Supraclinoid Internal Carotid Artery',\n    'Left Middle Cerebral Artery',\n    'Right Middle Cerebral Artery',\n    'Anterior Communicating Artery',\n    'Left Anterior Cerebral Artery',\n    'Right Anterior Cerebral Artery',\n    'Left Posterior Communicating Artery',\n    'Right Posterior Communicating Artery',\n    'Basilar Tip',\n    'Other Posterior Circulation',\n    'Aneurysm Present'\n]\n\n# load submission\nif not os.path.exists(SUB_PATH):\n    raise FileNotFoundError(f\"submission file not found at {SUB_PATH}\")\n\nsub = pd.read_csv(SUB_PATH)\nprint(\"Loaded submission:\", SUB_PATH, \"shape:\", sub.shape)\n\n# find sample_submission if available\nsample_path = None\nfor p in SAMPLE_PATHS:\n    if os.path.exists(p):\n        sample_path = p\n        break\n\nif sample_path:\n    sample = pd.read_csv(sample_path)\n    print(\"Found sample_submission at:\", sample_path, \"shape:\", sample.shape)\n    uid_col = sample.columns[0]\n    # ensure columns match order\n    expected_cols = [uid_col] + TARGET_COLS\n    if list(sub.columns) != expected_cols:\n        print(\"Submission columns do not match sample. Attempting to reorder/align...\")\n        # try to align by name\n        if sub.columns[0] != uid_col:\n            # try to detect uid column\n            if uid_col in sub.columns:\n                sub = sub.set_index(uid_col).reset_index()\n        # ensure the rest exist\n        miss = [c for c in TARGET_COLS if c not in sub.columns]\n        if miss:\n            raise ValueError(\"Submission missing columns: \" + \", \".join(miss))\n        sub = sub[[uid_col] + TARGET_COLS]\nelse:\n    # No official sample; ensure columns exactly match expected names:\n    if sub.shape[1] != 1 + len(TARGET_COLS):\n        raise ValueError(f\"Submission has {sub.shape[1]} columns but expected {1 + len(TARGET_COLS)}\")\n    if list(sub.columns[1:]) != TARGET_COLS:\n        print(\"Warning: submission prediction column names differ from expected TARGET_COLS.\")\n        # try to rename if names are index-based like col1..col14\n        # we will force columns to expected names but keep UIDs as is\n        sub.columns = [sub.columns[0]] + TARGET_COLS\n        print(\"Renamed prediction columns to standard TARGET_COLS.\")\n\n# Basic checks on values\nvals = sub.iloc[:,1:].values.astype(float)\nif np.isnan(vals).any():\n    raise ValueError(\"NaN found in prediction values.\")\nif (vals < 0).any() or (vals > 1).any():\n    print(\"Warning: some values are outside [0,1]. Clipping to [0,1].\")\n    vals = np.clip(vals, 0.0, 1.0)\n    sub.iloc[:,1:] = vals\n\n# Row count check if series folder present\nseries_dir = \"/kaggle/input/rsna-intracranial-aneurysm-detection/series\"\nif os.path.exists(series_dir):\n    test_uid_count = len([d for d in os.listdir(series_dir) if os.path.isdir(os.path.join(series_dir,d))])\n    print(\"Test series folders:\", test_uid_count)\n    if sub.shape[0] != test_uid_count:\n        print(\"Warning: submission row count does not match number of series folders.\")\nelse:\n    print(\"Note: series/ folder not found in notebook environment; cannot check row count.\")\n\n# Save aligned/cleaned submission (backup)\nALIGNED_SUB = \"/kaggle/working/submission_aligned.csv\"\nsub.to_csv(ALIGNED_SUB, index=False)\nprint(\"Saved aligned submission to:\", ALIGNED_SUB)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T12:46:36.056445Z","iopub.execute_input":"2025-11-13T12:46:36.057191Z","iopub.status.idle":"2025-11-13T12:46:41.972116Z","shell.execute_reply.started":"2025-11-13T12:46:36.057157Z","shell.execute_reply":"2025-11-13T12:46:41.971307Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# compute local RSNA competition metric if you have a ground-truth CSV\nfrom sklearn.metrics import roc_auc_score\ndef rsna_metric(y_true, y_pred):\n    # y_true, y_pred shape (N,14)\n    individual = []\n    for i in range(13):\n        if len(np.unique(y_true[:,i]))>1:\n            individual.append(roc_auc_score(y_true[:,i], y_pred[:,i]))\n        else:\n            individual.append(0.5)\n    # aneurysm present last\n    aneurysm_auc = roc_auc_score(y_true[:,13], y_pred[:,13]) if len(np.unique(y_true[:,13]))>1 else 0.5\n    return (np.mean(individual) + aneurysm_auc)/2.0, aneurysm_auc, np.mean(individual), individual\n\n# Example usage:\n# true_df = pd.read_csv(\"/kaggle/input/my-val/val_labels.csv\")  # must contain SeriesInstanceUID + TARGET_COLS\n# pred_df = pd.read_csv(ALIGNED_SUB)\n# merged = true_df.merge(pred_df, on=\"SeriesInstanceUID\", suffixes=(\"_true\",\"_pred\"))\n# y_true = merged[[c + \"_true\" for c in TARGET_COLS]].values\n# y_pred = merged[[c for c in TARGET_COLS]].values\n# score, aneur_auc, avg_indiv, indivs = rsna_metric(y_true, y_pred)\n# print(\"Estimated RSNA metric:\", score, \"aneurysm_auc:\", aneur_auc, \"avg_indiv:\", avg_indiv)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T12:47:05.152583Z","iopub.execute_input":"2025-11-13T12:47:05.15317Z","iopub.status.idle":"2025-11-13T12:47:05.158387Z","shell.execute_reply.started":"2025-11-13T12:47:05.153146Z","shell.execute_reply":"2025-11-13T12:47:05.157778Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Auto-submit using Kaggle CLI\n# Make sure you really want to submit (limited daily submissions)\nSUBMIT_FILE = ALIGNED_SUB  # or SUB_PATH if you prefer\n\n# Change message as needed\nmessage = \"eightframe_efficientnetv2s_ensemble_demo_submission\"\n\n# Submit\nprint(\"Submitting to Kaggle...\")\ncmd = f\"kaggle competitions submit -c rsna-intracranial-aneurysm-detection -f {SUBMIT_FILE} -m \\\"{message}\\\"\"\nprint(\"Running:\", cmd)\nstatus = os.system(cmd)\nif status != 0:\n    print(\"kaggle CLI returned non-zero exit. Ensure kaggle CLI is installed and ~/.kaggle/kaggle.json is configured.\")\nelse:\n    print(\"Submitted. Now listing recent submissions:\")\n    os.system(\"kaggle competitions submissions -c rsna-intracranial-aneurysm-detection\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T12:47:35.712251Z","iopub.execute_input":"2025-11-13T12:47:35.71256Z","iopub.status.idle":"2025-11-13T12:47:35.989308Z","shell.execute_reply.started":"2025-11-13T12:47:35.712538Z","shell.execute_reply":"2025-11-13T12:47:35.988525Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --------------------------\n# RSNA inference-server wrapper cell\n# Produces /kaggle/working/submission.parquet required for notebook submission\n# --------------------------\n\nimport os, glob, shutil, traceback\nfrom collections import defaultdict\nimport numpy as np\nimport polars as pl\nimport pandas as pd\nimport pydicom\nimport torch\nimport torch.nn as nn\nimport cv2\nfrom typing import List\nimport timm\n\n# The RSNA inference server\nimport kaggle_evaluation.rsna_inference_server as rsna_inference_server\n\n# --------------------------\n# Config: change only if paths differ\n# --------------------------\nCKPT_DIR = \"/kaggle/input/effecientnet\"   # path containing your .pth files\nTEST_SERIES_DIR = \"/kaggle/input/rsna-intracranial-aneurysm-detection/series\"\nOUTPUT_PARQUET = \"/kaggle/working/submission.parquet\"\n\nIMAGE_SIZE = 224\nNUM_FRAMES = 8\nNUM_CLASSES = 14\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nID_COL = 'SeriesInstanceUID'\nLABEL_COLS = [\n    'Left Infraclinoid Internal Carotid Artery',\n    'Right Infraclinoid Internal Carotid Artery',\n    'Left Supraclinoid Internal Carotid Artery',\n    'Right Supraclinoid Internal Carotid Artery',\n    'Left Middle Cerebral Artery',\n    'Right Middle Cerebral Artery',\n    'Anterior Communicating Artery',\n    'Left Anterior Cerebral Artery',\n    'Right Anterior Cerebral Artery',\n    'Left Posterior Communicating Artery',\n    'Right Posterior Communicating Artery',\n    'Basilar Tip',\n    'Other Posterior Circulation',\n    'Aneurysm Present',\n]\n\n# --------------------------\n# Utility functions (mirrors training preprocessing)\n# --------------------------\ndef robust_normalization(volume: np.ndarray) -> np.ndarray:\n    p1, p99 = np.percentile(volume.flatten(), [1, 99])\n    volume_norm = np.clip(volume, p1, p99)\n    if p99 > p1:\n        volume_norm = (volume_norm - p1) / (p99 - p1 + 1e-7)\n    else:\n        volume_norm = np.zeros_like(volume_norm)\n    return (volume_norm * 255).astype(np.uint8)\n\ndef create_3channel_input_8frame(volume: np.ndarray) -> np.ndarray:\n    if len(volume) == 0:\n        return np.zeros((IMAGE_SIZE, IMAGE_SIZE, 3), dtype=np.uint8)\n    middle_slice = volume[len(volume)//2]\n    mip = np.max(volume, axis=0)\n    std_proj = np.std(volume, axis=0).astype(np.float32)\n    if std_proj.max() > std_proj.min():\n        p1, p99 = np.percentile(std_proj, [5,95])\n        std_proj = np.clip(std_proj, p1, p99)\n        std_proj = ((std_proj - p1) / (p99 - p1 + 1e-7) * 255).astype(np.uint8)\n    else:\n        std_proj = np.zeros_like(middle_slice, dtype=np.uint8)\n    # ensure uint8\n    middle_slice = middle_slice.astype(np.uint8) if middle_slice.dtype != np.uint8 else middle_slice\n    mip = mip.astype(np.uint8) if mip.dtype != np.uint8 else mip\n    std_proj = std_proj.astype(np.uint8) if std_proj.dtype != np.uint8 else std_proj\n    return np.stack([middle_slice, mip, std_proj], axis=-1)\n\ndef apply_dicom_windowing(img: np.ndarray, window_center: float = 40, window_width: float = 80) -> np.ndarray:\n    img_min = window_center - window_width // 2\n    img_max = window_center + window_width // 2\n    img = np.clip(img, img_min, img_max)\n    img = (img - img_min) / (img_max - img_min + 1e-7)\n    return (img * 255).astype(np.uint8)\n\ndef sample_eight_from_list(paths: List[str]) -> List[str]:\n    n = len(paths)\n    if n <= 8:\n        out = paths[:]\n        while len(out) < 8:\n            out += paths[:(8 - len(out))]\n        return out[:8]\n    idxs = np.linspace(0, n - 1, 8).astype(int).tolist()\n    return [paths[i] for i in idxs]\n\n# --------------------------\n# Re-create ImprovedMultiFrameModel (exact architecture you used)\n# --------------------------\n# NOTE: this references `config` in your notebook; if `config` isn't present,\n# define a minimal fallback (only used to set backbone name).\nif 'config' not in globals():\n    import types\n    config = types.SimpleNamespace(\n        MODEL_NAME_BACKBONE=\"tf_efficientnetv2_s.in1k\",\n        USE_METADATA=True,\n        USE_3CHANNEL_INPUT=True\n    )\n\nclass ImprovedMultiFrameModel(nn.Module):\n    def __init__(self, num_frames=8, num_classes=14, pretrained=True):\n        super().__init__()\n        self.num_frames = num_frames\n        self.num_classes = num_classes\n        self.use_metadata = config.USE_METADATA\n\n        self.backbone = timm.create_model(\n            config.MODEL_NAME_BACKBONE,\n            pretrained=pretrained,\n            num_classes=0,\n            global_pool='avg'\n        )\n        self.feature_dim = self.backbone.num_features\n\n        if self.use_metadata:\n            self.meta_fc = nn.Sequential(\n                nn.Linear(2,16),\n                nn.ReLU(),\n                nn.Dropout(0.2),\n                nn.Linear(16,32),\n                nn.ReLU()\n            )\n            classifier_input_dim = self.feature_dim + 32\n        else:\n            classifier_input_dim = self.feature_dim\n\n        self.classifier = nn.Sequential(\n            nn.Linear(classifier_input_dim, 512),\n            nn.BatchNorm1d(512),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(512,256),\n            nn.BatchNorm1d(256),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, num_classes)\n        )\n\n    def forward(self, x, meta=None):\n        feats = self.backbone(x)\n        if self.use_metadata and meta is not None:\n            meta_feats = self.meta_fc(meta)\n            feats = torch.cat([feats, meta_feats], dim=1)\n        out = self.classifier(feats)\n        return out\n\n# --------------------------\n# Load ensemble checkpoints (keeps them in CPU first; moves to device per inference)\n# --------------------------\ndef discover_ckpts(ckpt_dir):\n    files = sorted(glob.glob(os.path.join(ckpt_dir, \"*.pth\")) + glob.glob(os.path.join(ckpt_dir, \"*.pt\")))\n    return files\n\nCKPT_PATHS = discover_ckpts(CKPT_DIR)\nif len(CKPT_PATHS) == 0:\n    raise FileNotFoundError(f\"No .pth files found in {CKPT_DIR}; attach your checkpoint dataset to the notebook.\")\n\nprint(\"Found checkpoints:\", CKPT_PATHS)\n\n# We'll lazily load model and weights on first invocation for memory safety.\n_LOADED_MODELS = None\ndef _load_models_to_device(device):\n    global _LOADED_MODELS\n    if _LOADED_MODELS is None:\n        _LOADED_MODELS = []\n        for p in CKPT_PATHS:\n            print(\"Loading checkpoint:\", p)\n            ck = torch.load(p, map_location='cpu', weights_only=False)\n            # support either raw state_dict or dict with 'model_state_dict'\n            if isinstance(ck, dict) and 'model_state_dict' in ck:\n                state = ck['model_state_dict']\n            elif isinstance(ck, dict) and 'state_dict' in ck:\n                state = ck['state_dict']\n            elif isinstance(ck, dict):\n                state = ck  # heuristic (raw dict)\n            else:\n                state = ck\n            # clean 'module.' prefix if necessary\n            sd = {}\n            if isinstance(state, dict):\n                for k,v in state.items():\n                    nk = k[len('module.'):] if k.startswith('module.') else k\n                    sd[nk] = v\n            else:\n                sd = state\n            model = ImprovedMultiFrameModel(num_frames=NUM_FRAMES, num_classes=NUM_CLASSES, pretrained=False)\n            try:\n                model.load_state_dict(sd, strict=False)\n            except Exception:\n                # second attempt — sometimes top-level dict has nested 'model'\n                if isinstance(sd, dict) and 'model_state_dict' in sd:\n                    try:\n                        model.load_state_dict(sd['model_state_dict'], strict=False)\n                    except Exception as e:\n                        print(\"Warning: load_state_dict failed:\", e)\n                else:\n                    print(\"Warning: load_state_dict encountered issues but continuing with strict=False.\")\n            model.to(device)\n            model.eval()\n            _LOADED_MODELS.append(model)\n    else:\n        # ensure moved to current device\n        for m in _LOADED_MODELS:\n            m.to(device)\n\n# --------------------------\n# The predict() function expected by RSNAInferenceServer\n# NOTE: This function should return a Polars or Pandas DataFrame WITHOUT the ID_COL (the server will drop ID before writing)\n# However the demo format creates DF including the ID and then returns predictions.drop(ID_COL). We'll follow that pattern.\n# --------------------------\ndef predict(series_path: str) -> pl.DataFrame | pd.DataFrame:\n    \"\"\"\n    series_path: folder path that contains the DICOM files for one SeriesInstanceUID (one test sample)\n    returns: a Polars or Pandas DataFrame of shape (1, 14) [no ID column], matching LABEL_COLS order.\n    \"\"\"\n    # ------------- collect files -------------\n    series_id = os.path.basename(series_path.rstrip(\"/\"))\n    all_filepaths = []\n    for root, _, files in os.walk(series_path):\n        for f in files:\n            if f.lower().endswith('.dcm'):\n                all_filepaths.append(os.path.join(root, f))\n    all_filepaths.sort()\n    # If no DICOMs found, return neutral probabilities\n    if len(all_filepaths) == 0:\n        row = [0.5] * len(LABEL_COLS)\n        df = pl.DataFrame([row], schema=LABEL_COLS)\n        # cleanup (important per demo)\n        shutil.rmtree('/kaggle/shared', ignore_errors=True)\n        return df  # already no ID column\n\n    # ------------- sample frames (8) and preprocess -------------\n    chosen = sample_eight_from_list(all_filepaths)\n    frames = []\n    for fp in chosen:\n        try:\n            ds = pydicom.dcmread(fp, force=True)\n            img = ds.pixel_array.astype(np.float32)\n            # apply rescale if present\n            if hasattr(ds, 'RescaleSlope') and hasattr(ds, 'RescaleIntercept'):\n                img = img * float(ds.RescaleSlope) + float(ds.RescaleIntercept)\n            img = apply_dicom_windowing(img, 40, 80)   # CT default window\n            if img.ndim == 3:\n                # reduce multi-channel to gray if needed\n                if img.shape[2] == 3:\n                    img = cv2.cvtColor(img.astype(np.uint8), cv2.COLOR_BGR2GRAY).astype(np.float32)\n                else:\n                    img = img[:,:,0].astype(np.float32)\n            img = cv2.resize(img.astype(np.uint8), (IMAGE_SIZE, IMAGE_SIZE), interpolation=cv2.INTER_AREA)\n        except Exception as e:\n            # fallback to zeros for this frame\n            img = np.zeros((IMAGE_SIZE, IMAGE_SIZE), dtype=np.uint8)\n        frames.append(img)\n    # pad if necessary\n    while len(frames) < NUM_FRAMES:\n        frames.append(frames[-1] if frames else np.zeros((IMAGE_SIZE, IMAGE_SIZE), dtype=np.uint8))\n    vol = np.array(frames[:NUM_FRAMES])\n    vol = robust_normalization(vol)\n    input_hwc = create_3channel_input_8frame(vol)  # H,W,3\n    # ensure shape and dtype\n    if input_hwc.ndim == 2:\n        input_hwc = np.stack([input_hwc]*3, axis=-1)\n    if input_hwc.shape[2] != 3:\n        # pad/truncate channels to 3\n        if input_hwc.ndim == 3 and input_hwc.shape[2] > 3:\n            input_hwc = input_hwc[:,:,:3]\n        else:\n            chs = [input_hwc[:,:,i] if i < input_hwc.shape[2] else input_hwc[:,:,0] for i in range(3)]\n            input_hwc = np.stack(chs, axis=-1)\n    img_tensor = torch.from_numpy(input_hwc.transpose(2,0,1)).float().unsqueeze(0) / 255.0  # (1,3,H,W)\n    meta_tensor = torch.tensor([[0.0, 0.0]], dtype=torch.float32)  # placeholder\n\n    # ------------- model inference (ensemble average) -------------\n    _load_models_to_device(DEVICE)\n    preds_accum = None\n    with torch.no_grad():\n        for model in _LOADED_MODELS:\n            model.eval()\n            inp = img_tensor.to(DEVICE)\n            meta = meta_tensor.to(DEVICE)\n            logits = model(inp, meta)\n            probs = torch.sigmoid(logits).detach().cpu().numpy()  # (1,14)\n            # TTA simple horizontal flip\n            flipped = torch.flip(inp, dims=[-1])\n            logits_f = model(flipped.to(DEVICE), meta)\n            probs_f = torch.sigmoid(logits_f).detach().cpu().numpy()\n            probs = (probs + probs_f) / 2.0\n            preds_accum = probs if preds_accum is None else preds_accum + probs\n\n    if preds_accum is None:\n        preds_accum = np.array([[0.5]*len(LABEL_COLS)])\n    else:\n        preds_accum = preds_accum / max(1, len(_LOADED_MODELS))\n\n    # Build a Polars DataFrame INCLUDING ID (demo does then drop ID)\n    row = [series_id] + preds_accum[0].tolist()\n    predictions = pl.DataFrame([row], schema=[ID_COL, *LABEL_COLS])\n\n    # ---------------- IMPORTANT cleanup ----------------\n    # Remove /kaggle/shared to prevent \"out of disk space\" in the evaluation harness\n    shutil.rmtree('/kaggle/shared', ignore_errors=True)\n\n    # The demo returns predictions.drop(ID_COL) — server expects predictions without ID column\n    return predictions.drop(ID_COL)\n\n# --------------------------\n# Hook into the RSNA inference server\n# --------------------------\ninference_server = rsna_inference_server.RSNAInferenceServer(predict)\n\n# If Kaggle runs this as competition re-run it will set KAGGLE_IS_COMPETITION_RERUN\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    # server mode (competition evaluation)\n    inference_server.serve()\nelse:\n    # local gateway: will run through the available series in /kaggle/input/.../series\n    inference_server.run_local_gateway()\n\n    # After run_local_gateway finishes, the server writes /kaggle/working/submission.parquet\n    if os.path.exists(OUTPUT_PARQUET):\n        print(\"Created:\", OUTPUT_PARQUET)\n        display(pl.read_parquet(OUTPUT_PARQUET).head())\n    else:\n        print(\"Warning: expected parquet not created at\", OUTPUT_PARQUET)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T13:08:58.509635Z","iopub.execute_input":"2025-11-13T13:08:58.510268Z","iopub.status.idle":"2025-11-13T13:09:11.244425Z","shell.execute_reply.started":"2025-11-13T13:08:58.510243Z","shell.execute_reply":"2025-11-13T13:09:11.243664Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}