{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":129543,"databundleVersionId":15525987},{"sourceType":"datasetVersion","sourceId":15238093,"datasetId":9570070,"databundleVersionId":16135128}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"from IPython.display import display, clear_output, Image\n\n\n!pip install pytorch-metric-learning\n!pip install faiss-cpu\n\n# !pip uninstall -y torch torchvision torchaudio\n# !pip3 install torch torchvision\n\n\nclear_output()","metadata":{"_uuid":"106035cc-a7cf-4ca7-a940-a80e0c0cabc4","_cell_guid":"fec2f706-22c5-4a94-a01c-3e7947df00b3","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-07T13:01:18.434237Z","iopub.execute_input":"2026-04-07T13:01:18.434806Z","iopub.status.idle":"2026-04-07T13:01:27.970512Z","shell.execute_reply.started":"2026-04-07T13:01:18.434774Z","shell.execute_reply":"2026-04-07T13:01:27.969736Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from IPython.display import display, clear_output, Image\nfrom glob import glob\nimport cv2\nfrom tqdm import tqdm\nimport faiss\nprint(f\"Faiss version: {faiss.__version__}\")\nfrom glob import glob\nimport os\nimport numpy as np\nimport pandas as pd\nfrom IPython.display import display, Image\nimport matplotlib.pyplot as plt\nimport albumentations as albu\nfrom tqdm import tqdm\nimport random\nimport base64\nimport timm\nfrom scipy.stats import mode\nimport shutil\nimport time\nimport random\n\nimport torchmetrics\nfrom torchmetrics import F1Score, PrecisionRecallCurve\n\nimport torch\nimport torchvision\nimport torch.nn.functional as F\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\n\nimport sklearn\nfrom sklearn.metrics import average_precision_score\nfrom sklearn.metrics.pairwise import cosine_distances, cosine_similarity, paired_distances, pairwise_distances\n\nfrom pytorch_metric_learning import distances, losses, miners, reducers, testers\nfrom pytorch_metric_learning.utils.accuracy_calculator import AccuracyCalculator\nfrom pytorch_metric_learning.samplers import MPerClassSampler\n\n\n#==========================================================================================================================================================\ndf_path = '/kaggle/input/competitions/round-2-jaguar-reidentification-challenge/train.csv'\ndf = pd.read_csv(df_path)\n# df = df[df['ground_truth'].isin(['Ousado', 'Medrosa'])].reset_index(drop=True)\ndf.head(3)\n#==========================================================================================================================================================\n\nclass_label_dict, label_class_dict = {}, {}\n\nfor i, class_name in enumerate(df['ground_truth'].unique()):\n    class_label_dict[class_name] = i\n    label_class_dict[i] = class_name\n    pass\n\n#==========================================================================================================================================================\n\nimage_h, image_w = 224, 224\n\n\ntrain_transforms = albu.Compose([\n\n    albu.Resize(height=image_h, width=image_w, p=1.0),\n\n    albu.RandomResizedCrop(\n        size=(image_h, image_w),  # Target output size (height, width)\n        scale=(0.5, 1.0), # Range of size of the origin size to be cropped\n        ratio=(0.75, 1.33), # Range of aspect ratio of the origin aspect ratio to be cropped\n        p=0.3             # Probability of applying the transform\n    ),\n\n    # Randomly selects exactly ONE of these color effects\n    albu.OneOf([\n        albu.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=1.0),\n        albu.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=10, p=1.0),\n        albu.CLAHE(clip_limit=4.0, tile_grid_size=(8, 8), p=1.0),\n        albu.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1, p=1.0),\n        albu.ToGray(p=1.0),\n    ], p=0.5), # 50% total chance to trigger this block\n    \n    albu.ShiftScaleRotate (p=0.2, scale_limit=(0.0, 0.3,)),\n\n    # flip horizontal, vertical\n    albu.OneOf([\n        albu.HorizontalFlip(),\n        albu.VerticalFlip(),\n        albu.Rotate(),\n    ], p=0.7),\n    \n    # distortion\n    albu.OneOf([\n            albu.ElasticTransform(),\n            albu.GridDistortion(),\n            albu.OpticalDistortion(),\n    ], p=0.5),\n    \n    # noise, shapen\n    albu.OneOf([\n            albu.Sharpen(),\n            albu.AdvancedBlur(),\n    ], p=0.5),\n    \n    albu.CoarseDropout(p=0.1),\n    \n    ], is_check_shapes=False)\n\n\ndef cutmix_data(x, y):\n    alpha=1.0\n    lam = np.random.beta(alpha, alpha)\n    idx = torch.randperm(x.size(0), device=x.device)\n    W, H = x.size(3), x.size(2)\n    cut_rat = np.sqrt(1 - lam)\n    cut_w, cut_h = int(W * cut_rat), int(H * cut_rat)\n    cx, cy = np.random.randint(W), np.random.randint(H)\n    x1 = max(cx - cut_w//2, 0); x2 = min(cx + cut_w//2, W)\n    y1 = max(cy - cut_h//2, 0); y2 = min(cy + cut_h//2, H)\n    mixed = x.clone()\n    mixed[:, :, y1:y2, x1:x2] = x[idx, :, y1:y2, x1:x2]\n    lam_actual = 1 - (x2-x1)*(y2-y1)/(W*H)\n    return mixed, y, y[idx], lam_actual\n\n\ndef mixup_data(x, y):\n    alpha=1.0\n    lam = np.random.beta(alpha, alpha)\n    idx = torch.randperm(x.size(0), device=x.device)\n    x = lam * x + (1-lam) * x[idx]\n    y_a, y_b, mixed = y, y[idx], True\n    return x, y_a, y_b, lam\n\n#==========================================================================================================================================================\n\nvalid_transforms = albu.Compose([\n    albu.Resize(height=image_h, width=image_w, p=1.0)\n], is_check_shapes=False)\n\n\n#==========================================================================================================================================================\n\nfrom collections import defaultdict\n\nclass MyDataset(Dataset):\n    \n    def __init__(self, df, is_train):\n        self.is_train = is_train\n        self.df = df\n        \n        pass\n        \n    def __len__(self):\n        return len(self.df)\n    \n    def read_image(self, image_path):\n        image = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)\n        rgb = image[:, :, :3]\n        alpha = image[:, :, 3]\n        masked = rgb.copy()\n        masked[alpha == 0] = 0\n        image = cv2.cvtColor(masked, cv2.COLOR_BGR2RGB)\n        return image\n        \n    def __getitem__(self, index):\n        filename = self.df.iloc[index]['filename']\n        dir_path = '/kaggle/input/competitions/round-2-jaguar-reidentification-challenge/train'\n        image_path = os.path.join(dir_path, filename)\n        \n        class_name = self.df.iloc[index]['ground_truth']\n        class_label = class_label_dict[class_name]\n\n        image = self.read_image(image_path)\n            \n        # augmentation\n        if self.is_train:\n            transformed = train_transforms(image=image)\n            image = transformed['image']\n        else:\n            transformed = valid_transforms(image=image)\n            image = transformed['image']\n        \n        # image tensor. 3, 128, 800\n        img_tensor = torch.tensor(image).permute(2,0,1).float()\n        img_tensor = img_tensor/255.0\n\n        label_tensor = torch.tensor(class_label)\n        \n        return img_tensor, label_tensor\n\n    def show_dataset(self, image):\n        image = image.permute(1,2,0).detach().cpu().numpy()\n        plt.imshow(image)\n        plt.show()\n        pass\n\n\n\ndataset = MyDataset(df, is_train=True)\n\nfor i in tqdm(range(len(df))):\n    \n    image, label = dataset[i]\n    print(image.shape)\n    dataset.show_dataset(image)\n    if i == 5:\n        break\n\n\n#==========================================================================================================================================================\n\nclass Net(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.backbone = timm.create_model(\n            \"eva02_large_patch14_224\",\n            pretrained=True,\n            num_classes=256  # remove classification head\n        )\n        pass\n\n    def get_embedding(self, x):\n        x = self.backbone(x)\n        x = F.normalize(x, p=2, dim=1)  # Normalize embeddings\n        return x\n    \n    def forward(self, input):\n        output = self.get_embedding(input)\n        return output\n        \n#==========================================================================================================================================================\n        \nmodel = Net()\ninputs = torch.rand((10, 3, image_h, image_w))\nout_embd = model(inputs)\nembedding_size = out_embd.shape[-1]\nprint(embedding_size)\n\n\n#==========================================================================================================================================================\n\ndevice = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')\n\n#==========================================================================================================================================================\n\n\n# triplet margin loss, training loop\ndef train(loader, epoch):\n    model.train()\n    \n    loss_list = []\n    tepochs = tqdm(loader)\n    for batch_idx, (data, labels) in enumerate(tepochs):\n        data, labels = data.to(device), labels.to(device)\n        optimizer.zero_grad()\n        \n        out_embd = model(data)\n\n        # r = random.random()\n        # mixed_aug = False\n        # if r >= 0.6:\n        #     data,y_a,y_b,lam  = cutmix_data(data, labels)\n        #     mixed_aug = True    \n        # elif r >= 0.5:\n        #     data,y_a,y_b,lam  = mixup_data(data, labels)\n        #     mixed_aug = True\n        # else:\n        #     mixed_aug = False\n        \n        # if mixed_aug:\n        #     loss_arcface = lam * loss_ARCFACE(out_embd, y_a) + (1-lam) * loss_ARCFACE(out_embd, y_b)\n        #     ## Super-con Loss\n        #     indices_tuple = miner_pair_margin(out_embd, labels)\n        #     loss_supcon = lam * loss_SUPER_CONTRASTIVE(out_embd, y_a) + (1-lam) * loss_SUPER_CONTRASTIVE(out_embd, y_b)\n        #     # ## Triplet Margin Loss\n        #     # indices_tuple = miner_tripletmargin(out_embd, labels)\n        #     # loss_tripletmargin = lam * loss_TRIPLET_MARGIN(out_embd, y_a) + (1-lam) * loss_TRIPLET_MARGIN(out_embd, y_b)\n        # else:\n        #     loss_arcface = loss_ARCFACE(out_embd, labels)\n        #     ## Super-Con Loss\n        #     indices_tuple = miner_pair_margin(out_embd, labels) \n        #     loss_supcon = loss_SUPER_CONTRASTIVE(out_embd, labels, indices_tuple)\n        \n        #     # ## Triplet Margin Loss\n        #     # indices_tuple = miner_tripletmargin(out_embd, labels) \n        #     # loss_tripletmargin = loss_TRIPLET_MARGIN(out_embd, labels, indices_tuple)\n\n        loss_arcface = loss_ARCFACE(out_embd, labels)\n        \n        ## Total loss\n        loss = loss_arcface\n\n        loss.backward()\n        optimizer.step()\n        loss_list.append(loss.item())\n        tepochs.set_postfix(\n            epoch=epoch,\n            iter=batch_idx, \n            loss=np.mean(loss_list), \n        )\n        del data\n        del labels\n        \n        # break\n\n    return np.round(np.mean(loss_list), 4).item()\n\n\n#==========================================================================================================================================================\n# def get_all_embeddings(dataset, model):\n#     tester = testers.BaseTester()\n#     return tester.get_all_embeddings(dataset, model)\n\ndef get_all_embeddings(loader, model):\n    model.eval()\n    with torch.no_grad():\n        embeddings_list, labels_list = [], []\n        tepochs = tqdm(loader)\n        for batch_idx, (data, labels) in enumerate(tepochs):\n            data, labels = data.to(device), labels.to(device)\n            embeddings = model(data)\n            embeddings_list.append(embeddings)\n            labels_list.append(labels)\n            pass\n        embeddings_list = torch.concat(embeddings_list, dim=0)\n        labels_list = torch.concat(labels_list, dim=0)\n        embeddings_list = embeddings_list.detach().cpu().numpy()\n        labels_list = labels_list.detach().cpu().numpy()\n\n        return embeddings_list, labels_list\n\n#==========================================================================================================================================================\n\ndef test(train_loader, valid_loader, model):\n    valid_embds, valid_labels = get_all_embeddings(valid_loader, model)\n    train_embds, train_labels = get_all_embeddings(train_loader, model)\n    ## calculate accuracy\n    accuracies = AccuracyCalculator().get_accuracy(\n                query=valid_embds,\n                query_labels=valid_labels.flatten(),\n                reference=train_embds,  # Same as query\n                reference_labels=train_labels.flatten(),  # Same as query_labels\n                ref_includes_query=False)\n    \n    print(accuracies)\n    mAP = round(accuracies['mean_average_precision'], 4)\n    cluster_quality = round(accuracies['NMI'], 4) ## Normalized Mutual Information, [0,1]\n    acc_1 = round(accuracies['precision_at_1'], 4)\n    \n    del valid_embds ; del train_embds\n    del valid_labels ; del train_labels\n    \n    return acc_1\n\n#========================================================================================================================================================== \n \nTRAIN_LOG_DIR = 'TRAIN_LOG' ; shutil.rmtree(TRAIN_LOG_DIR, ignore_errors=True) ; os.makedirs(TRAIN_LOG_DIR, exist_ok=True)\n\nfrom sklearn.model_selection import GroupKFold, KFold, StratifiedKFold, StratifiedGroupKFold\nfrom pytorch_metric_learning import distances, losses, miners, reducers, testers\nfrom sklearn.utils import class_weight\n\ndef upsample_rare_classes(df):\n    MIN_REPS = 10\n    # 1. Identify rows that belong to classes with count < MIN_REPS\n    counts = df['new_identity'].value_counts()\n    rare_ids = counts[counts < MIN_REPS].index\n    # 2. Filter the rare rows and calculate how many repeats each needs\n    rare_df = df[df['new_identity'].isin(rare_ids)]\n    # 3. Create the \"filler\" rows\n    # We group by ID and sample up to the requirement\n    filler_rows = rare_df.groupby('new_identity').apply(\n        lambda x: x.sample(MIN_REPS - len(x), replace=True)\n    ).reset_index(drop=True)\n    # 4. Concatenate with original\n    df_final = pd.concat([df, filler_rows], ignore_index=True)\n    return df_final\n\n\ndef get_class_weight(train, dataset):\n    class_label_dict = dataset.class_label_dict\n    y = [class_label_dict[x] for x in train['new_identity'].tolist()]\n    weights = class_weight.compute_class_weight(class_weight='balanced', classes=np.unique(y), y=y)\n    return weights\n\n\ndef move_data_valid_to_train(train_df, valid_df):\n    ## 1. Identify classes with 3 or fewer samples\n    valid_class_counts = valid_df['ground_truth'].value_counts().to_dict()\n    valid_class_list = []\n    for identity, count in valid_class_counts.items():\n        if count <= 1:\n            valid_class_list.append(identity)\n    ## 2. Identify the rows to be moved\n    query_df = valid_df[valid_df['ground_truth'].isin(valid_class_list)]\n    ## 3. Update valid_df by EXCLUDING those rows\n    valid_df = valid_df[~(valid_df['ground_truth'].isin(valid_class_list))].reset_index(drop=True)\n    ## 4. Update train_df by APPENDING those rows\n    train_df = pd.concat([train_df, query_df]).reset_index(drop=True)\n    return train_df, valid_df\n\n#==========================================================================================================================================================\n\nn_folds = 5\nkfold_stratified = StratifiedKFold(n_splits=n_folds, shuffle=True, random_state=1)\nkfold_group = GroupKFold(n_splits=n_folds, shuffle=True, random_state=1)\nkfold_stratified_group = StratifiedGroupKFold(n_splits=n_folds, shuffle=True, random_state=1)\n\n\nfor fold, (train_idx, valid_idx) in enumerate(kfold_stratified.split(X=df, y=df['ground_truth'])):\n    # if fold == 0:\n    #     continue\n    ## train and valid dataframe from train and valid index\n    train_df = df.iloc[train_idx].reset_index(drop=True)\n    valid_df = df.iloc[valid_idx].reset_index(drop=True)\n\n    ###############################\n    ## IDENTITIES IN VALID HAVING LESS NUMBER OF INSTANCES, MOVE TO TRAIN\n    ###############################\n\n    # train_df, valid_df = move_data_valid_to_train(train_df, valid_df) ## those identities having less than 3 count, add to train_df\n\n    ###############################\n    ## UPSAMPLE DATASET\n    ###############################\n    train_df, valid_df = move_data_valid_to_train(train_df, valid_df)\n    \n    print(train_df['ground_truth'].value_counts().to_dict())\n    print(valid_df['ground_truth'].value_counts().to_dict())\n    # display(train_df['ground_truth'].unique().shape[0])\n    # display(valid_df['ground_truth'].unique().shape[0])\n    display(len(train_df), len(valid_df))\n\n    ###############################\n    ## SAVE TRAIN & VALID DATAFRAMES\n    ###############################\n    \n    train_df.to_csv(f\"{TRAIN_LOG_DIR}/train_{fold}.csv\", index=False)\n    valid_df.to_csv(f\"{TRAIN_LOG_DIR}/valid_{fold}.csv\", index=False)\n\n    ###############################\n    ## BATCH SIZE\n    ###############################\n\n    batch_size = 120 ## BATCH SIZE, Multiple of 8\n\n    ###############################\n    ## TRAIN LOADER\n    ###############################\n    \n    train_dataset = MyDataset(train_df, is_train=True)\n    #=====================\n    ## Balanced Sampler\n    #=====================\n    labels = [class_label_dict[x] for x in train_df['ground_truth']]\n    num_classes = len(np.unique(labels))\n    m = 1\n    batch_size = m * num_classes\n    balanced_sampler = MPerClassSampler(\n        labels,\n        m=m,                      # samples per class\n        batch_size=batch_size,            # must be divisible by m\n        length_before_new_iter=len(train_dataset)\n    )\n    #=====================\n    train_loader = torch.utils.data.DataLoader(train_dataset, \n                                                   batch_size=batch_size, \n                                                   sampler=balanced_sampler)\n\n    ###############################\n    ## VALID LOADER\n    ###############################\n    \n    valid_dataset = MyDataset(valid_df, is_train=False)\n    valid_loader = torch.utils.data.DataLoader(valid_dataset, \n                                               batch_size=batch_size, \n                                               num_workers=4, \n                                               shuffle=False)\n    ## metric learning stuff\n    \n    ###############################\n    ## DISTANCE\n    ###############################\n    distance_cosine = distances.CosineSimilarity()\n    distance_l2 = distances.LpDistance(p=2, power=1, normalize_embeddings=True)\n    distance_snr = distances.SNRDistance()\n\n    ###############################\n    ## MINERS\n    ###############################\n    miner_tripletmargin = miners.TripletMarginMiner(type_of_triplets=\"semihard\", distance=distance_snr)\n    miner_batcheasyhard = miners.BatchEasyHardMiner(pos_strategy=\"hard\", neg_strategy=\"semihard\", distance=distance_snr)\n    miner_angular = miners.AngularMiner(angle=20, distance=distance_l2)\n    miner_pair_margin = miners.PairMarginMiner(pos_margin=0.2, neg_margin=0.8, distance=distance_cosine)\n\n    ###############################\n    ## LOSS FUNCTION\n    ###############################\n    num_classes = len(df['ground_truth'].unique()) ; print(f\"no of classes: {num_classes}\")\n\n    loss_ARCFACE = losses.ArcFaceLoss(num_classes=num_classes,\n                                      embedding_size=embedding_size, \n                                      margin=0.5,\n                                      scale=64,\n                                      distance=distance_cosine)\n    \n    loss_ARCFACE_SUBCENTER = losses.SubCenterArcFaceLoss(sub_centers=3, \n                                                         margin=0.5, \n                                                         scale=64, \n                                                         num_classes=num_classes,\n                                                         embedding_size=embedding_size,\n                                                         distance=distance_cosine)\n    \n    loss_TRIPLET_MARGIN = losses.TripletMarginLoss(distance=distance_cosine)\n\n    loss_SUPER_CONTRASTIVE = losses.SupConLoss(temperature=0.1, distance=distance_cosine)\n\n    ###############################\n    ## MODEL\n    ###############################\n\n    model = Net().to(device)\n    if torch.cuda.device_count() > 1:\n        print(f\"Using {torch.cuda.device_count()} GPUs!\")\n        model = nn.DataParallel(model)\n        \n    # if fold == 0:\n    #     ckpt_path = \"/kaggle/input/notebooks/deviprasad09/jaguar-reid-metric-learning/TRAIN_LOG/0_model.pth\"\n    #     ckpt = torch.load(ckpt_path)\n    #     model.load_state_dict(ckpt)\n\n\n    ###############################\n    ## OPTIMIZERS\n    # optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)\n    # optimizer_loss = torch.optim.Adam(model.parameters(), lr=1e-4)\n    optimizer = torch.optim.Adam([\n                    {'params': model.parameters(), 'lr': 1e-4},\n                    {'params': loss_ARCFACE.parameters(), 'lr': 1e-4} # Learning rate for proxies\n                ], lr=1e-4)\n    ###############################\n    \n    \n\n    train_loss_list, valid_loss_list = [], []\n    valid_map_list, valid_nmi_list = [], []\n\n    ###############################\n    ## EPOCHS\n    n_epochs = 10\n    ###############################\n    \n    for epoch in range(n_epochs):\n        start_time = time.time()\n        # training for all the orientation individually\n        # validate on all types of orientation images, \n        # train eacj orientation images individually\n\n        ## TRAIN LOOP\n        epoch_loss = train(train_loader, epoch)\n\n        # ## VALID LOOP\n        # epoch_valid_loss = valid(valid_loader, epoch)\n        # valid_loss_list.append(epoch_valid_loss)\n\n        ## VALID MAP\n        valid_map = test(train_loader, valid_loader, model)\n        valid_map_list.append(valid_map)\n\n        end_time = time.time()\n\n        # print(\"epoch: {}, train_loss: {}, valid_loss: {}, acc: {}\".format(epoch, epoch_loss, epoch_valid_loss, epoch_acc))\n        result_dict = {\n            'epoch': epoch,\n            'train_loss' : epoch_loss,\n            'val_acc@1'  : valid_map,\n            'time' : round((end_time-start_time)/60.0)\n        }\n        print(\"\\n\", result_dict)\n        \n        if np.argmax(valid_map_list) == epoch:\n            torch.save(model.state_dict(), f\"{TRAIN_LOG_DIR}/{fold}_model.pth\")\n        \n        print(\"\\n\")\n\n    \n    plt.figure(figsize=(8, 5))\n    plt.plot(train_loss_list, label='Train Loss', marker='o', linestyle='-')  # Circle\n    # plt.plot(valid_loss_list, label='Valid Loss', marker='^', linestyle='-.') # Triangle\n    plt.plot(valid_map_list, label='Valid mAP', marker='*', linestyle=':')\n    # plt.plot(acc_list, label='Validation acc', marker='s')\n    plt.title('Validation mAP')\n    plt.xlabel('Epoch')\n    plt.ylabel('mAP')\n    plt.legend()\n    plt.grid(True)\n    plt.tight_layout()\n    plt.show()\n\n    # plt.figure(figsize=(8, 5))\n    # plt.plot(train_loss_list, label='Train Loss', marker='o', linestyle='-')  # Circle\n    # plt.plot(valid_loss_list, label='Valid Loss', marker='^', linestyle='-.') # Triangle\n    # # plt.plot(acc_list, label='Validation acc', marker='s')\n    # plt.title('Training and Validation Loss')\n    # plt.xlabel('Epoch')\n    # plt.ylabel('Loss')\n    # plt.legend()\n    # plt.grid(True)\n    # plt.tight_layout()\n    # plt.show()\n    \n    break","metadata":{"_uuid":"f2bd877d-5ac1-4a41-919b-935e3f5f8622","_cell_guid":"1a461d24-5056-4fdf-a3c2-f57f5dbebaa1","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-07T13:08:44.286608Z","iopub.execute_input":"2026-04-07T13:08:44.287559Z","execution_failed":"2026-04-07T13:11:26.437Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}