{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":91498,"databundleVersionId":11655853,"sourceType":"competition"},{"sourceId":7884485,"sourceType":"datasetVersion","datasetId":4628051},{"sourceId":8026384,"sourceType":"datasetVersion","datasetId":4726252},{"sourceId":11924468,"sourceType":"datasetVersion","datasetId":6988459},{"sourceId":4534,"sourceType":"modelInstanceVersion","modelInstanceId":3326,"modelId":986},{"sourceId":17191,"sourceType":"modelInstanceVersion","modelInstanceId":14317,"modelId":21716},{"sourceId":17555,"sourceType":"modelInstanceVersion","modelInstanceId":14611,"modelId":22086}],"dockerImageVersionId":30919,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# IMPORTANT \n#Install dependencies and copy model weights to run the notebook without internet access when submitting to the competition.\n\n!pip install --no-index /kaggle/input/imc2024-packages-lightglue-rerun-kornia/* --no-deps\n!mkdir -p /root/.cache/torch/hub/checkpoints\n!cp /kaggle/input/aliked/pytorch/aliked-n16/1/aliked-n16.pth /root/.cache/torch/hub/checkpoints/\n!cp /kaggle/input/lightglue/pytorch/aliked/1/aliked_lightglue.pth /root/.cache/torch/hub/checkpoints/\n!cp /kaggle/input/lightglue/pytorch/aliked/1/aliked_lightglue.pth /root/.cache/torch/hub/checkpoints/aliked_lightglue_v0-1_arxiv-pth","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T07:33:59.18412Z","iopub.execute_input":"2025-11-10T07:33:59.184407Z","iopub.status.idle":"2025-11-10T07:34:04.030031Z","shell.execute_reply.started":"2025-11-10T07:33:59.184387Z","shell.execute_reply":"2025-11-10T07:34:04.028876Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import sys\nimport os\nfrom time import time, sleep\nfrom copy import deepcopy\nfrom collections import defaultdict\nimport gc\n\nimport numpy as np\nimport h5py\nimport dataclasses\nimport pandas as pd\n\nfrom tqdm import tqdm\nfrom IPython.display import clear_output\nfrom PIL import Image\n\nimport cv2\n\nimport torch\nimport torch.nn.functional as F\n\nimport kornia as K\nimport kornia.feature as KF\n\nfrom lightglue import match_pair\nfrom lightglue import ALIKED, LightGlue\nfrom lightglue.utils import load_image, rbd\n\nfrom transformers import AutoImageProcessor, AutoModel\n\n# IMPORTANT Utilities: importing data into colmap and competition metric\nimport pycolmap\nsys.path.append('/kaggle/input/imc25-utils')\nfrom database import *\nfrom h5_to_db import *\nimport metric\n\nprint(\"============ Fine tuning Parameters ==============\")\n\n# Do not forget to select an accelerator on the sidebar to the right.\ndevice = K.utils.get_cuda_device_if_available(0)\nprint(f'{device=}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T07:34:04.03135Z","iopub.execute_input":"2025-11-10T07:34:04.031601Z","iopub.status.idle":"2025-11-10T07:34:26.28245Z","shell.execute_reply.started":"2025-11-10T07:34:04.031579Z","shell.execute_reply":"2025-11-10T07:34:26.281685Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =====================\n# 2. Utility functions\n# =====================\n\ndef load_torch_image(image_path, device=torch.device('cpu')):\n    \"\"\"Load an image as a torch tensor in RGB32 format.\"\"\"\n    image_tensor = K.io.load_image(\n        image_path,\n        K.io.ImageLoadType.RGB32,\n        device=device\n    )[None, ...]\n    return image_tensor\n\n\ndef get_global_descriptors(image_paths, device=torch.device('cpu')):\n    \"\"\"\n    Compute global image descriptors using DINOv2.\n    \"\"\"\n    processor = AutoImageProcessor.from_pretrained(\n        '/kaggle/input/dinov2/pytorch/base/1'\n    )\n    model = AutoModel.from_pretrained(\n        '/kaggle/input/dinov2/pytorch/base/1'\n    )\n    model = model.eval().to(device)\n\n    dino_descriptors = []\n\n    for _, image_path in tqdm(enumerate(image_paths), total=len(image_paths)):\n        image_tensor = load_torch_image(image_path)\n        with torch.inference_mode():\n            inputs = processor(\n                images=image_tensor,\n                return_tensors=\"pt\",\n                do_rescale=False\n            ).to(device)\n            outputs = model(**inputs)\n            # MAC pooling over tokens (excluding CLS)\n            dino_mac = F.normalize(\n                outputs.last_hidden_state[:, 1:].max(dim=1)[0],\n                dim=1,\n                p=2\n            )\n        dino_descriptors.append(dino_mac.detach().cpu())\n\n    dino_descriptors = torch.cat(dino_descriptors, dim=0)\n    return dino_descriptors\n\n\ndef get_exhaustive_image_pairs(image_paths):\n    \"\"\"Return all unique index pairs for a list of images.\"\"\"\n    index_pairs = []\n    num_images = len(image_paths)\n    for i in range(num_images):\n        for j in range(i + 1, num_images):\n            index_pairs.append((i, j))\n    return index_pairs\n\n\ndef get_image_pairs_shortlist(\n    image_paths,\n    sim_th=0.6,               # similarity threshold (strict)\n    min_pairs=30,              # minimum pairs per image\n    exhaustive_if_less=20,\n    device=torch.device('cpu')\n):\n    \"\"\"\n    Build a shortlist of image pairs:\n    - If #images <= exhaustive_if_less, use all pairs.\n    - Else use global descriptors + distance threshold to pick candidate pairs.\n    \"\"\"\n    num_images = len(image_paths)\n    if num_images <= exhaustive_if_less:\n        return get_exhaustive_image_pairs(image_paths)\n\n    descriptors = get_global_descriptors(image_paths, device=device)\n    distance_matrix = torch.cdist(descriptors, descriptors, p=2).detach().cpu().numpy()\n\n    similarity_mask = distance_matrix <= sim_th\n    all_indices = np.arange(num_images)\n\n    matching_pairs = []\n\n    for source_index in range(num_images - 1):\n        mask_row = similarity_mask[source_index]\n        candidate_indices = all_indices[mask_row]\n\n        # Ensure at least min_pairs per image\n        if len(candidate_indices) < min_pairs:\n            candidate_indices = np.argsort(distance_matrix[source_index])[:min_pairs]\n\n        for target_index in candidate_indices:\n            if source_index == target_index:\n                continue\n            if distance_matrix[source_index, target_index] < 1000:\n                matching_pairs.append(\n                    tuple(sorted((source_index, target_index.item())))\n                )\n\n    matching_pairs = sorted(list(set(matching_pairs)))\n    return matching_pairs\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T07:34:26.283934Z","iopub.execute_input":"2025-11-10T07:34:26.284402Z","iopub.status.idle":"2025-11-10T07:34:26.293752Z","shell.execute_reply.started":"2025-11-10T07:34:26.28438Z","shell.execute_reply":"2025-11-10T07:34:26.292899Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def detect_aliked_keypoints_and_descriptors(\n    image_paths,\n    feature_dir='.featureout',\n    num_features=4096,\n    resize_to=1024,\n    device=torch.device('cpu')\n):\n    \"\"\"\n    Detect ALIKED keypoints and descriptors for each image and save to HDF5.\n    \"\"\"\n    dtype = torch.float32  # ALIKED has issues with float16\n\n    extractor = ALIKED(\n        max_num_keypoints=num_features,\n        detection_threshold=0.3,\n        resize=resize_to\n    ).eval().to(device, dtype)\n\n    if not os.path.isdir(feature_dir):\n        os.makedirs(feature_dir)\n\n    keypoints_path = f'{feature_dir}/keypoints.h5'\n    descriptors_path = f'{feature_dir}/descriptors.h5'\n\n    with h5py.File(keypoints_path, mode='w') as h5_keypoints, \\\n         h5py.File(descriptors_path, mode='w') as h5_descriptors:\n\n        for image_path in tqdm(image_paths):\n            image_filename = image_path.split('/')[-1]\n            key = image_filename\n\n            with torch.inference_mode():\n                image_tensor = load_torch_image(image_path, device=device).to(dtype)\n                features = extractor.extract(image_tensor)  # auto-resize enabled\n\n                keypoints = features['keypoints'].reshape(-1, 2).detach().cpu().numpy()\n                descriptors = features['descriptors'].reshape(len(keypoints), -1).detach().cpu().numpy()\n\n                h5_keypoints[key] = keypoints\n                h5_descriptors[key] = descriptors\n\n\ndef match_with_lightglue(\n    image_paths,\n    index_pairs,\n    feature_dir='.featureout',\n    device=torch.device('cpu'),\n    min_matches=20,\n    verbose=True\n):\n    \"\"\"\n    Perform feature matching between image pairs using LightGlue (ALIKED backend).\n    Save matches to HDF5.\n    \"\"\"\n    matcher = KF.LightGlueMatcher(\n        \"aliked\",\n        {\n            \"width_confidence\": -1,\n            \"depth_confidence\": -1,\n            \"mp\": True if 'cuda' in str(device) else False\n        }\n    ).eval().to(device)\n\n    keypoints_path = f'{feature_dir}/keypoints.h5'\n    descriptors_path = f'{feature_dir}/descriptors.h5'\n    matches_path = f'{feature_dir}/matches.h5'\n\n    with h5py.File(keypoints_path, mode='r') as h5_keypoints, \\\n         h5py.File(descriptors_path, mode='r') as h5_descriptors, \\\n         h5py.File(matches_path, mode='w') as h5_matches:\n\n        for pair_indices in tqdm(index_pairs):\n            idx1, idx2 = pair_indices\n            image_path_1, image_path_2 = image_paths[idx1], image_paths[idx2]\n\n            filename_1 = image_path_1.split('/')[-1]\n            filename_2 = image_path_2.split('/')[-1]\n\n            keypoints_1 = torch.from_numpy(h5_keypoints[filename_1][...]).to(device)\n            keypoints_2 = torch.from_numpy(h5_keypoints[filename_2][...]).to(device)\n            descriptors_1 = torch.from_numpy(h5_descriptors[filename_1][...]).to(device)\n            descriptors_2 = torch.from_numpy(h5_descriptors[filename_2][...]).to(device)\n\n            with torch.inference_mode():\n                distances, match_indices = matcher(\n                    descriptors_1,\n                    descriptors_2,\n                    KF.laf_from_center_scale_ori(keypoints_1[None]),\n                    KF.laf_from_center_scale_ori(keypoints_2[None])\n                )\n\n            if len(match_indices) == 0:\n                continue\n\n            num_matches = len(match_indices)\n            if verbose:\n                print(f'{filename_1}-{filename_2}: {num_matches} matches')\n\n            group = h5_matches.require_group(filename_1)\n            if num_matches >= min_matches:\n                group.create_dataset(\n                    filename_2,\n                    data=match_indices.detach().cpu().numpy().reshape(-1, 2)\n                )\n\n\ndef import_features_into_colmap(\n    images_dir,\n    feature_dir='.featureout',\n    database_path='colmap.db'\n):\n    \"\"\"\n    Create COLMAP database and import keypoints & matches.\n    \"\"\"\n    db = COLMAPDatabase.connect(database_path)\n    db.create_tables()\n    single_camera = False\n\n    filename_to_image_id = add_keypoints(\n        db,\n        feature_dir,\n        images_dir,\n        '',\n        'simple-pinhole',\n        single_camera\n    )\n\n    add_matches(\n        db,\n        feature_dir,\n        filename_to_image_id,\n    )\n    db.commit()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T07:34:26.29517Z","iopub.execute_input":"2025-11-10T07:34:26.295452Z","iopub.status.idle":"2025-11-10T07:34:26.315087Z","shell.execute_reply.started":"2025-11-10T07:34:26.295415Z","shell.execute_reply":"2025-11-10T07:34:26.314425Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================\n# 3. Prediction data structure\n# ================================\n\n@dataclasses.dataclass\nclass Prediction:\n    \"\"\"\n    Container for per-image prediction.\n    For test data, image_id is used to align with sample_submission.csv.\n    \"\"\"\n    image_id: str | None\n    dataset: str\n    filename: str\n    cluster_index: int | None = None\n    rotation: np.ndarray | None = None\n    translation: np.ndarray | None = None\n\n\n# ==================================\n# 4. Configuration & data loading\n# ==================================\n\n# Set is_train=True to run on the training data (for local evaluation).\n# Set is_train=False for competition submission (hidden test).\nis_train = False\n\ndata_dir = '/kaggle/input/image-matching-challenge-2025'\nworkdir = '/kaggle/working/result/'\nos.makedirs(workdir, exist_ok=True)\n\nif is_train:\n    labels_csv_path = os.path.join(data_dir, 'train_labels.csv')\nelse:\n    labels_csv_path = os.path.join(data_dir, 'sample_submission.csv')\n\nsamples_by_dataset: dict[str, list[Prediction]] = {}\n\ncompetition_data = pd.read_csv(labels_csv_path)\n\nfor _, row in competition_data.iterrows():\n    dataset_name = row.dataset\n\n    if dataset_name not in samples_by_dataset:\n        samples_by_dataset[dataset_name] = []\n\n    samples_by_dataset[dataset_name].append(\n        Prediction(\n            image_id=None if is_train else row.image_id,\n            dataset=row.dataset,\n            filename=row.image\n        )\n    )\n\nfor dataset_name in samples_by_dataset:\n    print(f'Dataset \"{dataset_name}\" -> num_images={len(samples_by_dataset[dataset_name])}')\n\ngc.collect()\n\n# Optional caps/filters\nmax_images = None\ndatasets_to_process = None\n\nif is_train:\n    datasets_to_process = [\n        'amy_gardens',\n        'ETs',\n        'fbk_vineyard',\n        'stairs',\n    ]\n\n# Timings for profiling\ntimings = {\n    \"shortlisting\": [],\n    \"feature_detection\": [],\n    \"feature_matching\": [],\n    \"RANSAC\": [],\n    \"Reconstruction\": [],\n}\n\nmapping_result_summaries = []\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T07:34:26.315885Z","iopub.execute_input":"2025-11-10T07:34:26.316171Z","iopub.status.idle":"2025-11-10T07:34:26.781462Z","shell.execute_reply.started":"2025-11-10T07:34:26.316151Z","shell.execute_reply":"2025-11-10T07:34:26.780784Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===========================================\n# 5. Main reconstruction loop over datasets\n# ===========================================\n\nfor dataset_name, predictions in samples_by_dataset.items():\n    if datasets_to_process and dataset_name not in datasets_to_process:\n        print(f'Skipping \"{dataset_name}\"')\n        continue\n\n    images_dir = os.path.join(\n        data_dir,\n        'train' if is_train else 'test',\n        dataset_name\n    )\n\n    image_paths = [\n        os.path.join(images_dir, pred.filename)\n        for pred in predictions\n    ]\n    if max_images is not None:\n        image_paths = image_paths[:max_images]\n\n    print(f'\\nProcessing dataset \"{dataset_name}\": {len(image_paths)} images')\n\n    filename_to_prediction_index = {\n        pred.filename: idx for idx, pred in enumerate(predictions)\n    }\n\n    feature_dir = os.path.join(workdir, 'featureout', dataset_name)\n    os.makedirs(feature_dir, exist_ok=True)\n\n    # Wrap algorithms in try-except so submission is filled even if a scene crashes.\n    try:\n        # 5.1 Shortlist image pairs\n        start_time = time()\n        index_pairs = get_image_pairs_shortlist(\n            image_paths,\n            sim_th=0.59,        # strict\n            min_pairs=20,       # at least min_pairs per image\n            exhaustive_if_less=20,\n            device=device\n        )\n        elapsed = time() - start_time\n        timings['shortlisting'].append(elapsed)\n        print(f'Shortlisting. Number of pairs to match: {len(index_pairs)}. Done in {elapsed:.4f} sec')\n        gc.collect()\n\n        # 5.2 Feature detection\n        start_time = time()\n        detect_aliked_keypoints_and_descriptors(\n            image_paths,\n            feature_dir=feature_dir,\n            num_features=4096,\n            device=device\n        )\n        elapsed = time() - start_time\n        timings['feature_detection'].append(elapsed)\n        print(f'Features detected in {elapsed:.4f} sec')\n        gc.collect()\n\n        # 5.3 Feature matching\n        start_time = time()\n        match_with_lightglue(\n            image_paths,\n            index_pairs,\n            feature_dir=feature_dir,\n            device=device,\n            verbose=False\n        )\n        elapsed = time() - start_time\n        timings['feature_matching'].append(elapsed)\n        print(f'Features matched in {elapsed:.4f} sec')\n\n        # 5.4 Import into COLMAP & run reconstruction\n        database_path = os.path.join(feature_dir, 'colmap.db')\n        if os.path.isfile(database_path):\n            os.remove(database_path)\n        gc.collect()\n        sleep(1)\n\n        import_features_into_colmap(\n            images_dir,\n            feature_dir=feature_dir,\n            database_path=database_path\n        )\n\n        reconstruction_output_path = f'{feature_dir}/colmap_rec_aliked'\n\n        # RANSAC + mapping\n        start_time = time()\n        pycolmap.match_exhaustive(database_path)\n        elapsed = time() - start_time\n        timings['RANSAC'].append(elapsed)\n        print(f'Ran RANSAC in {elapsed:.4f} sec')\n\n        mapper_options = pycolmap.IncrementalPipelineOptions()\n        # Lower min_model_size to 8 as in original code.\n        mapper_options.min_model_size = 8\n        mapper_options.max_num_models = 25\n        os.makedirs(reconstruction_output_path, exist_ok=True)\n\n        start_time = time()\n        reconstructions = pycolmap.incremental_mapping(\n            database_path=database_path,\n            image_path=images_dir,\n            output_path=reconstruction_output_path,\n            options=mapper_options\n        )\n        sleep(1)\n        elapsed = time() - start_time\n        timings['Reconstruction'].append(elapsed)\n        print(f'Reconstruction done in {elapsed:.4f} sec')\n        print(reconstructions)\n\n        clear_output(wait=False)\n\n        # 5.5 Attach reconstruction results to predictions\n        num_registered_images = 0\n        for cluster_index, reconstruction in reconstructions.items():\n            for _, image in reconstruction.images.items():\n                prediction_index = filename_to_prediction_index[image.name]\n                predictions[prediction_index].cluster_index = cluster_index\n                predictions[prediction_index].rotation = deepcopy(\n                    image.cam_from_world.rotation.matrix()\n                )\n                predictions[prediction_index].translation = deepcopy(\n                    image.cam_from_world.translation\n                )\n                num_registered_images += 1\n\n        summary_str = (\n            f'Dataset \"{dataset_name}\" -> '\n            f'Registered {num_registered_images} / {len(image_paths)} images '\n            f'with {len(reconstructions)} clusters'\n        )\n        mapping_result_summaries.append(summary_str)\n        print(summary_str)\n        gc.collect()\n\n    except Exception as e:\n        print(e)\n        failure_str = f'Dataset \"{dataset_name}\" -> Failed!'\n        mapping_result_summaries.append(failure_str)\n        print(failure_str)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T07:34:26.782319Z","iopub.execute_input":"2025-11-10T07:34:26.782682Z","iopub.status.idle":"2025-11-10T07:36:54.649948Z","shell.execute_reply.started":"2025-11-10T07:34:26.782645Z","shell.execute_reply":"2025-11-10T07:36:54.649137Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================\n# 6. Print summary & timings\n# ============================\n\nprint('\\nResults')\nfor summary in mapping_result_summaries:\n    print(summary)\n\nprint('\\nTimings')\nfor stage_name, stage_times in timings.items():\n    print(f'{stage_name} -> total={sum(stage_times):.02f} sec.')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T07:36:54.650983Z","iopub.execute_input":"2025-11-10T07:36:54.651322Z","iopub.status.idle":"2025-11-10T07:36:54.65981Z","shell.execute_reply.started":"2025-11-10T07:36:54.651297Z","shell.execute_reply":"2025-11-10T07:36:54.659034Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===========================\n# 7. Write submission CSV\n# ===========================\n\ndef array_to_str(array):\n    return ';'.join([f\"{x:.09f}\" for x in array])\n\n\ndef none_to_str(num_elements):\n    return ';'.join(['nan'] * num_elements)\n\n\nsubmission_file = '/kaggle/working/submission.csv'\n\nwith open(submission_file, 'w') as f:\n    if is_train:\n        f.write('dataset,scene,image,rotation_matrix,translation_vector\\n')\n        for dataset_name, predictions in samples_by_dataset.items():\n            for pred in predictions:\n                cluster_name = (\n                    'outliers'\n                    if pred.cluster_index is None\n                    else f'cluster{pred.cluster_index}'\n                )\n                rotation_str = (\n                    none_to_str(9)\n                    if pred.rotation is None\n                    else array_to_str(pred.rotation.flatten())\n                )\n                translation_str = (\n                    none_to_str(3)\n                    if pred.translation is None\n                    else array_to_str(pred.translation)\n                )\n                f.write(\n                    f'{pred.dataset},{cluster_name},'\n                    f'{pred.filename},{rotation_str},{translation_str}\\n'\n                )\n    else:\n        f.write('image_id,dataset,scene,image,rotation_matrix,translation_vector\\n')\n        for dataset_name, predictions in samples_by_dataset.items():\n            for pred in predictions:\n                cluster_name = (\n                    'outliers'\n                    if pred.cluster_index is None\n                    else f'cluster{pred.cluster_index}'\n                )\n                rotation_str = (\n                    none_to_str(9)\n                    if pred.rotation is None\n                    else array_to_str(pred.rotation.flatten())\n                )\n                translation_str = (\n                    none_to_str(3)\n                    if pred.translation is None\n                    else array_to_str(pred.translation)\n                )\n                f.write(\n                    f'{pred.image_id},{pred.dataset},{cluster_name},'\n                    f'{pred.filename},{rotation_str},{translation_str}\\n'\n                )\n\n!head {submission_file}\n\nprint(\"Definitely compute results if running on the training set.\")\nprint(\"Do NOT compute metric when submitting to the competition.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T07:36:54.660871Z","iopub.execute_input":"2025-11-10T07:36:54.661148Z","iopub.status.idle":"2025-11-10T07:36:54.83558Z","shell.execute_reply.started":"2025-11-10T07:36:54.661114Z","shell.execute_reply":"2025-11-10T07:36:54.83467Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================\n# 8. Optional: compute metric on train only\n# =========================================\n\nif is_train:\n    start_time = time()\n    final_score, dataset_scores = metric.score(\n        gt_csv='/kaggle/input/image-matching-challenge-2025/train_labels.csv',\n        user_csv=submission_file,\n        thresholds_csv='/kaggle/input/image-matching-challenge-2025/train_thresholds.csv',\n        mask_csv=None if is_train else os.path.join(data_dir, 'mask.csv'),\n        inl_cf=0,\n        strict_cf=-1,\n        verbose=True,\n    )\n    print(f'Computed metric in: {time() - start_time:.02f} sec.')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T07:43:27.252292Z","iopub.execute_input":"2025-11-10T07:43:27.252649Z","iopub.status.idle":"2025-11-10T07:43:27.257524Z","shell.execute_reply.started":"2025-11-10T07:43:27.252623Z","shell.execute_reply":"2025-11-10T07:43:27.256585Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}