{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<h1><center><b>ChemistrAI</b></center></h1>","metadata":{}},{"cell_type":"markdown","source":"This is the final prototype after the development of this proyect called \"ChemistrAI\"\n\nIt aims to help chemists broaden access to collective chemical research. \n\nIn turn, this would accelerate research and development efforts in many key fields by avoiding duplication of previously published chemistries and identifying novel trends by mining large data sets.","metadata":{}},{"cell_type":"markdown","source":"In this notebook you will find the arquitecture of the model and an image input to predict the <b>International Chemical Identifier</b> (InChI)\n\nIf you want to upload your image you can clone this notebook","metadata":{}},{"cell_type":"code","source":"#Libraries\nimport os\nfrom matplotlib import pyplot as plt\n\nOUTPUT_DIR = './'\nif not os.path.exists(OUTPUT_DIR):\n    os.makedirs(OUTPUT_DIR)\n    \nimport numpy as np\nimport pandas as pd\nimport gc\nimport torch\nimport torch.nn as nn\nimport cv2\nfrom tqdm.auto import tqdm\nimport timm\nfrom torch.utils.data import DataLoader, Dataset\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport warnings \nwarnings.filterwarnings('ignore')","metadata":{"execution":{"iopub.status.busy":"2023-04-23T13:43:56.128027Z","iopub.execute_input":"2023-04-23T13:43:56.128996Z","iopub.status.idle":"2023-04-23T13:43:58.681359Z","shell.execute_reply.started":"2023-04-23T13:43:56.128949Z","shell.execute_reply":"2023-04-23T13:43:58.680114Z"},"_kg_hide-input":true,"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# **Device (CUDA, GPU100)**","metadata":{}},{"cell_type":"code","source":"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')","metadata":{"execution":{"iopub.status.busy":"2023-04-23T13:43:58.684003Z","iopub.execute_input":"2023-04-23T13:43:58.684744Z","iopub.status.idle":"2023-04-23T13:43:58.713759Z","shell.execute_reply.started":"2023-04-23T13:43:58.684700Z","shell.execute_reply":"2023-04-23T13:43:58.712041Z"},"_kg_hide-input":true,"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# **Configuration**","metadata":{}},{"cell_type":"code","source":"class CFG:\n    debug          = True\n    apex           = False\n    max_len        = 275\n    print_freq     = 250\n    num_workers    = 4\n    model_name     = 'efficientnet_b2'\n    enc_size       = 1408\n    samp_size      = 500000\n    size           = 288\n    scheduler      = 'CosineAnnealingLR' \n    epochs         = 1 \n    T_max          = 4  \n    encoder_lr     = 1e-4\n    decoder_lr     = 4e-4\n    min_lr         = 1e-6\n    batch_size     = 32\n    weight_decay   = 1e-6\n    gradient_accumulation_steps = 1\n    max_grad_norm  = 10\n    attention_dim  = 256\n    embed_dim      = 512\n    decoder_dim    = 512\n    decoder_layers = 2     # number of LSTM layers\n    dropout        = 0.5\n    seed           = 42\n    n_fold         = 5\n    trn_fold       = 0 \n    prep_path      = '../input/tokenizer-and-preprocessing-of-chemical-formulas/'\n    pred_model     = '../input/model-effientnet/best_model.pth'","metadata":{"execution":{"iopub.status.busy":"2023-04-23T13:43:58.715984Z","iopub.execute_input":"2023-04-23T13:43:58.716424Z","iopub.status.idle":"2023-04-23T13:43:58.726736Z","shell.execute_reply.started":"2023-04-23T13:43:58.716356Z","shell.execute_reply":"2023-04-23T13:43:58.725836Z"},"_kg_hide-input":true,"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# **Tokenizer**","metadata":{}},{"cell_type":"code","source":"class Tokenizer(object):\n    \n    def __init__(self):\n        self.stoi = {}\n        self.itos = {}\n\n    def __len__(self):\n        return len(self.stoi)\n    \n    def fit_on_texts(self, texts):\n        vocab = set()\n        for text in texts:\n            vocab.update(text.split(' '))\n        vocab = sorted(vocab)\n        vocab.append('<sos>')\n        vocab.append('<eos>')\n        vocab.append('<pad>')\n        for i, s in enumerate(vocab):\n            self.stoi[s] = i\n        self.itos = {item[1]: item[0] for item in self.stoi.items()}\n        \n    def text_to_sequence(self, text):\n        sequence = []\n        sequence.append(self.stoi['<sos>'])\n        for s in text.split(' '):\n            sequence.append(self.stoi[s])\n        sequence.append(self.stoi['<eos>'])\n        return sequence\n    \n    def texts_to_sequences(self, texts):\n        sequences = []\n        for text in texts:\n            sequence = self.text_to_sequence(text)\n            sequences.append(sequence)\n        return sequences\n\n    def sequence_to_text(self, sequence):\n        return ''.join(list(map(lambda i: self.itos[i], sequence)))\n    \n    def sequences_to_texts(self, sequences):\n        texts = []\n        for sequence in sequences:\n            text = self.sequence_to_text(sequence)\n            texts.append(text)\n        return texts\n    \n    def predict_caption(self, sequence):\n        caption = ''\n        for i in sequence:\n            if i == self.stoi['<eos>'] or i == self.stoi['<pad>']:\n                break\n            caption += self.itos[i]\n        return caption\n    \n    def predict_captions(self, sequences):\n        captions = []\n        for sequence in sequences:\n            caption = self.predict_caption(sequence)\n            captions.append(caption)\n        return captions\n\ntokenizer = torch.load(CFG.prep_path + 'tokenizer.pth')","metadata":{"execution":{"iopub.status.busy":"2023-04-23T13:43:58.729814Z","iopub.execute_input":"2023-04-23T13:43:58.731095Z","iopub.status.idle":"2023-04-23T13:43:58.748092Z","shell.execute_reply.started":"2023-04-23T13:43:58.731040Z","shell.execute_reply":"2023-04-23T13:43:58.746915Z"},"_kg_hide-input":true,"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# **Model (RNN Encoder & CNN decoder)**","metadata":{}},{"cell_type":"code","source":"def get_transforms(*, data):\n    \n    if data == 'train':\n        return A.Compose([\n            A.Resize(CFG.size, CFG.size),\n            A.Normalize(\n                mean=[0.485, 0.456, 0.406],\n                std=[0.229, 0.224, 0.225],\n            ),\n            ToTensorV2(),\n        ])\n    \n    elif data == 'valid':\n        return A.Compose([\n            A.Resize(CFG.size, CFG.size),\n            A.Normalize(\n                mean=[0.485, 0.456, 0.406],\n                std=[0.229, 0.224, 0.225],\n            ),\n            ToTensorV2(),\n        ])\n\n\n#CNN Encoder\n\nclass Encoder(nn.Module):\n    def __init__(self, model_name = CFG.model_name, pretrained = False):\n        super().__init__()\n        self.cnn = timm.create_model(model_name, pretrained = pretrained)\n\n    def forward(self, x):\n        bs       = x.size(0)\n        features = self.cnn.forward_features(x)\n        features = features.permute(0, 2, 3, 1)\n        return features\n\n#RNN Decoder\n\n#Attention module\nclass Attention(nn.Module):\n    '''\n    Attention network for calculate attention value\n    '''\n    def __init__(self, encoder_dim, decoder_dim, attention_dim):\n        '''\n        :param encoder_dim: input size of encoder network\n        :param decoder_dim: input size of decoder network\n        :param attention_dim: input size of attention network\n        '''\n        super(Attention, self).__init__()\n        self.encoder_att = nn.Linear(encoder_dim, attention_dim)  # linear layer to transform encoded image\n        self.decoder_att = nn.Linear(decoder_dim, attention_dim)  # linear layer to transform decoder's output\n        self.full_att    = nn.Linear(attention_dim, 1)            # linear layer to calculate values to be softmax-ed\n        self.relu        = nn.ReLU()\n        self.softmax     = nn.Softmax(dim = 1)  # softmax layer to calculate weights\n\n    def forward(self, encoder_out, decoder_hidden):\n        att1  = self.encoder_att(encoder_out)     # (batch_size, num_pixels, attention_dim)\n        att2  = self.decoder_att(decoder_hidden)  # (batch_size, attention_dim)\n        att   = self.full_att(self.relu(att1 + att2.unsqueeze(1))).squeeze(2)  # (batch_size, num_pixels)\n        alpha = self.softmax(att)                 # (batch_size, num_pixels)\n        attention_weighted_encoding = (encoder_out * alpha.unsqueeze(2)).sum(dim = 1)  # (batch_size, encoder_dim)\n        return attention_weighted_encoding, alpha\n    \n    \n#Custom LSTM cell\ndef LSTMCell(input_size, hidden_size, **kwargs):\n    m = nn.LSTMCell(input_size, hidden_size, **kwargs)\n    for name, param in m.named_parameters():\n        if 'weight' in name or 'bias' in name:\n            param.data.uniform_(-0.1, 0.1)\n    return m\n\n#Decoder\nclass DecoderWithAttention(nn.Module):\n    '''\n    Decoder network with attention network used for training\n    '''\n\n    def __init__(self, attention_dim, embed_dim, decoder_dim, vocab_size, device, encoder_dim, dropout, num_layers):\n        '''\n        :param attention_dim: input size of attention network\n        :param embed_dim: input size of embedding network\n        :param decoder_dim: input size of decoder network\n        :param vocab_size: total number of characters used in training\n        :param encoder_dim: input size of encoder network\n        :param num_layers: number of the LSTM layers\n        :param dropout: dropout rate\n        '''\n        super(DecoderWithAttention, self).__init__()\n        self.encoder_dim   = encoder_dim\n        self.attention_dim = attention_dim\n        self.embed_dim     = embed_dim\n        self.decoder_dim   = decoder_dim\n        self.vocab_size    = vocab_size\n        self.dropout       = dropout\n        self.num_layers    = num_layers\n        self.device        = device\n        self.attention     = Attention(encoder_dim, decoder_dim, attention_dim)  # attention network\n        self.embedding     = nn.Embedding(vocab_size, embed_dim)                 # embedding layer\n        self.dropout       = nn.Dropout(p = self.dropout)\n        self.decode_step   = nn.ModuleList([LSTMCell(embed_dim + encoder_dim if layer == 0 else embed_dim, embed_dim) for layer in range(self.num_layers)]) # decoding LSTMCell        \n        self.init_h        = nn.Linear(encoder_dim, decoder_dim)  # linear layer to find initial hidden state of LSTMCell\n        self.init_c        = nn.Linear(encoder_dim, decoder_dim)  # linear layer to find initial cell state of LSTMCell\n        self.f_beta        = nn.Linear(decoder_dim, encoder_dim)  # linear layer to create a sigmoid-activated gate\n        self.sigmoid       = nn.Sigmoid()\n        self.fc            = nn.Linear(decoder_dim, vocab_size)  # linear layer to find scores over vocabulary\n        self.init_weights()                                      # initialize some layers with the uniform distribution\n\n    def init_weights(self):\n        self.embedding.weight.data.uniform_(-0.1, 0.1)\n        self.fc.bias.data.fill_(0)\n        self.fc.weight.data.uniform_(-0.1, 0.1)\n\n    def load_pretrained_embeddings(self, embeddings):\n        self.embedding.weight = nn.Parameter(embeddings)\n\n    def fine_tune_embeddings(self, fine_tune = True):\n        for p in self.embedding.parameters():\n            p.requires_grad = fine_tune\n\n    def init_hidden_state(self, encoder_out):\n        mean_encoder_out = encoder_out.mean(dim = 1)\n        h = [self.init_h(mean_encoder_out) for i in range(self.num_layers)]  # (batch_size, decoder_dim)\n        c = [self.init_c(mean_encoder_out) for i in range(self.num_layers)]\n        return h, c\n\n    def forward(self, encoder_out, encoded_captions, caption_lengths):\n        '''\n        :param encoder_out: output of encoder network\n        :param encoded_captions: transformed sequence from character to integer\n        :param caption_lengths: length of transformed sequence\n        '''\n        batch_size       = encoder_out.size(0)\n        encoder_dim      = encoder_out.size(-1)\n        vocab_size       = self.vocab_size\n        encoder_out      = encoder_out.view(batch_size, -1, encoder_dim)  # (batch_size, num_pixels, encoder_dim)\n        num_pixels       = encoder_out.size(1)\n        caption_lengths, sort_ind = caption_lengths.squeeze(1).sort(dim = 0, descending = True)\n        encoder_out      = encoder_out[sort_ind]\n        encoded_captions = encoded_captions[sort_ind]\n        \n        # embedding transformed sequence for vector\n        embeddings = self.embedding(encoded_captions)  # (batch_size, max_caption_length, embed_dim)\n        \n        # Initialize LSTM state, initialize cell_vector and hidden_vector\n        prev_h, prev_c = self.init_hidden_state(encoder_out)  # (batch_size, decoder_dim)\n        \n        # set decode length by caption length - 1 because of omitting start token\n        decode_lengths = (caption_lengths - 1).tolist()\n        predictions    = torch.zeros(batch_size, max(decode_lengths), vocab_size, device = self.device)\n        alphas         = torch.zeros(batch_size, max(decode_lengths), num_pixels, device = self.device)\n        \n        # predict sequence\n        for t in range(max(decode_lengths)):\n            batch_size_t = sum([l > t for l in decode_lengths])\n            attention_weighted_encoding, alpha = self.attention(encoder_out[:batch_size_t],\n                                                                prev_h[-1][:batch_size_t])\n            gate = self.sigmoid(self.f_beta(prev_h[-1][:batch_size_t]))  # gating scalar, (batch_size_t, encoder_dim)\n            attention_weighted_encoding = gate * attention_weighted_encoding\n\n            input = torch.cat([embeddings[:batch_size_t, t, :], attention_weighted_encoding], dim=1)\n            \n            for i, rnn in enumerate(self.decode_step):\n                # recurrent cell\n                h, c = rnn(input, (prev_h[i][:batch_size_t], prev_c[i][:batch_size_t])) # cell_vector and hidden_vector\n\n                # hidden state becomes the input to the next layer\n                input = self.dropout(h)\n\n                # save state for next time step\n                prev_h[i] = h\n                prev_c[i] = c\n                \n            preds = self.fc(self.dropout(h))  # (batch_size_t, vocab_size)\n            predictions[:batch_size_t, t, :] = preds\n            alphas[:batch_size_t, t, :]      = alpha\n            \n        return predictions, encoded_captions, decode_lengths, alphas, sort_ind\n    \n    def predict(self, encoder_out, decode_lengths, tokenizer):\n        \n        # size variables\n        batch_size  = encoder_out.size(0)\n        encoder_dim = encoder_out.size(-1)\n        vocab_size  = self.vocab_size\n        encoder_out = encoder_out.view(batch_size, -1, encoder_dim)  # (batch_size, num_pixels, encoder_dim)\n        num_pixels  = encoder_out.size(1)\n        \n        # embed start tocken for LSTM input\n        start_tockens = torch.ones(batch_size, dtype = torch.long, device = self.device) * tokenizer.stoi['<sos>']\n        embeddings    = self.embedding(start_tockens)\n        \n        # initialize hidden state and cell state of LSTM cell\n        h, c        = self.init_hidden_state(encoder_out)  # (batch_size, decoder_dim)\n        predictions = torch.zeros(batch_size, decode_lengths, vocab_size, device = self.device)\n        \n        # predict sequence\n        end_condition = torch.zeros(batch_size, dtype=torch.long, device = self.device)\n        for t in range(decode_lengths):\n            awe, alpha = self.attention(encoder_out, h[-1])  # (s, encoder_dim), (s, num_pixels)\n            gate       = self.sigmoid(self.f_beta(h[-1]))    # gating scalar, (s, encoder_dim)\n            awe        = gate * awe\n            \n            input = torch.cat([embeddings, awe], dim=1)\n \n            for j, rnn in enumerate(self.decode_step):\n                at_h, at_c = rnn(input, (h[j], c[j]))  # (s, decoder_dim)\n                input = self.dropout(at_h)\n                h[j]  = at_h\n                c[j]  = at_c\n            \n            preds = self.fc(self.dropout(h[-1]))  # (batch_size_t, vocab_size)\n            predictions[:, t, :] = preds\n            end_condition |= (torch.argmax(preds, -1) == tokenizer.stoi[\"<eos>\"])\n            if end_condition.sum() == batch_size:\n                break\n            embeddings = self.embedding(torch.argmax(preds, -1))\n        \n        return predictions\n    \n    # beam search\n    def forward_step(self, prev_tokens, hidden, encoder_out, function):\n        \n        h, c = hidden\n        #h, c = h.squeeze(0), c.squeeze(0)\n        h, c = [hi.squeeze(0) for hi in h], [ci.squeeze(0) for ci in c]\n        \n        embeddings = self.embedding(prev_tokens)\n        if embeddings.dim() == 3:\n            embeddings = embeddings.squeeze(1)\n            \n        awe, alpha = self.attention(encoder_out, h[-1])  # (s, encoder_dim), (s, num_pixels)\n        gate       = self.sigmoid(self.f_beta(h[-1]))    # gating scalar, (s, encoder_dim)\n        awe        = gate * awe\n        \n        input = torch.cat([embeddings, awe], dim = 1)\n        for j, rnn in enumerate(self.decode_step):\n            at_h, at_c = rnn(input, (h[j], c[j]))  # (s, decoder_dim)\n            input = self.dropout(at_h)\n            h[j]  = at_h\n            c[j]  = at_c\n\n        preds = self.fc(self.dropout(h[-1]))  # (batch_size_t, vocab_size)\n\n        #hidden = (h.unsqueeze(0), c.unsqueeze(0))\n        hidden = [hi.unsqueeze(0) for hi in h], [ci.unsqueeze(0) for ci in c]\n        predicted_softmax = function(preds, dim = 1)\n        \n        return predicted_softmax, hidden, Nonea\n\n\n#Load model\n\nstates = torch.load(CFG.pred_model, map_location = torch.device('cpu'))\n\nencoder = Encoder(CFG.model_name, pretrained = False)\nencoder.load_state_dict(states['encoder'])\nencoder.to(device)\n\ndecoder = DecoderWithAttention(attention_dim = CFG.attention_dim, \n                               embed_dim     = CFG.embed_dim, \n                               encoder_dim   = CFG.enc_size,\n                               decoder_dim   = CFG.decoder_dim,\n                               num_layers    = CFG.decoder_layers,\n                               vocab_size    = len(tokenizer), \n                               dropout       = CFG.dropout, \n                               device        = device)\ndecoder.load_state_dict(states['decoder'])\ndecoder.to(device)\n\ndel states; gc.collect()","metadata":{"execution":{"iopub.status.busy":"2023-04-23T13:43:58.750063Z","iopub.execute_input":"2023-04-23T13:43:58.750597Z","iopub.status.idle":"2023-04-23T13:44:01.494527Z","shell.execute_reply.started":"2023-04-23T13:43:58.750560Z","shell.execute_reply":"2023-04-23T13:44:01.493163Z"},"_kg_hide-input":true,"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# **Dataset transformer**","metadata":{}},{"cell_type":"code","source":"class TestDataset(Dataset):\n    def __init__(self, df, transform=None):\n        super().__init__()\n        self.df            = df\n        self.file_paths    = df['file_path'].values\n        self.transform     = transform\n        self.fix_transform = A.Compose([A.Transpose(p=1), A.VerticalFlip(p=1)])\n    \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        file_path = self.file_paths[idx]\n        image = cv2.imread(file_path)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)\n        h, w, _ = image.shape\n        if h > w:\n            image = self.fix_transform(image=image)['image']\n        if self.transform:\n            augmented = self.transform(image=image)\n            image     = augmented['image']\n        return image","metadata":{"execution":{"iopub.status.busy":"2023-04-23T13:44:01.497276Z","iopub.execute_input":"2023-04-23T13:44:01.498177Z","iopub.status.idle":"2023-04-23T13:44:01.507477Z","shell.execute_reply.started":"2023-04-23T13:44:01.498128Z","shell.execute_reply":"2023-04-23T13:44:01.506249Z"},"_kg_hide-input":true,"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# **Inference function**","metadata":{}},{"cell_type":"code","source":"def inference(test_loader, encoder, decoder, tokenizer, device):\n    \n    encoder.eval()\n    decoder.eval()\n    \n    text_preds = []\n    tk0 = tqdm(test_loader, total = len(test_loader))\n    \n    for images in tk0:\n        \n        images = images.to(device)\n        \n        with torch.no_grad():\n            features = encoder(images)\n            predictions = decoder.predict(features, CFG.max_len, tokenizer)\n            \n        predicted_sequence = torch.argmax(predictions.detach().cpu(), -1).numpy()\n        _text_preds = tokenizer.predict_captions(predicted_sequence)\n        text_preds.append(_text_preds)\n        \n    text_preds = np.concatenate(text_preds)\n    \n    return text_preds","metadata":{"execution":{"iopub.status.busy":"2023-04-23T13:44:01.511263Z","iopub.execute_input":"2023-04-23T13:44:01.511573Z","iopub.status.idle":"2023-04-23T13:44:01.521958Z","shell.execute_reply.started":"2023-04-23T13:44:01.511537Z","shell.execute_reply":"2023-04-23T13:44:01.520884Z"},"_kg_hide-input":true,"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# **Input image**","metadata":{}},{"cell_type":"code","source":"from ipywidgets import FileUpload\nupload = FileUpload(\n    accept='.png',  # Accepted file extension e.g. '.txt', '.pdf', 'image/*', 'image/*,.pdf'\n    multiple=False  # True to accept multiple files upload else False\n)\nprint(\"Upload your dataset here please\")\nprint(\"=\"*64)\nprint(\"\")\ndisplay(upload)","metadata":{"execution":{"iopub.status.busy":"2023-04-23T13:44:01.523638Z","iopub.execute_input":"2023-04-23T13:44:01.524533Z","iopub.status.idle":"2023-04-23T13:44:01.541986Z","shell.execute_reply.started":"2023-04-23T13:44:01.524493Z","shell.execute_reply":"2023-04-23T13:44:01.540313Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# **Projecting the inference**","metadata":{}},{"cell_type":"code","source":"file_name = \"\"\nif len(upload.data) > 0:\n    with open(\"test_image.png\", \"w+b\") as i:\n        i.write(upload.data[0]) \n    print(\"Saving loaded image\")\n    file_name = \"../working/test_image.png\"\nelse:\n    print(\"Loading sample image\")\n    file_name = \"../input/sample-molecular-image/sample.png\"\nprint(\"The filename is: \"+file_name)\n\n\ndata_raw = [{\"file_path\":file_name}]\ntest = pd.DataFrame(data_raw)\ntest_dataset = TestDataset(test, transform = get_transforms(data = 'valid'))\ntest_loader  = DataLoader(test_dataset, batch_size = 256, shuffle = False, num_workers = CFG.num_workers)\nprediction  = inference(test_loader, encoder, decoder, tokenizer, device)\nprediction = \"InChI=1S/\"+prediction[0]\n\nimg = cv2.imread(file_name)\ngray = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)\nplt.imshow(gray)\nplt.title('Image')\nplt.show()\n\nprint(\"Prediction:\")\nprint(prediction)","metadata":{"execution":{"iopub.status.busy":"2023-04-23T13:45:19.962687Z","iopub.execute_input":"2023-04-23T13:45:19.963863Z","iopub.status.idle":"2023-04-23T13:45:25.768845Z","shell.execute_reply.started":"2023-04-23T13:45:19.963812Z","shell.execute_reply":"2023-04-23T13:45:25.767433Z"},"trusted":true},"execution_count":null,"outputs":[]}]}