{"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":117682,"databundleVersionId":14443416,"sourceType":"competition"},{"sourceId":674398,"sourceType":"modelInstanceVersion","modelInstanceId":511176,"modelId":525859}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import numpy as np \nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom pathlib import Path\nfrom tqdm.auto import tqdm\nimport warnings\nimport pandas as pd\nimport zipfile\nfrom PIL import Image\nimport io\n\nwarnings.filterwarnings('ignore')\n\n# ============================================================================\n# CONFIGURATION\n# ============================================================================\n\nclass PredictionConfig:\n    \"\"\"Configuration for prediction\"\"\"\n    \n    # Paths - KAGGLE\n    DATA_DIR = \"/kaggle/input/vesuvius-challenge-surface-detection\"\n    MODEL_PATH = \"/kaggle/input/vesuvius-model/pytorch/default/1/final_model_weights_only.pth\"\n    OUTPUT_DIR = \"/kaggle/working/predictions\"\n    SUBMISSION_DIR = \"/kaggle/working\"\n    \n    # Model architecture (MUST MATCH training config!)\n    IN_CHANNELS = 1\n    NUM_CLASSES = 1\n    BASE_FILTERS = 16\n    \n    # Prediction settings\n    BATCH_SIZE = 64\n    PATCH_SIZE = 256\n    STRIDE = 256\n    \n    # Device\n    DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    \n    # Preprocessing (MUST MATCH training preprocessing!)\n    NORM_METHOD = \"minmax\"\n    CLIP_PERCENTILE = (1, 99)\n    NORMALIZE_PER_VOLUME = False\n\n# ============================================================================\n# TIFF READER - NO EXTERNAL DEPENDENCIES\n# ============================================================================\n\nclass SimpleTiffReader:\n    \"\"\"Simple TIFF reader using PIL - works offline in Kaggle\"\"\"\n    \n    @staticmethod\n    def read_multipage_tiff(filepath):\n        \"\"\"Read multi-page TIFF file and return as 3D numpy array\"\"\"\n        img = Image.open(filepath)\n        \n        images = []\n        try:\n            for i in range(img.n_frames):\n                img.seek(i)\n                frame = np.array(img)\n                images.append(frame)\n        except EOFError:\n            pass\n        \n        volume = np.stack(images, axis=0)\n        return volume\n    \n    @staticmethod\n    def write_multipage_tiff(filepath, volume, compress=True):\n        \"\"\"Write 3D numpy array as multi-page TIFF\"\"\"\n        images = []\n        for i in range(volume.shape[0]):\n            slice_data = volume[i]\n            if slice_data.dtype != np.uint8:\n                slice_data = slice_data.astype(np.uint8)\n            \n            img = Image.fromarray(slice_data, mode='L')\n            images.append(img)\n        \n        if len(images) > 0:\n            images[0].save(\n                filepath,\n                save_all=True,\n                append_images=images[1:],\n                compression='tiff_deflate' if compress else None\n            )\n\n# ============================================================================\n# MODEL ARCHITECTURE (EXACT COPY FROM TRAINING)\n# ============================================================================\n\nclass ConvBlock(nn.Module):\n    \"\"\"Optimized double convolution block\"\"\"\n    def __init__(self, in_ch, out_ch):\n        super().__init__()\n        self.conv = nn.Sequential(\n            nn.Conv2d(in_ch, out_ch, 3, padding=1, bias=False),\n            nn.BatchNorm2d(out_ch),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(out_ch, out_ch, 3, padding=1, bias=False),\n            nn.BatchNorm2d(out_ch),\n            nn.ReLU(inplace=True)\n        )\n    \n    def forward(self, x):\n        return self.conv(x)\n\nclass LightweightUNetPlusPlus(nn.Module):\n    \"\"\"Lightweight U-Net++ - EXACT COPY FROM TRAINING\"\"\"\n    def __init__(self, in_channels=1, num_classes=1, base_filters=16):\n        super().__init__()\n        \n        filters = [base_filters, base_filters*2, base_filters*4, base_filters*8, base_filters*16]\n        \n        # Encoder\n        self.conv0_0 = ConvBlock(in_channels, filters[0])\n        self.conv1_0 = ConvBlock(filters[0], filters[1])\n        self.conv2_0 = ConvBlock(filters[1], filters[2])\n        self.conv3_0 = ConvBlock(filters[2], filters[3])\n        self.conv4_0 = ConvBlock(filters[3], filters[4])\n        \n        # Nested skip pathways\n        self.conv0_1 = ConvBlock(filters[0] + filters[1], filters[0])\n        self.conv0_2 = ConvBlock(filters[0]*2 + filters[1], filters[0])\n        self.conv0_3 = ConvBlock(filters[0]*3 + filters[1], filters[0])\n        self.conv0_4 = ConvBlock(filters[0]*4 + filters[1], filters[0])\n        \n        self.conv1_1 = ConvBlock(filters[1] + filters[2], filters[1])\n        self.conv1_2 = ConvBlock(filters[1]*2 + filters[2], filters[1])\n        self.conv1_3 = ConvBlock(filters[1]*3 + filters[2], filters[1])\n        \n        self.conv2_1 = ConvBlock(filters[2] + filters[3], filters[2])\n        self.conv2_2 = ConvBlock(filters[2]*2 + filters[3], filters[2])\n        \n        self.conv3_1 = ConvBlock(filters[3] + filters[4], filters[3])\n        \n        # Pooling & Upsampling\n        self.pool = nn.MaxPool2d(2)\n        self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)\n        \n        # Output\n        self.final = nn.Conv2d(filters[0], num_classes, 1)\n    \n    def forward(self, x):\n        # Encoder\n        x0_0 = self.conv0_0(x)\n        x1_0 = self.conv1_0(self.pool(x0_0))\n        x2_0 = self.conv2_0(self.pool(x1_0))\n        x3_0 = self.conv3_0(self.pool(x2_0))\n        x4_0 = self.conv4_0(self.pool(x3_0))\n        \n        # Nested connections\n        x0_1 = self.conv0_1(torch.cat([x0_0, self.up(x1_0)], 1))\n        x1_1 = self.conv1_1(torch.cat([x1_0, self.up(x2_0)], 1))\n        x2_1 = self.conv2_1(torch.cat([x2_0, self.up(x3_0)], 1))\n        x3_1 = self.conv3_1(torch.cat([x3_0, self.up(x4_0)], 1))\n        \n        x0_2 = self.conv0_2(torch.cat([x0_0, x0_1, self.up(x1_1)], 1))\n        x1_2 = self.conv1_2(torch.cat([x1_0, x1_1, self.up(x2_1)], 1))\n        x2_2 = self.conv2_2(torch.cat([x2_0, x2_1, self.up(x3_1)], 1))\n        \n        x0_3 = self.conv0_3(torch.cat([x0_0, x0_1, x0_2, self.up(x1_2)], 1))\n        x1_3 = self.conv1_3(torch.cat([x1_0, x1_1, x1_2, self.up(x2_2)], 1))\n        \n        x0_4 = self.conv0_4(torch.cat([x0_0, x0_1, x0_2, x0_3, self.up(x1_3)], 1))\n        \n        return self.final(x0_4)\n\n# ============================================================================\n# PREPROCESSING FUNCTIONS (EXACT COPY FROM TRAINING)\n# ============================================================================\n\ndef normalize_slice(slice_2d, method=\"minmax\", clip_percentile=(1, 99)):\n    \"\"\"Normalize single 2D slice - MUST MATCH TRAINING!\"\"\"\n    if clip_percentile:\n        low, high = np.percentile(slice_2d, clip_percentile)\n        slice_2d = np.clip(slice_2d, low, high)\n\n    if method == \"minmax\":\n        img_min = slice_2d.min()\n        img_max = slice_2d.max()\n        if img_max - img_min > 0:\n            normalized = (slice_2d - img_min) / (img_max - img_min)\n        else:\n            normalized = slice_2d - img_min\n    elif method == \"standardize\":\n        mean = slice_2d.mean()\n        std = slice_2d.std()\n        if std > 0:\n            normalized = (slice_2d - mean) / std\n        else:\n            normalized = slice_2d - mean\n    else:\n        normalized = slice_2d\n\n    return normalized.astype(np.float32)\n\ndef normalize_volume(volume, method=\"minmax\", clip_percentile=(1, 99)):\n    \"\"\"Normalize entire 3D volume\"\"\"\n    if clip_percentile:\n        low, high = np.percentile(volume, clip_percentile)\n        volume = np.clip(volume, low, high)\n\n    if method == \"minmax\":\n        vol_min = volume.min()\n        vol_max = volume.max()\n        if vol_max - vol_min > 0:\n            normalized = (volume - vol_min) / (vol_max - vol_min)\n        else:\n            normalized = volume - vol_min\n    elif method == \"standardize\":\n        mean = volume.mean()\n        std = volume.std()\n        if std > 0:\n            normalized = (volume - mean) / std\n        else:\n            normalized = volume - mean\n    else:\n        normalized = volume\n\n    return normalized.astype(np.float32)\n\n# ============================================================================\n# PREDICTION FUNCTIONS\n# ============================================================================\n\ndef extract_patches_for_prediction(slice_2d, patch_size=256, stride=256):\n    \"\"\"\n    Extract patches from slice for prediction\n    Returns: patches, positions\n    \"\"\"\n    h, w = slice_2d.shape\n    patches = []\n    positions = []\n    \n    # Calculate patch positions\n    y_positions = list(range(0, h - patch_size + 1, stride))\n    x_positions = list(range(0, w - patch_size + 1, stride))\n    \n    # Handle edge cases\n    if y_positions[-1] + patch_size < h:\n        y_positions.append(h - patch_size)\n    if x_positions[-1] + patch_size < w:\n        x_positions.append(w - patch_size)\n    \n    for y in y_positions:\n        for x in x_positions:\n            patch = slice_2d[y:y+patch_size, x:x+patch_size]\n            if patch.shape == (patch_size, patch_size):\n                patches.append(patch)\n                positions.append((y, x))\n    \n    return patches, positions\n\ndef reconstruct_slice_from_patches(predictions, positions, slice_shape, patch_size=256):\n    \"\"\"\n    Reconstruct full slice from patch predictions\n    Handles overlapping patches by averaging\n    \"\"\"\n    h, w = slice_shape\n    \n    # Create accumulation arrays\n    mask_sum = np.zeros((h, w), dtype=np.float32)\n    count = np.zeros((h, w), dtype=np.int32)\n    \n    for pred, (y, x) in zip(predictions, positions):\n        mask_sum[y:y+patch_size, x:x+patch_size] += pred\n        count[y:y+patch_size, x:x+patch_size] += 1\n    \n    # Average where overlaps exist\n    mask = np.zeros_like(mask_sum)\n    mask[count > 0] = mask_sum[count > 0] / count[count > 0]\n    \n    return mask\n\n@torch.no_grad()\ndef predict_volume(model, volume, config, verbose=False):\n    \"\"\"\n    Predict mask for entire 3D volume\n    Process slice-by-slice like in training\n    \"\"\"\n    model.eval()\n    \n    depth, height, width = volume.shape\n    predicted_volume = np.zeros((depth, height, width), dtype=np.uint8)\n    \n    if verbose:\n        print(f\"   Shape: {volume.shape}\")\n    \n    # Normalize volume if needed\n    if config.NORMALIZE_PER_VOLUME:\n        volume = normalize_volume(volume, config.NORM_METHOD, config.CLIP_PERCENTILE)\n    \n    # Process each slice\n    iterator = tqdm(range(depth), desc=\"   Processing\", leave=False, disable=not verbose)\n    for slice_idx in iterator:\n        slice_2d = volume[slice_idx]\n        \n        # Normalize slice\n        if not config.NORMALIZE_PER_VOLUME:\n            slice_2d = normalize_slice(slice_2d, config.NORM_METHOD, config.CLIP_PERCENTILE)\n        \n        # Extract patches\n        patches, positions = extract_patches_for_prediction(\n            slice_2d, config.PATCH_SIZE, config.STRIDE\n        )\n        \n        if len(patches) == 0:\n            continue\n        \n        # Predict in batches\n        predictions = []\n        for i in range(0, len(patches), config.BATCH_SIZE):\n            batch_patches = patches[i:i + config.BATCH_SIZE]\n            \n            # Convert to tensor\n            batch_tensor = torch.stack([\n                torch.from_numpy(p).unsqueeze(0) for p in batch_patches\n            ]).to(config.DEVICE)\n            \n            # Predict\n            with torch.cuda.amp.autocast():\n                outputs = model(batch_tensor)\n                preds = torch.sigmoid(outputs).cpu().numpy()\n            \n            predictions.extend([p[0] for p in preds])\n        \n        # Reconstruct full slice\n        mask_slice = reconstruct_slice_from_patches(\n            predictions, positions, (height, width), config.PATCH_SIZE\n        )\n        \n        # Threshold and convert to uint8\n        mask_binary = (mask_slice > 0.5).astype(np.uint8)\n        predicted_volume[slice_idx] = mask_binary\n    \n    return predicted_volume\n\n# ============================================================================\n# MAIN PREDICTION PIPELINE\n# ============================================================================\n\ndef create_submission(verbose=True):\n    \"\"\"Main function to create submission\"\"\"\n    config = PredictionConfig()\n    \n    # Create output directories\n    Path(config.OUTPUT_DIR).mkdir(parents=True, exist_ok=True)\n    \n    # Load model\n    model = LightweightUNetPlusPlus(\n        in_channels=config.IN_CHANNELS,\n        num_classes=config.NUM_CLASSES,\n        base_filters=config.BASE_FILTERS\n    ).to(config.DEVICE)\n    \n    # Load weights\n    checkpoint = torch.load(config.MODEL_PATH, map_location=config.DEVICE)\n    \n    # Handle different checkpoint formats\n    if isinstance(checkpoint, dict):\n        if 'model_state_dict' in checkpoint:\n            model.load_state_dict(checkpoint['model_state_dict'])\n        else:\n            model.load_state_dict(checkpoint)\n    else:\n        model.load_state_dict(checkpoint)\n    \n    model.eval()\n    \n    # Load test.csv to get test image IDs\n    test_csv = pd.read_csv(f\"{config.DATA_DIR}/test.csv\")\n    \n    # Get test image directory\n    test_images_dir = Path(config.DATA_DIR) / \"test_images\"\n    \n    # Process each test image\n    predicted_masks = []\n    tiff_reader = SimpleTiffReader()\n    \n    for idx, row in test_csv.iterrows():\n        image_id = str(row['id'])\n        image_path = test_images_dir / f\"{image_id}.tif\"\n        \n        if not image_path.exists():\n            if verbose:\n                print(f\"File not found: {image_path}\")\n            continue\n        \n        try:\n            # Load test volume using PIL\n            test_volume = tiff_reader.read_multipage_tiff(str(image_path))\n            \n            # Predict\n            predicted_mask = predict_volume(model, test_volume, config, verbose=verbose)\n            \n            # Save prediction using PIL\n            output_path = Path(config.OUTPUT_DIR) / f\"{image_id}.tif\"\n            tiff_reader.write_multipage_tiff(str(output_path), predicted_mask, compress=True)\n            \n            predicted_masks.append(output_path)\n            \n            # Clear cache\n            if torch.cuda.is_available():\n                torch.cuda.empty_cache()\n                \n        except Exception as e:\n            if verbose:\n                print(f\"Error processing {image_id}: {e}\")\n            continue\n    \n    # Create submission ZIP\n    if len(predicted_masks) > 0:\n        submission_path = Path(config.SUBMISSION_DIR) / \"submission.zip\"\n        \n        with zipfile.ZipFile(submission_path, 'w', zipfile.ZIP_DEFLATED) as zipf:\n            for mask_path in predicted_masks:\n                zipf.write(mask_path, mask_path.name)\n        \n        return submission_path\n    else:\n        return None\n\n# ============================================================================\n# RUN\n# ============================================================================\n\nif __name__ == \"__main__\":\n    # Set verbose=False untuk output minimal, True untuk detail\n    submission_path = create_submission(verbose=False)\n    \n    if submission_path:\n        print(f\"✅ Submission created: {submission_path}\")\n    else:\n        print(\"❌ Failed to create submission\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-07T14:50:38.201342Z","iopub.execute_input":"2025-12-07T14:50:38.202167Z","iopub.status.idle":"2025-12-07T14:50:54.149408Z","shell.execute_reply.started":"2025-12-07T14:50:38.202141Z","shell.execute_reply":"2025-12-07T14:50:54.146820Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport zipfile\nfrom pathlib import Path\nfrom PIL import Image\n\nclass SimpleTiffReader:\n    \"\"\"Simple TIFF reader using PIL\"\"\"\n    \n    @staticmethod\n    def write_multipage_tiff(filepath, volume, compress=True):\n        \"\"\"Write 3D numpy array as multi-page TIFF - SUBMISSION FORMAT\"\"\"\n        images = []\n        for i in range(volume.shape[0]):\n            slice_data = volume[i]\n            \n            # Convert to uint8 if needed (matching train mask format)\n            if slice_data.dtype != np.uint8:\n                slice_data = slice_data.astype(np.uint8)\n            \n            img = Image.fromarray(slice_data, mode='L')\n            images.append(img)\n        \n        if len(images) > 0:\n            images[0].save(\n                filepath,\n                save_all=True,\n                append_images=images[1:],\n                compression='tiff_deflate' if compress else None\n            )\n\ndef save_predictions_as_tiff(predicted_volume, output_dir, image_id):\n    \"\"\"\n    Save predicted volume as multi-page TIFF file.\n    \n    :param predicted_volume: 3D numpy array (predicted mask)\n    :param output_dir: directory to save the output\n    :param image_id: name of the image (e.g., 'image_1.tif')\n    \"\"\"\n    output_path = Path(output_dir) / f\"{image_id}.tif\"\n    \n    # Ensure the output directory exists\n    output_dir = Path(output_dir)\n    output_dir.mkdir(parents=True, exist_ok=True)\n    \n    # Save the TIFF\n    SimpleTiffReader.write_multipage_tiff(str(output_path), predicted_volume, compress=True)\n    print(f\"✅ Saved prediction for {image_id} at {output_path}\")\n\n# Example usage\n\n# Simulating predicted volume (replace this with actual predicted volume)\n# predicted_volume = np.random.randint(0, 2, (10, 256, 256))  # For example, 10 slices of 256x256\npredicted_volume = np.random.randint(0, 2, (15, 256, 256))  # Simulating 15 slices of 256x256\n\n# Directory where you want to save the predictions\noutput_dir = \"/kaggle/working/predictions\"\n\n# Save predictions as TIFF\nimage_id = \"sample_image\"  # You can use an actual image ID from your dataset\nsave_predictions_as_tiff(predicted_volume, output_dir, image_id)\n\n# Now you can zip these files to create the submission\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-07T14:50:54.151139Z","iopub.execute_input":"2025-12-07T14:50:54.151378Z","iopub.status.idle":"2025-12-07T14:50:54.276168Z","shell.execute_reply.started":"2025-12-07T14:50:54.151358Z","shell.execute_reply":"2025-12-07T14:50:54.275484Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\ndef visualize_prediction(test_volume, predicted_mask, slice_idx=10):\n    \"\"\"\n    Visualize the CT slice and the predicted segmentation mask for a specific slice.\n    \"\"\"\n    # Extract slice\n    slice_2d = test_volume[slice_idx]\n    mask_slice = predicted_mask[slice_idx]\n    \n    # Create figure for comparison\n    plt.figure(figsize=(12, 6))\n    \n    # Plot original slice (CT scan)\n    plt.subplot(1, 2, 1)\n    plt.imshow(slice_2d, cmap='gray')\n    plt.title(f\"Original CT Slice {slice_idx}\")\n    plt.axis('off')\n    \n    # Plot the segmented mask\n    plt.subplot(1, 2, 2)\n    plt.imshow(mask_slice, cmap='hot')\n    plt.title(f\"Predicted Segmentation Mask {slice_idx}\")\n    plt.axis('off')\n    \n    plt.tight_layout()\n    plt.show()\n\n# Example usage inside the `create_submission` function:\ndef create_submission(verbose=True):\n    # After processing the image and making the prediction:\n    for idx, row in test_csv.iterrows():\n        image_id = str(row['id'])\n        image_path = test_images_dir / f\"{image_id}.tif\"\n        \n        if verbose:\n            print(f\"[{idx+1}/{len(test_csv)}] Processing: {image_id}\")\n        \n        if not image_path.exists():\n            print(f\"  ❌ File not found: {image_path}\")\n            continue\n        \n        try:\n            # Load test volume\n            test_volume = tiff_reader.read_multipage_tiff(str(image_path))\n            \n            if verbose:\n                print(f\"  📦 Loaded volume: {test_volume.shape} dtype={test_volume.dtype}\")\n            \n            # Predict\n            predicted_mask = predict_volume(model, test_volume, config, verbose=verbose)\n            \n            # Visualize a specific slice (e.g., slice 10)\n            visualize_prediction(test_volume, predicted_mask, slice_idx=10)\n            \n            # Save prediction with correct format\n            output_path = Path(config.OUTPUT_DIR) / f\"{image_id}.tif\"\n            tiff_reader.write_multipage_tiff(str(output_path), predicted_mask, compress=True)\n            \n            predicted_masks.append(output_path)\n            \n            if verbose:\n                print(f\"  ✅ Saved: {output_path.name}\")\n            \n            # Clear cache\n            if torch.cuda.is_available():\n                torch.cuda.empty_cache()\n                \n        except Exception as e:\n            print(f\"  ❌ Error processing {image_id}: {e}\")\n            import traceback\n            traceback.print_exc()\n            continue\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-07T14:51:01.847243Z","iopub.execute_input":"2025-12-07T14:51:01.847537Z","iopub.status.idle":"2025-12-07T14:51:01.855879Z","shell.execute_reply.started":"2025-12-07T14:51:01.847515Z","shell.execute_reply":"2025-12-07T14:51:01.855330Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nclass SimpleTiffReader:\n    \"\"\"Simple TIFF reader using PIL\"\"\"\n    \n    @staticmethod\n    def read_multipage_tiff(filepath):\n        \"\"\"Read multi-page TIFF file and return as 3D numpy array\"\"\"\n        img = Image.open(filepath)\n        \n        images = []\n        try:\n            for i in range(img.n_frames):\n                img.seek(i)\n                frame = np.array(img)\n                images.append(frame)\n        except EOFError:\n            pass\n        \n        volume = np.stack(images, axis=0)\n        return volume\n\ndef load_and_display_middle_slice(tiff_path):\n    # Load the 3D TIFF file\n    volume = SimpleTiffReader.read_multipage_tiff(tiff_path)\n    \n    # Get the middle slice index\n    middle_index = volume.shape[0] // 2\n    \n    # Extract the middle slice (2D)\n    middle_slice = volume[middle_index]\n    \n    # Display the middle slice using matplotlib\n    plt.figure(figsize=(8, 8))\n    plt.imshow(middle_slice, cmap='gray')\n    plt.title(f'Middle Slice (Index: {middle_index})')\n    plt.axis('off')\n    plt.show()\n\n# Example usage\ntiff_path = '/kaggle/working/predictions/1407735.tif'  # Ganti dengan path file TIFF Anda\nload_and_display_middle_slice(tiff_path)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-07T14:51:51.974231Z","iopub.execute_input":"2025-12-07T14:51:51.974913Z","iopub.status.idle":"2025-12-07T14:51:52.384562Z","shell.execute_reply.started":"2025-12-07T14:51:51.974888Z","shell.execute_reply":"2025-12-07T14:51:52.383853Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}