{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":99552,"databundleVersionId":13190393,"sourceType":"competition"},{"sourceId":12687919,"sourceType":"datasetVersion","datasetId":7976292}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":79.101254,"end_time":"2025-07-30T17:08:48.446116","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-07-30T17:07:29.344862","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## RSNA Intracranial Aneurysm Detection - Inference Notebook\n","metadata":{"papermill":{"duration":0.003275,"end_time":"2025-07-30T17:07:33.344873","exception":false,"start_time":"2025-07-30T17:07:33.341598","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"## 1. Setup and Imports","metadata":{}},{"cell_type":"code","source":"import os\nimport sys\nimport gc\nimport json\nimport shutil\nimport warnings\nwarnings.filterwarnings('ignore')\nfrom pathlib import Path\nfrom collections import defaultdict\nfrom typing import List, Dict, Optional, Tuple\nfrom IPython.display import display\n\n# Data handling\nimport numpy as np\nimport polars as pl\nimport pandas as pd\n\n# Medical imaging\nimport pydicom\nimport cv2\n\n# ML/DL\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.cuda.amp import autocast\nimport timm\n\n# Transformations\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\n# Competition API\nimport kaggle_evaluation.rsna_inference_server\n\n# Set device\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {device}\")\n","metadata":{"execution":{"iopub.status.busy":"2025-07-30T17:17:56.938165Z","iopub.execute_input":"2025-07-30T17:17:56.938324Z","iopub.status.idle":"2025-07-30T17:18:44.955919Z","shell.execute_reply.started":"2025-07-30T17:17:56.938307Z","shell.execute_reply":"2025-07-30T17:18:44.955207Z"},"papermill":{"duration":48.035666,"end_time":"2025-07-30T17:08:21.383337","exception":false,"start_time":"2025-07-30T17:07:33.347671","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Constants and Configuration","metadata":{}},{"cell_type":"code","source":"# Competition constants\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# Model selection - Change this to select which model to use for inference\n# Options: 'tf_efficientnetv2_s', 'convnext_small', 'swin_small_patch4_window7_224', 'ensemble'\nSELECTED_MODEL = 'ensemble' \n\n\n# Model paths configuration\nMODEL_PATHS = {\n    'tf_efficientnetv2_s': '/kaggle/input/rsna-iad-trained-models/models/tf_efficientnetv2_s_fold0_best.pth',\n    'convnext_small': '/kaggle/input/rsna-iad-trained-models/models/convnext_small_fold0_best.pth',\n    'swin_small_patch4_window7_224': '/kaggle/input/rsna-iad-trained-models/models/swin_small_patch4_window7_224_fold0_best.pth'\n}\n\nclass InferenceConfig:\n    # Model selection\n    model_selection = SELECTED_MODEL\n    use_ensemble = (SELECTED_MODEL == 'ensemble')\n    \n    # Default model settings (will be overridden by checkpoint)\n    image_size = 512\n    num_slices = 32\n    use_windowing = True\n    \n    # Inference settings\n    batch_size = 1\n    use_amp = True\n    use_tta = True\n    tta_transforms = 4\n    \n    # Ensemble weights (if using ensemble)\n    ensemble_weights = {\n        'tf_efficientnetv2_s': 0.4,\n        'convnext_small': 0.3,\n        'swin_small_patch4_window7_224': 0.3\n    }\n\nCFG = InferenceConfig()\n","metadata":{"execution":{"iopub.status.busy":"2025-07-30T17:18:44.957411Z","iopub.execute_input":"2025-07-30T17:18:44.957869Z","iopub.status.idle":"2025-07-30T17:18:44.96321Z","shell.execute_reply.started":"2025-07-30T17:18:44.957848Z","shell.execute_reply":"2025-07-30T17:18:44.962428Z"},"papermill":{"duration":0.009181,"end_time":"2025-07-30T17:08:21.39509","exception":false,"start_time":"2025-07-30T17:08:21.385909","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Model Architecture","metadata":{}},{"cell_type":"code","source":"class MultiBackboneModel(nn.Module):\n    \"\"\"Flexible model that can use different backbones\"\"\"\n    def __init__(self, model_name, num_classes=14, pretrained=True, \n                 drop_rate=0.3, drop_path_rate=0.2):\n        super().__init__()\n        \n        self.model_name = model_name\n        \n        if 'swin' in model_name:\n            # Swin transformer requires 224x224 by default\n            self.backbone = timm.create_model(\n                model_name, \n                pretrained=pretrained,\n                in_chans=3,\n                drop_rate=drop_rate,\n                drop_path_rate=drop_path_rate,\n                img_size=CFG.image_size,  # Override default size\n                num_classes=0,  # Remove classifier head\n                global_pool=''  # Remove global pooling\n            )\n        else:\n            self.backbone = timm.create_model(\n                model_name, \n                pretrained=pretrained,\n                in_chans=3,\n                drop_rate=drop_rate,\n                drop_path_rate=drop_path_rate,\n                num_classes=0,  # Remove classifier head\n                global_pool=''  # Remove global pooling\n            )\n        \n        with torch.no_grad():\n            dummy_input = torch.zeros(1, 3, CFG.image_size, CFG.image_size)\n            features = self.backbone(dummy_input)\n            \n            if len(features.shape) == 4:\n                # Conv features (batch, channels, height, width)\n                num_features = features.shape[1]\n                self.needs_pool = True\n            elif len(features.shape) == 3:\n                # Transformer features (batch, sequence, features)\n                num_features = features.shape[-1]\n                self.needs_pool = False\n                self.needs_seq_pool = True\n            else:\n                # Already flat features (batch, features)\n                num_features = features.shape[1]\n                self.needs_pool = False\n                self.needs_seq_pool = False\n        \n        print(f\"Model {model_name}: detected {num_features} features, output shape: {features.shape}\")\n        \n        # Add global pooling for models that output spatial features\n        if self.needs_pool:\n            self.global_pool = nn.AdaptiveAvgPool2d(1)\n        \n        # Metadata processing\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        \n        # Combined classifier with batch norm for stability\n        self.classifier = nn.Sequential(\n            nn.Linear(num_features + 32, 512),\n            nn.BatchNorm1d(512),\n            nn.ReLU(),\n            nn.Dropout(drop_rate),\n            nn.Linear(512, 256),\n            nn.BatchNorm1d(256),\n            nn.ReLU(),\n            nn.Dropout(drop_rate),\n            nn.Linear(256, num_classes)\n        )\n        \n    def forward(self, image, meta):\n        # Extract image features\n        img_features = self.backbone(image)\n        \n        # Apply appropriate pooling based on model type\n        if hasattr(self, 'needs_pool') and self.needs_pool:\n            # Conv features - apply global pooling\n            img_features = self.global_pool(img_features)\n            img_features = img_features.flatten(1)\n        elif hasattr(self, 'needs_seq_pool') and self.needs_seq_pool:\n            # Transformer features - average across sequence dimension\n            img_features = img_features.mean(dim=1)\n        elif len(img_features.shape) == 4:\n            # Fallback for any 4D output\n            img_features = F.adaptive_avg_pool2d(img_features, 1).flatten(1)\n        elif len(img_features.shape) == 3:\n            # Fallback for any 3D output\n            img_features = img_features.mean(dim=1)\n        \n        # Process metadata\n        meta_features = self.meta_fc(meta)\n        \n        # Combine features\n        combined = torch.cat([img_features, meta_features], dim=1)\n        \n        # Classification\n        output = self.classifier(combined)\n        \n        return output","metadata":{"execution":{"iopub.status.busy":"2025-07-30T17:18:44.963963Z","iopub.execute_input":"2025-07-30T17:18:44.964242Z","iopub.status.idle":"2025-07-30T17:18:45.049852Z","shell.execute_reply.started":"2025-07-30T17:18:44.964217Z","shell.execute_reply":"2025-07-30T17:18:45.049136Z"},"papermill":{"duration":0.0138,"end_time":"2025-07-30T17:08:21.411432","exception":false,"start_time":"2025-07-30T17:08:21.397632","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. DICOM Processing Functions\n","metadata":{}},{"cell_type":"code","source":"def apply_dicom_windowing(img: np.ndarray, window_center: float, window_width: float) -> np.ndarray:\n    \"\"\"Apply DICOM windowing\"\"\"\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 get_windowing_params(modality: str) -> Tuple[float, float]:\n    \"\"\"Get appropriate windowing for different modalities\"\"\"\n    windows = {\n        'CT': (40, 80),\n        'CTA': (50, 350),\n        'MRA': (600, 1200),\n        'MRI': (40, 80),\n    }\n    return windows.get(modality, (40, 80))\n\ndef process_dicom_series(series_path: str) -> Tuple[np.ndarray, Dict]:\n    \"\"\"Process a DICOM series and extract metadata\"\"\"\n    series_path = Path(series_path)\n    \n    # Find all DICOM files\n    all_filepaths = []\n    for root, _, files in os.walk(series_path):\n        for file in files:\n            if file.endswith('.dcm'):\n                all_filepaths.append(os.path.join(root, file))\n    all_filepaths.sort()\n    \n    if len(all_filepaths) == 0:\n        # Return default values\n        volume = np.zeros((CFG.num_slices, CFG.image_size, CFG.image_size), dtype=np.uint8)\n        metadata = {'age': 50, 'sex': 0, 'modality': 'CT'}\n        return volume, metadata\n    \n    # Process DICOM files\n    slices = []\n    metadata = {}\n    \n    for i, filepath in enumerate(all_filepaths):\n        try:\n            ds = pydicom.dcmread(filepath, force=True)\n            img = ds.pixel_array.astype(np.float32)\n            \n            # Handle multi-frame or color images\n            if img.ndim == 3:\n                if img.shape[-1] == 3:\n                    img = cv2.cvtColor(img.astype(np.uint8), cv2.COLOR_BGR2GRAY).astype(np.float32)\n                else:\n                    img = img[:, :, 0]\n            \n            # Extract metadata from first file\n            if i == 0:\n                metadata['modality'] = getattr(ds, 'Modality', 'CT')\n                \n                try:\n                    age_str = getattr(ds, 'PatientAge', '050Y')\n                    age = int(''.join(filter(str.isdigit, age_str[:3])) or '50')\n                    metadata['age'] = min(age, 100)\n                except:\n                    metadata['age'] = 50\n                \n                try:\n                    sex = getattr(ds, 'PatientSex', 'M')\n                    metadata['sex'] = 1 if sex == 'M' else 0\n                except:\n                    metadata['sex'] = 0\n            \n            # Apply rescale if available\n            if hasattr(ds, 'RescaleSlope') and hasattr(ds, 'RescaleIntercept'):\n                img = img * ds.RescaleSlope + ds.RescaleIntercept\n            \n            # Apply windowing\n            if CFG.use_windowing:\n                window_center, window_width = get_windowing_params(metadata['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            \n            # Resize\n            img = cv2.resize(img, (CFG.image_size, CFG.image_size))\n            slices.append(img)\n            \n        except Exception as e:\n            print(f\"Error processing {filepath}: {e}\")\n            continue\n    \n    # Handle slice sampling\n    if len(slices) == 0:\n        volume = np.zeros((CFG.num_slices, CFG.image_size, CFG.image_size), dtype=np.uint8)\n    else:\n        volume = np.array(slices)\n        if len(slices) > CFG.num_slices:\n            indices = np.linspace(0, len(slices) - 1, CFG.num_slices).astype(int)\n            volume = volume[indices]\n        elif len(slices) < CFG.num_slices:\n            pad_size = CFG.num_slices - len(slices)\n            volume = np.pad(volume, ((0, pad_size), (0, 0), (0, 0)), mode='edge')\n    \n    return volume, metadata\n","metadata":{"execution":{"iopub.status.busy":"2025-07-30T17:18:45.050626Z","iopub.execute_input":"2025-07-30T17:18:45.050871Z","iopub.status.idle":"2025-07-30T17:18:45.064359Z","shell.execute_reply.started":"2025-07-30T17:18:45.050853Z","shell.execute_reply":"2025-07-30T17:18:45.063702Z"},"papermill":{"duration":0.016392,"end_time":"2025-07-30T17:08:21.430308","exception":false,"start_time":"2025-07-30T17:08:21.413916","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Transform Functions\n","metadata":{}},{"cell_type":"code","source":"def get_inference_transform():\n    \"\"\"Get inference transformation\"\"\"\n    return A.Compose([\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\ndef get_tta_transforms():\n    \"\"\"Get test time augmentation transforms\"\"\"\n    transforms = [\n        A.Compose([  # Original\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ]),\n        A.Compose([  # Horizontal flip\n            A.HorizontalFlip(p=1.0),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ]),\n        A.Compose([  # Vertical flip\n            A.VerticalFlip(p=1.0),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ]),\n        A.Compose([  # 90 degree rotation\n            A.RandomRotate90(p=1.0),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ])\n    ]\n    return transforms\n\n","metadata":{"execution":{"iopub.status.busy":"2025-07-30T17:18:45.066371Z","iopub.execute_input":"2025-07-30T17:18:45.066594Z","iopub.status.idle":"2025-07-30T17:18:45.07909Z","shell.execute_reply.started":"2025-07-30T17:18:45.066568Z","shell.execute_reply":"2025-07-30T17:18:45.078395Z"},"papermill":{"duration":0.009704,"end_time":"2025-07-30T17:08:21.44247","exception":false,"start_time":"2025-07-30T17:08:21.432766","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Model Loading Functions","metadata":{}},{"cell_type":"code","source":"# Global variables\nMODELS = {}\nTRANSFORM = None\nTTA_TRANSFORMS = None\n\ndef load_single_model(model_name: str, model_path: str) -> nn.Module:\n    \"\"\"Load a single model\"\"\"\n    print(f\"Loading {model_name} from {model_path}...\")\n    \n    if not os.path.exists(model_path):\n        raise FileNotFoundError(f\"Model file not found: {model_path}\")\n    \n    # Load checkpoint with weights_only=False to handle numpy scalars\n    checkpoint = torch.load(model_path, map_location=device, weights_only=False)\n    \n    # Extract config\n    model_config = checkpoint.get('model_config', {})\n    training_config = checkpoint.get('training_config', {})\n    \n    # Update global config if needed\n    if 'image_size' in training_config:\n        CFG.image_size = training_config['image_size']\n    \n    # Initialize model\n    model = MultiBackboneModel(\n        model_name=model_name,\n        num_classes=training_config.get('num_classes', 14),\n        pretrained=False,\n        drop_rate=0.0,\n        drop_path_rate=0.0\n    )\n    \n    # Load weights\n    model.load_state_dict(checkpoint['model_state_dict'])\n    model = model.to(device)\n    model.eval()\n    \n    print(f\"Loaded {model_name} with best score: {checkpoint.get('best_score', 'N/A'):.4f}\")\n    \n    return model\n\ndef load_models():\n    \"\"\"Load models based on configuration\"\"\"\n    global MODELS, TRANSFORM, TTA_TRANSFORMS\n    \n    print(\"Loading models...\")\n    \n    if CFG.use_ensemble:\n        # Load all models for ensemble\n        for model_name, model_path in MODEL_PATHS.items():\n            try:\n                MODELS[model_name] = load_single_model(model_name, model_path)\n            except Exception as e:\n                print(f\"Warning: Could not load {model_name}: {e}\")\n    else:\n        # Load single selected model\n        if CFG.model_selection in MODEL_PATHS:\n            model_path = MODEL_PATHS[CFG.model_selection]\n            MODELS[CFG.model_selection] = load_single_model(CFG.model_selection, model_path)\n        else:\n            raise ValueError(f\"Unknown model: {CFG.model_selection}\")\n    \n    # Initialize transforms\n    TRANSFORM = get_inference_transform()\n    if CFG.use_tta:\n        TTA_TRANSFORMS = get_tta_transforms()\n    \n    print(f\"Models loaded: {list(MODELS.keys())}\")\n    \n    # Warm up models\n    print(\"Warming up models...\")\n    dummy_image = torch.randn(1, 3, CFG.image_size, CFG.image_size).to(device)\n    dummy_meta = torch.randn(1, 2).to(device)\n    \n    with torch.no_grad():\n        for model in MODELS.values():\n            _ = model(dummy_image, dummy_meta)\n    \n    print(\"Ready for inference!\")\n\n","metadata":{"execution":{"iopub.status.busy":"2025-07-30T17:18:45.079754Z","iopub.execute_input":"2025-07-30T17:18:45.079994Z","iopub.status.idle":"2025-07-30T17:18:45.092049Z","shell.execute_reply.started":"2025-07-30T17:18:45.079973Z","shell.execute_reply":"2025-07-30T17:18:45.091325Z"},"papermill":{"duration":0.012883,"end_time":"2025-07-30T17:08:21.457742","exception":false,"start_time":"2025-07-30T17:08:21.444859","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Prediction Functions","metadata":{}},{"cell_type":"code","source":"\ndef predict_single_model(model: nn.Module, image: np.ndarray, meta_tensor: torch.Tensor) -> np.ndarray:\n    \"\"\"Make prediction with a single model\"\"\"\n    predictions = []\n    \n    if CFG.use_tta and TTA_TRANSFORMS:\n        # Test time augmentation\n        for transform in TTA_TRANSFORMS[:CFG.tta_transforms]:\n            aug_image = transform(image=image)['image']\n            aug_image = aug_image.unsqueeze(0).to(device)\n            \n            with torch.no_grad():\n                with autocast(enabled=CFG.use_amp):\n                    output = model(aug_image, meta_tensor)\n                    pred = torch.sigmoid(output)\n                    predictions.append(pred.cpu().numpy())\n        \n        # Average TTA predictions\n        return np.mean(predictions, axis=0).squeeze()\n    else:\n        # Single prediction\n        image_tensor = TRANSFORM(image=image)['image']\n        image_tensor = image_tensor.unsqueeze(0).to(device)\n        \n        with torch.no_grad():\n            with autocast(enabled=CFG.use_amp):\n                output = model(image_tensor, meta_tensor)\n                return torch.sigmoid(output).cpu().numpy().squeeze()\n\ndef predict_ensemble(image: np.ndarray, meta_tensor: torch.Tensor) -> np.ndarray:\n    \"\"\"Make ensemble prediction\"\"\"\n    all_predictions = []\n    weights = []\n    \n    for model_name, model in MODELS.items():\n        pred = predict_single_model(model, image, meta_tensor)\n        all_predictions.append(pred)\n        weights.append(CFG.ensemble_weights.get(model_name, 1.0))\n    \n    # Weighted average\n    weights = np.array(weights) / np.sum(weights)\n    predictions = np.array(all_predictions)\n    \n    return np.average(predictions, weights=weights, axis=0)\n\ndef _predict_inner(series_path: str) -> pl.DataFrame:\n    \"\"\"Main prediction logic (internal).\"\"\"\n    global MODELS\n    \n    # Load models if not already loaded\n    if not MODELS:\n        load_models()\n    \n    # Extract series ID\n    series_id = os.path.basename(series_path)\n    \n    # Process DICOM series\n    volume, metadata = process_dicom_series(series_path)\n    \n    # Create multi-channel input\n    middle_slice = volume[CFG.num_slices // 2]\n    mip = np.max(volume, axis=0)\n    std_proj = np.std(volume, axis=0).astype(np.float32)\n    \n    # Normalize std projection\n    if std_proj.max() > std_proj.min():\n        std_proj = ((std_proj - std_proj.min()) / (std_proj.max() - std_proj.min()) * 255).astype(np.uint8)\n    else:\n        std_proj = np.zeros_like(std_proj, dtype=np.uint8)\n    \n    image = np.stack([middle_slice, mip, std_proj], axis=-1)\n    \n    # Prepare metadata\n    age_normalized = metadata['age'] / 100.0\n    sex = metadata['sex']\n    meta_tensor = torch.tensor([[age_normalized, sex]], dtype=torch.float32).to(device)\n    \n    # Make predictions\n    if CFG.use_ensemble:\n        final_pred = predict_ensemble(image, meta_tensor)\n    else:\n        # Use single selected model\n        model = MODELS[CFG.model_selection]\n        final_pred = predict_single_model(model, image, meta_tensor)\n    \n    # Create output dataframe\n    predictions_df = pl.DataFrame(\n        data=[[series_id] + final_pred.tolist()],\n        schema=[ID_COL] + LABEL_COLS,\n        orient='row'\n    )\n\n    \n    # Return without ID column, as required by the API\n    return predictions_df.drop(ID_COL)","metadata":{"execution":{"iopub.status.busy":"2025-07-30T17:18:45.092716Z","iopub.execute_input":"2025-07-30T17:18:45.092878Z","iopub.status.idle":"2025-07-30T17:18:45.105229Z","shell.execute_reply.started":"2025-07-30T17:18:45.092863Z","shell.execute_reply":"2025-07-30T17:18:45.104595Z"},"papermill":{"duration":0.014866,"end_time":"2025-07-30T17:08:21.475187","exception":false,"start_time":"2025-07-30T17:08:21.460321","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8. Fallback and Error Handling\n","metadata":{}},{"cell_type":"code","source":"def predict_fallback(series_path: str) -> pl.DataFrame:\n    \"\"\"Fallback prediction function\"\"\"\n    series_id = os.path.basename(series_path)\n    \n    # Return conservative predictions\n    predictions = pl.DataFrame(\n        data=[[series_id] + [0.1] * len(LABEL_COLS)],\n        schema=[ID_COL] + LABEL_COLS,\n        orient='row'\n    )\n    \n    # Clean up\n    shutil.rmtree('/kaggle/shared', ignore_errors=True)\n    \n    return predictions.drop(ID_COL)\n\ndef predict(series_path: str) -> pl.DataFrame:\n    \"\"\"\n    Top-level prediction function passed to the server.\n    It calls the core logic and guarantees cleanup in a `finally` block.\n    \"\"\"\n    try:\n        # Call the internal prediction logic\n        return _predict_inner(series_path)\n    except Exception as e:\n        print(f\"Error during prediction for {os.path.basename(series_path)}: {e}\")\n        print(\"Using fallback predictions.\")\n        # Return a fallback dataframe with the correct schema\n        predictions = pl.DataFrame(\n            data=[[0.1] * len(LABEL_COLS)],\n            schema=LABEL_COLS,\n            orient='row'\n        )\n        return predictions\n    finally:\n        # This code is required to prevent \"out of disk space\" and \"directory not empty\" errors.\n        # It deletes the shared folder and then immediately recreates it, ensuring it's\n        # empty and ready for the next prediction.\n        shared_dir = '/kaggle/shared'\n        shutil.rmtree(shared_dir, ignore_errors=True)\n        os.makedirs(shared_dir, exist_ok=True)\n        \n        # Also perform memory cleanup here\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n        gc.collect()\n","metadata":{"execution":{"iopub.status.busy":"2025-07-30T17:18:45.105947Z","iopub.execute_input":"2025-07-30T17:18:45.106286Z","iopub.status.idle":"2025-07-30T17:18:45.12338Z","shell.execute_reply.started":"2025-07-30T17:18:45.106257Z","shell.execute_reply":"2025-07-30T17:18:45.122706Z"},"papermill":{"duration":0.010205,"end_time":"2025-07-30T17:08:21.488223","exception":false,"start_time":"2025-07-30T17:08:21.478018","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"## 9. Main Execution\n\nload_models()\n\n# Initialize the inference server with our main `predict` function.\ninference_server = kaggle_evaluation.rsna_inference_server.RSNAInferenceServer(predict)\n\n# Check if the notebook is running in the competition environment or a local session.\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    inference_server.run_local_gateway()\n    \n    submission_df = pl.read_parquet('/kaggle/working/submission.parquet')\n    display(submission_df)","metadata":{"execution":{"iopub.status.busy":"2025-07-30T17:18:45.124289Z","iopub.execute_input":"2025-07-30T17:18:45.124525Z","iopub.status.idle":"2025-07-30T17:19:06.380016Z","shell.execute_reply.started":"2025-07-30T17:18:45.124503Z","shell.execute_reply":"2025-07-30T17:19:06.379427Z"},"papermill":{"duration":24.918596,"end_time":"2025-07-30T17:08:46.419293","exception":false,"start_time":"2025-07-30T17:08:21.500697","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null}]}