{"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":12780021,"sourceType":"datasetVersion","datasetId":8079690}],"dockerImageVersionId":31154,"isInternetEnabled":false,"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\n# import numpy as np # linear algebra\n# import 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\n# import os\n# for 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":"import os\nimport numpy as np\nimport pydicom\nimport cv2\nfrom pathlib import Path\nfrom typing import List, Tuple, Dict, Optional\nfrom scipy import ndimage\nimport warnings\nimport gc\n\n# ----------------------------------------------------------------------\n# Suppress warnings\n# ----------------------------------------------------------------------\nwarnings.filterwarnings('ignore')\n\n# ----------------------------------------------------------------------\n# DICOM PROCESSING CLASS\n# ----------------------------------------------------------------------\n\nclass DICOMPreprocessorKaggle:\n    \"\"\"\n    DICOM preprocessing system for Kaggle Code Competition\n    Converts original DICOMPreprocessor logic to single series processing\n    \"\"\"\n    \n    def __init__(self, target_shape: Tuple[int, int, int] = (32, 384, 384)):\n        self.target_depth, self.target_height, self.target_width = target_shape\n        \n    def load_dicom_series(self, series_path: str) -> Tuple[List[pydicom.Dataset], str]:\n        \"\"\"Load DICOM series\"\"\"\n        series_path = Path(series_path)\n        series_name = series_path.name\n        \n        # Search for DICOM files\n        dicom_files = []\n        for root, _, files in os.walk(series_path):\n            for file in files:\n                if file.endswith('.dcm'):\n                    dicom_files.append(os.path.join(root, file))\n        \n        if not dicom_files:\n            raise ValueError(f\"No DICOM files found in {series_path}\")\n        \n        # Load DICOM datasets\n        datasets = []\n        for filepath in dicom_files:\n            try:\n                ds = pydicom.dcmread(filepath, force=True)\n                datasets.append(ds)\n            except Exception:\n                # Silently skip bad DICOM files (Robustness)\n                continue\n        \n        if not datasets:\n            raise ValueError(f\"No valid DICOM files in {series_path}\")\n            \n        return datasets, series_name\n    \n    def extract_slice_info(self, datasets: List[pydicom.Dataset]) -> List[Dict]:\n        \"\"\"Extract position information for each slice\"\"\"\n        slice_info = []\n        \n        for i, ds in enumerate(datasets):\n            info = {\n                'dataset': ds,\n                'index': i,\n                'instance_number': getattr(ds, 'InstanceNumber', i),\n            }\n            \n            # Get z-coordinate from ImagePositionPatient and ImageOrientationPatient \n            try:\n                ipp = np.array(getattr(ds, 'ImagePositionPatient', None))\n                iop = np.array(getattr(ds, 'ImageOrientationPatient', None))\n                n_vec = np.cross(iop[:3],iop[3:])\n                info['z_position'] = float((ipp*n_vec).sum())\n            except Exception:\n                info['z_position'] = float(i)\n                \n            slice_info.append(info)\n        \n        return slice_info\n    \n    def sort_slices_by_position(self, slice_info: List[Dict]) -> List[Dict]:\n        \"\"\"Sort slices by z-coordinate\"\"\"\n        return sorted(slice_info, key=lambda x: x['z_position'])\n    \n    def get_windowing_params(self, ds: pydicom.Dataset, img: np.ndarray = None) -> Tuple[Optional[float], Optional[float]]:\n        \"\"\"Get windowing parameters based on modality\"\"\"\n        modality = getattr(ds, 'Modality', 'CT')\n        \n        if modality == 'CT':\n            # Fixed window for CTA [0, 500] HU range for normalization\n            return 50.0, 350.0 # Returning nominal center/width for logic path\n        elif modality == 'MR':\n            return None, None # Use statistical normalization\n        else:\n            return 50.0, 350.0 # Default to CTA windowing\n    \n    def apply_windowing_or_normalize(self, img: np.ndarray, center: Optional[float], width: Optional[float]) -> np.ndarray:\n        \"\"\"Apply CTA fixed-window or statistical normalization\"\"\"\n        \n        if center is not None and width is not None:\n            # Fixed CTA windowing for CT/CTA\n            p1, p99 = 0, 500 # Fixed window [0, 500] HU for robust CTA preprocessing\n            \n            if p99 > p1:\n                normalized = np.clip(img, p1, p99)\n                normalized = (normalized - p1) / (p99 - p1)\n                result = (normalized * 255).astype(np.uint8)\n                return result\n            else:\n                return np.zeros_like(img, dtype=np.uint8)\n        \n        else:\n            # Statistical normalization (for MR or fallback)\n            p1, p99 = np.percentile(img, [1, 99])\n            \n            if p99 > p1:\n                normalized = np.clip(img, p1, p99)\n                normalized = (normalized - p1) / (p99 - p1)\n                result = (normalized * 255).astype(np.uint8)\n                return result\n            else:\n                # If image has no variation (safety)\n                return np.zeros_like(img, dtype=np.uint8)\n    \n    def extract_pixel_array(self, ds: pydicom.Dataset) -> np.ndarray:\n        \"\"\"Extract 2D pixel array and apply DICOM rescaling\"\"\"\n        \n        # Get pixel data\n        img = ds.pixel_array.astype(np.float32)\n        \n        # For 3D volume case (multiple frames) - select middle frame\n        if img.ndim == 3:\n            frame_idx = img.shape[0] // 2\n            img = img[frame_idx]\n            \n        # Convert color image to grayscale\n        if img.ndim == 3 and img.shape[-1] == 3:\n            img = cv2.cvtColor(img.astype(np.uint8), cv2.COLOR_RGB2GRAY).astype(np.float32)\n            \n        # Apply RescaleSlope and RescaleIntercept (Keep this for correctness)\n        slope = getattr(ds, 'RescaleSlope', 1)\n        intercept = getattr(ds, 'RescaleIntercept', 0)\n        \n        # NOTE: Your original code hardcoded slope/intercept to 1/0. I'm restoring the DICOM standard\n        # behavior for better robustness, as the previous failed submission might have been due\n        # to not applying these tags on certain test files.\n        if slope != 1 or intercept != 0:\n            img = img * float(slope) + float(intercept)\n            \n        return img\n    \n    def resize_volume_3d(self, volume: np.ndarray) -> np.ndarray:\n        \"\"\"Resize 3D volume to target size\"\"\"\n        current_shape = volume.shape\n        target_shape = (self.target_depth, self.target_height, self.target_width)\n        \n        if current_shape == target_shape:\n            return volume\n        \n        # 3D resizing using scipy.ndimage\n        zoom_factors = [\n            target_shape[i] / current_shape[i] for i in range(3)\n        ]\n        \n        # Resize with linear interpolation\n        resized_volume = ndimage.zoom(volume, zoom_factors, order=1, mode='nearest')\n        \n        # Padding if necessary\n        pad_width = [\n            (0, max(0, self.target_depth - resized_volume.shape[0])),\n            (0, max(0, self.target_height - resized_volume.shape[1])),\n            (0, max(0, self.target_width - resized_volume.shape[2]))\n        ]\n        \n        if any(pw[1] > 0 for pw in pad_width):\n            resized_volume = np.pad(resized_volume, pad_width, mode='edge')\n            \n        return resized_volume[:self.target_depth, :self.target_height, :self.target_width].astype(np.uint8)\n    \n    def process_series(self, series_path: str) -> np.ndarray:\n        \"\"\"Process DICOM series and return as NumPy array (for Kaggle: no file saving)\"\"\"\n        try:\n            # 1. Load DICOM files\n            datasets, series_name = self.load_dicom_series(series_path)\n            \n            # Check first DICOM to determine 3D/2D\n            first_ds = datasets[0]\n            first_img = first_ds.pixel_array\n            \n            if len(datasets) == 1 and first_img.ndim == 3:\n                # Case 1: Single 3D DICOM file (e.g., from an MR scan)\n                return self._process_single_3d_dicom(first_ds, series_name)\n            else:\n                # Case 2: Multiple 2D DICOM files (standard CT/CTA series)\n                return self._process_multiple_2d_dicoms(datasets, series_name)\n            \n        except Exception as e:\n            # CRITICAL ROBUSTNESS FIX: Return a zero volume on any failure\n            print(f\"!! CRITICAL DICOM ERROR processing series {Path(series_path).name}: {e}. Returning blank volume.\")\n            # Ensure the returned shape and dtype match the expected output\n            return np.zeros(\n                (self.target_depth, self.target_height, self.target_width), \n                dtype=np.uint8\n            )\n    \n    def _process_single_3d_dicom(self, ds: pydicom.Dataset, series_name: str) -> np.ndarray:\n        \"\"\"Process single 3D DICOM file\"\"\"\n        volume = self.extract_pixel_array(ds) # extract_pixel_array handles slope/intercept\n        volume = volume.astype(np.float32)\n        \n        window_center, window_width = self.get_windowing_params(ds)\n        \n        # Apply windowing/normalization to each slice\n        processed_slices = []\n        for i in range(volume.shape[0]):\n            slice_img = volume[i]\n            processed_img = self.apply_windowing_or_normalize(slice_img, window_center, window_width)\n            processed_slices.append(processed_img)\n        \n        volume = np.stack(processed_slices, axis=0)\n        \n        # 3D resize\n        final_volume = self.resize_volume_3d(volume)\n        return final_volume\n    \n    def _process_multiple_2d_dicoms(self, datasets: List[pydicom.Dataset], series_name: str) -> np.ndarray:\n        \"\"\"Process multiple 2D DICOM files\"\"\"\n        \n        slice_info = self.extract_slice_info(datasets)\n        sorted_slices = self.sort_slices_by_position(slice_info)\n        \n        # Use parameters from the first slice for consistency\n        first_ds = sorted_slices[0]['dataset']\n        window_center, window_width = self.get_windowing_params(first_ds)\n        \n        processed_slices = []\n        \n        for slice_data in sorted_slices:\n            ds = slice_data['dataset']\n            img = self.extract_pixel_array(ds) # img has slope/intercept applied\n            processed_img = self.apply_windowing_or_normalize(img, window_center, window_width)\n            \n            # 2D resize before stacking\n            resized_img = cv2.resize(processed_img, (self.target_width, self.target_height), interpolation=cv2.INTER_LINEAR)\n            \n            processed_slices.append(resized_img)\n\n        volume = np.stack(processed_slices, axis=0)\n        \n        # 3D resize (for depth normalization)\n        final_volume = self.resize_volume_3d(volume)\n        return final_volume\n\n# ----------------------------------------------------------------------\n# WRAPPER FUNCTIONS (API-compatible)\n# ----------------------------------------------------------------------\n\ndef process_dicom_series_kaggle(series_path: str, target_shape: Tuple[int, int, int] = (32, 384, 384)) -> np.ndarray:\n    \"\"\"DICOM processing function for Kaggle inference (single series)\"\"\"\n    preprocessor = DICOMPreprocessorKaggle(target_shape=target_shape)\n    return preprocessor.process_series(series_path)\n\ndef process_dicom_series_safe(series_path: str, target_shape: Tuple[int, int, int] = (32, 384, 384)) -> np.ndarray:\n    \"\"\"Safe DICOM processing with memory cleanup and error handling.\"\"\"\n    # The DICOMPreprocessorKaggle.process_series now handles exceptions internally\n    try:\n        volume = process_dicom_series_kaggle(series_path, target_shape)\n        return volume\n    finally:\n        gc.collect()\n\n# ----------------------------------------------------------------------\n# TEST FUNCTION (Unchanged)\n# ----------------------------------------------------------------------\n\ndef test_single_series(series_path: str, target_shape: Tuple[int, int, int] = (32, 384, 384)):\n    \"\"\"Test processing for single series\"\"\"\n    try:\n        # Execute processing\n        volume = process_dicom_series_safe(series_path, target_shape)\n        return volume\n        \n    except Exception:\n        # This block is for non-DICOM-related errors (e.g., file system failure)\n        return None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T19:53:24.15175Z","iopub.execute_input":"2025-10-14T19:53:24.152286Z","iopub.status.idle":"2025-10-14T19:53:24.756974Z","shell.execute_reply.started":"2025-10-14T19:53:24.152259Z","shell.execute_reply":"2025-10-14T19:53:24.756176Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import sys\nimport gc\nimport json\nimport shutil\nimport warnings\nwarnings.filterwarnings('ignore')\nfrom pathlib import Path\nfrom typing import List, Dict, Optional, Tuple\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# DICOM preprocessor (DICOMPreprocessorKaggle class defined in previous cell)\n# In actual use, define in the same file or import appropriately\n\n# Set device\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n#print(f\"Using device: {device}\")\n\n# ====================================================\n# Competition constants\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]\nprint(np.array(LABEL_COLS)[[1,0,3,2,5,4,6,8,7,10,9,11,12,13]])\n\n# ====================================================\n# Configuration\n# ====================================================\nclass InferenceConfig:\n    # Model settings\n    model_name = \"tf_efficientnetv2_s.in21k_ft_in1k\"\n    size = 384\n    target_cols = LABEL_COLS\n    num_classes = len(target_cols)\n    in_chans = 32\n    \n    # Preprocessing settings\n    target_shape = (32, 384, 384)  # (depth, height, width)\n    \n    # Inference settings\n    batch_size = 1\n    use_amp = True\n    use_tta = False  # TTA is prohibited due to left/right positional information\n    tta_transforms = 0\n    \n    # Model paths\n    model_dir = '/kaggle/input/rsna2025-effnetv2-32ch'\n    n_fold = 5\n    trn_fold = [0, 1, 2, 3, 4]\n    \n    # Ensemble weights (equal weight for all folds)\n    ensemble_weights = None  # None means equal weights\n\nCFG = InferenceConfig()\n\n# ====================================================\n# Transforms\n# ====================================================\ndef get_inference_transform():\n    \"\"\"Get inference transformation\"\"\"\n    return A.Compose([\n        A.Resize(CFG.size, CFG.size),\n        A.Normalize(),\n        ToTensorV2(),\n    ])\n\n# TTA is not used due to left/right positional information\n# def get_tta_transforms():\n#     \"\"\"TTA is prohibited for brain aneurysms due to left/right positioning\"\"\"\n#     pass\n\n# ====================================================\n# Model Loading Functions\n# ====================================================\n# Global variables\nMODELS = {}\nTRANSFORM = None\nTTA_TRANSFORMS = None\n\ndef load_model_fold(fold: int) -> nn.Module:\n    \"\"\"Load a single fold model\"\"\"\n    model_path = Path(CFG.model_dir) / f'{CFG.model_name}_fold{fold}_best.pth'\n    \n    if not model_path.exists():\n        raise FileNotFoundError(f\"Model file not found: {model_path}\")\n    \n    #print(f\"Loading fold {fold} model from {model_path}...\")\n    \n    # Load checkpoint\n    checkpoint = torch.load(model_path, map_location=device, weights_only=False)\n    \n    # Initialize model with same architecture as training\n    model = timm.create_model(\n        CFG.model_name, \n        num_classes=CFG.num_classes, \n        pretrained=False,  # Don't load pretrained weights\n        in_chans=CFG.in_chans\n    )\n    \n    # Load trained weights\n    model.load_state_dict(checkpoint['model'])\n    model = model.to(device)\n    model.eval()\n    \n    #print(f\"Successfully loaded fold {fold} model\")\n    return model\n\ndef load_models():\n    \"\"\"Load all fold models\"\"\"\n    global MODELS, TRANSFORM, TTA_TRANSFORMS\n    \n    #print(\"Loading all fold models...\")\n    \n    for fold in CFG.trn_fold:\n        try:\n            MODELS[fold] = load_model_fold(fold)\n        except Exception as e:\n            print(f\"Warning: Could not load fold {fold}: {e}\")\n    \n    if not MODELS:\n        raise ValueError(\"No models were loaded successfully\")\n    \n    # Initialize transforms\n    TRANSFORM = get_inference_transform()\n    # TTA is not used due to left/right positioning\n    TTA_TRANSFORMS = None\n    \n    #print(f\"Loaded {len(MODELS)} models: folds {list(MODELS.keys())}\")\n    \n    # Warm up models\n    #print(\"Warming up models...\")\n    dummy_image = torch.randn(1, CFG.in_chans, CFG.size, CFG.size).to(device)\n    \n    with torch.no_grad():\n        for fold, model in MODELS.items():\n            _ = model(dummy_image)\n    \n    #print(\"Models ready for inference!\")\n\n# ====================================================\n# Prediction Functions\n# ====================================================\ndef predict_single_model(model: nn.Module, image: np.ndarray) -> np.ndarray:\n    \"\"\"Make prediction with a single model (NO TTA due to left/right anatomy)\"\"\"\n    \n    # Same processing as training code\n    # image shape: (D, H, W) = (32, 384, 384)\n    image = image.transpose(1, 2, 0)  # (D,H,W) -> (H,W,D) = (384, 384, 32)\n    \n    # Apply same transform as training\n    transformed = TRANSFORM(image=image)\n    image_tensor = transformed['image']  # Shape: (32, 384, 384)\n    image_tensor = image_tensor.unsqueeze(0).to(device)  # (1, 32, 384, 384)\n    \n    with torch.no_grad():\n        with autocast(enabled=CFG.use_amp):\n            output = model(image_tensor)\n            return torch.sigmoid(output).cpu().numpy().squeeze()\n\ndef predict_ensemble(image: np.ndarray) -> np.ndarray:\n    \"\"\"Make ensemble prediction across all folds\"\"\"\n    all_predictions = []\n    weights = []\n    \n    for fold, model in MODELS.items():\n        pred = predict_single_model(model, image)\n        all_predictions.append(pred)\n        \n        # Use equal weights if not specified\n        if CFG.ensemble_weights is not None:\n            weights.append(CFG.ensemble_weights.get(fold, 1.0))\n        else:\n            weights.append(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    try:\n        # Process DICOM series using our preprocessor\n        volume = process_dicom_series_safe(series_path, CFG.target_shape)\n        \n        # Make ensemble prediction\n        final_pred = (predict_ensemble(volume) + predict_ensemble(np.flip(volume,-1))[[1,0,3,2,5,4,6,8,7,10,9,11,12,13]])/2\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        # Return without ID column, as required by the API\n        return predictions_df.drop(ID_COL)\n        \n    except Exception as e:\n        #print(f\"Error processing {series_id}: {e}\")\n        # Return conservative predictions\n        conservative_preds = [0.1] * len(LABEL_COLS)\n        predictions_df = pl.DataFrame(\n            data=[conservative_preds],\n            schema=LABEL_COLS,\n            orient='row'\n        )\n        return predictions_df\n\n# ====================================================\n# DICOM Processing (using DICOMPreprocessorKaggle defined in previous cell)\n# ====================================================\ndef process_dicom_series_safe(series_path: str, target_shape: Tuple[int, int, int] = (32, 384, 384)) -> np.ndarray:\n    \"\"\"\n    Safe DICOM processing with memory cleanup\n    Uses DICOMPreprocessorKaggle defined in previous cell\n    \n    Args:\n        series_path: Path to DICOM series\n        target_shape: Target volume size (depth, height, width)\n    \n    Returns:\n        np.ndarray: Processed volume\n    \"\"\"\n    try:\n        preprocessor = DICOMPreprocessorKaggle(target_shape=target_shape)\n        volume = preprocessor.process_series(series_path)\n        return volume\n    finally:\n        # Memory cleanup\n        gc.collect()\n\ndef predict_fallback(series_path: str) -> pl.DataFrame:\n    \"\"\"Fallback prediction function\"\"\"\n    #print(f\"Using fallback predictions for {os.path.basename(series_path)}\")\n    \n    # Return conservative predictions\n    conservative_preds = [0.1] * len(LABEL_COLS)\n    predictions_df = pl.DataFrame(\n        data=[conservative_preds],\n        schema=LABEL_COLS,\n        orient='row'\n    )\n    \n    # Clean up\n    shutil.rmtree('/kaggle/shared', ignore_errors=True)\n    \n    return predictions_df\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        conservative_preds = [0.1] * len(LABEL_COLS)\n        predictions = pl.DataFrame(\n            data=[conservative_preds],\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()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T19:54:08.504217Z","iopub.execute_input":"2025-10-14T19:54:08.505043Z","iopub.status.idle":"2025-10-14T19:54:13.858195Z","shell.execute_reply.started":"2025-10-14T19:54:08.505015Z","shell.execute_reply":"2025-10-14T19:54:13.857449Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# Main Execution\n# ====================================================\n\n# Load models at startup\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":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-14T19:54:21.941309Z","iopub.execute_input":"2025-10-14T19:54:21.941906Z","iopub.status.idle":"2025-10-14T19:54:47.786746Z","shell.execute_reply.started":"2025-10-14T19:54:21.941873Z","shell.execute_reply":"2025-10-14T19:54:47.785943Z"}},"outputs":[],"execution_count":null}]}