{"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":71549,"databundleVersionId":8561470,"sourceType":"competition"},{"sourceId":14535717,"sourceType":"datasetVersion","datasetId":9009659}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport gc\nimport cv2\nimport math\nimport random\nimport numpy as np\nimport pandas as pd\nimport pydicom\nimport torch\nimport torch.nn as nn\nimport torchvision\nimport timm\nimport matplotlib.pyplot as plt\nfrom torch.utils.data import Dataset\nfrom fastai.vision.all import *\n\n# --- CONFIGURATION ---\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nSEED = 108\nPATCH_H, PATCH_W = 512, 512  # Sagittal Resize\nAXIAL_SIZE = 384             # Axial Resize\npatch_size = 64              # ResNet Stem Output\nLmax = 15                    # Sagittal Depth\nANGLE = 30\nLR_MAX = 5e-6\nBS = 24\nEPOCHS = 16\n\nLEVELS = ['L1/L2', 'L2/L3', 'L3/L4', 'L4/L5', 'L5/S1']\nLABELS_MAP = {'Normal/Mild': 0, 'Moderate': 1, 'Severe': 2}\n\ndef seed_everything(seed):\n    random.seed(seed)\n    np.random.seed(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nseed_everything(SEED)","metadata":{"_uuid":"aae2ad18-10cc-41c5-838a-82c39ec6d8f8","_cell_guid":"a6191695-d119-492a-899b-3298da459d03","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-25T10:41:11.224599Z","iopub.execute_input":"2026-01-25T10:41:11.224892Z","iopub.status.idle":"2026-01-25T10:41:26.611974Z","shell.execute_reply.started":"2026-01-25T10:41:11.224859Z","shell.execute_reply":"2026-01-25T10:41:26.611335Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_axial_slice(path, x, y):\n    if pd.isna(path) or not os.path.exists(path):\n        return torch.zeros(1, AXIAL_SIZE, AXIAL_SIZE)\n    try:\n        dcm = pydicom.dcmread(path)\n        img = dcm.pixel_array.astype(np.float32)\n        \n        # Robust Normalization\n        img = img - img.min()\n        v_max = np.percentile(img, 99)\n        img = (img / v_max if v_max > 0 else img / (img.max() + 1e-6))\n        img = np.clip(img, 0, 1)\n        img = (img * 255).astype(np.uint8)\n\n        # Crop & Resize\n        h, w = img.shape\n        cx, cy = (int(x), int(y)) if (pd.notna(x) and pd.notna(y)) else (w//2, h//2)\n        pad_h, pad_w = int(0.15 * h), int(0.15 * w)\n        \n        crop = img[max(0, cy-pad_h):min(h, cy+pad_h), max(0, cx-pad_w):min(w, cx+pad_w)]\n        if crop.size == 0: return torch.zeros(1, AXIAL_SIZE, AXIAL_SIZE)\n        \n        img_resized = cv2.resize(crop, (AXIAL_SIZE, AXIAL_SIZE), interpolation=cv2.INTER_LINEAR)\n        return torch.from_numpy(img_resized).unsqueeze(0).float() / 255.0\n    except Exception:\n        return torch.zeros(1, AXIAL_SIZE, AXIAL_SIZE)\n\nclass DualView_Spinal_Dataset(Dataset):\n    def __init__(self, sot_df, label_df=None, VALID=False, P=patch_size):\n        self.sot_df = sot_df.copy()\n        self.sot_df['study_id'] = self.sot_df['study_id'].astype(str)\n        self.VALID = VALID\n        self.P = P\n        self.sag_resize = torchvision.transforms.Resize((PATCH_H, PATCH_W), antialias=True)\n        \n        self.label_lookup = {}\n        if label_df is not None:\n            self.label_lookup = label_df.copy().set_index('study_id').to_dict('index')\n\n        self.study_groups = self.sot_df.groupby('study_id')\n        self.study_ids = list(self.study_groups.groups.keys())\n\n    def __len__(self): return len(self.study_ids)\n\n    def __getitem__(self, index):\n        study_id = self.study_ids[index]\n        group = self.study_groups.get_group(study_id).set_index('level')\n        \n        # Init Containers\n        sag_imgs = torch.zeros(5, Lmax, 2*self.P, 2*self.P)\n        sag_masks = torch.ones(5, Lmax).bool() # True = Padding\n        ax_imgs = torch.zeros(5, 2, 1, AXIAL_SIZE, AXIAL_SIZE)\n        targets = []\n\n        lbl_row = self.label_lookup.get(study_id, {})\n\n        for idx, level in enumerate(LEVELS):\n            # Label\n            val = lbl_row.get(f\"spinal_canal_stenosis_{level.replace('/', '_').lower()}\", 'UNK')\n            targets.append(LABELS_MAP.get(val, -100))\n\n            if level not in group.index: continue\n            row = group.loc[level]\n\n            # --- Axial ---\n            ax_imgs[idx, 0] = load_axial_slice(row['ax_0_path'], row['ax_0_x'], row['ax_0_y'])\n            ax_imgs[idx, 1] = load_axial_slice(row['ax_1_path'], row['ax_1_x'], row['ax_1_y'])\n\n            # --- Sagittal ---\n            if pd.notna(row['sag_path']) and os.path.exists(row['sag_path']):\n                try:\n                    self._load_sagittal_volume(row, sag_imgs, sag_masks, idx)\n                except: pass\n\n        # Augmentation (Rotate Sagittal)\n        if not self.VALID:\n            B, D, H, W = sag_imgs.shape\n            angle = random.uniform(-ANGLE, ANGLE)\n            sag_imgs = torchvision.transforms.functional.rotate(sag_imgs.view(-1, H, W), angle).view(B, D, H, W)\n\n        # Center Crop Patch\n        c = self.P // 2\n        sag_imgs = sag_imgs[:, :, c:c+self.P, c:c+self.P]\n\n        return [sag_imgs, sag_masks, ax_imgs], torch.tensor(targets, dtype=torch.long)\n\n    def _load_sagittal_volume(self, row, sag_imgs, sag_masks, idx):\n        # ... (Logic identical to previous code, condensed for brevity) ...\n        dcm_ref = pydicom.dcmread(row['sag_path'])\n        img_ref = dcm_ref.pixel_array.astype(np.float32)\n        H_orig, W_orig = img_ref.shape\n        \n        cx, cy = (row['sag_x'], row['sag_y']) if pd.notna(row['sag_x']) else (W_orig/2, H_orig/2)\n        \n        # Transpose logic\n        transpose = False\n        if H_orig > W_orig:\n            cy -= (H_orig - W_orig) // 2\n            H_orig, transpose = W_orig, True\n        elif H_orig < W_orig:\n            cx -= (W_orig - H_orig) // 2\n            W_orig = H_orig\n\n        sc_y = int(cy * PATCH_H / H_orig + self.P)\n        sc_x = int(cx * PATCH_W / W_orig + self.P)\n        \n        if not self.VALID:\n            sc_y += int(random.gauss(0, 5))\n            sc_x += int(random.gauss(0, 5))\n\n        folder = os.path.dirname(row['sag_path'])\n        center = int(os.path.basename(row['sag_path']).split('.')[0])\n        \n        for k in range(Lmax):\n            fpath = os.path.join(folder, f\"{center - Lmax//2 + k}.dcm\")\n            if os.path.exists(fpath):\n                px = pydicom.dcmread(fpath).pixel_array.astype(np.float32)\n                px = (px - px.min()) / (np.quantile(px, 0.99) + 1e-6)\n                px = torch.tensor(np.clip(px, 0, 1))\n                \n                if transpose: px = px[(px.shape[0]-px.shape[1])//2 : (px.shape[0]-px.shape[1])//2 + px.shape[1], :]\n                elif px.shape[0] < px.shape[1]: px = px[:, (px.shape[1]-px.shape[0])//2 : (px.shape[1]-px.shape[0])//2 + px.shape[0]]\n\n                px = self.sag_resize(px.unsqueeze(0))\n                px = nn.functional.pad(px, [self.P]*4, 'reflect').squeeze(0)\n                \n                if sc_y-self.P >= 0 and sc_x-self.P >= 0:\n                     sag_imgs[idx, k] = px[sc_y-self.P:sc_y+self.P, sc_x-self.P:sc_x+self.P]\n                     sag_masks[idx, k] = False","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-25T10:41:26.615982Z","iopub.execute_input":"2026-01-25T10:41:26.616313Z","iopub.status.idle":"2026-01-25T10:41:26.636869Z","shell.execute_reply.started":"2026-01-25T10:41:26.616233Z","shell.execute_reply":"2026-01-25T10:41:26.636131Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Legacy class for loading pretrained weights\nclass Sagittal_T2_spine_Discriminator(nn.Module):\n    def __init__(self, dim=512):\n        super().__init__()\n        self.emb = torchvision.models.resnet18(weights=None)\n        self.emb.conv1 = nn.Conv2d(1, patch_size, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)\n        self.emb.fc = nn.Identity()\n        self.proj_out = nn.Linear(dim, 2)\n\nclass Axial_Feature_Extractor(nn.Module):\n    def __init__(self, weight_path=None):\n        super().__init__()\n        self.backbone = timm.create_model(\"efficientnetv2_rw_t.ra2_in1k\", pretrained=True, in_chans=1, num_classes=0)\n        for p in self.backbone.parameters(): p.requires_grad = False\n        self.proj = nn.Linear(self.backbone.num_features, 512)\n        \n        if weight_path and os.path.exists(weight_path):\n            state = torch.load(weight_path, map_location=\"cpu\", weights_only=True)\n            if \"state_dict\" in state: state = state[\"state_dict\"]\n            state = {k.replace(\"module.\", \"\"): v for k, v in state.items()}\n            self.load_state_dict(state, strict=False)\n\n    def forward(self, x):\n        return self.proj(self.backbone(x))\n\nclass DualView_ViT(nn.Module):\n    def __init__(self, axial_weight_path=None, dim=512, depth=6, head_size=64):\n        super().__init__()\n        \n        # Sagittal\n        self.sag_encoder = torchvision.models.resnet18(weights='DEFAULT')\n        self.sag_encoder.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)\n        self.sag_encoder.fc = nn.Identity()\n        self.sag_proj = nn.Linear(512, dim)\n\n        # Axial\n        self.ax_encoder = Axial_Feature_Extractor(axial_weight_path)\n        self.ax_adapt = nn.Linear(512, dim)\n\n        # Transformers & Embeddings\n        self.slices_enc = nn.Parameter(self._get_sinusoid(Lmax, dim), requires_grad=False)\n        self.pos_enc = nn.Parameter(self._get_sinusoid(5, dim), requires_grad=False)\n        \n        encoder_layer = nn.TransformerEncoderLayer(d_model=dim, nhead=dim//head_size, dim_feedforward=4*dim, dropout=0.1, activation=nn.GELU(), batch_first=True, norm_first=True)\n        self.sag_slice_transformer = nn.TransformerEncoder(encoder_layer, depth)\n        self.level_transformer = nn.TransformerEncoder(encoder_layer, depth)\n        \n        self.cross_attn = nn.MultiheadAttention(dim, dim//head_size, batch_first=True)\n        self.norm_sag = nn.LayerNorm(dim)\n        self.norm_ax = nn.LayerNorm(dim)\n        self.proj_out = nn.Linear(dim, 3)\n\n    def _get_sinusoid(self, n_pos, d_model):\n        position = torch.arange(0, n_pos, dtype=torch.float).unsqueeze(1)\n        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))\n        pe = torch.zeros(n_pos, d_model)\n        pe[:, 0::2] = torch.sin(position * div_term)\n        pe[:, 1::2] = torch.cos(position * div_term)\n        return pe.to(device)\n\n    def forward(self, x):\n        sag_imgs, sag_masks, ax_imgs = x\n        B = sag_imgs.shape[0]\n\n        # 1. Sagittal Path\n        s = self.sag_encoder(sag_imgs.view(-1, 1, patch_size, patch_size))\n        s = self.sag_proj(s).view(B*5, Lmax, -1) + self.slices_enc\n        flat_mask = sag_masks.view(B*5, Lmax)\n        s = self.sag_slice_transformer(s, src_key_padding_mask=flat_mask)\n        # Average pooling ignoring masked slices\n        s = s.masked_fill(flat_mask.unsqueeze(-1), 0)\n        s_emb = s.sum(1) / (~flat_mask).sum(1).unsqueeze(-1).clamp(min=1)\n\n        # 2. Axial Path\n        a = self.ax_encoder(ax_imgs.view(-1, 1, AXIAL_SIZE, AXIAL_SIZE))\n        a = self.ax_adapt(a).view(B*5, 2, -1)\n\n        # 3. Fusion (Cross Attention)\n        q = self.norm_sag(s_emb.unsqueeze(1))\n        kv = self.norm_ax(a)\n        attn_out, _ = self.cross_attn(q, kv, kv)\n        fused = s_emb + attn_out.squeeze(1)\n\n        # 4. Level Aggregation\n        lvl_mask = (sag_masks.sum(2) == Lmax) # True if all slices masked\n        out = self.level_transformer(fused.view(B, 5, -1) + self.pos_enc, src_key_padding_mask=lvl_mask)\n        return self.proj_out(out)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-25T10:42:05.904500Z","iopub.execute_input":"2026-01-25T10:42:05.905245Z","iopub.status.idle":"2026-01-25T10:42:05.919989Z","shell.execute_reply.started":"2026-01-25T10:42:05.905210Z","shell.execute_reply":"2026-01-25T10:42:05.919171Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- THE FIX: Class-based Callback ---\ndef nt(nmin, nmax, tcur, tmax):\n    return (nmax - .5*(nmax-nmin)*(1+np.cos(tcur*np.pi/tmax))).astype(np.float32)\n\nclass AlphaScheduler(Callback):\n    def before_batch(self):\n        # We access self.learn safely here\n        # Assuming you wanted to log it or use it for mixup (logic preserved from your snippet)\n        # We calculate it based on total estimated iterations\n        total_iter = self.learn.n_epoch * len(self.learn.dls.train)\n        current_iter = self.learn.train_iter\n        alpha = torch.as_tensor(nt(0.25, 1, current_iter, total_iter))\n        # Store it in the learner so it can be accessed if needed by other components\n        self.learn.alpha_val = alpha \n\ndef myLoss(preds, target):\n    return nn.CrossEntropyLoss(weight=torch.tensor([1., 2., 4.], device=device), ignore_index=-100)(preds.view(-1, 3), target.view(-1))\n\ndef run_pipeline(sot_df, train_labels_df, folds=[1], sag_folder=None, ax_path=None):\n    # Data Setup\n    sot_df['study_id'] = sot_df['study_id'].astype(str)\n    train_labels_df['study_id'] = train_labels_df['study_id'].astype(str)\n    \n    if set(sot_df.study_id).isdisjoint(set(train_labels_df.study_id)):\n        raise ValueError(\"No matching study_ids found.\")\n\n    for f in folds:\n        print(f\"\\n=== Fold {f} ===\")\n        seed_everything(SEED)\n        \n        # Model Init\n        model = DualView_ViT(axial_weight_path=ax_path).to(device)\n        \n        # Load Sagittal Pretrained\n        if sag_folder:\n            sp = os.path.join(sag_folder, f\"Sagittal_T2_spine_discriminator_{f}\")\n            if os.path.exists(sp):\n                try:\n                    old = torch.load(sp, map_location=device, weights_only=False)\n                    model.sag_encoder.load_state_dict(old.emb.state_dict(), strict=False)\n                    print(\"Sagittal weights loaded.\")\n                    del old\n                except Exception as e:\n                    print(f\"Sagittal load error: {e}\")\n\n        # Data Split\n        t_ids = train_labels_df[train_labels_df.fold != f].study_id.unique()\n        v_ids = train_labels_df[train_labels_df.fold == f].study_id.unique()\n        \n        tds = DualView_Spinal_Dataset(sot_df[sot_df.study_id.isin(t_ids)], train_labels_df, VALID=False)\n        vds = DualView_Spinal_Dataset(sot_df[sot_df.study_id.isin(v_ids)], train_labels_df, VALID=True)\n        \n        dls = DataLoaders(\n            torch.utils.data.DataLoader(tds, batch_size=BS, shuffle=True, drop_last=True, num_workers=4, pin_memory=True),\n            torch.utils.data.DataLoader(vds, batch_size=BS, shuffle=False, num_workers=4, pin_memory=True)\n        )\n\n        # Training\n        learn = Learner(dls, model, loss_func=myLoss, \n                        cbs=[ShowGraphCallback(), GradientClip(3.0), AlphaScheduler()])\n        \n        learn.fit_one_cycle(EPOCHS, lr_max=LR_MAX, wd=0.05, pct_start=0.02)\n        torch.save(model.state_dict(), f'DualView_ViT_Fold{f}.pth')\n        \n        # Cleanup\n        del model, learn, dls\n        gc.collect()\n        torch.cuda.empty_cache()\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-25T10:42:12.036562Z","iopub.execute_input":"2026-01-25T10:42:12.037177Z","iopub.status.idle":"2026-01-25T10:42:12.047707Z","shell.execute_reply.started":"2026-01-25T10:42:12.037144Z","shell.execute_reply":"2026-01-25T10:42:12.046857Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_coors = pd.read_csv(\"/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_label_coordinates.csv\")\nlabels = pd.read_csv(\"/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train.csv\")\ntrain_desc = pd.read_csv(\"/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_series_descriptions.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-25T10:43:43.185119Z","iopub.execute_input":"2026-01-25T10:43:43.185435Z","iopub.status.idle":"2026-01-25T10:43:43.269254Z","shell.execute_reply.started":"2026-01-25T10:43:43.185404Z","shell.execute_reply":"2026-01-25T10:43:43.268513Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df = df_coors.merge(train_desc, on=['study_id', 'series_id'], how='left')\ndf = df[df['series_description'] != 'Sagittal T1']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-25T10:43:43.363210Z","iopub.execute_input":"2026-01-25T10:43:43.363541Z","iopub.status.idle":"2026-01-25T10:43:43.390316Z","shell.execute_reply.started":"2026-01-25T10:43:43.363510Z","shell.execute_reply":"2026-01-25T10:43:43.389563Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\ndata = dict()   \nTRAIN_PATH = '/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images'\n\nfor i in range(df.shape[0]):\n    study_id = str(df.iloc[i]['study_id'])\n    series_id = str(df.iloc[i]['series_id'])\n    instance_number = str(df.iloc[i]['instance_number'])\n    level = str(df.iloc[i]['level'])\n    x, y = df.iloc[i]['x'], df.iloc[i]['y']\n    desc = df.iloc[i]['series_description'].split()[0].lower()\n    if study_id not in data:\n        data[study_id] = dict()\n\n    if level not in data[study_id]:\n        data[study_id][level] = {\n            'sagittal' : {},\n            'axial' : []\n        }\n    if desc == 'axial':\n        data[study_id][level]['axial'].append({\n            'x' : x,\n            'y' : y,\n            'path' : os.path.join(TRAIN_PATH, str(study_id), str(series_id), str(instance_number) + '.dcm')\n        })\n    else:\n        data[study_id][level]['sagittal'] = {\n            'x' : x,\n            'y' : y,\n            'path' : os.path.join(TRAIN_PATH, str(study_id), str(series_id), str(instance_number) + '.dcm')\n        }\n\n\n# Convert nested dictionary to DataFrame\nrows = []\nfor study_id, levels_dict in data.items():\n    for level, views in levels_dict.items():\n        row = {\n            'study_id': study_id,\n            'level': level\n        }\n        \n        # Add sagittal data\n        if 'sagittal' in views and views['sagittal']:\n            row['sag_x'] = views['sagittal']['x']\n            row['sag_y'] = views['sagittal']['y']\n            row['sag_path'] = views['sagittal']['path']\n        \n        # Add axial data (concatenate multiple slices)\n        if 'axial' in views:\n            for idx, ax_slice in enumerate(views['axial']):\n                row[f'ax_{idx}_x'] = ax_slice['x']\n                row[f'ax_{idx}_y'] = ax_slice['y']\n                row[f'ax_{idx}_path'] = ax_slice['path']\n        \n        rows.append(row)\n\ndf = pd.DataFrame(rows)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-25T10:43:43.699639Z","iopub.execute_input":"2026-01-25T10:43:43.700656Z","iopub.status.idle":"2026-01-25T10:43:51.047985Z","shell.execute_reply.started":"2026-01-25T10:43:43.700598Z","shell.execute_reply":"2026-01-25T10:43:51.047073Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"spinal = [\n    'spinal_canal_stenosis_l1_l2',\n    'spinal_canal_stenosis_l2_l3',\n    'spinal_canal_stenosis_l3_l4',\n    'spinal_canal_stenosis_l4_l5',\n    'spinal_canal_stenosis_l5_s1'\n]\n\nn_folds = 5\nlabels[\"fold\"] = (np.arange(len(labels)) % n_folds) + 1\nlabels = labels[['study_id','fold']+spinal][labels[spinal].isna().sum(1) < len(spinal)].reset_index(drop=True)\nlabels.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-25T10:45:15.480583Z","iopub.execute_input":"2026-01-25T10:45:15.481472Z","iopub.status.idle":"2026-01-25T10:45:15.499108Z","shell.execute_reply.started":"2026-01-25T10:45:15.481435Z","shell.execute_reply":"2026-01-25T10:45:15.498287Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- EXECUTION ---\nrun_pipeline(\n    df, \n    labels, \n    sag_folder=\"/kaggle/input/lumbar-spine-keypoint-detection-models\",\n    ax_path=\"/kaggle/input/lumbar-spine-keypoint-detection-models/Axial_PreTrain_EffNetV2.pth\" \n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-25T10:45:24.072900Z","iopub.execute_input":"2026-01-25T10:45:24.073537Z","iopub.status.idle":"2026-01-25T15:00:17.915376Z","shell.execute_reply.started":"2026-01-25T10:45:24.073501Z","shell.execute_reply":"2026-01-25T15:00:17.914537Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}