{"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":"gpu","dataSources":[{"sourceId":117682,"databundleVersionId":15062069,"sourceType":"competition"}],"dockerImageVersionId":31259,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Competition Explained\n\nBuild a Model to segment the scroll's surface in CT scans to reveal the texts hidden inside.\n\n**Segmentation** means assigning a label to each pixel in the image.\n\nThe dataset includes 3D chunks of binary labeled CT scans of the closed and carbonized Herculaneum scrolls. Data was acquired at the ESRF synchrotron in Grenoble, on beamline BM18 and at the DLS synchrotron in Oxford, on beamline I12.\n\nThe dimension of a sample chunk is not fixed, and can vary across the dataset.\n\n> Herculaneum Scrolls are ancient texts discovered in the 18th century preserved by the eruption of Mount Vesuvius in 79 AD and are significant for their philosophical content and historical context.\n>\n> Papyrus is an ancient writing material made from the pith of the papyrus plant, Cyperus papyrus, and was widely used in ancient Egypt and throughout the Mediterranean for documenting texts and creating scrolls.\n\nA papyrus sheet is composed of two layers, the recto and the verso, one with horizontal fibers and one with vertical fibers on top of each other.\n\nThe ideal solution is detecting the recto surface – the surface of the papyrus sheet that faces the umbilicus (the center of the scroll). The recto surface lies on the layer composed of horizontal fibers. Since the scrolls survived a carbonized eruption, the sheets can be partially damaged and frayed. Detecting, within a reasonable approximation, just the position of a sheet, no matter whether the segmentation surface encompasses both the recto and the verso, is also fine for the purpose of virtually unwrapping the scrolls.\n\n","metadata":{}},{"cell_type":"markdown","source":"**What You are given**: You are given a 3D CT Volume:  $X$ $\\in$ ${\\mathbb{R}^{D \\times H \\times W}}$\n\nEach element is a voxel intensity (X-ray absorption).\n\n> A Voxel (short of voxel element) in CT scan is a 3D cube shaped unit of data representing a specific volume of tissue acting as the 3D counterpart to a 2D pixel. It combines the 2D pixel area(width/height) with the slice of thickness(depth) where each voxel's numerical value represents the density withtin that small space.\n\n\n**What you must predict**: You must predict a binary label for every voxel: $Y$ $\\in$ ${\\mathbb{\\{0, 1\\}}^{D \\times H \\times W}}$\n\nwhere:\n- `1` = voxel belongs to the papyrus sheet surface (recto/verso acceptable)\n- `0` = everything else (air, ash, background, noise)\n\nSo, the model will learn to trace the surface of the sheet through the volume.\n\n> Learn a function that estimates whether a point lies on the papyrus surface.","metadata":{}},{"cell_type":"markdown","source":"Given a volumetric scalar fielD: $I(x, y, z)$ (CT intensity)\n\nYou want to estimate a surface: $S$ = ${\\{(x, y, z) : f(x, y, x) = 0\\}}$, $S$ represents the papyrus sheet embedded inside the volume.\n\nYour netowrk actually outputs: ${P(x, y, z)}$ ≈ ${\\mathbb{P[(x, y, z) \\in S}]}$\n\nThen:  $Y(x,y,z)$ = \\begin{cases} 1 & \\text{if } P(x,y,z) > \\tau \\\\ 0 & \\text{otherwise} \\end{cases}","metadata":{}},{"cell_type":"markdown","source":"**What Counts as a correct Solution**\n\nA good segmentation:\n- Captures the sheet location\n- Is continuous (no holes)\n- Does not merger different sheets\n- Follows folds accuractely\n- Is thin (doesn't inflate thickness unnecessarily)","metadata":{}},{"cell_type":"markdown","source":"---\n\n**LIBRARY INSTALLATION**","metadata":{}},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport os\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport cv2\nimport zipfile\nfrom PIL import Image\nimport torch.nn.functional as F\nimport numpy as np","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:34:32.554408Z","iopub.execute_input":"2026-02-08T18:34:32.554725Z","iopub.status.idle":"2026-02-08T18:34:36.364643Z","shell.execute_reply.started":"2026-02-08T18:34:32.554688Z","shell.execute_reply":"2026-02-08T18:34:36.364047Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BASE_PATH = \"/kaggle/input/vesuvius-challenge-surface-detection\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:34:36.366054Z","iopub.execute_input":"2026-02-08T18:34:36.366473Z","iopub.status.idle":"2026-02-08T18:34:36.369831Z","shell.execute_reply.started":"2026-02-08T18:34:36.366449Z","shell.execute_reply":"2026-02-08T18:34:36.369166Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**CSV FILES**","metadata":{}},{"cell_type":"code","source":"train_csv = pd.read_csv(os.path.join(BASE_PATH, \"train.csv\"))\ndisplay(train_csv.head())\ndisplay(train_csv.info())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:34:36.370767Z","iopub.execute_input":"2026-02-08T18:34:36.371075Z","iopub.status.idle":"2026-02-08T18:34:36.424508Z","shell.execute_reply.started":"2026-02-08T18:34:36.371045Z","shell.execute_reply":"2026-02-08T18:34:36.423841Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_csv = pd.read_csv(os.path.join(BASE_PATH, \"test.csv\"))\ntest_csv.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:34:36.425951Z","iopub.execute_input":"2026-02-08T18:34:36.426213Z","iopub.status.idle":"2026-02-08T18:34:36.435765Z","shell.execute_reply.started":"2026-02-08T18:34:36.426191Z","shell.execute_reply":"2026-02-08T18:34:36.435182Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**IMAGE FILES**","metadata":{}},{"cell_type":"code","source":"TRAIN_IMAGES_DIR = os.path.join(BASE_PATH, \"train_images\")\nTEST_IMAGES_DIR = os.path.join(BASE_PATH, \"test_images\")\nTRAIN_LABELS_DIR = os.path.join(BASE_PATH, \"train_labels\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:34:36.436768Z","iopub.execute_input":"2026-02-08T18:34:36.437026Z","iopub.status.idle":"2026-02-08T18:34:36.440618Z","shell.execute_reply.started":"2026-02-08T18:34:36.437003Z","shell.execute_reply":"2026-02-08T18:34:36.439928Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:34:36.441572Z","iopub.execute_input":"2026-02-08T18:34:36.441802Z","iopub.status.idle":"2026-02-08T18:34:36.496198Z","shell.execute_reply.started":"2026-02-08T18:34:36.441783Z","shell.execute_reply":"2026-02-08T18:34:36.495366Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class Config:\n    batch_size = 8\n    learning_rate = 1e-3\n    epochs = 4\n    patch_size = 256\n    num_slices = 8\n\nconfig = Config()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:34:36.497102Z","iopub.execute_input":"2026-02-08T18:34:36.497396Z","iopub.status.idle":"2026-02-08T18:34:36.506748Z","shell.execute_reply.started":"2026-02-08T18:34:36.497364Z","shell.execute_reply":"2026-02-08T18:34:36.506000Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class SimpleDataset(Dataset):\n    def __init__(self, image_paths, mask_paths=None, is_train=True):\n        self.image_paths = image_paths\n        self.mask_paths = mask_paths\n        self.is_train = is_train\n    \n    def __len__(self):\n        return len(self.image_paths)\n    \n    def load_volume_slice(self, path):\n        with Image.open(path) as img:\n            if img.n_frames == 1:\n                volume = np.array(img)[np.newaxis, ...]\n            else:\n                slices = []\n                for i in range(img.n_frames):\n                    img.seek(i)\n                    slices.append(np.array(img))\n                volume = np.stack(slices, axis=0)\n        \n        depth = volume.shape[0]\n        center = depth // 2\n        start = max(0, center - config.num_slices // 2)\n        end = min(depth, start + config.num_slices)\n        \n        if end - start < config.num_slices:\n            if start == 0:\n                end = config.num_slices\n            else:\n                start = depth - config.num_slices\n        \n        return volume[start:end]\n    \n    def __getitem__(self, idx):\n        volume_path = self.image_paths[idx]\n        volume_slice = self.load_volume_slice(volume_path)\n        \n        if self.mask_paths is not None:\n            mask_path = self.mask_paths[idx]\n            mask_slice = self.load_volume_slice(mask_path)\n            mask_slice = (mask_slice == 1).astype(np.float32)\n        else:\n            mask_slice = np.zeros_like(volume_slice, dtype=np.float32)\n        \n        height, width = volume_slice.shape[1:]\n        \n        h_start = max(0, (height - config.patch_size) // 2)\n\n        w_start = max(0, (width - config.patch_size) // 2)\n        \n        volume_patch = volume_slice[:, \n                                   h_start:h_start+config.patch_size, \n                                   w_start:w_start+config.patch_size]\n        \n        mask_patch = mask_slice[:, \n                               h_start:h_start+config.patch_size, \n                               w_start:w_start+config.patch_size]\n        \n        max_proj = np.max(volume_patch, axis=0)\n        mean_proj = np.mean(volume_patch, axis=0)\n        std_proj = np.std(volume_patch, axis=0)\n        \n        combined = np.stack([max_proj, mean_proj, std_proj], axis=-1)\n        \n        min_val = combined.min()\n        max_val = combined.max()\n        if max_val > min_val:\n            combined = (combined - min_val) / (max_val - min_val)\n        \n        combined = (combined * 255).astype(np.uint8)\n        \n        image_tensor = torch.from_numpy(combined).permute(2, 0, 1).float() / 255.0\n        mask_tensor = torch.from_numpy(mask_patch[0]).float()\n        \n        return image_tensor, mask_tensor","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:34:36.507686Z","iopub.execute_input":"2026-02-08T18:34:36.508259Z","iopub.status.idle":"2026-02-08T18:34:36.519490Z","shell.execute_reply.started":"2026-02-08T18:34:36.508228Z","shell.execute_reply":"2026-02-08T18:34:36.518879Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# class SimpleModel(nn.Module):\n#     def __init__(self):\n#         super().__init__()\n        \n#         self.encoder = nn.Sequential(\n#             nn.Conv2d(3, 32, 3, padding=1),\n#             nn.BatchNorm2d(32),\n#             nn.ReLU(),\n#             nn.MaxPool2d(2),\n            \n#             nn.Conv2d(32, 64, 3, padding=1),\n#             nn.BatchNorm2d(64),\n#             nn.ReLU(),\n#             nn.MaxPool2d(2),\n            \n#             nn.Conv2d(64, 128, 3, padding=1),\n#             nn.BatchNorm2d(128),\n#             nn.ReLU(),\n#             nn.MaxPool2d(2),\n            \n#             nn.Conv2d(128, 256, 3, padding=1),\n#             nn.BatchNorm2d(256),\n#             nn.ReLU(),\n#         )\n        \n#         self.decoder = nn.Sequential(\n#             nn.ConvTranspose2d(256, 128, 2, stride=2),\n#             nn.BatchNorm2d(128),\n#             nn.ReLU(),\n            \n#             nn.ConvTranspose2d(128, 64, 2, stride=2),\n#             nn.BatchNorm2d(64),\n#             nn.ReLU(),\n            \n#             nn.ConvTranspose2d(64, 32, 2, stride=2),\n#             nn.BatchNorm2d(32),\n#             nn.ReLU(),\n            \n#             nn.Conv2d(32, 1, 1)\n#         )\n    \n#     def forward(self, x):\n#         x = self.encoder(x)\n#         x = self.decoder(x)\n#         return x\n\n\n\n\n# -------- Residual Block --------\nclass ResBlock(nn.Module):\n    def __init__(self, in_channels, out_channels):\n        super().__init__()\n\n        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, padding=1)\n        self.bn1 = nn.BatchNorm2d(out_channels)\n\n        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1)\n        self.bn2 = nn.BatchNorm2d(out_channels)\n\n        if in_channels != out_channels:\n            self.skip = nn.Conv2d(in_channels, out_channels, 1)\n        else:\n            self.skip = nn.Identity()\n\n    def forward(self, x):\n        identity = self.skip(x)\n\n        x = F.relu(self.bn1(self.conv1(x)))\n        x = self.bn2(self.conv2(x))\n\n        return F.relu(x + identity)\n\n\n# -------- Model --------\nclass RobustUNet(nn.Module):\n    def __init__(self):\n        super().__init__()\n\n        self.pool = nn.MaxPool2d(2)\n\n        # Encoder\n        self.e1 = ResBlock(3, 32)\n        self.e2 = ResBlock(32, 64)\n        self.e3 = ResBlock(64, 128)\n        self.e4 = ResBlock(128, 256)\n\n        # Bottleneck\n        self.b = ResBlock(256, 512)\n\n        # Decoder\n        self.up4 = nn.ConvTranspose2d(512, 256, 2, 2)\n        self.d4 = ResBlock(512, 256)\n\n        self.up3 = nn.ConvTranspose2d(256, 128, 2, 2)\n        self.d3 = ResBlock(256, 128)\n\n        self.up2 = nn.ConvTranspose2d(128, 64, 2, 2)\n        self.d2 = ResBlock(128, 64)\n\n        self.up1 = nn.ConvTranspose2d(64, 32, 2, 2)\n        self.d1 = ResBlock(64, 32)\n\n        self.out = nn.Conv2d(32, 1, 1)\n\n    def forward(self, x):\n\n        e1 = self.e1(x)\n        e2 = self.e2(self.pool(e1))\n        e3 = self.e3(self.pool(e2))\n        e4 = self.e4(self.pool(e3))\n\n        b = self.b(self.pool(e4))\n\n        d4 = self.up4(b)\n        d4 = self.d4(torch.cat([d4, e4], 1))\n\n        d3 = self.up3(d4)\n        d3 = self.d3(torch.cat([d3, e3], 1))\n\n        d2 = self.up2(d3)\n        d2 = self.d2(torch.cat([d2, e2], 1))\n\n        d1 = self.up1(d2)\n        d1 = self.d1(torch.cat([d1, e1], 1))\n\n        return self.out(d1)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:34:36.520464Z","iopub.execute_input":"2026-02-08T18:34:36.520692Z","iopub.status.idle":"2026-02-08T18:34:36.542727Z","shell.execute_reply.started":"2026-02-08T18:34:36.520672Z","shell.execute_reply":"2026-02-08T18:34:36.542096Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_train_data():\n    train_files = []\n    mask_files = []\n    \n    available = []\n    for f in os.listdir(TRAIN_IMAGES_DIR):\n        if f.endswith('.tif'):\n            img_path = os.path.join(TRAIN_IMAGES_DIR, f)\n            mask_path = os.path.join(TRAIN_LABELS_DIR, f)\n            \n            if os.path.exists(mask_path):\n                try:\n                    with Image.open(img_path) as img:\n                        if img.n_frames > 0:\n                            available.append((img_path, mask_path))\n                except:\n                    continue\n    \n    if not available:\n        return None, None\n    \n    train_files, mask_files = zip(*available)\n    train_files = list(train_files)[:2]\n    mask_files = list(mask_files)[:2]\n    \n    dataset = SimpleDataset(train_files, mask_files, is_train=True)\n    dataloader = DataLoader(dataset, batch_size=config.batch_size, shuffle=True)\n    \n    return dataset, dataloader","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:34:36.545024Z","iopub.execute_input":"2026-02-08T18:34:36.545309Z","iopub.status.idle":"2026-02-08T18:34:36.555333Z","shell.execute_reply.started":"2026-02-08T18:34:36.545288Z","shell.execute_reply":"2026-02-08T18:34:36.554560Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_one_epoch(model, dataloader, optimizer):\n    model.train()\n    total_loss = 0\n    \n    for images, masks in dataloader:\n        images = images.to(device)\n        masks = masks.to(device).unsqueeze(1)\n        \n        optimizer.zero_grad()\n        \n        outputs = model(images)\n        \n        bce_loss = F.binary_cross_entropy_with_logits(outputs, masks)\n        \n        pred = torch.sigmoid(outputs)\n        pred_flat = pred.view(-1)\n        target_flat = masks.view(-1)\n        intersection = (pred_flat * target_flat).sum()\n        union = pred_flat.sum() + target_flat.sum()\n        dice_loss_val = 1 - (2. * intersection) / (union + 1e-8)\n        \n        loss = bce_loss + dice_loss_val\n        total_loss += loss.item()\n        \n        loss.backward()\n        optimizer.step()\n    \n    return model, total_loss / len(dataloader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:34:36.556345Z","iopub.execute_input":"2026-02-08T18:34:36.556631Z","iopub.status.idle":"2026-02-08T18:34:36.570147Z","shell.execute_reply.started":"2026-02-08T18:34:36.556607Z","shell.execute_reply":"2026-02-08T18:34:36.569455Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_volume(model, volume_path):\n    model.eval()\n    \n    dataset = SimpleDataset([volume_path], is_train=False)\n    image, _ = dataset[0]\n    image = image.unsqueeze(0).to(device)\n    \n    with torch.no_grad():\n        output = model(image)\n        pred = torch.sigmoid(output)\n        pred_np = pred.squeeze().cpu().numpy()\n    \n    with Image.open(volume_path) as img:\n        if img.n_frames == 1:\n            original_shape = (1, np.array(img).shape[0], np.array(img).shape[1])\n        else:\n            original_shape = (img.n_frames, np.array(img).shape[0], np.array(img).shape[1])\n    \n    full_mask = np.zeros(original_shape, dtype=np.uint8)\n    \n    depth, height, width = original_shape\n    \n    h_start = max(0, (height - config.patch_size) // 2)\n    w_start = max(0, (width - config.patch_size) // 2)\n    \n    center_z = depth // 2\n    start_z = max(0, center_z - config.num_slices // 2)\n    end_z = min(depth, start_z + config.num_slices)\n    \n    binary_pred = (pred_np > 0.5).astype(np.uint8)\n    \n    patch_height = min(config.patch_size, height - h_start)\n    patch_width = min(config.patch_size, width - w_start)\n    \n    for z in range(start_z, end_z):\n        if h_start + patch_height <= height and w_start + patch_width <= width:\n            full_mask[z, \n                     h_start:h_start+patch_height, \n                     w_start:w_start+patch_width] = binary_pred[:patch_height, :patch_width]\n    \n    return full_mask","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:34:36.570948Z","iopub.execute_input":"2026-02-08T18:34:36.571184Z","iopub.status.idle":"2026-02-08T18:34:36.582238Z","shell.execute_reply.started":"2026-02-08T18:34:36.571164Z","shell.execute_reply":"2026-02-08T18:34:36.581601Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def save_tiff(mask, path):\n    mask_8bit = (mask * 255).astype(np.uint8)\n    \n    if mask_8bit.shape[0] == 1:\n        Image.fromarray(mask_8bit[0]).save(path)\n    else:\n        with Image.fromarray(mask_8bit[0]) as img:\n            images = [Image.fromarray(mask_8bit[i]) for i in range(1, mask_8bit.shape[0])]\n            img.save(path, save_all=True, append_images=images, compression=None)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:34:36.583079Z","iopub.execute_input":"2026-02-08T18:34:36.583308Z","iopub.status.idle":"2026-02-08T18:34:36.595503Z","shell.execute_reply.started":"2026-02-08T18:34:36.583282Z","shell.execute_reply":"2026-02-08T18:34:36.594820Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\ndef create_submission(model, test_df):\n    temp_files = []\n    \n    for idx, row in test_df.iterrows():\n        test_path = os.path.join(TEST_IMAGES_DIR, f\"{row['id']}.tif\")\n        \n        if os.path.exists(test_path):\n            prediction = predict_volume(model, test_path)\n        else:\n            prediction = np.zeros((64, 512, 512), dtype=np.uint8)\n            prediction[32, 256-32:256+32, 256-32:256+32] = 1\n        \n        temp_path = f\"{row['id']}.tif\"\n        save_tiff(prediction, temp_path)\n        temp_files.append(temp_path)\n    \n    with zipfile.ZipFile('submission.zip', 'w') as z:\n        for temp_path in temp_files:\n            z.write(temp_path)\n            os.remove(temp_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:34:36.596498Z","iopub.execute_input":"2026-02-08T18:34:36.597004Z","iopub.status.idle":"2026-02-08T18:34:36.604631Z","shell.execute_reply.started":"2026-02-08T18:34:36.596975Z","shell.execute_reply":"2026-02-08T18:34:36.604002Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_dataset, train_loader = get_train_data()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:34:36.605483Z","iopub.execute_input":"2026-02-08T18:34:36.605758Z","iopub.status.idle":"2026-02-08T18:40:30.088355Z","shell.execute_reply.started":"2026-02-08T18:34:36.605737Z","shell.execute_reply":"2026-02-08T18:40:30.087714Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if train_loader is None:\n    test_df = pd.read_csv(os.path.join(BASE_PATH, \"test.csv\"))\n    dummy_model = SimpleModel().to(device)\n    create_submission(dummy_model, test_df)\n\nmodel = RobustUNet().to(device)\noptimizer = torch.optim.Adam(model.parameters(), lr=config.learning_rate)\n\nfor epoch in range(config.epochs):\n    model, epoch_loss = train_one_epoch(model, train_loader, optimizer)\n\ntest_df = pd.read_csv(os.path.join(BASE_PATH, \"test.csv\"))\ncreate_submission(model, test_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T18:42:07.186718Z","iopub.execute_input":"2026-02-08T18:42:07.187334Z","iopub.status.idle":"2026-02-08T18:42:27.057500Z","shell.execute_reply.started":"2026-02-08T18:42:07.187304Z","shell.execute_reply":"2026-02-08T18:42:27.056764Z"}},"outputs":[],"execution_count":null}]}