{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":97984,"databundleVersionId":14096757,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 🫀 Project: PhysioNet Multi-Agent Digitization System 2.0 (Guardian 3.0)\n\n**PhysioNet - Digitization of ECG Images: Extract the ECG time-series data from scans and photographs of paper printouts of the ECGs.**\n\n---\n\n## 1. Executive Summary\n\nThis is the latest iteration of the ECG digitization pipeline, referred to as **Guardian 3.0**. It represents a highly robust architecture designed as a **Deep Learning-first system** with an optimized **Computer Vision (CV) Heuristic Fallback**.\n\nThe system's primary goal is reliability, ensuring accurate signal extraction even if cutting-edge AI models (YOLOv8 and Swin Transformer) fail to load in the execution environment. The entire process is managed by a Multi-Agent architecture to handle segmentation, calibration, and extraction.","metadata":{}},{"cell_type":"markdown","source":"## 2. System Architecture: Multi-Agent Pipeline\n\nThe core logic is managed by the `PhysioNetManager` class, which orchestrates the specialized agents in a sequence.\n\n### Pipeline Flow:\n1.  **Load Image**: Reads the ECG image file (`cv2.imread`).\n2.  **Layout Agent:** Identifies and crops the 12 leads and the calibration box.\n3.  **Calibration Agent:** Calculates the voltage scaling factor (`pixels_per_mV`) from the calibration pulse.\n4.  **Signal Agent:** Extracts the raw pixel trace of the ECG waveform from each cropped lead.\n5.  **Manager (Normalization):** Converts the pixel trace into a time-series voltage (mV) using the formula: `(Raw Signal - Mean) / pixels_per_mV`.\n6.  **Formatting:** Resamples and formats the output into the required Kaggle submission structure.","metadata":{}},{"cell_type":"code","source":"import os\nimport cv2\nimport gc\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport warnings\nimport matplotlib.pyplot as plt\nfrom scipy.signal import resample, butter, filtfilt\n\n# --- Config & Offline Handling ---\nwarnings.filterwarnings(\"ignore\")\n\nclass Config:\n    # Directories\n    BASE_DIR = \"/kaggle/input/physionet-ecg-image-digitization\"\n    TEST_CSV = f\"{BASE_DIR}/test.csv\"\n    TEST_IMGS = f\"{BASE_DIR}/test\"\n    SUBMISSION_FILE = \"submission.csv\"\n    \n    # Model Weights (User needs to upload these as Kaggle Datasets)\n    # Recommended: Upload your trained 'best.pt' to a dataset called 'ecg-weights'\n    YOLO_PATH = \"/kaggle/input/ecg-weights/yolo_layout.pt\" \n    SWIN_PATH = \"/kaggle/input/ecg-weights/swin_signal.pth\"\n    \n    # Signal Specs\n    LEAD_NAMES = ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6']\n    # Lead II is usually the long strip at the bottom\n    LONG_LEAD = 'II' \n\n# Import deep learning libs with offline fallback\ntry:\n    from ultralytics import YOLO\n    from transformers import SwinModel\n    DL_AVAILABLE = True\nexcept ImportError:\n    DL_AVAILABLE = False\n    print(\"⚠️ DL Libraries not found. Running in Pure-CV Heuristic Mode.\")\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"✅ Environment Ready. Device: {device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T19:39:31.478353Z","iopub.execute_input":"2025-12-11T19:39:31.478578Z","iopub.status.idle":"2025-12-11T19:39:31.489672Z","shell.execute_reply.started":"2025-12-11T19:39:31.478556Z","shell.execute_reply":"2025-12-11T19:39:31.488642Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Agent Detail: Layout Agent (`LayoutAgent`)\n\nThis agent is responsible for locating and cropping the individual ECG lead strips.\n\n| Mode | Technology | Key Feature |\n| :--- | :--- | :--- |\n| **Deep Learning** (Preferred) | **YOLOv8** (Object Detection) | Dynamically detects bounding boxes for all 12 leads and the rhythm strip, adapting to variable ECG layouts. |\n| **Heuristic Fallback** (Active if DL fails) | Hardcoded CV Logic (MOCK) | Assumes a fixed layout: a **3x4 grid** in the top 75% of the image, and a **10-second rhythm strip (`II_Long`)** in the bottom 25%. It provides a mock **`Calibration`** box at the start of the last grid row. |","metadata":{}},{"cell_type":"code","source":"class LayoutAgent:\n    def __init__(self, model_path):\n        self.model = None\n        # Check if weights exist AND libraries are loaded\n        if DL_AVAILABLE and os.path.exists(model_path):\n            print(f\"🔄 Loading YOLOv8 from {model_path}...\")\n            self.model = YOLO(model_path)\n        else:\n            print(\"⚠️ Using MOCK Layout (Standard 3x4 + Rhythm Strip).\")\n\n    def detect_layout(self, img: np.ndarray) -> dict:\n        results = {}\n        h, w, _ = img.shape\n        \n        if self.model:\n            # --- REAL INFERENCE ---\n            # Run YOLO with a low confidence threshold to capture faint grids\n            preds = self.model.predict(img, conf=0.15, verbose=False)[0]\n            for box in preds.boxes:\n                cls_id = int(box.cls)\n                cls_name = self.model.names[cls_id] # e.g., 'I', 'V6', 'Lead_II_Long'\n                x, y, bw, bh = box.xywh[0].cpu().numpy()\n                # Convert Center-XYWH to TopLeft-XYWH\n                results[cls_name] = [int(x - bw/2), int(y - bh/2), int(bw), int(bh)]\n        \n        else:\n            # --- MOCK INFERENCE (Updated for 10s Lead II) ---\n            # Logic: Top 75% is the 3x4 grid. Bottom 25% is the 10s Rhythm Strip (Lead II).\n            \n            # 1. The 3x4 Grid (2.5s segments)\n            grid_h = int(h * 0.75)\n            row_h = grid_h // 3\n            col_w = w // 4\n            \n            layout_map = {\n                (0, 0): 'I',   (1, 0): 'II',  (2, 0): 'III',\n                (0, 1): 'aVR', (1, 1): 'aVL', (2, 1): 'aVF',\n                (0, 2): 'V1',  (1, 2): 'V2',  (2, 2): 'V3',\n                (0, 3): 'V4',  (1, 3): 'V5',  (2, 3): 'V6'\n            }\n            \n            for (r, c), name in layout_map.items():\n                results[name] = [c*col_w, r*row_h, col_w, row_h]\n            \n            # 2. The Long Rhythm Strip (Lead II - 10s)\n            # We explicitly map this to 'II_Long' so the pipeline knows it's the 10s version\n            results['II_Long'] = [0, grid_h, w, h - grid_h]\n            \n            # 3. Calibration box (Assume start of last row of grid)\n            results['Calibration'] = [0, 2*row_h, int(col_w*0.2), row_h]\n\n        return results\n\n    def crop(self, img, bbox):\n        x, y, w, h = bbox\n        x, y = max(0, x), max(0, y)\n        return img[y:y+h, x:x+w]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T19:41:17.198914Z","iopub.execute_input":"2025-12-11T19:41:17.199216Z","iopub.status.idle":"2025-12-11T19:41:17.213474Z","shell.execute_reply.started":"2025-12-11T19:41:17.199194Z","shell.execute_reply":"2025-12-11T19:41:17.212449Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Agent Detail: Calibration Agent (`CalibrationAgent`)\n\nThis agent solves the critical challenge of converting pixel amplitude into millivolts (mV).\n\n### Methodology (`get_scaling_factor`)\n* **Preprocessing:** Uses `cv2.cvtColor` and **Otsu Thresholding** (`cv2.THRESH_OTSU`) for robust binarization of the calibration pulse crop.\n* **Height Calculation:** It identifies the active vertical region of the pulse by analyzing **row sums** of the binarized image.\n* **Validation:** A heuristic check ensures the detected pulse height is plausible (greater than 10px and less than 90% of the image height).\n* **Result:** The final output is the calculated `height_pixels` (i.e., `pixels_per_mV`) or a standard fallback value of **40.0** if detection fails.","metadata":{}},{"cell_type":"code","source":"class CalibrationAgent:\n    def get_scaling_factor(self, calib_crop: np.ndarray) -> float:\n        \"\"\"Calculates pixels per mV.\"\"\"\n        if calib_crop is None or calib_crop.size == 0: return 40.0\n            \n        # Standardize\n        gray = cv2.cvtColor(calib_crop, cv2.COLOR_BGR2GRAY)\n        \n        # Otsu Thresholding\n        _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)\n        \n        # Find active vertical region\n        row_sums = np.sum(binary, axis=1)\n        active_rows = np.where(row_sums > (binary.shape[1] * 0.05))[0] # 5% noise threshold\n        \n        if len(active_rows) > 5:\n            height_pixels = active_rows[-1] - active_rows[0]\n            # Heuristic Bounds: 10px < pulse < 90% of image height\n            if 10 < height_pixels < calib_crop.shape[0] * 0.9:\n                return float(height_pixels)\n        \n        return 40.0 # Standard fallback (10mm/mV @ 4 dots/mm is common)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T19:41:20.712445Z","iopub.execute_input":"2025-12-11T19:41:20.712818Z","iopub.status.idle":"2025-12-11T19:41:20.721092Z","shell.execute_reply.started":"2025-12-11T19:41:20.712793Z","shell.execute_reply":"2025-12-11T19:41:20.719789Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Agent Detail: Signal Agent (`SignalAgent`)\n\nThis agent extracts the pixel trace of the ECG waveform. It features an advanced heuristic method for maximum reliability.\n\n### Core Heuristic Function: `_heuristic_extract_smooth`\n1.  **Grid/Noise Removal:** Uses **Adaptive Gaussian Thresholding** (`ADAPTIVE_THRESH_GAUSSIAN_C`) tuned for ECG grids (block size 25, C=10).\n2.  **Trace Extraction (Vectorized CoM):** Calculates the vertical position of the signal in each column using a vectorized **Center of Mass (CoM)** approach, optimizing for speed over traditional looping.\n3.  **Smoothing:** The raw pixel trace is filtered using a **3rd order Butterworth Low-pass filter** (`Wn=0.15`) to reduce high-frequency pixel noise from the trace.\n4.  **Resampling:** The smoothed signal is resampled to the exact `target_samples` count required for the specified lead duration (10.0s or 2.5s).","metadata":{}},{"cell_type":"code","source":"class SignalAgent:\n    def __init__(self, model_path):\n        self.model = None\n        if DL_AVAILABLE and os.path.exists(model_path):\n            try:\n                # Placeholder for Swin initialization\n                # self.model = SwinModel.from_pretrained(...) \n                # self.model.load_state_dict(torch.load(model_path))\n                pass\n            except:\n                pass\n        \n    def extract(self, crop: np.ndarray, target_samples: int) -> np.ndarray:\n        # If we had a trained Swin model, we would use it here.\n        # Since we are focusing on the pipeline logic, we use the Enhanced Heuristic.\n        return self._heuristic_extract_smooth(crop, target_samples)\n\n    def _heuristic_extract_smooth(self, img: np.ndarray, n_samples: int) -> np.ndarray:\n        # 1. Preprocessing: Convert to Gray\n        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n        \n        # 2. Adaptive Thresholding (Better for stained/shadowed images)\n        # Block size 25, C=10 are tuned for ECG grid removal\n        binary = cv2.adaptiveThreshold(\n            gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, \n            cv2.THRESH_BINARY_INV, 25, 10\n        )\n        \n        # 3. Column-wise Center of Mass\n        trace = []\n        h, w = binary.shape\n        \n        # Optimization: Process as matrix instead of loop\n        # Create a meshgrid of indices\n        y_indices = np.arange(h).reshape(-1, 1)\n        \n        # Calculate weighted sum of indices (Center of Mass)\n        # Add epsilon to avoid division by zero\n        col_sums = np.sum(binary, axis=0)\n        col_sums[col_sums == 0] = 1 \n        \n        weighted_sums = np.sum(binary * y_indices, axis=0)\n        raw_signal = h - (weighted_sums / col_sums)\n        \n        # 4. Filter: Low-pass Butterworth (Remove high-freq pixel noise)\n        # fs (sampling of image) is effectively width. Cutoff at relative freq.\n        b, a = butter(N=3, Wn=0.15, btype='low') \n        smooth_signal = filtfilt(b, a, raw_signal)\n\n        # 5. Resample to target duration (fs * seconds)\n        return resample(smooth_signal, n_samples)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T19:41:23.069078Z","iopub.execute_input":"2025-12-11T19:41:23.069431Z","iopub.status.idle":"2025-12-11T19:41:23.078893Z","shell.execute_reply.started":"2025-12-11T19:41:23.069403Z","shell.execute_reply":"2025-12-11T19:41:23.077577Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Key Compliance and Runtime Features\n\nThe `PhysioNetManager` ensures the final output meets all competition requirements.\n\n* **Duration Logic:** Lead **II** is strictly processed for a **10.0-second** duration, while all other leads are processed for **2.5 seconds**.\n* **Lead II Priority:** The system explicitly checks for and prioritizes a dynamically detected **`II_Long` strip** over the standard `II` lead from the grid for the 10-second data.\n* **Compliance Audit:** A post-processing audit step verifies two critical rules:\n    * **ID Structure:** Confirms the required `{base_id}_{sample_idx}_{lead}` format.\n    * **Duration Ratio:** Confirms that the sample count ratio of **Lead II / Lead I is 4.00x** (10s/2.5s).\n* **Memory Management:** Periodic calls to `gc.collect()` are implemented within the main processing loop to aggressively manage memory consumption, which is critical for long-running Kaggle competition limits.","metadata":{}},{"cell_type":"code","source":"# [CELL 5: Pipeline Manager & Execution]\nclass PhysioNetManager:\n    def __init__(self):\n        # Initialize the specialized agents\n        self.layout_agent = LayoutAgent(Config.YOLO_PATH)\n        self.calib_agent = CalibrationAgent()\n        self.signal_agent = SignalAgent(Config.SWIN_PATH)\n\n    def process_record(self, img_path: str, base_id: str, fs: float):\n        # 1. Load Image\n        img = cv2.imread(img_path)\n        if img is None: \n            return self._get_zeros(base_id, fs)\n\n        # 2. AI Detect Layout\n        # Returns dict of bounding boxes e.g., {'I': [x,y,w,h], 'II_Long': [...]}\n        layout = self.layout_agent.detect_layout(img)\n        \n        # 3. Dynamic Calibration\n        px_per_mv = 40.0 # Default fallback\n        if 'Calibration' in layout:\n            calib_crop = self.layout_agent.crop(img, layout['Calibration'])\n            px_per_mv = self.calib_agent.get_scaling_factor(calib_crop)\n            \n        # 4. Extract Signals\n        extracted_data = {}\n        \n        # Determine which box to use for Lead II\n        # If the layout detected a dedicated \"II_Long\" strip, we prefer that.\n        # Otherwise, we fallback to the standard \"II\" box from the grid.\n        lead_ii_source = 'II_Long' if 'II_Long' in layout else 'II'\n\n        for lead in Config.LEAD_NAMES:\n            # Setup Duration Logic\n            if lead == 'II':\n                target_seconds = 10.0\n                roi_key = lead_ii_source\n            else:\n                target_seconds = 2.5\n                roi_key = lead\n            \n            target_samples = int(target_seconds * fs)\n\n            if roi_key in layout:\n                # A. Crop ROI\n                lead_crop = self.layout_agent.crop(img, layout[roi_key])\n                \n                # B. Extract Signal (Smooth Heuristic or AI)\n                raw_sig = self.signal_agent.extract(lead_crop, target_samples)\n                \n                # C. Physics Scaling (Normalize)\n                # (Signal - Mean) / Calibration Factor\n                mv_sig = (raw_sig - np.mean(raw_sig)) / px_per_mv\n                \n                extracted_data[lead] = mv_sig\n            else:\n                # Missing lead in image -> Fill with zeros\n                extracted_data[lead] = np.zeros(target_samples)\n\n        return self._format(base_id, extracted_data, fs)\n\n    def _get_zeros(self, base_id, fs):\n        \"\"\"Returns flatline data if image load fails.\"\"\"\n        dummy = {l: np.zeros(int((10.0 if l=='II' else 2.5)*fs)) for l in Config.LEAD_NAMES}\n        return self._format(base_id, dummy, fs)\n\n    def _format(self, bid, sigs, fs):\n        \"\"\"Formats data into the required Kaggle submission structure.\"\"\"\n        rows = []\n        for lead in Config.LEAD_NAMES:\n            target_len = int((10.0 if lead=='II' else 2.5) * fs)\n            data = sigs.get(lead, np.zeros(target_len))\n            \n            # Strict Length Enforcement (Vital for scoring)\n            if len(data) != target_len: \n                data = resample(data, target_len)\n            \n            # Generate Rows: {base_id}_{sample_idx}_{lead_name}\n            for i, val in enumerate(data):\n                rows.append({\"id\": f\"{bid}_{i}_{lead}\", \"value\": val})\n        return rows\n\n# --- MAIN EXECUTION LOOP ---\nif __name__ == \"__main__\":\n    # 1. Environment Setup (FIXED)\n    # Only try to create a directory if the path actually contains one\n    output_dir = os.path.dirname(Config.SUBMISSION_FILE)\n    if output_dir:\n        os.makedirs(output_dir, exist_ok=True)\n    \n    # Load Metadata or Create Mock Data\n    if os.path.exists(Config.TEST_CSV):\n        test_df = pd.read_csv(Config.TEST_CSV)\n        print(f\"📂 Loaded Test Set: {len(test_df)} records.\")\n    else:\n        print(\"⚠️ Test CSV not found. Running in DEMO mode.\")\n        test_df = pd.DataFrame({'id': ['001_demo'], 'fs': [500]})\n        if not os.path.exists(Config.TEST_IMGS): os.makedirs(Config.TEST_IMGS)\n        # Create dummy noisy image\n        dummy_img = np.random.randint(200, 255, (1000, 2000, 3), dtype=np.uint8)\n        cv2.imwrite(f\"{Config.TEST_IMGS}/001_demo.png\", dummy_img)\n\n    pipeline = PhysioNetManager()\n    all_rows = []\n    \n    print(\"▶️ Guardian 3.0 Pipeline Started...\")\n    \n    # 2. Processing Loop\n    for idx, row in test_df.iterrows():\n        base_id = str(row['id'])\n        fs = float(row['fs'])\n        \n        # Handle Extensions (.png vs .jpg)\n        img_path = os.path.join(Config.TEST_IMGS, f\"{base_id}.png\")\n        if not os.path.exists(img_path):\n             img_path = os.path.join(Config.TEST_IMGS, f\"{base_id}.jpg\")\n        \n        # Process\n        img_rows = pipeline.process_record(img_path, base_id, fs)\n        all_rows.extend(img_rows)\n            \n        # Memory Management (Critical for 9h runtime limit)\n        if idx % 50 == 0:\n            print(f\"   Processed {idx}/{len(test_df)}...\")\n            gc.collect()\n\n    # 3. Export\n    if all_rows:\n        submission_df = pd.DataFrame(all_rows)\n        # Enforce column order\n        submission_df = submission_df[['id', 'value']]\n        \n        submission_df.to_csv(Config.SUBMISSION_FILE, index=False)\n        print(f\"\\n✅ SUCCESS: Pipeline completed.\")\n        print(f\"📄 Saved {len(submission_df)} rows to {Config.SUBMISSION_FILE}\")\n    else:\n        print(\"❌ ERROR: No data generated.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T19:43:20.429559Z","iopub.execute_input":"2025-12-11T19:43:20.429941Z","iopub.status.idle":"2025-12-11T19:43:28.616275Z","shell.execute_reply.started":"2025-12-11T19:43:20.429915Z","shell.execute_reply":"2025-12-11T19:43:28.614684Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# [CELL 6: Compliance Audit & Validation]\ndef audit_submission():\n    print(\"\\n🕵️‍♂️ STARTING COMPLIANCE AUDIT (Guardian 3.0)...\")\n    \n    if not os.path.exists(Config.SUBMISSION_FILE):\n        print(\"❌ CRITICAL: Submission file missing.\"); return\n\n    df = pd.read_csv(Config.SUBMISSION_FILE)\n    \n    # 1. Validation: ID Structure\n    # Required: {base_id}_{row_id}_{lead}\n    try:\n        sample_id = df.iloc[0]['id']\n        parts = sample_id.split('_')\n        if len(parts) != 3:\n            print(f\"❌ INVALID ID FORMAT: {sample_id}\")\n        else:\n            print(f\"✅ ID Format Valid: {sample_id} (Base: {parts[0]}, Idx: {parts[1]}, Lead: {parts[2]})\")\n    except Exception as e:\n        print(f\"❌ Error parsing ID: {e}\")\n\n    # 2. Validation: The 'Lead II' Ratio Rule\n    # Lead II (10s) must have ~4x more samples than Lead I (2.5s)\n    first_base_id = df.iloc[0]['id'].split('_')[0]\n    subset = df[df['id'].str.startswith(f\"{first_base_id}_\")]\n    \n    # Extract lead names from ID string\n    subset['lead_name'] = subset['id'].apply(lambda x: x.split('_')[2])\n    counts = subset['lead_name'].value_counts()\n    \n    if 'II' in counts and 'I' in counts:\n        count_II = counts['II']\n        count_I = counts['I']\n        ratio = count_II / count_I\n        \n        print(f\"📊 Data Points -> Lead II: {count_II}, Lead I: {count_I}\")\n        print(f\"📊 Ratio (Lead II / Lead I): {ratio:.2f}x\")\n        \n        # Allow small margin of error for rounding\n        if 3.9 <= ratio <= 4.1:\n            print(f\"✅ DURATION CHECK: PASS (Target 4.0x)\")\n        else:\n            print(f\"⚠️ DURATION CHECK: SUSPICIOUS (Expected ~4.0x, got {ratio:.2f}x)\")\n            print(\"   (Check if LayoutAgent is correctly detecting the Long Strip)\")\n    else:\n        print(\"⚠️ Cannot verify ratios (Leads missing in first sample).\")\n\n    # 3. Validation: Data Integrity\n    if df.isnull().values.any():\n        print(\"❌ FAILURE: NaNs detected in submission.\")\n    else:\n        print(\"✅ Data Integrity: PASS (No NaNs)\")\n        \n    # 4. Validation: File Size/Columns\n    if list(df.columns) == ['id', 'value']:\n         print(\"✅ Column Names: PASS\")\n    else:\n         print(f\"❌ Column Names: FAIL {list(df.columns)}\")\n\naudit_submission()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T19:43:37.467002Z","iopub.execute_input":"2025-12-11T19:43:37.468027Z","iopub.status.idle":"2025-12-11T19:43:38.453813Z","shell.execute_reply.started":"2025-12-11T19:43:37.467988Z","shell.execute_reply":"2025-12-11T19:43:38.452702Z"}},"outputs":[],"execution_count":null}]}