{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Damage Localization in Grid System Using Support Vector Machine\n",
    "\n",
    "Muhammad Ibnu Syamsi[1][*] and Rifqi Damar Panuluh[2]\n",
    "\n",
    "[1]: Teknik Sipil, Universitas Muhammadiyah Yogyakarta, Yogyakarta, Indonesia\n",
    "\n",
    "[2]: Teknik Sipil, Universitas Muhammadiyah Yogyakarta, Yogyakarta, Indonesia\n",
    "\n",
    "[*]: Email: syamsibnu@umy.ac.id\n",
    "\n",
    "**Abstract.** Structural Health Monitoring (SHM) plays a vital role in ensuring the safety and integrity of civil structures. However, traditional SHM systems often rely on dense sensor networks and complex analytical or numerical models, which may be costly, difficult to deploy, and impractical for real-world applications. This study aims to investigate whether vibration signals acquired from a minimal sensor configuration—specifically, a single pair of sensors—can provide sufficient information for accurate structural damage location classification. The research utilizes the Qatar University Grandstand Simulator (QUGS) dataset, which represents a controlled steel-frame structure consisting of a 6×5 joint grid, allowing systematic evaluation of damage scenarios. Vibration signals collected from the structure are transformed into time-frequency domain features using the Short-Time Fourier Transform (STFT), which captures both temporal and spectral characteristics. These features are then classified using Support Vector Machines (SVM), a widely used supervised machine learning algorithm. Cross-validation is employed to evaluate the generalization performance of the model across different sensor paths. The results demonstrate that even with limited sensor data, classification accuracy consistently exceeds 90% across multiple damage scenarios. These findings suggest that with proper feature extraction and sensor placement, effective and interpretable SHM systems can be achieved using significantly fewer resources, paving the way for simpler, low-cost damage detection solutions in structural engineering.\n",
    "\n",
    "**Keywords:** damage localization, vibration, support vector machine, short-time fourier transform, grid system"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Data Acquisition\n",
    "\n",
    "The dataset employed in this research originates from a publicly accessible vibration database published by [3](doi.org/10.1016/j.jsv.2016.10.043). This dataset was generated through controlled experimental procedures using the Qatar's University Grandstand Simulator (QUGS), a specialized steel-frame structure specifically designed for systematic and repeatable evaluations of structural health monitoring techniques.\n",
    "\n",
    "### Instrumentation and Setup\n",
    "<!-- ![The Qatar’s University Grandstand Simulator setup with numbered layout of the 30 beam-to-girder joints on the test structure used to simulate structural damage.](https://www.structuralvibration.com/img/i2.png) -->\n",
    "<!-- ![Loosened bolts](https://www.structuralvibration.com/img/i3.png)\n",
    "![(a) PCB 393B04 (b) B&K 8344](https://www.structuralvibration.com/img/i4.png)\n",
    "![modal shaker machine (TMS 2100E11](https://www.structuralvibration.com/img/i5.png)\n",
    "![data acquisition devices (DT9857E-16) along with the shaker’s power amplifier (SmartAmp 2100E21-400)](https://www.structuralvibration.com/img/i6.png) -->\n",
    "\n",
    "Organized systematically, the dataset comprises two primary folders: Dataset A, designated for model training, and Dataset B, intended for testing. Each folder contains 31 files named according to structural conditions. The undamaged structure conditions are represented by files named zzzAU.TXT and zzzBU.TXT. Damaged conditions, reflecting single-joint damage scenarios, are named sequentially from zzzAD1.TXT to zzzAD30.TXT for Dataset A, and from zzzBD1.TXT to zzzBD30.TXT for Dataset B. During acquiring data, white noise is also given to the shaker machine.\n",
    "\n",
    "Each file begins with ten lines of metadata providing details about the test configurations, sampling rates, and specific channel information. The acceleration data commences from line 11, organized into 31 columns: the first column indicating time in seconds, and columns 2 to 31 containing acceleration magnitudes from joints 1 through 30.\n",
    "\n",
    "All vibration signals were captured at a consistent sampling frequency of 1024 Hz over a duration of 256 seconds, yielding 262,144 samples per channel. Thus, each file, representing either damaged or undamaged conditions, can be succinctly represented as a matrix with dimensions of 262,144 rows by 30 columns."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Data Preprocessing\n",
    "\n",
    "All raw vibration signals are stored in text files named zzzAD.TXT, where corresponds to each sensor node. Each file contains all nodes 262,144-sample time series recorded at 1024 Hz over 256 seconds, representing one damage scenario at a specific joint node."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Data grouping"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from src.data_preprocessing import complement_pairs, generate_df_tuples, DataProcessor"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Generate undamaged case index from complementary pairs for each damaged file of Dataset A\n",
    "\n",
    "The undamaged case were constructed using complementary rule to ensure  a balanced dataset. For each damaged file `zzzAD{n}` associated with $a$ specific bottom Sensor A, the signals from the other four bottom sensors (and their corresponding top ensors) in that column group were treated as “undamaged” for that specific test. Detailed group pattern is shown in following figure in the \"Feature Extraction\" section below where for each damaged scenario, a set of complementary “healthy” pairs $[a^\\prime,\\ b^\\prime]$ is generated where $a^\\prime$ includes all column indices in ${1,2,3,4,5}$ except the original $a$. \n",
    "\n",
    "For example, `zzzAD1.TXT` data only took column $[1,6,11,16,21,26]$ to be used in $d_1$ while its complement, the other four column: $[[2,7,12,17,22,27], [3,8,13,18,23,28], [4,9,14,19,24,29]$ and $[5,10,15,20,25,30]]$ will be used in $d_0$. This yielding to $120$ sensor pair $\\left[a^\\prime,b^\\prime\\right]$ that goes into STFT processing. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "a_complement = [(comp)\n",
    "                for n in range(1, 31)\n",
    "                for comp in complement_pairs(n, prefix=\"zzzAD\", extension=\"TXT\")]\n",
    "print(len(a_complement))  # Display the total number of pairs\n",
    "a_complement[:10] # Display only the first 10 pairs"
   ]
  },
  {
   "attachments": {
    "A4 - 7.png": {
     "image/png": "[encoded data, 272688 characters]"
    }
   },
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Generate damaged case (d1--d6)\n",
    "\n",
    "For  analysis, the 30 nodes indexed $n = {1,2,3,...30}$. Each file is named `AD{n}.TXT` are first grouped into six damage cases, each encompassing five consecutive joints. As shown in following figure, each grey block represents a damaged-case file (AD01–AD30), containing vibration signals from 30 sensor nodes (S1–S30). For each damage case, only five are grouped into one damage group $(d_1–d_6)$. These five element for each group will be called as test number (`test_num`) later. Within each group, only two sensor nodes—Sensor A (bottom-most) and Sensor B (top-most)—are retained for analysis represented as $[a, b]$ pair. Sensor nodes shown with transparency indicate intermediate nodes that are stripped out during preprocessing, leaving only the top and bottom measurements to simulate minimal sensor configurations. Red-highlighted sensors denote the actual damaged joints, which define the ground truth labels for classification. This means the pair $[a, b]$ for given damage file `zzzAD{n}` is defined by formulas $a=\\left(\\left(n-1\\right)\\ mod\\ 5\\right)+1$ and $b=a+25$. \n",
    "\n",
    "![Visual illustration of the dataset grouping and sensor selection strategy.](<attachment:A4 - 7.png>)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "a = generate_df_tuples(special_groups=a_complement, prefix=\"zzzAD\")\n",
    "a[1:6]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Process raw data\n",
    "\n",
    "The raw data is processed to extract relevant features for classification using previous generated index."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_A = DataProcessor(file_index=a, base_path=\"D:/thesis/data/dataset_A\", include_time=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Export preprocessed data (Dataset A)\n",
    "\n",
    "This step involves exporting the preprocessed data into a structured format suitable for further analysis and modeling. The processed data is saved in a CSV file seperated on each damage case label folder, each containing the vibration signals from the respective sensors."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_A.export_to_csv(\"D:/thesis/data/converted/raw\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Each folder damage case contains bunch of CSV with format naming `DAMAGE_{label}_TEST{test_num}_{sensor}.csv`, where `label` is the damage case label, `test_num` is the test number, and `sensor` is the sensor type (A or B)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Example for undamaged case (DAMAGE_0) processed data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Example for undamaged case (DAMAGE_0) processed data\n",
    "print(len(os.listdir(\"D:/thesis/data/converted/raw/DAMAGE_0\")))\n",
    "os.listdir(\"D:/thesis/data/converted/raw/DAMAGE_0\")[:10]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Example for other damaged case (DAMAGE_{n > 0}) processed data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Example for damaged case (d1--d6) processed data\n",
    "for n in range(1,7):\n",
    "    print(os.listdir(f\"D:/thesis/data/converted/raw/DAMAGE_{n}\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Feature Extraction"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "from scipy.signal.windows import hann\n",
    "import multiprocessing"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The feature extraction technique in this proposed method incorporates Short Time Fourier Transform (STFT) with Hann windowing size set to 1024 and 512 as hop size to convert time-series signal into a time time–frequency representation. The STFT output—a 513×513 spectrogram with 513 frequency bins and 513 timeframes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Define STFT parameters\n",
    "window_size = 1024\n",
    "hop_size = 512\n",
    "window = hann(window_size)\n",
    "Fs = 1024"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Perform STFT for each damage case\n",
    "\n",
    "Each  spectrogram is treated as a collection of feature vectors by extracting its rows sequentially, where each row corresponds to a single time frame and contains 513 frequency components. \n",
    "\n",
    "For every damage case, five time-series signals are available—one from each joint in the group—resulting in five spectrograms per case. Since each spectrogram contains 513 timeframes, this yields $5 \\times 513=2565$ feature vectors per damage case for each sensor. With one undamaged case $(d_0)$ and six distinct damage cases $(d_1—d_6)$ there are seven classes in total. This process generates $7 \\times 2565=17955$ feature vectors per sensor side (Sensor A and B). Each vector has $513$ features, leading to a final dataset of shape $17955 \\times 513$ for Sensor A and likewise for Sensor B. Each vector is paired with a class label from 0 to 6, indicating the corresponding undamaged-case and damaged-case group."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The following code use a custom function `process_damage_case` to process each damage case in parallel using Python's `multiprocessing` module. This function is defined in the `src.process_stft` module, which handles the STFT processing and data export for all damage case including undamage case."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Define the base directory where DAMAGE_X folders are located\n",
    "damage_base_path = 'D:/thesis/data/converted/raw'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The path above is specifically intended to process STFT data using Dataset A"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Define output directories for each sensor exported data\n",
    "output_dirs = {\n",
    "    'sensorA': os.path.join(damage_base_path, 'sensorA'),\n",
    "    'sensorB': os.path.join(damage_base_path, 'sensorB')\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Import custom in-house functions\n",
    "from src.process_stft import process_damage_case\n",
    "\n",
    "num_damage_cases = 7  # DAMAGE_0 to DAMAGE_6\n",
    "\n",
    "with multiprocessing.Pool() as pool:\n",
    "    # Process each DAMAGE_X case in parallel\n",
    "    pool.map(process_damage_case, range(num_damage_cases))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Load STFT data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Exported data is stored in csv seperated on each sensor folder. Inside each folder, the data is organized into subfolders named according to the damage case label (e.g., `0`, `1`,`2`, etc.). Each subfolder contains CSV files named in the format `stft_data{sensor}_{label}.csv`, where `{sensor}` indicates whether the data is from Sensor A or Sensor B, and `{label}` is the damage case label"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for n in [1,2]:\n",
    "    print(os.listdir(f'D:/thesis/data/converted/raw/sensor{n}'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Load the processed data for Sensor 1\n",
    "import os\n",
    "import pandas as pd\n",
    "\n",
    "ready_data1a: list[pd.DataFrame] = []\n",
    "for file in os.listdir('D:/thesis/data/converted/raw/sensor1'):\n",
    "    ready_data1a.append(pd.read_csv(os.path.join('D:/thesis/data/converted/raw/sensor1', file), skiprows=1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Load the processed data for Sensor 2\n",
    "ready_data2a: list[pd.DataFrame] = []\n",
    "for file in os.listdir('D:/thesis/data/converted/raw/sensor2'):\n",
    "    ready_data2a.append(pd.read_csv(os.path.join('D:/thesis/data/converted/raw/sensor2', file), skiprows=1))"
   ]
  },
  {
   "attachments": {
    "image.png": {
     "image/png": "[encoded data, 373980 characters]"
    }
   },
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Undamaged case (d0)\n",
    "![Overview of the signal preprocessing for undamaged case](attachment:image.png)\n",
    "The figure above shows the overview of the signal preprocessing pipeline for undamaged case $(d_0)$. Notice that undamaged case $(d_0)$ is formed from complementary of each `zzzAD{n}` in damage case $(d_1—d_6)$ as explained in [Generate undamaged case index from complementary pairs for each damaged file of Dataset A](#Generate-undamaged-case-index-from-complementary-pairs-for-each-damaged-file-of-Dataset-A).\n",
    "\n",
    "To balance the data for undamaged case $(d_0)$, each produced STFT from $d_0$ ($513 \\times 513$) will only be took its first $22$ timeframes for the first 45 $(\\verb|{test_num}|\\le 45)$ input data and its first $21$ timeframes for the rest of the input data $(\\verb|{test_num}|>45)$. This number was determined by solving linear equation \n",
    "\n",
    "$$\n",
    "\\begin{align*}\n",
    "\\frac{2565}{120} = 21.375\n",
    "\\begin{cases}\n",
    "21 & \\text{or} \\\\\n",
    "22\n",
    "\\end{cases}\n",
    "\\end{align*}\n",
    "$$\n",
    "\n",
    "$$\n",
    "\\begin{align*}\n",
    "21x + 22y &= 2565 \\\\\n",
    "x + y &= 120 \\\\\n",
    "\\end{align*}\n",
    "$$\n",
    "\n",
    "$$\n",
    "y = 45, x = 75\n",
    "$$\n",
    "\n",
    "to achieve same samples number $(2565)$ for all labels $(d_0—d_6)$."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### Preview Plot"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###### Function plot"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "from typing import Union"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def preview_stft(data: Union[pd.DataFrame, list[pd.DataFrame]], x_num_ticks: int = 6, y_num_ticks: int = 5, cols: int=3):\n",
    "    \"\"\"\n",
    "    Preview the Short-Time Fourier Transform (STFT) of the given data.\n",
    "\n",
    "    Parameters:\n",
    "    -------\n",
    "    data (pd.DataFrame): The STFT data to be visualized.\n",
    "    x_num_ticks (int): Number of ticks on the x-axis (time frames). Defaults to 6.\n",
    "    y_num_ticks (int): Number of ticks on the y-axis (frequency bins). Defaults to 5.\n",
    "    \"\"\"\n",
    "    if type(data) == pd.DataFrame:\n",
    "        plt.figure(dpi=300)  # Set figure size and DPI\n",
    "        plt.pcolormesh(data.transpose(), cmap='jet', vmax=0.03, vmin=0.0, rasterized=True)\n",
    "        # plt.title('STFT Preview')\n",
    "        plt.colorbar(label='Magnitude ($m/s^2$)')\n",
    "        plt.xlabel('Segmen Waktu')\n",
    "        plt.ylabel('Sampel Frekuensi (Hz)')\n",
    "        plt.xticks(np.linspace(0, len(data)-1, x_num_ticks))  # Set x-ticks at regular intervals\n",
    "        plt.yticks(np.linspace(0, len(data.columns)-1, y_num_ticks))  # Set y-ticks at regular intervals\n",
    "        plt.savefig(\"output_single.svg\", format=\"svg\", dpi=100)\n",
    "        plt.show()\n",
    "\n",
    "    elif type(data) == list and len(data) > 1:\n",
    "        # Create a figure and subplots\n",
    "        fig, axes = plt.subplots(len(data)//cols, cols, figsize=(15, 8), sharex=True, sharey=True)\n",
    "\n",
    "        # Flatten the axes array for easier iteration\n",
    "        axes = axes.flatten()\n",
    "\n",
    "        # Loop through each subplot and plot the data\n",
    "        for i in range(len(data)):\n",
    "            pcm = axes[i].pcolormesh(data[i].transpose(), cmap='jet', vmax=0.03, vmin=0.0, rasterized=True)\n",
    "            axes[i].set_title(f'd_{i+1}', fontsize=12)\n",
    "            axes[i].set_xticks(np.linspace(0, len(data[i])-1, 6))  # Set x-ticks at regular intervals\n",
    "            axes[i].set_yticks(np.linspace(0, len(data[i].columns)-1, 9))  # Set y-ticks at regular intervals\n",
    "\n",
    "        # Add a single color bar for all subplots\n",
    "        # Use the first `pcolormesh` object (or any valid one) for the color bar\n",
    "        cbar = fig.colorbar(pcm, ax=axes, orientation='vertical')\n",
    "        cbar.set_label('Magnitude ($m/s^2$)', fontsize=12)\n",
    "\n",
    "        # Set shared labels\n",
    "        fig.text(0.5, 0.04, 'Segmen Waktu', ha='center', fontsize=12)\n",
    "        fig.text(0.04, 0.5, 'Sampel Frekuensi (Hz)', va='center', rotation='vertical', fontsize=12)\n",
    "        # fig.suptitle('STFT of Sensor A Damage Case (d1--d6) Dataset A', fontsize=16)\n",
    "\n",
    "        # Adjust layout\n",
    "        # plt.tight_layout(rect=[0.05, 0.05, 1, 1])  # Leave space for shared labels\n",
    "        plt.subplots_adjust(left=0.1, right=0.75, top=0.9, bottom=0.1, wspace=0.2, hspace=0.2)\n",
    "        plt.savefig(\"output_multiple.svg\", format=\"svg\", dpi=80)\n",
    "        plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###### Sensor A"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# colormesh give title x is frequency and y is time and rotate/transpose the data\n",
    "# Plotting the STFT Data\n",
    "preview_stft(ready_data1a[0], y_num_ticks=9)  # Preview for Sensor 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###### Sensor B"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "preview_stft(ready_data2a[0], y_num_ticks=9)  # Preview for Sensor 2"
   ]
  },
  {
   "attachments": {
    "image.png": {
     "image/png": "[encoded data, 377552 characters]"
    }
   },
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Damaged case (d1—d6)\n",
    "![Overview of the signal preprocessing pipeline for damage case 1 to 6](attachment:image.png)\n",
    "The above figure shows overview of the signal preprocessing pipeline for damage case $(d_1—d_6)$. The stacking illustration represents other damage case scenario $(d_1—d_6)$. Each retained (bottom-most and top-most) node’s signal undergoes a Short-Time Fourier Transform (STFT) with Hann windowing to convert time-domain data into time–frequency representations. STFT spectrograms are generated separately for each sensor (Sensor A and Sensor B). Finally, the spectrograms across nodes are concatenated per sensor to form unified feature matrices, which serve as inputs for downstream analysis or classification tasks."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### Preview Plot"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###### Sensor A"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "preview_stft(ready_data1a[1:], cols=3, y_num_ticks=9)  # Preview for Sensor 1 Damage Cases (d1--d6)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###### Sensor B"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "preview_stft(ready_data2a[1:], cols=3, y_num_ticks=9)  # Preview for Sensor 1 Damage Cases (d1--d6)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Training and Evaluation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from src.ml.model_selection import create_ready_data\n",
    "import pandas as pd"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The study employs a supervised learning framework to train a multi-class classifier for structural damage localization using minimal sensor data. Following the STFT-based feature extraction described earlier, each training sample consists of a 513-dimensional feature derived from either Sensor A or Sensor B as shown in following DataFrame. The corresponding class label indicates the specific case (0--6), reflecting the joint group in which damage was induced."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X1a, y = create_ready_data('D:/thesis/data/converted/raw/sensor1')\n",
    "\n",
    "# Display DataFrame\n",
    "pd.concat([\n",
    "           pd.DataFrame(y, columns=['label']),   # Labels\n",
    "           X1a,                                 # Features\n",
    "           ], \n",
    "           axis=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X2a, y = create_ready_data('D:/thesis/data/converted/raw/sensor2')\n",
    "\n",
    "# Display DataFrame\n",
    "pd.concat([\n",
    "           pd.DataFrame(y, columns=['label']),   # Labels\n",
    "           X2a,                                 # Features\n",
    "           ], \n",
    "           axis=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To enhance generalisability and account for signal variability across different damage instances, the complete dataset—comprising $17955$ samples per sensor side—was randomly shuffled and stratified using the `train_test_split` method from scikit-learn, ensuring consistent class distribution across the training and validation sets. A standard $80:20$ split was applied, assigning $80\\%$ of the data to the training set and $20\\%$ to the validation set. During training, only data from Dataset A was used to fit the model, where each sample was preprocessed and grouped into either Sensor A or Sensor B. Inference was later performed using Dataset B to evaluate how well the trained model generalized to a separate but structurally similar dataset. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "# sensor A\n",
    "x_train1, x_test1, y_train1, y_test1 = train_test_split(X1a, y, test_size=0.2, random_state=2)\n",
    "# sensor B\n",
    "x_train2, x_test2, y_train2, y_test2 = train_test_split(X2a, y, test_size=0.2, random_state=2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Check the shapes of x_train and y_train\n",
    "print(\"Shape of x1_train:\", x_train1.shape)\n",
    "print(\"Shape of x2_train:\", x_train2.shape)\n",
    "print(\"Shape of y1_train:\", y_train1.shape)\n",
    "print(\"Shape of y2_train:\", y_train1.shape)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from src.ml.model_selection import train_and_evaluate_model\n",
    "from sklearn.svm import SVC\n",
    "from sklearn.pipeline import make_pipeline\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.svm import SVC\n",
    "from sklearn.decomposition import PCA\n",
    "# from xgboost import XGBClassifier\n",
    "# from sklearn.ensemble import RandomForestClassifier, BaggingClassifier\n",
    "# from sklearn.tree import DecisionTreeClassifier\n",
    "# from sklearn.neighbors import KNeighborsClassifier\n",
    "# from sklearn.discriminant_analysis import LinearDiscriminantAnalysis\n",
    "from sklearn.neural_network import MLPClassifier\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The classification model selected for this task is the Support Vector Machine (SVM) with a radial basis function (RBF) kernel (automatically selected due to high-dimensional features), chosen for its robustness in handling non-linear decision boundaries in high-dimensional spaces. The model was trained using scikit-learn’s SVC implementation with default hyperparameter $(C=1.0,\\gamma=scale)$ as baseline. Two separate models were trained: one exclusively on features from Sensor A and features from Sensor B"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Define models for sensor1\n",
    "models_sensor1 = {\n",
    "    # \"Random Forest\": RandomForestClassifier(),\n",
    "    # \"Bagged Trees\": BaggingClassifier(estimator=DecisionTreeClassifier(), n_estimators=10),\n",
    "    # \"Decision Tree\": DecisionTreeClassifier(),\n",
    "    # \"KNN\": KNeighborsClassifier(),\n",
    "    # \"LDA\": LinearDiscriminantAnalysis(),\n",
    "    # \"SVM\": make_pipeline(\n",
    "    #     StandardScaler(),\n",
    "    #     SVC(kernel='rbf', probability=True)\n",
    "    # ),\n",
    "    \"SVM with StandardScaler and PCA\": make_pipeline(\n",
    "                                        StandardScaler(),\n",
    "                                        PCA(n_components=10),\n",
    "                                        SVC(kernel='rbf', probability=True)\n",
    "                                        ),\n",
    "\n",
    "    # \"XGBoost\": XGBClassifier()\n",
    "    \"MLPClassifier\": make_pipeline(\n",
    "        StandardScaler(),\n",
    "        MLPClassifier(hidden_layer_sizes=(1, 10), max_iter=500, random_state=42)\n",
    "    )\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "results_sensor1 = []\n",
    "for name, model in models_sensor1.items():\n",
    "    res = train_and_evaluate_model(model, name, \"Sensor A\", x_train1, y_train1, x_test1, y_test1, \n",
    "                                   export='D:/thesis/models/Sensor A')\n",
    "    results_sensor1.append(res)\n",
    "    print(f\"{name} on sensor1: Accuracy = {res['accuracy']:.2f}%\")\n",
    "\n",
    "# Display result\n",
    "results_sensor1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "models_sensor2 = {\n",
    "    # \"Random Forest\": RandomForestClassifier(),\n",
    "    # \"Bagged Trees\": BaggingClassifier(estimator=DecisionTreeClassifier(), n_estimators=10),\n",
    "    # \"Decision Tree\": DecisionTreeClassifier(),\n",
    "    # \"KNN\": KNeighborsClassifier(),\n",
    "    # \"LDA\": LinearDiscriminantAnalysis(),\n",
    "    \"SVM\": SVC(),\n",
    "    # \"SVM with StandardScaler and PCA\": make_pipeline(\n",
    "    # StandardScaler(),\n",
    "    # PCA(n_components=10),\n",
    "    # SVC(kernel='rbf')\n",
    "    # ),\n",
    "    # \"XGBoost\": XGBClassifier()\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "results_sensor2 = []\n",
    "for name, model in models_sensor2.items():\n",
    "    res = train_and_evaluate_model(model, name, \"sensor2\", x_train2, y_train2, x_test2, y_test2, \n",
    "                                   export='D:/thesis/models/sensor2')\n",
    "    results_sensor2.append(res)\n",
    "    print(f\"{name} on sensor2: Accuracy = {res['accuracy']:.2f}%\")\n",
    "\n",
    "# Display result\n",
    "results_sensor2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Confusion Matrix Validation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from src.ml.model_selection import plot_confusion_matrix\n",
    "\n",
    "# Plot confusion matrix for sensor1\n",
    "plot_confusion_matrix(results_sensor1, y_test1, \"Confusion Matrix of Sensor A Validation on Dataset A\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from src.ml.model_selection import plot_confusion_matrix\n",
    "\n",
    "# Plot confusion matrix for sensor1\n",
    "plot_confusion_matrix(results_sensor2, y_test2, \"Confusion Matrix of Sensor B Validation on Dataset A\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Test"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The performance of the trained SVM models was tested on Dataset B, an entirely unseen dataset, to test for generalization. Following code is assuming that preprocessing data as described in the previous section has been completed and the data is available in the expected format. The code loads the preprocessed STFT data for each sensor side (Sensor A and Sensor B)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from src.ml.model_selection import create_ready_data\n",
    "\n",
    "X1b, y1 = create_ready_data('D:/thesis/data/converted/raw_B/sensor1') # sensor A\n",
    "X2b, y2 = create_ready_data('D:/thesis/data/converted/raw_B/sensor2') # sensor B"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Following code is load the exported model for specific sensor side that was trained on Dataset A and then used to predict the class labels for the unseen Dataset B."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Sensor A"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Rename first column using proper pandas method\n",
    "X1b = X1b.rename(columns={X1b.columns[0]: \"Freq_0.00\"})\n",
    "X2b = X2b.rename(columns={X2b.columns[0]: \"Freq_0.00\"})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 4. Sensor A Validate on Dataset B\n",
    "from joblib import load\n",
    "from sklearn.svm import SVC\n",
    "svm_model: SVC = load('D:/thesis/models/Sensor A/SVM with StandardScaler and PCA.joblib')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "\n",
    "time_taken = np.array([])\n",
    "for i in range(5):  # Run multiple times to get an average time\n",
    "    start_time = time.time()\n",
    "    y_pred_svm_1 = svm_model.predict(X1b)\n",
    "    end_time = time.time()\n",
    "    time_taken = np.append(time_taken, end_time - start_time)\n",
    "\n",
    "print(time_taken)\n",
    "print(time_taken.mean())\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "# Set NumPy to display full decimal values\n",
    "np.set_printoptions(suppress=True, precision=6)  # Suppress scientific notation, set precision to 6 decimals"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import accuracy_score, classification_report\n",
    "\n",
    "# 5. Evaluate\n",
    "print(\"Accuracy on Dataset B:\", accuracy_score(y1, y_pred_svm_1))\n",
    "df = pd.DataFrame(classification_report(y1, y_pred_svm_1, output_dict=True)).T\n",
    "# Round numbers nicely and move 'accuracy' into a row that fits your desired layout\n",
    "df_rounded = df.round(2)\n",
    "\n",
    "# Export to LaTeX\n",
    "latex_table = df_rounded.to_latex(index=True, float_format=\"%.2f\", caption=\"Classification report on Dataset B\", label=\"tab:clf_report_auto\")\n",
    "print(latex_table)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Sensor B"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# svm_model = load('D:/thesis/models/sensor2/SVM.joblib')\n",
    "svm_model = load('D:/thesis/models/sensor2/SVM with StandardScaler and PCA.joblib')\n",
    "y_pred_svm_2 = svm_model.predict(X2b)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 5. Evaluate\n",
    "import pandas as pd\n",
    "\n",
    "df = pd.DataFrame(classification_report(y2, y_pred_svm_2, output_dict=True)).T\n",
    "# Round numbers nicely and move 'accuracy' into a row that fits your desired layout\n",
    "df_rounded = df.round(2)\n",
    "\n",
    "# Export to LaTeX\n",
    "latex_table = df_rounded.to_latex(index=True, float_format=\"%.2f\", caption=\"Classification report on Dataset B\", label=\"tab:clf_report_auto\")\n",
    "print(latex_table)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Confusion Matrix Sensor A and B"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay\n",
    "import numpy as np\n",
    "\n",
    "# Create a fresh figure with subplots\n",
    "fig, axes = plt.subplots(1, 2, figsize=(12, 5))\n",
    "\n",
    "# Plot confusion matrix for Sensor A\n",
    "cm_A = confusion_matrix(y, y_pred_svm_1)\n",
    "disp_A = ConfusionMatrixDisplay(confusion_matrix=cm_A, display_labels=labels)\n",
    "disp_A.plot(ax=axes[0], cmap=plt.cm.Blues)\n",
    "axes[0].set_title(\"Sensor A\")\n",
    "\n",
    "# Plot confusion matrix for Sensor B\n",
    "cm_B = confusion_matrix(y, y_pred_svm_2)\n",
    "disp_B = ConfusionMatrixDisplay(confusion_matrix=cm_B, display_labels=labels)\n",
    "disp_B.plot(ax=axes[1], cmap=plt.cm.Blues)\n",
    "axes[1].set_title(\"Sensor B\")\n",
    "\n",
    "# Find and modify colorbars to show max values\n",
    "# The colorbars are typically the 3rd and 4th axes in the figure\n",
    "for i, (cbar_idx, cm) in enumerate(zip([2, 3], [cm_A, cm_B])):\n",
    "    if cbar_idx < len(fig.axes):\n",
    "        cbar_ax = fig.axes[cbar_idx]\n",
    "        \n",
    "        # Get max value from the confusion matrix\n",
    "        max_val = cm.max()\n",
    "        \n",
    "        # Create a new set of ticks with reasonable spacing and ending with max_val\n",
    "        # For example, if max is around 2560, create ticks: [0, 500, 1000, 1500, 2000, 2560]\n",
    "        tick_interval = 500\n",
    "        new_ticks = list(range(0, int(max_val), tick_interval))\n",
    "        if np.isclose(new_ticks[-1], max_val, rtol=0.05):\n",
    "            new_ticks[-1] = max_val \n",
    "        else:\n",
    "            new_ticks.extend([max_val])\n",
    "        # Set the new ticks\n",
    "        cbar_ax.set_yticks(new_ticks)\n",
    "        \n",
    "        # Format tick labels as integers\n",
    "        # cbar_ax.set_yticklabels([f\"{int(t)}\" if t.is_integer() else f\"{t:.1f}\" for t in new_ticks])\n",
    "\n",
    "# Set SVG font rendering for better PDF output\n",
    "plt.rcParams['svg.fonttype'] = 'none'\n",
    "\n",
    "# Adjust layout\n",
    "plt.tight_layout()\n",
    "\n",
    "# Save and show\n",
    "plt.savefig(\"output.svg\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Model sensor 1 to predict sensor 2 data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import accuracy_score, classification_report\n",
    "# 4. Validate on Dataset B\n",
    "from joblib import load\n",
    "svm_model = load('D:/thesis/models/sensor1/SVM.joblib')\n",
    "y_pred_svm = svm_model.predict(X2b)\n",
    "\n",
    "# 5. Evaluate\n",
    "print(\"Accuracy on Dataset B:\", accuracy_score(y, y_pred_svm))\n",
    "print(classification_report(y, y_pred_svm))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Test with AU"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "file_path = 'D:/thesis/data/dataset_B/zzzBU.TXT'\n",
    "df = pd.read_csv(file_path, delim_whitespace=True, skiprows=10, header=0, memory_map=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df1= df.iloc[:, [1]]\n",
    "df2 = df.iloc[:, [26]]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from scipy.signal.windows import hann\n",
    "from scipy.signal import stft\n",
    "\n",
    "window = 1024\n",
    "hop = 512\n",
    "\n",
    "_, _, stft1 = stft(df1.values.flatten(), window=hann(window), nperseg=window, noverlap=hop, fs=window)\n",
    "_, _, stft2 = stft(df2.values.flatten(), window=hann(window), nperseg=window, noverlap=hop, fs=window)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "a = pd.DataFrame(np.abs(stft1.T), columns=[f\"Freq_{freq:.2f}\" for freq in np.linspace(0, window/2, stft1.shape[1])]).rename(columns={a.columns[0]: \"00\"})\n",
    "b = pd.DataFrame(np.abs(stft2.T), columns=[f\"Freq_{freq:.2f}\" for freq in np.linspace(0, window/2, stft2.shape[1])]).rename(columns={b.columns[0]: \"00\"})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = [0]*len(a)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from joblib import load\n",
    "\n",
    "svm_model_a = load('D:/thesis/models/sensor1/SVM with StandardScaler and PCA.joblib')\n",
    "svm_model_b = load('D:/thesis/models/sensor2/SVM with StandardScaler and PCA.joblib')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_pred_svm = svm_model_b.predict(b)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_pred_svm"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
