{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":690166,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":523183,"modelId":537191}],"dockerImageVersionId":31040,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\n\nimport pandas as pd\nimport polars as pl\n\nimport kaggle_evaluation.cmi_inference_server","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-05-29T23:31:28.762062Z","iopub.execute_input":"2025-05-29T23:31:28.762435Z","iopub.status.idle":"2025-05-29T23:31:30.304963Z","shell.execute_reply.started":"2025-05-29T23:31:28.762409Z","shell.execute_reply":"2025-05-29T23:31:30.303684Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The evaluation API requires that you set up a server which will respond to inference requests. We have already defined the server; you just need write the predict function. When we evaluate your submission on the hidden test set the client defined in the gateway will run in a different container with direct access to the hidden test set and hand off the one sequence at a time.\n\nYour code will always have access to the published copies of the files.","metadata":{"execution":{"iopub.status.busy":"2025-05-29T22:29:18.955791Z","iopub.execute_input":"2025-05-29T22:29:18.956193Z","iopub.status.idle":"2025-05-29T22:29:18.965179Z","shell.execute_reply.started":"2025-05-29T22:29:18.956169Z","shell.execute_reply":"2025-05-29T22:29:18.963797Z"}}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# SUBMISSION-READY INCEPTIONTIME MODEL FOR KAGGLE\n# ============================================================\n\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset\nfrom sklearn.preprocessing import LabelEncoder\nimport joblib\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# ============================================================\n# CONFIGURATION (MUST MATCH TRAINING)\n# ============================================================\n\nWINDOW_BEFORE = 30\nWINDOW_AFTER = 30\nWINDOW_LEN = WINDOW_BEFORE + WINDOW_AFTER + 1\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\n# TOF/Thermal configuration (from suggestions)\nTOF_MISSING_VALUE = -1  # Use -1 for missing TOF data (not 0!)\nTOF_MAX_RANGE = 300  # Max reasonable TOF value\nTHERM_MISSING_VALUE = -100  # Use -100 for missing thermal data\n\n# ============================================================\n# PREPROCESSING FUNCTIONS\n# ============================================================\n\ndef simple_quaternion_normalization(rot_data: np.ndarray) -> np.ndarray:\n    \"\"\"Fast quaternion normalization\"\"\"\n    rot_norm = rot_data.copy()\n    norms = np.linalg.norm(rot_data, axis=1, keepdims=True)\n    norms[norms < 1e-6] = 1.0\n    return rot_norm / norms\n\ndef preprocess_tof_data(tof_data: np.ndarray) -> np.ndarray:\n    \"\"\"\n    Preprocess TOF data according to suggestions:\n    - Convert -1 (missing) to TOF_MAX_RANGE (300)\n    - In simulation, set missing values to TOF_MISSING_VALUE (-1)\n    \"\"\"\n    tof_processed = tof_data.copy()\n    \n    # Convert -1 to TOF_MAX_RANGE for actual measurements\n    tof_processed[tof_processed == -1] = TOF_MAX_RANGE\n    \n    # Clip to reasonable range\n    tof_processed = np.clip(tof_processed, 0, TOF_MAX_RANGE)\n    \n    return tof_processed\n\ndef preprocess_thermal_data(therm_data: np.ndarray) -> np.ndarray:\n    \"\"\"\n    Preprocess thermal data:\n    - Handle missing values\n    - Normalize\n    \"\"\"\n    therm_processed = therm_data.copy()\n    \n    # Replace NaN with missing value\n    therm_processed = np.nan_to_num(therm_processed, nan=THERM_MISSING_VALUE)\n    \n    return therm_processed\n\ndef extract_features(acc, rot, tof=None, therm=None, simulate_imu_only=False):\n    \"\"\"\n    Extract features (must match training)\n    Now supports IMU + TOF + Thermal data\n    \"\"\"\n    # Handle empty arrays\n    if len(acc) == 0:\n        return np.zeros((WINDOW_LEN, 23), dtype=np.float32)  # Increased for TOF+Thermal\n    \n    # Normalize acceleration\n    acc_mean = acc.mean(0)\n    acc_std = acc.std(0) + 1e-6\n    acc_norm = (acc - acc_mean) / acc_std\n    \n    # Normalize quaternions\n    rot_norm = simple_quaternion_normalization(rot)\n    \n    # Process TOF data if available\n    if tof is not None and not simulate_imu_only:\n        tof_processed = preprocess_tof_data(tof)\n        # Normalize TOF (0 to TOF_MAX_RANGE -> 0 to 1)\n        tof_norm = tof_processed / TOF_MAX_RANGE\n    else:\n        # Set TOF to missing value (-1) for IMU-only simulation\n        tof_norm = np.full((len(acc), 1), TOF_MISSING_VALUE, dtype=np.float32)\n    \n    # Process thermal data if available\n    if therm is not None and not simulate_imu_only:\n        therm_processed = preprocess_thermal_data(therm)\n        # Simple normalization for thermal\n        therm_mean = therm_processed.mean(0)\n        therm_std = therm_processed.std(0) + 1e-6\n        therm_norm = (therm_processed - therm_mean) / therm_std\n    else:\n        # Set thermal to missing value for IMU-only simulation\n        therm_norm = np.full((len(acc), 3), THERM_MISSING_VALUE, dtype=np.float32)\n    \n    # Add magnitude features\n    acc_magnitude = np.linalg.norm(acc_norm, axis=1, keepdims=True)\n    rot_magnitude = np.linalg.norm(rot_norm, axis=1, keepdims=True)\n    \n    # Add temporal differences\n    if len(acc_norm) > 1:\n        acc_diff = np.diff(acc_norm, axis=0, prepend=acc_norm[0:1])\n        rot_diff = np.diff(rot_norm, axis=0, prepend=rot_norm[0:1])\n        \n        # TOF and thermal differences if available\n        if tof is not None and not simulate_imu_only:\n            tof_diff = np.diff(tof_norm, axis=0, prepend=tof_norm[0:1])\n        else:\n            tof_diff = np.zeros_like(tof_norm)\n            \n        if therm is not None and not simulate_imu_only:\n            therm_diff = np.diff(therm_norm, axis=0, prepend=therm_norm[0:1])\n        else:\n            therm_diff = np.zeros_like(therm_norm)\n    else:\n        acc_diff = np.zeros_like(acc_norm)\n        rot_diff = np.zeros_like(rot_norm)\n        tof_diff = np.zeros_like(tof_norm)\n        therm_diff = np.zeros_like(therm_norm)\n    \n    # Combine features (23 total with TOF+Thermal)\n    features = np.concatenate([\n        acc_norm,        # 3\n        rot_norm,        # 4\n        acc_magnitude,   # 1\n        rot_magnitude,   # 1\n        tof_norm,        # 1 (TOF)\n        therm_norm,      # 3 (thermal_x, thermal_y, thermal_z)\n        acc_diff,        # 3\n        rot_diff,        # 4\n        tof_diff,        # 1 (TOF diff)\n        therm_diff       # 2 (thermal diff, reduced from 3)\n    ], axis=1)\n    \n    # Ensure we have exactly 23 features\n    if features.shape[1] > 23:\n        features = features[:, :23]\n    elif features.shape[1] < 23:\n        # Pad if needed\n        pad_width = ((0, 0), (0, 23 - features.shape[1]))\n        features = np.pad(features, pad_width, mode='constant')\n    \n    return features\n\nclass InceptionModule(nn.Module):\n    \"\"\"Improved Inception module with better feature extraction\"\"\"\n    def __init__(self, in_channels, out_channels):\n        super().__init__()\n        \n        # Branch 1: 1x1 convolution\n        self.branch1 = nn.Conv1d(in_channels, out_channels, kernel_size=1)\n        \n        # Branch 2: 1x1 -> 3x3 convolution\n        self.branch2 = nn.Sequential(\n            nn.Conv1d(in_channels, out_channels, kernel_size=1),\n            nn.Conv1d(out_channels, out_channels, kernel_size=3, padding=1),\n        )\n        \n        # Branch 3: 1x1 -> 5x5 convolution\n        self.branch3 = nn.Sequential(\n            nn.Conv1d(in_channels, out_channels, kernel_size=1),\n            nn.Conv1d(out_channels, out_channels, kernel_size=5, padding=2),\n        )\n        \n        # Branch 4: 3x3 max pool -> 1x1 convolution\n        self.branch4 = nn.Sequential(\n            nn.MaxPool1d(kernel_size=3, stride=1, padding=1),\n            nn.Conv1d(in_channels, out_channels, kernel_size=1),\n        )\n        \n        # Batch normalization and activation\n        self.bn = nn.BatchNorm1d(out_channels * 4)\n        self.activation = nn.ReLU()\n        \n        # Residual connection\n        self.residual = None\n        if in_channels != out_channels * 4:\n            self.residual = nn.Conv1d(in_channels, out_channels * 4, kernel_size=1)\n        \n    def forward(self, x):\n        # Store residual\n        residual = x\n        \n        # Process through all branches\n        out1 = self.branch1(x)\n        out2 = self.branch2(x)\n        out3 = self.branch3(x)\n        out4 = self.branch4(x)\n        \n        # Concatenate\n        out = torch.cat([out1, out2, out3, out4], dim=1)\n        \n        # Apply residual if needed\n        if self.residual is not None:\n            residual = self.residual(residual)\n        \n        # Add residual and apply batch norm + activation\n        out = out + residual\n        out = self.activation(self.bn(out))\n        \n        return out\n\nclass ImprovedInceptionTime(nn.Module):\n    \"\"\"Improved InceptionTime for IMU+TOF+Thermal data\"\"\"\n    def __init__(self, in_channels, num_classes, bottleneck=True):\n        super().__init__()\n        \n        # Initial convolution to increase channels\n        self.initial_conv = nn.Sequential(\n            nn.Conv1d(in_channels, 32, kernel_size=7, padding=3),\n            nn.BatchNorm1d(32),\n            nn.ReLU(),\n        )\n        \n        # Inception modules with increasing complexity\n        self.inception1 = InceptionModule(32, 32)  # Output: 128 channels\n        self.inception2 = InceptionModule(128, 32) # Output: 128 channels\n        self.inception3 = InceptionModule(128, 32) # Output: 128 channels\n        self.inception4 = InceptionModule(128, 32) # Output: 128 channels\n        \n        # Global pooling\n        self.global_pool = nn.AdaptiveAvgPool1d(1)\n        \n        # Optional bottleneck layer (reduces overfitting)\n        self.bottleneck = bottleneck\n        if bottleneck:\n            self.fc1 = nn.Linear(128, 64)\n            self.bn1 = nn.BatchNorm1d(64)\n            self.dropout = nn.Dropout(0.3)\n            self.fc2 = nn.Linear(64, num_classes)\n        else:\n            self.fc = nn.Linear(128, num_classes)\n        \n    def forward(self, x):\n        # Initial feature extraction\n        x = self.initial_conv(x)\n        \n        # Stack of inception modules\n        x = self.inception1(x)\n        x = self.inception2(x)\n        x = self.inception3(x)\n        x = self.inception4(x)\n        \n        # Global pooling\n        x = self.global_pool(x)\n        x = x.view(x.size(0), -1)\n        \n        # Classification\n        if self.bottleneck:\n            x = F.relu(self.bn1(self.fc1(x)))\n            x = self.dropout(x)\n            x = self.fc2(x)\n        else:\n            x = self.fc(x)\n        \n        return x\n\nclass GesturePredictor:\n    def __init__(self, model_path=\"inceptiontime_model.pth\", \n                 encoder_path=\"label_encoder.pkl\",\n                 metadata_path=\"model_metadata.pkl\"):\n        \"\"\"Load trained model for inference\"\"\"\n        try:\n            # Load metadata\n            metadata = joblib.load(metadata_path)\n            \n            # Load label encoder\n            self.label_encoder = joblib.load(encoder_path)\n            self.classes_ = self.label_encoder.classes_\n            \n            # Initialize model - UPDATED for 23 features (IMU+TOF+Thermal)\n            self.model = ImprovedInceptionTime(\n                in_channels=23,  # UPDATED: Now 23 features with TOF+Thermal\n                num_classes=len(self.classes_)\n            ).to(DEVICE)\n            \n            # Load weights\n            self.model.load_state_dict(\n                torch.load(model_path, map_location=DEVICE, weights_only=False)\n            )\n            self.model.eval()\n            \n            print(f\"✅ Model loaded successfully!\")\n            print(f\"   Classes: {list(self.classes_)}\")\n            print(f\"   Input channels: 23 (IMU+TOF+Thermal)\")\n            print(f\"   Window length: {WINDOW_LEN}\")\n            print(f\"   TOF missing value: {TOF_MISSING_VALUE}\")\n            print(f\"   Thermal missing value: {THERM_MISSING_VALUE}\")\n            \n        except Exception as e:\n            print(f\"❌ Error loading model: {e}\")\n            raise\n    \n    def find_activity_window(self, acc_data):\n        \"\"\"\n        Find the most active window in the sequence when phase is not available.\n        Uses variance of acceleration magnitude to find interesting parts.\n        \"\"\"\n        if len(acc_data) < WINDOW_LEN:\n            # If sequence is shorter than window, use the whole sequence\n            return 0, len(acc_data)\n        \n        # Calculate acceleration magnitude\n        acc_magnitude = np.linalg.norm(acc_data, axis=1)\n        \n        # Find window with maximum variance (most activity)\n        max_variance = -1\n        best_start = 0\n        \n        # Slide window through sequence\n        for start in range(0, len(acc_data) - WINDOW_LEN + 1, WINDOW_LEN // 4):\n            end = start + WINDOW_LEN\n            window_magnitude = acc_magnitude[start:end]\n            variance = np.var(window_magnitude)\n            \n            if variance > max_variance:\n                max_variance = variance\n                best_start = start\n        \n        return best_start, best_start + WINDOW_LEN\n    \n    def check_sensor_availability(self, sequence_df):\n        \"\"\"Check which sensors are available in the sequence\"\"\"\n        cols = sequence_df.columns\n        \n        has_tof = any('tof' in col.lower() for col in cols)\n        has_thermal = any('thermal' in col.lower() for col in cols)\n        \n        return has_tof, has_thermal\n    \n    def prepare_window(self, sequence_df):\n        \"\"\"Prepare a window from sequence for prediction\"\"\"\n        # Convert to pandas for easier processing\n        if hasattr(sequence_df, 'to_pandas'):\n            df = sequence_df.to_pandas()\n        else:\n            df = sequence_df\n        \n        # Check sensor availability\n        has_tof, has_thermal = self.check_sensor_availability(df)\n        \n        # Get accelerometer and rotation data\n        acc_cols = [\"acc_x\", \"acc_y\", \"acc_z\"]\n        rot_cols = [\"rot_w\", \"rot_x\", \"rot_y\", \"rot_z\"]\n        \n        # Get TOF data if available\n        tof_cols = []\n        if has_tof:\n            # Find TOF column (could be 'tof', 'tof_distance', etc.)\n            for col in df.columns:\n                if 'tof' in col.lower():\n                    tof_cols = [col]\n                    break\n        \n        # Get thermal data if available\n        therm_cols = []\n        if has_thermal:\n            # Find thermal columns\n            therm_cols = [col for col in df.columns if 'thermal' in col.lower()]\n            # Take up to 3 thermal columns\n            therm_cols = therm_cols[:3]\n        \n        # Check if phase column exists\n        has_phase = \"phase\" in df.columns\n        \n        if has_phase and \"Gesture\" in df[\"phase\"].values:\n            # If phase column exists and has \"Gesture\", use it\n            gesture_rows = df[df[\"phase\"] == \"Gesture\"]\n            if len(gesture_rows) > 0:\n                center_idx = gesture_rows.index[0]\n                \n                start_idx = max(0, center_idx - WINDOW_BEFORE)\n                end_idx = min(len(df), center_idx + WINDOW_AFTER + 1)\n                \n                acc_data = df.iloc[start_idx:end_idx][acc_cols].values.astype(np.float32)\n                rot_data = df.iloc[start_idx:end_idx][rot_cols].values.astype(np.float32)\n                \n                # Get TOF data if available\n                tof_data = None\n                if has_tof and len(tof_cols) > 0:\n                    tof_data = df.iloc[start_idx:end_idx][tof_cols].values.astype(np.float32)\n                \n                # Get thermal data if available\n                therm_data = None\n                if has_thermal and len(therm_cols) > 0:\n                    therm_data = df.iloc[start_idx:end_idx][therm_cols].values.astype(np.float32)\n            else:\n                # No gesture phase found\n                has_phase = False\n        else:\n            # No phase column or no gesture phase - find most active window\n            has_phase = False\n        \n        if not has_phase:\n            # Find most active window based on acceleration\n            acc_data_full = df[acc_cols].values.astype(np.float32)\n            start_idx, end_idx = self.find_activity_window(acc_data_full)\n            \n            acc_data = df.iloc[start_idx:end_idx][acc_cols].values.astype(np.float32)\n            rot_data = df.iloc[start_idx:end_idx][rot_cols].values.astype(np.float32)\n            \n            # Get TOF data if available\n            tof_data = None\n            if has_tof and len(tof_cols) > 0:\n                tof_data = df.iloc[start_idx:end_idx][tof_cols].values.astype(np.float32)\n            \n            # Get thermal data if available\n            therm_data = None\n            if has_thermal and len(therm_cols) > 0:\n                therm_data = df.iloc[start_idx:end_idx][therm_cols].values.astype(np.float32)\n        \n        # Handle missing values in IMU data\n        acc_data = np.nan_to_num(acc_data)\n        rot_data = np.nan_to_num(rot_data)\n        \n        # Extract features - simulate IMU-only if TOF/Thermal are not available\n        simulate_imu_only = not (has_tof and has_thermal)\n        features = extract_features(\n            acc_data, \n            rot_data, \n            tof=tof_data, \n            therm=therm_data,\n            simulate_imu_only=simulate_imu_only\n        )\n        \n        # Pad if necessary\n        if len(features) < WINDOW_LEN:\n            pad_len = WINDOW_LEN - len(features)\n            features = np.pad(features, ((0, pad_len), (0, 0)), mode='constant')\n        \n        # Trim to exact window length\n        features = features[:WINDOW_LEN]\n        \n        # Transpose for model (channels first)\n        return features.T  # Shape: (23, WINDOW_LEN)\n    \n    def predict(self, sequence_df):\n        \"\"\"Predict gesture from sequence\"\"\"\n        try:\n            # Prepare window\n            features = self.prepare_window(sequence_df)\n            \n            # Convert to tensor\n            features_tensor = torch.tensor(features, dtype=torch.float32).unsqueeze(0).to(DEVICE)\n            \n            # Predict\n            with torch.no_grad():\n                outputs = self.model(features_tensor)\n                probs = F.softmax(outputs, dim=1)\n                pred_idx = outputs.argmax(dim=1).item()\n            \n            # Get prediction\n            prediction = self.classes_[pred_idx]\n            confidence = probs[0, pred_idx].item()\n            \n            return prediction, confidence\n            \n        except Exception as e:\n            print(f\"❌ Error in prediction: {e}\")\n            # Return default prediction\n            return \"Text on phone\", 0.0\n\n# ============================================================\n# KAGGLE PREDICTION FUNCTION\n# ============================================================\n\n# Global predictor instance\n_predictor = None\n\ndef get_predictor():\n    \"\"\"Lazy loader for predictor\"\"\"\n    global _predictor\n    if _predictor is None:\n        print(\"📦 Loading model for inference...\")\n        try:\n            # Paths for Kaggle submission\n            model_paths = [\n                \"/kaggle/input/inceptiontime/pytorch/default/1/inceptiontime_model.pth\",\n            ]\n            \n            encoder_paths = [\n                \"/kaggle/input/inceptiontime/pytorch/default/1/label_encoder.pkl\",\n            ]\n            \n            metadata_paths = [\n                \"/kaggle/input/inceptiontime/pytorch/default/1/model_metadata.pkl\",\n            ]\n            \n            # Try different paths\n            for model_path, encoder_path, metadata_path in zip(model_paths, encoder_paths, metadata_paths):\n                try:\n                    _predictor = GesturePredictor(\n                        model_path=model_path,\n                        encoder_path=encoder_path,\n                        metadata_path=metadata_path\n                    )\n                    print(f\"✅ Model loaded from {model_path}\")\n                    break\n                except:\n                    continue\n            \n            if _predictor is None:\n                print(\"⚠️ Could not load model, using dummy predictor\")\n                # Create dummy predictor as fallback\n                class DummyPredictor:\n                    classes_ = [\"Text on phone\", \"Eating\", \"Drinking\", \"Brushing teeth\", \"Using laptop\"]\n                    def predict(self, seq):\n                        return \"Text on phone\", 1.0\n                _predictor = DummyPredictor()\n                \n        except Exception as e:\n            print(f\"❌ Error loading predictor: {e}\")\n            raise\n    \n    return _predictor\n\n\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    try:\n        # Get predictor\n        predictor = get_predictor()\n        \n        # Make prediction\n        prediction, confidence = predictor.predict(sequence)\n        \n        # Optional: Log for debugging\n        seq_length = len(sequence)\n        print(f\"📊 Sequence length: {seq_length}, Prediction: {prediction} (confidence: {confidence:.3f})\")\n        \n        return prediction\n        \n    except Exception as e:\n        print(f\"❌ Prediction error: {e}\")\n        # Return a sensible default\n        return \"Text on phone\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-29T23:31:30.306452Z","iopub.execute_input":"2025-05-29T23:31:30.306956Z","iopub.status.idle":"2025-05-29T23:31:30.312445Z","shell.execute_reply.started":"2025-05-29T23:31:30.306918Z","shell.execute_reply":"2025-05-29T23:31:30.311257Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"inference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\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-05-29T23:31:30.313433Z","iopub.execute_input":"2025-05-29T23:31:30.313835Z","iopub.status.idle":"2025-05-29T23:31:31.736421Z","shell.execute_reply.started":"2025-05-29T23:31:30.313805Z","shell.execute_reply":"2025-05-29T23:31:31.734779Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}