{"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":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":12727028,"sourceType":"datasetVersion","datasetId":7899268}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Imports","metadata":{"_kg_hide-input":true}},{"cell_type":"code","source":"import os, random, math\nimport pandas as pd, numpy as np, polars as pl\nfrom pathlib import Path\nfrom scipy.spatial.transform import Rotation as R\nfrom sklearn.preprocessing import LabelEncoder, StandardScaler\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.metrics import f1_score\nimport os\nos.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'  # Suppress ALL TensorFlow logs\nimport tensorflow as tf\ntf.get_logger().setLevel('ERROR')  # Only show errors \nfrom tensorflow.keras.utils import to_categorical, pad_sequences\nfrom tensorflow.keras.models import Model, load_model\nfrom tensorflow.keras.layers import (\n    Input, Conv1D, BatchNormalization, Activation, add, MaxPooling1D, \n    Dropout, Bidirectional, LSTM, GlobalAveragePooling1D, Dense, Multiply,\n    Reshape, Lambda, Concatenate, GRU, GaussianNoise\n)\nfrom tensorflow.keras.optimizers.schedules import CosineDecay, CosineDecayRestarts, ExponentialDecay\nfrom tensorflow.keras.regularizers import l2\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import EarlyStopping\nimport joblib\nimport warnings\nwarnings.filterwarnings('ignore')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T04:41:21.705489Z","iopub.execute_input":"2025-08-19T04:41:21.705728Z","iopub.status.idle":"2025-08-19T04:41:37.653031Z","shell.execute_reply.started":"2025-08-19T04:41:21.705708Z","shell.execute_reply":"2025-08-19T04:41:37.652406Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Configuration ","metadata":{}},{"cell_type":"code","source":"\nTRAIN = False \nBASE_DIR = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data\")\nPRETRAINED_DIR = Path(\"/kaggle/input/artifact0\")\nEXPORT_DIR = Path(\"/kaggle/working\")\nSEED = 42\nBATCH_SIZE = 64\nPAD_PERCENTILE = 96\nLR_INIT = 5e-4\nWD = 3e-3\nMIXUP_ALPHA = 0.4\nEPOCHS = 160\nPATIENCE = 40\nN_SPLITS = 10\nMASKING_PROB = 0.35\nGATE_LOSS_WEIGHT = 0.2\nUSE_LR_SCHEDULER = True\nLR_SCHEDULE_TYPE = \"cosine_decay_restarts\"  # Options: \"cosine_decay\", \"cosine_decay_restarts\", \"exponential_decay\"\n\n\ndef seed_everything(seed):\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    random.seed(seed)\n    np.random.seed(seed)\n    tf.random.set_seed(seed)\n    tf.experimental.numpy.random.seed(seed)\n    os.environ['TF_CUDNN_DETERMINISTIC'] = '1'\n    os.environ['TF_DETERMINISTIC_OPS'] = '1'\n\nseed_everything(SEED)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T04:41:37.654777Z","iopub.execute_input":"2025-08-19T04:41:37.655286Z","iopub.status.idle":"2025-08-19T04:41:37.661223Z","shell.execute_reply.started":"2025-08-19T04:41:37.655267Z","shell.execute_reply":"2025-08-19T04:41:37.660408Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Feature engineering functions","metadata":{}},{"cell_type":"code","source":"\ndef remove_gravity_from_acc(acc_data, rot_data):\n    acc_values = acc_data[['acc_x', 'acc_y', 'acc_z']].values\n    quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    linear_accel = np.zeros_like(acc_values)\n    gravity_world = np.array([0, 0, 9.81])\n    \n    for i in range(len(acc_values)):\n        if np.all(np.isnan(quat_values[i])) or np.all(np.isclose(quat_values[i], 0)):\n            linear_accel[i, :] = acc_values[i, :]\n            continue\n        try:\n            rotation = R.from_quat(quat_values[i])\n            gravity_sensor_frame = rotation.apply(gravity_world, inverse=True)\n            linear_accel[i, :] = acc_values[i, :] - gravity_sensor_frame\n        except ValueError:\n            linear_accel[i, :] = acc_values[i, :]\n    return linear_accel\n\ndef calculate_angular_velocity_from_quat(rot_data, time_delta=1/200):\n    quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    angular_vel = np.zeros((len(quat_values), 3))\n    \n    for i in range(len(quat_values) - 1):\n        q_t, q_t_plus_dt = quat_values[i], quat_values[i+1]\n        if np.all(np.isnan(q_t)) or np.all(np.isnan(q_t_plus_dt)):\n            continue\n        try:\n            rot_t = R.from_quat(q_t)\n            rot_t_plus_dt = R.from_quat(q_t_plus_dt)\n            delta_rot = rot_t.inv() * rot_t_plus_dt\n            angular_vel[i, :] = delta_rot.as_rotvec() / time_delta\n        except ValueError:\n            pass\n    return angular_vel\n\ndef calculate_angular_distance(rot_data):\n    quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    angular_dist = np.zeros(len(quat_values))\n    \n    for i in range(len(quat_values) - 1):\n        q1, q2 = quat_values[i], quat_values[i+1]\n        if np.all(np.isnan(q1)) or np.all(np.isnan(q2)):\n            continue\n        try:\n            r1, r2 = R.from_quat(q1), R.from_quat(q2)\n            relative_rotation = r1.inv() * r2\n            angular_dist[i] = np.linalg.norm(relative_rotation.as_rotvec())\n        except ValueError:\n            pass\n    return angular_dist","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T04:41:37.662263Z","iopub.execute_input":"2025-08-19T04:41:37.662933Z","iopub.status.idle":"2025-08-19T04:41:37.697291Z","shell.execute_reply.started":"2025-08-19T04:41:37.662912Z","shell.execute_reply":"2025-08-19T04:41:37.696774Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Evaluation Metric and helpers","metadata":{}},{"cell_type":"code","source":"def cmi_metric(y_true_gestures, y_pred_gestures, bfrb_gestures=None):\n    y_true_gestures = np.array(y_true_gestures)\n    y_pred_gestures = np.array(y_pred_gestures)\n    \n    y_true_binary = np.array(['target' if gesture in bfrb_gestures else 'non_target' \n                             for gesture in y_true_gestures])\n    y_pred_binary = np.array(['target' if gesture in bfrb_gestures else 'non_target' \n                             for gesture in y_pred_gestures])\n    \n    binary_f1 = f1_score(y_true_binary, y_pred_binary, pos_label='target')\n    \n    y_true_collapsed = []\n    y_pred_collapsed = []\n    \n    for true_gesture, pred_gesture in zip(y_true_gestures, y_pred_gestures):\n        if true_gesture in bfrb_gestures:\n            y_true_collapsed.append(true_gesture)\n        else:\n            y_true_collapsed.append('non_target')\n            \n        if pred_gesture in bfrb_gestures:\n            y_pred_collapsed.append(pred_gesture)\n        else:\n            y_pred_collapsed.append('non_target')\n    \n    y_true_collapsed = np.array(y_true_collapsed)\n    y_pred_collapsed = np.array(y_pred_collapsed)\n    \n    macro_f1 = f1_score(y_true_collapsed, y_pred_collapsed, average='macro')\n    composite_score = (binary_f1 + macro_f1) / 2.0\n    \n    return {\n        'binary_f1': binary_f1,\n        'macro_f1': macro_f1, \n        'composite_score': composite_score\n    }\n\ndef evaluate_with_cmi_metric(model, X_val, y_val_gestures, gesture_classes, bfrb_gestures):\n    predictions = model.predict(X_val, verbose=0)[0]\n    pred_gesture_indices = predictions.argmax(axis=1)\n    pred_gestures = gesture_classes[pred_gesture_indices]\n    scores = cmi_metric(y_val_gestures, pred_gestures, bfrb_gestures)\n    return scores\n\n\ndef get_individual_gesture_scores(model, X_val, y_val_gestures, gesture_classes, bfrb_gestures):\n    \"\"\"Calculate F1 score for each individual gesture\"\"\"\n    predictions = model.predict(X_val, verbose=0)[0]\n    pred_gesture_indices = predictions.argmax(axis=1)\n    pred_gestures = gesture_classes[pred_gesture_indices]\n    \n    gesture_scores = {}\n    unique_gestures = np.unique(y_val_gestures)\n    \n    for gesture in unique_gestures:\n        y_true_binary = (y_val_gestures == gesture).astype(int)\n        y_pred_binary = (pred_gestures == gesture).astype(int)\n        \n        if y_true_binary.sum() > 0:\n            f1 = f1_score(y_true_binary, y_pred_binary, zero_division=0)\n            gesture_scores[gesture] = f1\n    \n    return gesture_scores\n\ndef evaluate_dual_cmi_metric(model, X_val, y_val_gestures, gesture_classes, bfrb_gestures, imu_dim):\n    \n    cmi_full = evaluate_with_cmi_metric(model, X_val, y_val_gestures, gesture_classes, bfrb_gestures)\n    \n    \n    X_val_imu_only = X_val.copy()\n    X_val_imu_only[:, :, imu_dim:] = 0.0\n    \n    cmi_imu = evaluate_with_cmi_metric(model, X_val_imu_only, y_val_gestures, gesture_classes, bfrb_gestures)\n    \n    realistic_composite = (cmi_full['composite_score'] + cmi_imu['composite_score']) / 2.0\n    realistic_binary = (cmi_full['binary_f1'] + cmi_imu['binary_f1']) / 2.0\n    realistic_macro = (cmi_full['macro_f1'] + cmi_imu['macro_f1']) / 2.0\n    \n    sensor_dependency = cmi_full['composite_score'] - cmi_imu['composite_score']\n    \n    return {\n        'composite_score': realistic_composite,\n        'binary_f1': realistic_binary, \n        'macro_f1': realistic_macro,\n        'full_sensor_composite': cmi_full['composite_score'],\n        'full_sensor_binary': cmi_full['binary_f1'],\n        'full_sensor_macro': cmi_full['macro_f1'],\n        'imu_only_composite': cmi_imu['composite_score'],\n        'imu_only_binary': cmi_imu['binary_f1'],\n        'imu_only_macro': cmi_imu['macro_f1'],\n        'sensor_dependency': sensor_dependency,\n        'performance_stability': 1.0 - (sensor_dependency / max(cmi_full['composite_score'], 0.01))\n    }\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T04:41:37.698079Z","iopub.execute_input":"2025-08-19T04:41:37.698336Z","iopub.status.idle":"2025-08-19T04:41:37.717051Z","shell.execute_reply.started":"2025-08-19T04:41:37.698313Z","shell.execute_reply":"2025-08-19T04:41:37.716410Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Scaler","metadata":{}},{"cell_type":"code","source":"class IMUSpecificScaler:\n    def __init__(self):\n         self.imu_scaler = StandardScaler()\n         self.tof_scaler = StandardScaler()\n         self.imu_dim = None\n            \n    def fit(self, X, imu_dim):\n         self.imu_dim = imu_dim\n         self.imu_scaler.fit(X[:, :imu_dim])\n         self.tof_scaler.fit(X[:, imu_dim:])\n         return self\n            \n    def transform(self, X):\n         X_imu = self.imu_scaler.transform(X[:, :self.imu_dim])\n         X_tof = self.tof_scaler.transform(X[:, self.imu_dim:])\n         return np.concatenate([X_imu, X_tof], axis=1)\n        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T04:41:37.717735Z","iopub.execute_input":"2025-08-19T04:41:37.717946Z","iopub.status.idle":"2025-08-19T04:41:37.734786Z","shell.execute_reply.started":"2025-08-19T04:41:37.717921Z","shell.execute_reply":"2025-08-19T04:41:37.734143Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Architecture ","metadata":{}},{"cell_type":"code","source":"        \nclass EnhancedCMIMetricCallback(tf.keras.callbacks.Callback):\n    def __init__(self, X_val, y_val_gestures, gesture_classes, bfrb_gestures, imu_dim, patience=40, verbose=1):\n        super().__init__()\n        self.X_val = X_val\n        self.y_val_gestures = y_val_gestures\n        self.gesture_classes = gesture_classes\n        self.bfrb_gestures = bfrb_gestures\n        self.imu_dim = imu_dim\n        self.patience = patience\n        self.verbose = verbose\n        self.best_score = -np.inf\n        self.wait = 0\n        self.best_weights = None\n        \n    def on_epoch_end(self, epoch, logs=None):\n        dual_scores = evaluate_dual_cmi_metric(\n            self.model, self.X_val, self.y_val_gestures, \n            self.gesture_classes, self.bfrb_gestures, self.imu_dim\n        )\n        \n        realistic_composite = dual_scores['composite_score']\n        \n        logs = logs or {}\n        logs['val_realistic_composite'] = realistic_composite\n        logs['val_full_sensor_composite'] = dual_scores['full_sensor_composite']\n        logs['val_imu_only_composite'] = dual_scores['imu_only_composite']\n        logs['val_sensor_dependency'] = dual_scores['sensor_dependency']\n        \n        # Silent progress\n        if self.verbose > 0:\n            print('.', end='', flush=True)\n        \n        if realistic_composite > self.best_score:\n            self.best_score = realistic_composite\n            self.wait = 0\n            self.best_weights = self.model.get_weights()\n        else:\n            self.wait += 1\n            \n        if self.wait >= self.patience:\n            self.model.stop_training = True\n            \n    def on_train_end(self, logs=None):\n        if self.best_weights is not None:\n            self.model.set_weights(self.best_weights)\n        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T04:41:37.735437Z","iopub.execute_input":"2025-08-19T04:41:37.735669Z","iopub.status.idle":"2025-08-19T04:41:37.754821Z","shell.execute_reply.started":"2025-08-19T04:41:37.735649Z","shell.execute_reply":"2025-08-19T04:41:37.754217Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"        \ndef create_detailed_confusion_analysis(models, X_val_all, y_val_all, le_classes, bfrb_gestures):\n    \"\"\"Create detailed confusion matrix and misclassification analysis\"\"\"\n    import matplotlib.pyplot as plt\n    import seaborn as sns\n    from sklearn.metrics import confusion_matrix\n    \n    # Get ensemble predictions\n    all_predictions = []\n    for model in models:\n        pred = model.predict(X_val_all, verbose=0)[0]\n        all_predictions.append(pred)\n    \n    ensemble_pred = np.mean(all_predictions, axis=0)\n    pred_classes = ensemble_pred.argmax(axis=1)\n    true_classes = y_val_all.argmax(axis=1)\n    \n    # Create confusion matrix\n    cm = confusion_matrix(true_classes, pred_classes)\n    \n    print(\"\\n\" + \"=\"*80)\n    print(\"DETAILED CONFUSION MATRIX ANALYSIS\")\n    print(\"=\"*80)\n    \n    # 1. Overall confusion matrix visualization\n    plt.figure(figsize=(15, 12))\n    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', \n                xticklabels=[cls[:20] for cls in le_classes],\n                yticklabels=[cls[:20] for cls in le_classes])\n    plt.title('Gesture Confusion Matrix')\n    plt.ylabel('True Gesture')\n    plt.xlabel('Predicted Gesture')\n    plt.xticks(rotation=45, ha='right')\n    plt.yticks(rotation=0)\n    plt.tight_layout()\n    plt.savefig(EXPORT_DIR / 'confusion_matrix.png', dpi=150, bbox_inches='tight')\n    plt.show()\n    \n    # 2. Detailed misclassification analysis\n    print(\"\\n1. TOP MISCLASSIFICATIONS:\")\n    print(\"-\" * 50)\n    \n    misclassifications = []\n    for i, true_class in enumerate(le_classes):\n        for j, pred_class in enumerate(le_classes):\n            if i != j and cm[i][j] > 0:\n                misclassifications.append({\n                    'true': true_class,\n                    'predicted': pred_class, \n                    'count': cm[i][j],\n                    'true_is_bfrb': true_class in bfrb_gestures,\n                    'pred_is_bfrb': pred_class in bfrb_gestures\n                })\n    \n    # Sort by count\n    misclassifications.sort(key=lambda x: x['count'], reverse=True)\n    \n    print(\"Most frequent misclassifications:\")\n    for i, mc in enumerate(misclassifications[:15]):\n        true_marker = \" \" if mc['true_is_bfrb'] else \"  \"\n        pred_marker = \" \" if mc['pred_is_bfrb'] else \"  \"\n        print(f\"{i+1:2d}. {true_marker} {mc['true'][:25]:25} → {pred_marker} {mc['predicted'][:25]:25} ({mc['count']} times)\")\n    \n    # 3. BFRB-specific confusion analysis\n    print(\"\\n2. BFRB GESTURE CONFUSION PATTERNS:\")\n    print(\"-\" * 50)\n    \n    bfrb_confusions = [mc for mc in misclassifications if mc['true_is_bfrb']]\n    \n    print(\"BFRB gestures confused with other BFRB gestures:\")\n    bfrb_to_bfrb = [mc for mc in bfrb_confusions if mc['pred_is_bfrb']]\n    for mc in bfrb_to_bfrb[:10]:\n        print(f\"   {mc['true'][:30]:30} →  {mc['predicted'][:30]:30} ({mc['count']}x)\")\n    \n    print(f\"\\nBFRB gestures confused with Non-BFRB gestures:\")\n    bfrb_to_non = [mc for mc in bfrb_confusions if not mc['pred_is_bfrb']]\n    for mc in bfrb_to_non[:10]:\n        print(f\"   {mc['true'][:30]:30} →    {mc['predicted'][:30]:30} ({mc['count']}x)\")\n    \n    # 4. Gesture-specific accuracy\n    print(\"\\n3. INDIVIDUAL GESTURE ACCURACY:\")\n    print(\"-\" * 50)\n    \n    gesture_accuracy = {}\n    for i, gesture in enumerate(le_classes):\n        total_true = cm[i].sum()\n        correct = cm[i][i]\n        accuracy = correct / total_true if total_true > 0 else 0\n        gesture_accuracy[gesture] = accuracy\n    \n    print(\"BFRB Gesture Accuracy:\")\n    bfrb_acc = {g: a for g, a in gesture_accuracy.items() if g in bfrb_gestures}\n    for gesture, acc in sorted(bfrb_acc.items(), key=lambda x: x[1]):\n        print(f\"  {gesture[:35]:35} {acc:.3f}\")\n    \n    print(f\"\\nNon-BFRB Gesture Accuracy (top 10):\")\n    non_bfrb_acc = {g: a for g, a in gesture_accuracy.items() if g not in bfrb_gestures}\n    for gesture, acc in sorted(non_bfrb_acc.items(), key=lambda x: x[1], reverse=True)[:10]:\n        print(f\"  {gesture[:35]:35} {acc:.3f}\")\n    \n    # 5. Actionable insights\n    print(\"\\n4. ACTIONABLE INSIGHTS:\")\n    print(\"-\" * 50)\n    \n    worst_bfrb = sorted(bfrb_acc.items(), key=lambda x: x[1])[:3]\n    print(\"PRIORITY: Worst performing BFRB gestures:\")\n    for gesture, acc in worst_bfrb:\n        main_confusions = [mc for mc in misclassifications if mc['true'] == gesture][:3]\n        print(f\"\\n  • {gesture} (accuracy: {acc:.3f})\")\n        print(\"    Most confused with:\")\n        for mc in main_confusions:\n            conf_type = \"BFRB\" if mc['pred_is_bfrb'] else \"Non-BFRB\"\n            print(f\"      - {mc['predicted']} ({conf_type}, {mc['count']}x)\")\n    \n    return cm, misclassifications, gesture_accuracy\ndef time_sum(x):\n    return tf.reduce_sum(x, axis=1)\n\ndef squeeze_last_axis(x):\n    return tf.squeeze(x, axis=-1)\n\ndef expand_last_axis(x):\n    return tf.expand_dims(x, axis=-1)\n\ndef se_block(x, reduction=8):\n    ch = x.shape[-1]\n    se = GlobalAveragePooling1D()(x)\n    se = Dense(ch // reduction, activation='relu')(se)\n    se = Dense(ch, activation='sigmoid')(se)\n    se = Reshape((1, ch))(se)\n    return Multiply()([x, se])\n\ndef residual_se_cnn_block(x, filters, kernel_size, pool_size=2, drop=0.3, wd=1e-4):\n    shortcut = x\n    for _ in range(2):\n        x = Conv1D(filters, kernel_size, padding='same', use_bias=False, kernel_regularizer=l2(wd))(x)\n        x = BatchNormalization()(x)\n        x = Activation('relu')(x)\n    x = se_block(x)\n    \n    if shortcut.shape[-1] != filters:\n        shortcut = Conv1D(filters, 1, padding='same', use_bias=False, kernel_regularizer=l2(wd))(shortcut)\n        shortcut = BatchNormalization()(shortcut)\n    \n    x = add([x, shortcut])\n    x = Activation('relu')(x)\n    x = MaxPooling1D(pool_size)(x)\n    x = Dropout(drop)(x)\n    return x\n\ndef attention_layer(inputs):\n    score = Dense(1, activation='tanh')(inputs)\n    score = Lambda(squeeze_last_axis)(score)\n    weights = Activation('softmax')(score)\n    weights = Lambda(expand_last_axis)(weights)\n    context = Multiply()([inputs, weights])\n    context = Lambda(time_sum)(context)\n    return context\n\nclass GatedMixupGenerator(tf.keras.utils.Sequence):\n    def __init__(self, X, y, batch_size, imu_dim, class_weight=None, alpha=0.2, masking_prob=0.0):\n        self.X, self.y = X, y\n        self.batch = batch_size\n        self.imu_dim = imu_dim\n        self.class_weight = class_weight\n        self.alpha = alpha\n        self.masking_prob = masking_prob\n        self.indices = np.arange(len(X))\n        self.on_epoch_end()\n\n    def __len__(self):\n        return int(np.ceil(len(self.X) / self.batch))\n\n    def __getitem__(self, i):\n        idx = self.indices[i*self.batch:(i+1)*self.batch]\n        Xb, yb = self.X[idx].copy(), self.y[idx].copy()\n        \n        sample_weights = np.ones(len(Xb), dtype='float32')\n        if self.class_weight:\n            y_integers = yb.argmax(axis=1)\n            sample_weights = np.array([self.class_weight[i] for i in y_integers])\n        \n        gate_target = np.ones(len(Xb), dtype='float32')\n        if self.masking_prob > 0:\n            for i in range(len(Xb)):\n                if np.random.rand() < self.masking_prob:\n                    Xb[i, :, self.imu_dim:] = 0\n                    gate_target[i] = 0.0\n        \n        if self.alpha > 0:\n            lam = np.random.beta(self.alpha, self.alpha)\n            perm = np.random.permutation(len(Xb))\n            X_mix = lam * Xb + (1 - lam) * Xb[perm]\n            y_mix = lam * yb + (1 - lam) * yb[perm]\n            gate_target_mix = lam * gate_target + (1 - lam) * gate_target[perm]\n            sample_weights_mix = lam * sample_weights + (1 - lam) * sample_weights[perm]\n            return X_mix, {'main_output': y_mix, 'tof_gate': gate_target_mix}, sample_weights_mix\n        \n        return Xb, {'main_output': yb, 'tof_gate': gate_target}, sample_weights\n\n    def on_epoch_end(self):\n        np.random.shuffle(self.indices)\n\ndef build_gated_two_branch_model(pad_len, imu_dim, tof_dim, n_classes, wd=1e-4):\n    inp = Input(shape=(pad_len, imu_dim+tof_dim))\n    imu = Lambda(lambda t: t[:, :, :imu_dim])(inp)\n    tof = Lambda(lambda t: t[:, :, imu_dim:])(inp)\n    \n    x1 = residual_se_cnn_block(imu, 64, 3, drop=0.1, wd=wd)\n    x1 = residual_se_cnn_block(x1, 128, 5, drop=0.1, wd=wd)\n    \n    x2_base = Conv1D(64, 3, padding='same', use_bias=False, kernel_regularizer=l2(wd))(tof)\n    x2_base = BatchNormalization()(x2_base)\n    x2_base = Activation('relu')(x2_base)\n    x2_base = MaxPooling1D(2)(x2_base)\n    x2_base = Dropout(0.2)(x2_base)\n    \n    x2_base = Conv1D(128, 3, padding='same', use_bias=False, kernel_regularizer=l2(wd))(x2_base)\n    x2_base = BatchNormalization()(x2_base)\n    x2_base = Activation('relu')(x2_base)\n    x2_base = MaxPooling1D(2)(x2_base)\n    x2_base = Dropout(0.2)(x2_base)\n    \n    gate_input = GlobalAveragePooling1D()(tof)\n    gate_input = Dense(16, activation='relu')(gate_input)\n    gate = Dense(1, activation='sigmoid', name='tof_gate')(gate_input)\n    x2 = Multiply()([x2_base, gate])\n    \n    merged = Concatenate()([x1, x2])\n    xa = Bidirectional(LSTM(128, return_sequences=True, kernel_regularizer=l2(wd)))(merged)\n    xb = Bidirectional(GRU(128, return_sequences=True, kernel_regularizer=l2(wd)))(merged)\n    xc = GaussianNoise(0.09)(merged)\n    xc = Dense(16, activation='elu')(xc)\n    x = Concatenate()([xa, xb, xc])\n    x = Dropout(0.4)(x)\n    x = attention_layer(x)\n    \n    for units, drop in [(256, 0.5), (128, 0.3)]:\n        x = Dense(units, use_bias=False, kernel_regularizer=l2(wd))(x)\n        x = BatchNormalization()(x)\n        x = Activation('relu')(x)\n        x = Dropout(drop)(x)\n    \n    out = Dense(n_classes, activation='softmax', name='main_output', kernel_regularizer=l2(wd))(x)\n    return Model(inputs=inp, outputs=[out, gate])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T04:41:37.756376Z","iopub.execute_input":"2025-08-19T04:41:37.756572Z","iopub.status.idle":"2025-08-19T04:41:37.786904Z","shell.execute_reply.started":"2025-08-19T04:41:37.756556Z","shell.execute_reply":"2025-08-19T04:41:37.786328Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## TRAIN/INFERENCE","metadata":{}},{"cell_type":"code","source":"if TRAIN:\n    print(\"----------TRAINING MODE---------\")\n    \n    train = pd.read_csv(BASE_DIR / \"train.csv\")\n    train_dem = pd.read_csv(BASE_DIR / \"train_demographics.csv\")\n    df = pd.merge(train, train_dem, on='subject', how='left')\n    \n    le = LabelEncoder()\n    df['gesture_int'] = le.fit_transform(df['gesture'])\n    np.save(EXPORT_DIR / \"gesture_classes.npy\", le.classes_)\n    print(\"Data loaded | Unique gestures:\", len(le.classes_))\n    \n    bfrb_gestures = [\n        'Above ear - pull hair',\n        'Forehead - pull hairline', \n        'Forehead - scratch',\n        'Eyebrow - pull hair',\n        'Eyelash - pull hair',\n        'Neck - pinch skin',\n        'Neck - scratch',\n        'Cheek - pinch skin'\n    ]\n    \n    print(\"Calculating physics-based features with sequence grouping...\")\n    \n    linear_accel_list = []\n    for _, group in df.groupby('sequence_id'):\n        linear_accel = remove_gravity_from_acc(\n            group[['acc_x', 'acc_y', 'acc_z']], \n            group[['rot_x', 'rot_y', 'rot_z', 'rot_w']]\n        )\n        linear_accel_df = pd.DataFrame(\n            linear_accel,\n            columns=['linear_acc_x', 'linear_acc_y', 'linear_acc_z'],\n            index=group.index\n        )\n        linear_accel_list.append(linear_accel_df)\n    \n    df = pd.concat([df, pd.concat(linear_accel_list)], axis=1)\n    df['linear_acc_mag'] = np.sqrt(df['linear_acc_x']**2 + df['linear_acc_y']**2 + df['linear_acc_z']**2)\n    df['linear_acc_mag_jerk'] = df.groupby('sequence_id')['linear_acc_mag'].diff().fillna(0)\n    \n    angular_vel_list = []\n    for _, group in df.groupby('sequence_id'):\n        angular_vel = calculate_angular_velocity_from_quat(\n            group[['rot_x', 'rot_y', 'rot_z', 'rot_w']]\n        )\n        angular_vel_df = pd.DataFrame(\n            angular_vel,\n            columns=['angular_vel_x', 'angular_vel_y', 'angular_vel_z'],\n            index=group.index\n        )\n        angular_vel_list.append(angular_vel_df)\n    \n    df = pd.concat([df, pd.concat(angular_vel_list)], axis=1)\n    df['angular_vel_mag'] = np.sqrt(df['angular_vel_x']**2 + df['angular_vel_y']**2 + df['angular_vel_z']**2)\n    df['angular_vel_mag_jerk'] = df.groupby('sequence_id')['angular_vel_mag'].diff().fillna(0)\n    \n    angular_dist_list = []\n    for _, group in df.groupby('sequence_id'):\n        angular_dist = calculate_angular_distance(\n            group[['rot_x', 'rot_y', 'rot_z', 'rot_w']]\n        )\n        angular_dist_df = pd.DataFrame(\n            angular_dist,\n            columns=['angular_distance'],\n            index=group.index\n        )\n        angular_dist_list.append(angular_dist_df)\n    \n    df = pd.concat([df, pd.concat(angular_dist_list)], axis=1)\n    df['gesture_rhythm_signature'] = df.groupby('sequence_id')['linear_acc_mag'].transform(\n        lambda x: x.rolling(5, min_periods=1).std() / (x.rolling(5, min_periods=1).mean() + 1e-6)\n    )\n    imu_cols_base = ['linear_acc_x', 'linear_acc_y', 'linear_acc_z'] + [c for c in df.columns if c.startswith('rot_')]\n    imu_engineered = ['linear_acc_mag', 'linear_acc_mag_jerk', 'angular_vel_x', 'angular_vel_y', 'angular_vel_z', 'angular_distance','angular_vel_mag','angular_vel_mag_jerk','gesture_rhythm_signature']\n    imu_cols = list(dict.fromkeys(imu_cols_base + imu_engineered))\n    \n    thm_cols = [c for c in df.columns if c.startswith('thm_')]\n    tof_agg_cols = []\n    for i in range(1, 6):\n        tof_agg_cols.extend([f'tof_{i}_mean', f'tof_{i}_std', f'tof_{i}_min', f'tof_{i}_max'])\n    \n    final_feature_cols = imu_cols + thm_cols + tof_agg_cols\n    imu_dim = len(imu_cols)\n    tof_thm_dim = len(thm_cols) + len(tof_agg_cols)\n    \n    print(f\"Feature dimensions: IMU={imu_dim} | TOF/THM={tof_thm_dim} | Total={len(final_feature_cols)}\")\n    np.save(EXPORT_DIR / \"feature_cols.npy\", np.array(final_feature_cols))\n    \n    print(\"Building sequences...\")\n    seq_gp = df.groupby('sequence_id')\n    X_list_unscaled, y_list, groups_list, lens = [], [], [], []\n    \n    for seq_id, seq_df in seq_gp:\n        seq_df_copy = seq_df.copy()\n        for i in range(1, 6):\n            pixel_cols = [f\"tof_{i}_v{p}\" for p in range(64)]\n            tof_sensor_data = seq_df_copy[pixel_cols].replace(-1, np.nan)\n            seq_df_copy[f'tof_{i}_mean'] = tof_sensor_data.mean(axis=1)\n            seq_df_copy[f'tof_{i}_std'] = tof_sensor_data.std(axis=1)\n            seq_df_copy[f'tof_{i}_min'] = tof_sensor_data.min(axis=1)\n            seq_df_copy[f'tof_{i}_max'] = tof_sensor_data.max(axis=1)\n        \n        mat_unscaled = seq_df_copy[final_feature_cols].ffill().bfill().fillna(0).values.astype('float32')\n        X_list_unscaled.append(mat_unscaled)\n        y_list.append(seq_df_copy['gesture_int'].iloc[0])\n        groups_list.append(seq_df_copy['subject'].iloc[0])\n        lens.append(len(mat_unscaled))\n    \n    print(\"Fitting IMU-Specific StandardScalers...\")\n    all_steps_concatenated = np.concatenate(X_list_unscaled, axis=0)\n    scaler = IMUSpecificScaler().fit(all_steps_concatenated, imu_dim)\n    joblib.dump(scaler, EXPORT_DIR / \"imu_specific_scaler.pkl\")\n    \n    print(\"Scaling and padding sequences with IMU-specific normalization...\")\n    X_scaled_list = [scaler.transform(x_seq) for x_seq in X_list_unscaled]\n    del X_list_unscaled\n    \n    pad_len = int(np.percentile(lens, PAD_PERCENTILE))\n    np.save(EXPORT_DIR / \"sequence_maxlen.npy\", pad_len)\n    \n    X = pad_sequences(X_scaled_list, maxlen=pad_len, padding='post', truncating='post', dtype='float32')\n    del X_scaled_list\n    \n    y_stratify = np.array(y_list)\n    y = to_categorical(y_list, num_classes=len(le.classes_))\n    groups = np.array(groups_list)\n    \n    print(f\"Starting realistic {N_SPLITS}-fold training with dual evaluation...\")\n    sgkf = StratifiedGroupKFold(n_splits=N_SPLITS, shuffle=True, random_state=SEED)\n    fold_realistic_scores = []\n    fold_gesture_scores = []\n\n    for fold, (train_idx, val_idx) in enumerate(sgkf.split(X, y_stratify, groups)):\n        print(f\"\\n{'='*50}\")\n        print(f\"FOLD {fold+1}/{N_SPLITS} - Training in progress\", end='')\n        \n        X_train, X_val = X[train_idx], X[val_idx]\n        y_train, y_val = y[train_idx], y[val_idx]\n        y_val_gestures = le.classes_[y_stratify[val_idx]]\n\n        model = build_gated_two_branch_model(pad_len, imu_dim, tof_thm_dim, len(le.classes_), wd=WD)\n        # Learning rate scheduler setup\n        if USE_LR_SCHEDULER:\n            steps_per_epoch = len(X_train) // BATCH_SIZE\n            total_steps = steps_per_epoch * EPOCHS\n            \n            if LR_SCHEDULE_TYPE == \"cosine_decay\":\n                lr_schedule = CosineDecay(\n                    initial_learning_rate=LR_INIT,\n                    decay_steps=total_steps,\n                    alpha=0.01  # End at 1% of initial LR\n                )\n            elif LR_SCHEDULE_TYPE == \"cosine_decay_restarts\":\n                lr_schedule = CosineDecayRestarts(\n                    initial_learning_rate=LR_INIT,\n                    first_decay_steps=steps_per_epoch * 20,  # Restart every 20 epochs\n                    t_mul=1.2,  # Increase restart period by 20% each time\n                    m_mul=0.8,  # Reduce max LR by 20% each restart\n                    alpha=0.01  # Minimum LR as fraction of initial\n                )\n            elif LR_SCHEDULE_TYPE == \"exponential_decay\":\n                lr_schedule = ExponentialDecay(\n                    initial_learning_rate=LR_INIT,\n                    decay_steps=steps_per_epoch * 10,  # Decay every 10 epochs\n                    decay_rate=0.9,\n                    staircase=False\n                )\n            else:\n                lr_schedule = LR_INIT  # No scheduling\n            \n            optimizer = Adam(learning_rate=lr_schedule)\n        else:\n            optimizer = Adam(LR_INIT)\n        \n        \n        model.compile(\n            optimizer = optimizer,\n            loss={\n                'main_output': tf.keras.losses.CategoricalCrossentropy(label_smoothing=0.1),  \n                'tof_gate': 'binary_crossentropy'\n            },\n            loss_weights={'main_output': 1.0, 'tof_gate': GATE_LOSS_WEIGHT},\n            metrics={'main_output': 'accuracy'}\n        )\n        \n        class_weights = compute_class_weight(\n            'balanced', \n            classes=np.arange(len(le.classes_)), \n            y=y_train.argmax(1)\n        )\n        class_weight_dict = dict(enumerate(class_weights))\n        \n        train_gen = GatedMixupGenerator(\n            X_train, y_train, BATCH_SIZE, imu_dim,\n            class_weight=class_weight_dict, alpha=MIXUP_ALPHA, masking_prob=MASKING_PROB\n        )\n        val_gen = GatedMixupGenerator(X_val, y_val, BATCH_SIZE, imu_dim)\n        enhanced_callback = EnhancedCMIMetricCallback(\n            X_val, y_val_gestures, le.classes_, bfrb_gestures, imu_dim,\n            patience=PATIENCE, verbose=1\n        )\n        \n        model.fit(\n            train_gen, validation_data=val_gen, epochs=EPOCHS,\n            callbacks=[enhanced_callback], verbose=0\n        )\n        \n        final_scores = evaluate_dual_cmi_metric(\n            model, X_val, y_val_gestures, le.classes_, bfrb_gestures, imu_dim\n        )\n        \n        gesture_scores = get_individual_gesture_scores(\n            model, X_val, y_val_gestures, le.classes_, bfrb_gestures\n        )\n        \n        print(f\"\\nFOLD {fold+1} COMPLETED ✓\")\n        print(f\"Composite: {final_scores['composite_score']:.4f} | \"\n              f\"Binary: {final_scores['binary_f1']:.4f} | \"\n              f\"Macro: {final_scores['macro_f1']:.4f}\")\n        print(f\"IMU-only: {final_scores['imu_only_composite']:.4f} | \"\n              f\"Full: {final_scores['full_sensor_composite']:.4f} | \"\n              f\"Gap: {final_scores['sensor_dependency']:.4f}\")\n        \n        print(\"\\nIndividual Gesture F1 Scores:\")\n        for gesture, score in sorted(gesture_scores.items(), key=lambda x: x[1], reverse=True):\n            print(f\"  {gesture[:30]:30} {score:.3f}\")\n        \n        fold_realistic_scores.append(final_scores)\n        fold_gesture_scores.append(gesture_scores)\n        \n        model.save(EXPORT_DIR / f\"gesture_model_fold_{fold}.h5\")\n        print(f\"Model saved: fold_{fold}.h5\")\n\n    print(\"\\n----Training Complete----\")\n    print(\"\\nAverage Results Across All Folds:\")\n    avg_scores = {\n        'composite_score': np.mean([s['composite_score'] for s in fold_realistic_scores]),\n        'macro_f1': np.mean([s['macro_f1'] for s in fold_realistic_scores]),\n        'binary_f1': np.mean([s['binary_f1'] for s in fold_realistic_scores]),\n        'imu_only_composite': np.mean([s['imu_only_composite'] for s in fold_realistic_scores]),\n        'full_sensor_composite': np.mean([s['full_sensor_composite'] for s in fold_realistic_scores]),\n        'sensor_dependency': np.mean([s['sensor_dependency'] for s in fold_realistic_scores])\n    }\n    print(f\"Actual Composite: {avg_scores['composite_score']:.4f}\")\n    print(f\"Macro: {avg_scores['macro_f1']:.4f}\")\n    print(f\"Binary: {avg_scores['binary_f1']:.4f}\")\n    print(f\"Imu: {avg_scores['imu_only_composite']:.4f}\")\n    print(f\"Full: {avg_scores['full_sensor_composite']:.4f}\")\n    print(f\"Sensor Gap: {avg_scores['sensor_dependency']:.4f}\")\n    print(\"\\nGenerating detailed confusion analysis...\")\n    X_val_all = []\n    y_val_all = []\n    for fold, (train_idx, val_idx) in enumerate(sgkf.split(X, y_stratify, groups)):\n        X_val_all.append(X[val_idx])\n        y_val_all.append(y[val_idx])\n    X_val_combined = np.concatenate(X_val_all, axis=0)\n    y_val_combined = np.concatenate(y_val_all, axis=0)\n    \n    models = []\n    for fold in range(N_SPLITS):\n        model = load_model(EXPORT_DIR / f\"gesture_model_fold_{fold}.h5\", \n                           custom_objects={\n                               'time_sum': time_sum,\n                               'squeeze_last_axis': squeeze_last_axis,\n                               'expand_last_axis': expand_last_axis\n                           })\n        models.append(model)\n    cm, misclassifications, gesture_accuracy = create_detailed_confusion_analysis(\n        models, X_val_combined, y_val_combined, le.classes_, bfrb_gestures\n    )\n\nelse:\n    print(\"▶ INFERENCE MODE – loading artifacts from\", PRETRAINED_DIR)\n    \n    # Load all saved artifacts - UPDATED FOR NEW SCALER\n    final_feature_cols = np.load(PRETRAINED_DIR / \"feature_cols.npy\", allow_pickle=True).tolist()\n    pad_len = int(np.load(PRETRAINED_DIR / \"sequence_maxlen.npy\"))\n    scaler = joblib.load(PRETRAINED_DIR / \"imu_specific_scaler.pkl\")  # Updated scaler\n    gesture_classes = np.load(PRETRAINED_DIR / \"gesture_classes.npy\", allow_pickle=True)\n    \n    print(f\"  Loaded feature columns: {len(final_feature_cols)}\")\n    print(f\"  Sequence padding length: {pad_len}\")\n    print(f\"  Gesture classes: {len(gesture_classes)}\")\n    \n    # Define custom objects for model loading\n    custom_objects = {\n        'time_sum': time_sum,\n        'squeeze_last_axis': squeeze_last_axis, \n        'expand_last_axis': expand_last_axis,\n        'se_block': se_block,\n        'residual_se_cnn_block': residual_se_cnn_block,\n        'attention_layer': attention_layer,\n    }\n    \n    # Load ensemble of models\n    models = []\n    print(f\"  Loading {N_SPLITS} models for ensemble inference...\")\n    for fold in range(N_SPLITS):\n        model_path = PRETRAINED_DIR / f\"gesture_model_fold_{fold}.h5\"\n        if model_path.exists():\n            model = load_model(model_path, compile=False, custom_objects=custom_objects)\n            models.append(model)\n            print(f\"    ✓ Loaded fold {fold} model\")\n        else:\n            print(f\"    ✗ Model fold {fold} not found at {model_path}\")\n    \n    print(f\"  Successfully loaded {len(models)} models\")\n    print(\"  Models, scaler, feature_cols, pad_len loaded – ready for evaluation\")\n    \n    def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n        \"\"\"\n        Predict gesture from sequence data using ensemble of trained models\n        \"\"\"\n        try:\n            # Convert to pandas for processing\n            df_seq = sequence.to_pandas()\n            \n            # Calculate physics-based features\n            # 1. Linear acceleration (gravity removed)\n            linear_accel = remove_gravity_from_acc(\n                df_seq[['acc_x', 'acc_y', 'acc_z']], \n                df_seq[['rot_x', 'rot_y', 'rot_z', 'rot_w']]\n            )\n            df_seq['linear_acc_x'] = linear_accel[:, 0]\n            df_seq['linear_acc_y'] = linear_accel[:, 1] \n            df_seq['linear_acc_z'] = linear_accel[:, 2]\n            df_seq['linear_acc_mag'] = np.sqrt(\n                df_seq['linear_acc_x']**2 + df_seq['linear_acc_y']**2 + df_seq['linear_acc_z']**2\n            )\n            df_seq['linear_acc_mag_jerk'] = df_seq['linear_acc_mag'].diff().fillna(0)\n            \n            # 2. Angular velocity from quaternions\n            angular_vel = calculate_angular_velocity_from_quat(\n                df_seq[['rot_x', 'rot_y', 'rot_z', 'rot_w']]\n            )\n            df_seq['angular_vel_x'] = angular_vel[:, 0]\n            df_seq['angular_vel_y'] = angular_vel[:, 1]\n            df_seq['angular_vel_z'] = angular_vel[:, 2]\n            df_seq['angular_vel_mag'] = np.sqrt(df_seq['angular_vel_x']**2 + df_seq['angular_vel_y']**2 + df_seq['angular_vel_z']**2)\n            df_seq['angular_vel_mag_jerk'] = df_seq['angular_vel_mag'].diff().fillna(0)\n            \n            # 3. Angular distance\n            angular_dist = calculate_angular_distance(\n                df_seq[['rot_x', 'rot_y', 'rot_z', 'rot_w']]\n            )\n            df_seq['angular_distance'] = angular_dist\n            df_seq['gesture_rhythm_signature'] = df_seq['linear_acc_mag'].rolling(5, min_periods=1).std() / (df_seq['linear_acc_mag'].rolling(5, min_periods=1).mean() + 1e-6)\n            \n            \n            # 4. TOF sensor aggregations\n            for i in range(1, 6):\n                pixel_cols = [f\"tof_{i}_v{p}\" for p in range(64)]\n                tof_sensor_data = df_seq[pixel_cols].replace(-1, np.nan)\n                \n                df_seq[f'tof_{i}_mean'] = tof_sensor_data.mean(axis=1)\n                df_seq[f'tof_{i}_std'] = tof_sensor_data.std(axis=1)\n                df_seq[f'tof_{i}_min'] = tof_sensor_data.min(axis=1)\n                df_seq[f'tof_{i}_max'] = tof_sensor_data.max(axis=1)\n            \n            # Extract features in the same order as training\n            mat_unscaled = df_seq[final_feature_cols].ffill().bfill().fillna(0).values.astype('float32')\n            \n            # Scale features using training scaler - NOW USING IMU-SPECIFIC SCALING\n            mat_scaled = scaler.transform(mat_unscaled)  # No need to pass imu_dim\n            \n            # Pad sequence to match training length\n            padded_input = pad_sequences(\n                [mat_scaled], \n                maxlen=pad_len, \n                padding='post', \n                truncating='post', \n                dtype='float32'\n            )\n            \n            # Ensemble prediction from all folds\n            all_predictions = []\n            for model in models:\n                # Get main output predictions (ignore gate output)\n                pred = model.predict(padded_input, verbose=0)[0]\n                all_predictions.append(pred)\n            \n            # Average predictions across all models\n            ensemble_pred = np.mean(all_predictions, axis=0)\n            \n            # Return predicted gesture class\n            predicted_class_idx = ensemble_pred.argmax()\n            predicted_gesture = gesture_classes[predicted_class_idx]\n            \n            return str(predicted_gesture)\n            \n        except Exception as e:\n            print(f\"Error in prediction: {e}\")\n            # Return most common gesture as fallback\n            return str(gesture_classes[0])\n    \n    # Set up inference server\n    import kaggle_evaluation.cmi_inference_server\n    inference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n    \n    if os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n        print(\"  Starting competition inference server...\")\n        inference_server.serve()\n    else:\n        print(\"  Starting local inference gateway...\")\n        inference_server.run_local_gateway(\n            data_paths=(\n                '/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv',\n                '/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv'\n            )\n        )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T04:41:37.787773Z","iopub.execute_input":"2025-08-19T04:41:37.787976Z","execution_failed":"2025-08-19T04:45:16.181Z"}},"outputs":[],"execution_count":null}]}