{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"tpu1vmV38","dataSources":[{"sourceId":61446,"databundleVersionId":6962461,"sourceType":"competition"}],"dockerImageVersionId":30616,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## 1. 3D Patch based UNET model","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.layers import Conv3D, MaxPool3D, concatenate, Input, Dropout, PReLU, Conv3DTranspose, BatchNormalization, TimeDistributed, Conv2D, ConvLSTM2D\nfrom tensorflow.keras.models import Model\nimport tensorflow as tf\nfrom tqdm import tqdm  # Import tqdm\nimport glob\nimport os\nfrom tensorflow.keras.utils import to_categorical\n\npatch_size=[64,64,64]\nlabels=2\nsteps_per_epoch=50\nepochs=200\nvalidation_steps=16\n\ndef unet_core(x, filter_size=8, kernel_size=(3, 3, 3)):\n    x = Conv3D(filters=filter_size,\n               kernel_size=kernel_size,\n               padding='same',\n               kernel_initializer='he_normal',dtype=tf.float32)(x)\n    x = BatchNormalization()(x)\n    x = PReLU()(x)\n    x = Conv3D(filters=filter_size,\n               kernel_size=kernel_size,\n               padding='same',\n               kernel_initializer='he_normal',dtype=tf.float32)(x)\n    x = BatchNormalization()(x)\n    x = PReLU()(x)\n    return x\n\ndef unet3d(patch_size, n_label):\n    # with distributed.scope():\n    input_layer = Input(shape=patch_size,dtype=tf.float32)\n    d1 = unet_core(input_layer, filter_size=96, kernel_size=(3, 3, 3))\n    l = MaxPool3D(strides=(2, 2, 2))(d1)\n    d2 = unet_core(l, filter_size=96*2, kernel_size=(3, 3, 3))\n    l = MaxPool3D(strides=(2, 2, 2))(d2)\n    d3 = unet_core(l, filter_size=96*4, kernel_size=(3, 3, 3))\n    l = MaxPool3D(strides=(2, 2, 2))(d3)\n    d4 = unet_core(l, filter_size=96*8, kernel_size=(3, 3, 3))\n    l = MaxPool3D(strides=(2, 2, 2))(d4)\n\n    b = unet_core(l, filter_size=96*16, kernel_size=(3, 3, 3))\n\n    l = Conv3DTranspose(filters=96*8, kernel_size=(2, 2, 2),  padding='same', strides=2, kernel_initializer='he_normal',dtype=tf.float32)(b)\n    l = concatenate([l, d4])\n    u4 = unet_core(l, filter_size=96*4, kernel_size=(3, 3, 3))\n    l = Conv3DTranspose(filters=192, kernel_size=(2, 2, 2),  padding='same', strides=2, kernel_initializer='he_normal',dtype=tf.float32)(u4)\n    l = concatenate([l, d3])\n    u3 = unet_core(l, filter_size=96*4, kernel_size=(3, 3, 3))\n    l = Conv3DTranspose(filters=192, kernel_size=(2, 2, 2),  padding='same', strides=2, kernel_initializer='he_normal',dtype=tf.float32)(u3)\n    l = concatenate([l, d2])\n    u2 = unet_core(l, filter_size=96*2, kernel_size=(3, 3, 3))\n    l = Conv3DTranspose(filters=96*2, kernel_size=(2, 2, 2),  padding='same', strides=2, kernel_initializer='he_normal',dtype=tf.float32)(u2)\n    l = concatenate([l, d1])\n    u1 = unet_core(l, filter_size=96, kernel_size=(3, 3, 3))\n    output_layer = Conv3D(filters=n_label, kernel_size=(1, 1, 1), activation='sigmoid')(u1)\n    # output_layer = CRF(n_label)\n    model = Model(input_layer, output_layer)\n    return model\n\nmodel = unet3d(patch_size+[1],2)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2023-12-10T21:30:09.356474Z","iopub.execute_input":"2023-12-10T21:30:09.356758Z","iopub.status.idle":"2023-12-10T21:30:29.942428Z","shell.execute_reply.started":"2023-12-10T21:30:09.356730Z","shell.execute_reply":"2023-12-10T21:30:29.941560Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 2. Metrics","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow.keras import backend as K\nimport numpy as np\n\nepsilon = 1e-5\nsmooth = 1\n\ndef dice_arrary(y_true, y_pred):\n    epsilon = K.epsilon()\n    intersection = np.sum(np.logical_and(y_true, y_pred))\n    return (2. * intersection + epsilon) / (np.sum(y_true) +\n                                            np.sum(y_pred) +\n                                            epsilon)\n\n\ndef dice_tensor(y_true, y_pred):\n    ' calc the dice on tf.tensor object'\n    tmp = tf.logical_and(y_true, y_pred)\n    intersection = tf.reduce_sum(tf.cast(tmp, dtype=tf.float32))\n    dice = (2. * intersection + K.epsilon()) / (tf.reduce_sum(tf.cast(y_true, dtype=tf.float32))\n                                                + tf.reduce_sum(tf.cast(y_pred, dtype=tf.float32))\n                                                + K.epsilon())\n    return dice\n\n\n# def dice_coefficient_loss(y_true, y_pred):\n#     return 1-dice_tensor(y_true, y_pred)\n\n\ndef dice_multi(y_true, y_pred):\n    'work on the tensor'\n    dice_value = 0.0\n    n_labels = y_pred.get_shape().as_list()[-1]\n    prediction = tf.argmax(y_pred, -1)\n    for i in range(n_labels):\n        yi_true = tf.slice(y_true, [0, 0, 0, 0, i], [-1, -1, -1, -1, 1])\n        yi_true = tf.cast(yi_true[..., 0], dtype=tf.bool)\n        yi_pred = tf.equal(prediction, i)\n        dice_value += dice_tensor(yi_true, yi_pred)\n    return dice_value/n_labels\n\n\ndef dice_multi_array(y_true, y_pred, labels):\n    n_labels = len(labels)\n    dice_value = np.zeros(n_labels, dtype=np.float32)\n    for i in range(n_labels):\n        yi_true = (y_true == labels[i])\n        yi_pred = (y_pred == labels[i])\n        # print(yi_true.shape)\n        # print(yi_true.dtype)\n        dice_value[i] = dice_arrary(yi_true, yi_pred)\n    return dice_value\n\n\ndef dice_coef(y_true, y_pred, smooth=1):\n    \"\"\"\n    Dice = (2*|X & Y|)/ (|X|+ |Y|)\n         =  2*sum(|A*B|)/(sum(A^2)+sum(B^2))\n    ref: https://arxiv.org/pdf/1606.04797v1.pdf\n    \"\"\"\n    y_true = tf.cast(K.flatten(y_true), dtype=tf.float32)\n    y_pred = tf.cast(K.flatten(y_pred), dtype=tf.float32)\n    intersection = K.sum(K.abs(y_true * y_pred), axis=-1)\n    return (2. * intersection + smooth) / (K.sum(K.square(y_true),-1) + K.sum(K.square(y_pred),-1) + smooth)\n\n@tf.function\ndef dice_coef_loss(y_true, y_pred):\n    return 1-dice_coef(y_true, y_pred)\n\n\ndef jaccard_distance_loss(y_true, y_pred, smooth=1e-5):\n    \"\"\"\n    Jaccard = (|X & Y|)/ (|X|+ |Y| - |X & Y|)\n            = sum(|A*B|)/(sum(|A|)+sum(|B|)-sum(|A*B|))\n\n    The jaccard distance loss is usefull for unbalanced datasets. This has been\n    shifted so it converges on 0 and is smoothed to avoid exploding or disapearing\n    gradient.\n\n    Ref: https://en.wikipedia.org/wiki/Jaccard_index\n\n    @url: https://gist.github.com/wassname/f1452b748efcbeb4cb9b1d059dce6f96\n    @author: wassname\n    \"\"\"\n    intersection = K.sum(K.abs(y_true * y_pred), axis=-1)\n    sum_ = K.sum(K.abs(y_true) + K.abs(y_pred), axis=-1)\n    jac = (intersection + smooth) / (sum_ - intersection + smooth)\n    return (1 - jac) * smooth\n\ndef tversky(y_true, y_pred):\n    y_true_pos = tf.cast(K.flatten(y_true), dtype=tf.float32)\n    y_pred_pos = tf.cast(K.flatten(y_pred), dtype=tf.float32)\n    true_pos = K.sum(y_true_pos * y_pred_pos)\n    false_neg = K.sum(y_true_pos * (1-y_pred_pos))\n    false_pos = K.sum((1-y_true_pos)*y_pred_pos)\n    alpha = 0.7\n    return (true_pos + smooth)/(true_pos + alpha*false_neg + (1-alpha)*false_pos + smooth)\n\n\n@tf.function\ndef tversky_loss(y_true, y_pred):\n    return 1 - tversky(y_true,y_pred)\n\n@tf.function\ndef focal_tversky(y_true,y_pred):\n    pt_1 = tversky(y_true, y_pred)\n    gamma = 0.75\n    return K.pow((1-pt_1), gamma)\n\ndef precision(y_true, y_pred):\n    true_positives = K.sum(K.round(K.clip(y_true*y_pred, 0,1)))\n    possible_positives = K.sum(K.round(K.clip(y_pred, 0,1)))\n    precision = true_positives / (possible_positives + K.epsilon())\n    return precision\n\ndef recall(y_true, y_pred):\n    true_positives = K.sum(K.round(K.clip(y_true*y_pred, 0,1)))\n    predicted_positives = K.sum(K.round(K.clip(y_true, 0,1)))\n    recall = true_positives / (predicted_positives + K.epsilon())\n    return recall\n\ndef f1_score(y_true, y_pred):\n    true_positives = K.sum(K.round(K.clip(y_true*y_pred, 0,1)))\n    possible_positives = K.sum(K.round(K.clip(y_pred, 0,1)))\n    precision = true_positives / (possible_positives + K.epsilon())\n    true_positives = K.sum(K.round(K.clip(y_true*y_pred, 0,1)))\n    predicted_positives = K.sum(K.round(K.clip(y_true, 0,1)))\n    recall = true_positives / (predicted_positives + K.epsilon())\n    return 2*((precision*recall)/(precision+recall+K.epsilon()))\n\n@tf.function\ndef gen_dice_loss(y_true, y_pred):\n    y_true=K.flatten(y_true)\n    y_pred=K.flatten(y_pred)\n\n    BCE=tf.keras.losses.binary_crossentropy(y_true, y_pred)\n    DICE_LOSS=dice_coef_loss(y_true, y_pred)\n\n    return 0.5*BCE+0.5*DICE_LOSS\n\n@tf.function\ndef DiceBCELoss(y_true, y_pred):\n    n_labels = y_pred.get_shape().as_list()[-1]\n    loss=0\n    for c in range(n_labels):\n        loss+=gen_dice_loss(y_true[:,:,:,c], y_pred[:,:,:,c])\n    return loss\n\n@tf.function\ndef gen_dice_coef(y_true, y_pred):\n    n_labels = y_pred.get_shape().as_list()[-1]\n    dice=0\n    for c in range(n_labels):\n        dice+=dice_coef(y_true[:,:,:,c], y_pred[:,:,:,c])*0.5\n    return dice\n\ndef weighted_bce_loss(y_true, y_pred, weight):\n    # avoiding overflow\n    epsilon = 1e-7\n    y_pred = K.clip(y_pred, epsilon, 1. - epsilon)\n    logit_y_pred = K.log(y_pred / (1. - y_pred))\n    \n    # https://www.tensorflow.org/api_docs/python/tf/nn/weighted_cross_entropy_with_logits\n    loss = (1. - y_true) * logit_y_pred + (1. + (weight - 1.) * y_true) * \\\n    (K.log(1. + K.exp(-K.abs(logit_y_pred))) + K.maximum(-logit_y_pred, 0.))\n    return K.sum(loss) / K.sum(weight)\n\ndef weighted_dice_loss(y_true, y_pred, weight):\n    smooth = 1.\n    w, m1, m2 = weight * weight, y_true, y_pred\n    intersection = (m1 * m2)\n    score = (2. * K.sum(w * intersection) + smooth) / (K.sum(w * m1) + K.sum(w * m2) + smooth)\n    loss = 1. - K.sum(score)\n    return loss\n\ndef weighted_bce_dice_loss(y_true, y_pred):\n    y_true = K.cast(y_true, 'float32')\n    y_pred = K.cast(y_pred, 'float32')\n    # if we want to get same size of output, kernel size must be odd number\n    averaged_mask = K.pool3d(\n            y_true, pool_size=(11, 11), strides=(1, 1), padding='same', pool_mode='avg')\n    border = K.cast(K.greater(averaged_mask, 0.005), 'float32') * K.cast(K.less(averaged_mask, 0.995), 'float32')\n    weight = K.ones_like(averaged_mask)\n    w0 = K.sum(weight)\n    weight += border * 2\n    w1 = K.sum(weight)\n    weight *= (w0 / w1)\n    loss = weighted_bce_loss(y_true, y_pred, weight) + \\\n    weighted_dice_loss(y_true, y_pred, weight)\n    return loss\n\ndef normlize_mean_std(tmp):\n    tmp_std = np.std(tmp) + 0.0001\n    tmp_mean = np.mean(tmp)\n    tmp = (tmp - tmp_mean) / tmp_std\n    return tmp","metadata":{"execution":{"iopub.status.busy":"2023-12-10T21:30:29.943995Z","iopub.execute_input":"2023-12-10T21:30:29.944270Z","iopub.status.idle":"2023-12-10T21:30:29.973668Z","shell.execute_reply.started":"2023-12-10T21:30:29.944240Z","shell.execute_reply":"2023-12-10T21:30:29.972843Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 3. Load Images / Patch Extractor","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport random\n\ndef get_patch_indices(image_shape, patch_size, stride):\n    # Calculate the maximum starting index for each dimension\n    max_indices = [image_shape[dim] - patch_size[dim] for dim in range(3)]\n    \n    # Randomly select start indices for patch\n    start_indices = [random.randint(0, max_indices[dim]) for dim in range(3)]\n    end_indices = [start_indices[dim] + patch_size[dim] for dim in range(3)]\n\n    return start_indices, end_indices\n\ndef extract_patch(data, start_indices, end_indices):\n    return data[start_indices[0]:end_indices[0], start_indices[1]:end_indices[1], start_indices[2]:end_indices[2]]\n\ndef get_sorted_file_paths(directory, extension=None):\n    \"\"\" Returns a list of sorted file paths in the given directory with a specific extension. \"\"\"\n    file_paths = [os.path.join(directory, fname) for fname in sorted(os.listdir(directory)) if fname.endswith(extension)]\n    return file_paths\n\n\ndef load_data(image_dir, label_dir, image_ext='.tif', label_ext='.tif'):\n    \"\"\" Load and pair images and labels from directories. \"\"\"\n    image_paths = get_sorted_file_paths(image_dir, extension=image_ext)\n    label_paths = get_sorted_file_paths(label_dir, extension=label_ext)\n\n    # Create a dictionary of label paths with filenames without extension as keys\n    label_dict = {os.path.splitext(os.path.basename(path))[0]: path for path in label_paths}\n\n    paired_images = []\n    paired_labels = []\n\n    for img_path in image_paths:\n        img_filename_wo_ext = os.path.splitext(os.path.basename(img_path))[0]\n        if img_filename_wo_ext in label_dict:\n            paired_images.append(img_path)\n            paired_labels.append(label_dict[img_filename_wo_ext])\n\n    return paired_images, paired_labels","metadata":{"execution":{"iopub.status.busy":"2023-12-10T21:30:29.974519Z","iopub.execute_input":"2023-12-10T21:30:29.974766Z","iopub.status.idle":"2023-12-10T21:30:29.992159Z","shell.execute_reply.started":"2023-12-10T21:30:29.974739Z","shell.execute_reply":"2023-12-10T21:30:29.991426Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 4. Training","metadata":{}},{"cell_type":"code","source":"import glob\nimport os\nimport cv2\nfrom PIL import Image\nimport numpy as np\nimport random\nfrom tensorflow.keras import optimizers\nfrom tensorflow.keras.callbacks import ModelCheckpoint, TensorBoard, LambdaCallback\nimport tensorflow as tf\nos.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'  # or '3' to suppress all messages\n\n\n# Detect and initialize the TPU\ntry:\n    tpu = tf.distribute.cluster_resolver.TPUClusterResolver()  # TPU detection\n    print('Running on TPU ', tpu.master())\nexcept ValueError:\n    raise BaseException('ERROR: Not connected to a TPU runtime; please see the previous cell in this notebook for instructions!')\n\ntf.config.experimental_connect_to_cluster(tpu)\ntf.tpu.experimental.initialize_tpu_system(tpu)\nstrategy = tf.distribute.experimental.TPUStrategy(tpu)\n\ndef multi_class_labels(data, labels=[1]):\n    n_labels = len(labels)\n    new_shape = np.append(data.shape, n_labels)\n    y = np.zeros(new_shape, np.float32)\n    for label_index in range(n_labels):\n        # y[..., label_index][data == labels[label_index]] = 1\n        y[data == labels[label_index], label_index] = 1\n    # y[data == 8, 2] = 1.0\n    # y[data == 1, 1] = 1.0\n    # y[np.logical_and(data != 1, data != 8), 0] = 1.0\n    return y\n\nclass Generator:\n    \"\"\"\n    Generates data for Keras, based on array data X and Y\n    \"\"\"\n    def __init__(self, image_list, batch_size=32, patch_size=[32, 32, 32], stride = [16, 16, 16], labels=[0,1], resize_factor=1, augmentation=False):\n        'Initialization'\n        self.batch_size=batch_size\n        self.augmentation = augmentation\n        self.patch_size = np.asarray(patch_size)\n        self.stride=np.asarray(stride)\n        self.list_input_dir = image_list\n        self.labels = labels\n        self.resize_factor = resize_factor\n        self._data=[]\n        # subject_index = range(subject_list)\n        print(\"Reading data...\")\n        \n        _data = []  # Assuming _data is defined somewhere in your code\n\n        for image, label in tqdm(image_list, desc=\"Processing images\"):\n            _datadict = dict()\n            image_3d, label_3d = [], []\n\n            # Use tqdm to show progress for image processing\n            for _img, _lbl in zip(load_data(image_list[0][0], image_list[0][1])[0], load_data(image_list[0][0], image_list[0][1])[1]):\n                image_3d.append(self._preprocess_image(_img))\n                label_3d.append(self._preprocess_mask(_lbl))\n            _datadict['image'] = np.array(image_3d)\n            _datadict['label'] = np.array(label_3d)\n            _data.append(_datadict)\n            del image_3d\n            del label_3d\n        self._data=_data\n        self.n_subject = len(self._data)\n        print(\"Reading data completed {}\".format(len(self._data)))\n        \n    def get_item(self):\n        X = np.zeros((self.patch_size[0], self.patch_size[1], self.patch_size[2], 1), dtype=np.float32)\n        Y = np.zeros((self.patch_size[0], self.patch_size[1], self.patch_size[2], len(self.labels)), dtype=np.int16)  # channel_last by default\n        while True:\n            randomindex=random.randint(0,self.n_subject-1)\n            selectedimage=self._data[randomindex]\n            image_data = selectedimage['image']   \n            label_data = selectedimage['label']\n            start_indices, end_indices = get_patch_indices(image_data.shape, self.patch_size, self.stride)\n            image_patch = extract_patch(image_data, start_indices, end_indices)\n            label_patch = extract_patch(label_data, start_indices, end_indices)\n\n            image = image_patch\n            image = normlize_mean_std(image)\n            label = label_patch\n\n            X[:,:,:,0] = image\n            Y = multi_class_labels(label, self.labels)\n            if np.sum(Y)!=0:\n                yield X, Y\n\n    def _preprocess_image(self, image_path):\n        # Open the image, convert to numpy array, and normalize\n        image = np.array(Image.open(image_path))\n        image = image - np.min(image)\n        image = image / np.max(image)\n        image = (image*255).astype(np.uint8)\n        return image\n\n    def _preprocess_mask(self, mask_path):\n        mask = np.array(Image.open(mask_path))\n        if mask.ndim > 2 and mask.shape[2] > 1:\n            mask = mask[..., 0]\n        mask = (mask > 127).astype(np.uint8)  # Thresholding to ensure binary mask\n        return mask\n\n#     def augment(self, data):\n#         if random.randint(0,1)==1: data=contrast_augment(data, data.min() + np.random.randint(0, 1)/10  * (data.max() - data.min()), data.max() + np.random.randint(0, 15)/10  * (data.max() - data.min()))\n#         if random.randint(0,1)==1: data=apply_gaussian_filter (data, np.random.randint(0, 50)/100)\n#         # if random.randint(0,1)==1: data=add_speckle_noise(data, np.random.randint(0, 30)/100)\n#         # if random.randint(0,1)==1: data=add_gaussian_noise(data, np.random.randint(0, 7)/100)\n#         # if random.randint(0,1)==1: data=shot_noise(data)\n#         return data\n\n#     def crop_to_shape(image, target_shape):\n#         start = [(image.shape[dim] - target_shape[dim]) // 2 for dim in range(3)]\n#         end = [start[dim] + target_shape[dim] for dim in range(3)]\n#         return image[start[0]:end[0], start[1]:end[1], start[2]:end[2]]\n\nimage_list=[\n    (\"/kaggle/input/blood-vessel-segmentation/train/kidney_1_dense/images\", \"/kaggle/input/blood-vessel-segmentation/train/kidney_1_dense/labels\"),\n    (\"/kaggle/input/blood-vessel-segmentation/train/kidney_2/images\", \"/kaggle/input/blood-vessel-segmentation/train/kidney_2/labels\"),\n    (\"/kaggle/input/blood-vessel-segmentation/train/kidney_3_sparse/images\", \"/kaggle/input/blood-vessel-segmentation/train/kidney_3_sparse/labels\"),\n    (\"/kaggle/input/blood-vessel-segmentation/train/kidney_3_sparse/images\", \"/kaggle/input/blood-vessel-segmentation/train/kidney_3_dense/labels\")\n]\nwith strategy.scope():\n    training_generator=Generator(image_list, patch_size=patch_size)\n\n    train_tf_gen = tf.data.Dataset.from_generator(training_generator.get_item, \n                                  (tf.float32, tf.int16), \n                                  (tf.TensorShape(patch_size+[1]), \n                                   tf.TensorShape(patch_size+[2])))\n\n    train_batches = train_tf_gen.batch(32)\n\n    model = unet3d(patch_size+[1], labels)\n    optimizer = optimizers.Adam(learning_rate=1e-4)\n    model.compile(optimizer=optimizer,\n                          loss='binary_crossentropy',\n                          metrics=[dice_multi, \n                                   DiceBCELoss, \n                                   tf.keras.metrics.binary_crossentropy, \n                                   dice_coef_loss])\n\n    check_pointer = ModelCheckpoint(filepath=\"unet_3d.h5\", \n                                    verbose=1, \n                                    save_best_only=True, \n                                    save_weights_only=True)\n\n    tensorboard = TensorBoard(log_dir='./tensorboard')\n\n    model.fit(train_batches,\n                steps_per_epoch=steps_per_epoch,\n                epochs=epochs,\n                validation_data=train_batches,\n                validation_steps=validation_steps,\n                callbacks=[check_pointer, tensorboard])  ","metadata":{"execution":{"iopub.status.busy":"2023-12-10T21:30:29.993574Z","iopub.execute_input":"2023-12-10T21:30:29.993837Z","iopub.status.idle":"2023-12-10T21:41:02.465375Z","shell.execute_reply.started":"2023-12-10T21:30:29.993810Z","shell.execute_reply":"2023-12-10T21:41:02.463816Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}