SM-Bello commited on
Commit
f7125d8
Β·
0 Parent(s):

First release of AGTF30 Prognostics

Browse files
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.pt filter=lfs diff=lfs merge=lfs -text
2
+ *.npz filter=lfs diff=lfs merge=lfs -text
A320neo_protocol.xml ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ a320neo_protocol.xml
4
+ FlightGear Generic Output Protocol β€” Airbus A320neo (CFM LEAP-1A)
5
+
6
+ Place this file in:
7
+ <fg-root>/Protocol/a320neo_protocol.xml
8
+
9
+ Launch FlightGear with:
10
+ --generic=socket,out,10,127.0.0.1,5500,udp,a320neo_protocol
11
+
12
+ 15 comma-separated fields per packet, ASCII, newline-terminated.
13
+
14
+ JSBSim engine property paths are identical across aircraft models β€”
15
+ only the physical operating ranges differ between B787 and A320neo.
16
+
17
+ Author: Mohammed Bello Sani
18
+ -->
19
+ <PropertyList>
20
+ <generic>
21
+ <output>
22
+ <binary_mode>false</binary_mode>
23
+ <line_separator>\n</line_separator>
24
+ <var_separator>,</var_separator>
25
+
26
+ <!-- [00] EGT proxy for T45 / T25 / T3 β€” engine exhaust gas temp degF -->
27
+ <chunk>
28
+ <node>/engines/engine[0]/egt-degf</node>
29
+ <type>double</type>
30
+ <format>%.3f</format>
31
+ </chunk>
32
+
33
+ <!-- [01] N2 core shaft speed percent (HPT proxy) -->
34
+ <chunk>
35
+ <node>/engines/engine[0]/n2</node>
36
+ <type>double</type>
37
+ <format>%.6f</format>
38
+ </chunk>
39
+
40
+ <!-- [02] EGT again β€” inter-turbine temperature proxy T25 -->
41
+ <chunk>
42
+ <node>/engines/engine[0]/egt-degf</node>
43
+ <type>double</type>
44
+ <format>%.3f</format>
45
+ </chunk>
46
+
47
+ <!-- [03] N1 fan shaft speed percent (LPT proxy) -->
48
+ <chunk>
49
+ <node>/engines/engine[0]/n1</node>
50
+ <type>double</type>
51
+ <format>%.6f</format>
52
+ </chunk>
53
+
54
+ <!-- [04] EGT again β€” compressor exit temperature proxy T3 -->
55
+ <chunk>
56
+ <node>/engines/engine[0]/egt-degf</node>
57
+ <type>double</type>
58
+ <format>%.3f</format>
59
+ </chunk>
60
+
61
+ <!-- [05] Thrust lbf β€” combustor inlet pressure proxy Pt3 -->
62
+ <chunk>
63
+ <node>/engines/engine[0]/thrust-lbs</node>
64
+ <type>double</type>
65
+ <format>%.3f</format>
66
+ </chunk>
67
+
68
+ <!-- [06] Thrust lbf β€” static pressure proxy Ps3 -->
69
+ <chunk>
70
+ <node>/engines/engine[0]/thrust-lbs</node>
71
+ <type>double</type>
72
+ <format>%.3f</format>
73
+ </chunk>
74
+
75
+ <!-- [07] N2 repeated for sensor slot N2 -->
76
+ <chunk>
77
+ <node>/engines/engine[0]/n2</node>
78
+ <type>double</type>
79
+ <format>%.4f</format>
80
+ </chunk>
81
+
82
+ <!-- [08] N1 repeated for sensor slot N1 -->
83
+ <chunk>
84
+ <node>/engines/engine[0]/n1</node>
85
+ <type>double</type>
86
+ <format>%.4f</format>
87
+ </chunk>
88
+
89
+ <!-- [09] N1 again as N3 proxy (2-spool: no N3 in JSBSim) -->
90
+ <chunk>
91
+ <node>/engines/engine[0]/n1</node>
92
+ <type>double</type>
93
+ <format>%.4f</format>
94
+ </chunk>
95
+
96
+ <!-- [10] Net thrust lbf β€” Fnet -->
97
+ <chunk>
98
+ <node>/engines/engine[0]/thrust-lbs</node>
99
+ <type>double</type>
100
+ <format>%.2f</format>
101
+ </chunk>
102
+
103
+ <!-- [11] Altitude ft β€” context -->
104
+ <chunk>
105
+ <node>/position/altitude-ft</node>
106
+ <type>double</type>
107
+ <format>%.1f</format>
108
+ </chunk>
109
+
110
+ <!-- [12] Airspeed kt β€” context -->
111
+ <chunk>
112
+ <node>/velocities/airspeed-kt</node>
113
+ <type>double</type>
114
+ <format>%.2f</format>
115
+ </chunk>
116
+
117
+ <!-- [13] Heading deg β€” context -->
118
+ <chunk>
119
+ <node>/orientation/heading-deg</node>
120
+ <type>double</type>
121
+ <format>%.2f</format>
122
+ </chunk>
123
+
124
+ <!-- [14] Sim elapsed seconds β€” context -->
125
+ <chunk>
126
+ <node>/sim/time/elapsed-sec</node>
127
+ <type>double</type>
128
+ <format>%.2f</format>
129
+ </chunk>
130
+
131
+ </output>
132
+ </generic>
133
+ </PropertyList>
README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Real-Time Probabilistic Health Monitoring (AGTF30)
2
+
3
+ This repository contains the official implementation of the CNN-BiLSTM-Attention prognostic framework as described in:
4
+ *"Real-Time Probabilistic Health Monitoring of Turbofan Turbine Blades via CNN-BiLSTM-Attention with Physics-Informed Flight Simulator Integration"* (Ene et al., 2026).
5
+
6
+ ## Model Performance (Test Set)
7
+ | Metric | Value |
8
+ | :--- | :--- |
9
+ | **Test RMSE** | 3.352 cycles |
10
+ | **RΒ² Score** | 0.993 |
11
+ | **NASA Score** | 346.38 |
12
+
13
+ ## Getting Started
14
+ 1. **Clone the repo:**
15
+ ```bash
16
+ git clone [https://huggingface.co/SM-Bello/AGTF30-Turbofan-Prognostics](https://huggingface.co/SM-Bello/AGTF30-Turbofan-Prognostics)
agtf30_blade_model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a3427f5f0511ea11a82eb2b22be1d3bb0dfbd9743611d60273c232d849961e41
3
+ size 3948965
agtf30_scaler_params.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a7f87a314b5ca1bbe5d038405a80062ce80f4dfba5942b2c20274df58c487895
3
+ size 2004
cmapss_model.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ cmapss_model.py
3
+ ===============
4
+ CNN-BiLSTM-Attention model with MC Dropout for RUL prediction.
5
+ Used for both C-MAPSS and AGTF30 turbine blade datasets.
6
+
7
+ Author: Mohammed Bello Sani
8
+ """
9
+
10
+ import torch
11
+ import torch.nn as nn
12
+ import torch.nn.functional as F
13
+ import numpy as np
14
+
15
+
16
+ class CNNBiLSTMAttention(nn.Module):
17
+ """
18
+ CNN + Bidirectional LSTM + Self-Attention + MC Dropout
19
+ """
20
+ def __init__(self, hp):
21
+ super().__init__()
22
+ self.hp = hp
23
+ n_features = hp['n_features']
24
+ cnn_filters = hp['cnn_filters']
25
+ kernel_size = hp.get('cnn_kernel', 3)
26
+ lstm_hidden = hp['lstm_hidden']
27
+ lstm_layers = hp['lstm_layers']
28
+ fc_hidden = hp.get('fc_hidden', 64)
29
+ dropout = hp.get('mc_dropout', 0.5)
30
+
31
+ # CNN feature extractor
32
+ cnn_layers = []
33
+ in_channels = n_features
34
+ for i, filters in enumerate(cnn_filters):
35
+ cnn_layers.append(nn.Conv1d(in_channels, filters, kernel_size, padding='same'))
36
+ cnn_layers.append(nn.BatchNorm1d(filters))
37
+ cnn_layers.append(nn.GELU())
38
+ cnn_layers.append(nn.Dropout(dropout * 0.5)) # lighter dropout in CNN
39
+ in_channels = filters
40
+ self.cnn = nn.Sequential(*cnn_layers)
41
+ self.cnn_out_dim = cnn_filters[-1]
42
+
43
+ # BiLSTM
44
+ self.lstm = nn.LSTM(
45
+ input_size=self.cnn_out_dim,
46
+ hidden_size=lstm_hidden,
47
+ num_layers=lstm_layers,
48
+ batch_first=True,
49
+ bidirectional=True,
50
+ dropout=hp.get('lstm_dropout', 0.3) if lstm_layers > 1 else 0
51
+ )
52
+ lstm_out_dim = lstm_hidden * 2 # bidirectional
53
+
54
+ # Self-Attention
55
+ self.attn_heads = hp.get('attn_heads', 4)
56
+ self.attn = nn.MultiheadAttention(
57
+ embed_dim=lstm_out_dim,
58
+ num_heads=self.attn_heads,
59
+ dropout=dropout * 0.3,
60
+ batch_first=True
61
+ )
62
+ self.attn_norm = nn.LayerNorm(lstm_out_dim)
63
+
64
+ # Fully connected regressor
65
+ self.fc = nn.Sequential(
66
+ nn.Linear(lstm_out_dim, fc_hidden),
67
+ nn.GELU(),
68
+ nn.Dropout(dropout),
69
+ nn.Linear(fc_hidden, 1)
70
+ )
71
+
72
+ # MC Dropout layers are already included; we keep them active during inference
73
+ self.dropout_rate = dropout
74
+
75
+ def forward(self, x):
76
+ # x shape: (batch, seq_len, features)
77
+ # Permute for CNN: (batch, features, seq_len)
78
+ x_cnn = x.permute(0, 2, 1)
79
+ x_cnn = self.cnn(x_cnn) # (batch, filters, seq_len)
80
+ x_cnn = x_cnn.permute(0, 2, 1) # (batch, seq_len, filters)
81
+
82
+ # BiLSTM
83
+ lstm_out, _ = self.lstm(x_cnn) # (batch, seq_len, lstm_hidden*2)
84
+
85
+ # Self-Attention
86
+ attn_out, _ = self.attn(lstm_out, lstm_out, lstm_out)
87
+ attn_out = self.attn_norm(attn_out + lstm_out) # residual
88
+
89
+ # Global average pooling over time dimension
90
+ pooled = attn_out.mean(dim=1) # (batch, lstm_out_dim)
91
+
92
+ # Final prediction
93
+ out = self.fc(pooled).squeeze(-1) # (batch,)
94
+ return out
95
+
96
+ def predict_with_uncertainty(self, x, n_samples=100):
97
+ """
98
+ Monte Carlo Dropout inference.
99
+ Returns mean and standard deviation.
100
+ """
101
+ self.train() # keep dropout active
102
+ preds = []
103
+ with torch.no_grad():
104
+ for _ in range(n_samples):
105
+ preds.append(self.forward(x).cpu().numpy())
106
+ preds = np.stack(preds, axis=0)
107
+ mean = preds.mean(axis=0)
108
+ std = preds.std(axis=0)
109
+ return mean, std
110
+
111
+
112
+ # Compatibility: if the old code expects the model to be loaded via torch.load,
113
+ # we need to ensure the class is defined in this module.
114
+ if __name__ == '__main__':
115
+ print("CNNBiLSTMAttention model definition ready.")
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ torch
2
+ numpy
3
+ pandas
4
+ scikit-learn
5
+ matplotlib
6
+ scipy
7
+ h5py
train_agtf30.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Modified train_agtf30.py
3
+ Boosted architecture for higher tracking accuracy.
4
+ """
5
+ import sys, os, time, warnings
6
+ import numpy as np
7
+ import pandas as pd
8
+ import torch
9
+ import torch.nn as nn
10
+ import torch.optim as optim
11
+ from torch.utils.data import DataLoader, TensorDataset
12
+ from sklearn.preprocessing import MinMaxScaler
13
+ import matplotlib.pyplot as plt
14
+
15
+ # Replace with your actual path
16
+ sys.path.insert(0, r'C:\Users\User\Desktop\Steph')
17
+ from cmapss_model import CNNBiLSTMAttention
18
+
19
+ # ─────────────────────────────────────────────────────────
20
+ # CONFIG
21
+ # ─────────────────────────────────────────────────────────
22
+ DATA_FILE = r'C:\Users\User\Desktop\Steph\archive\AGTF30\AGTF30\agtf30_blade_dataset.csv'
23
+ MODEL_OUT = r'C:\Users\User\Desktop\Steph\archive\AGTF30\AGTF30\agtf30_blade_model.pt'
24
+ SCALER_OUT = r'C:\Users\User\Desktop\Steph\archive\AGTF30\AGTF30\agtf30_scaler_params.npz'
25
+
26
+ SENSOR_NAMES = ['T45','Pt45','T25','Pt25','T3','Pt3','Ps3','N2','N1','N3','Fnet']
27
+ HP = {
28
+ 'n_features' : 11,
29
+ 'cnn_filters' : [64, 128], # Increased depth
30
+ 'cnn_kernel' : 3,
31
+ 'cnn_dropout' : 0.25,
32
+ 'lstm_hidden' : 128,
33
+ 'lstm_layers' : 2,
34
+ 'lstm_dropout': 0.3,
35
+ 'attn_heads' : 8, # More heads for better focus
36
+ 'fc_hidden' : 128, # Wider FC layer
37
+ 'mc_dropout' : 0.3,
38
+ 'window_size' : 30,
39
+ 'batch_size' : 64, # Smaller batch for better gradients
40
+ 'epochs' : 200,
41
+ 'lr' : 1e-3, # Adjusted LR
42
+ 'weight_decay': 1e-4
43
+ }
44
+
45
+ DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
46
+
47
+ # ─────────────────────────────────────────────────────────
48
+ # 1. LOAD & PREP
49
+ # ─────────────────────────────────────────────────────────
50
+ df = pd.read_csv(DATA_FILE)
51
+ # Compute RUL
52
+ max_cycles = df.groupby('engine_id')['cycle'].max().reset_index()
53
+ max_cycles.columns = ['engine_id','max_cycle']
54
+ df = df.merge(max_cycles, on='engine_id')
55
+ df['RUL'] = (df['max_cycle'] - df['cycle']).clip(upper=125)
56
+
57
+ # Normalization
58
+ scaler = MinMaxScaler()
59
+ df[SENSOR_NAMES] = scaler.fit_transform(df[SENSOR_NAMES])
60
+
61
+ # ─────────────────────────────────────────────────────────
62
+ # 2. IMPROVED DATA SAMPLING
63
+ # ─────────────────────────────────────────────────────────
64
+ def create_dataset(data, window_size):
65
+ X, y = [], []
66
+ for eid, grp in data.groupby('engine_id'):
67
+ vals = grp[SENSOR_NAMES].values
68
+ rul = grp['RUL'].values
69
+ # Sample EVERY cycle, not just the last one
70
+ for i in range(len(vals) - window_size + 1):
71
+ X.append(vals[i:i+window_size])
72
+ y.append(rul[i+window_size-1])
73
+ return torch.tensor(np.array(X), dtype=torch.float32), torch.tensor(np.array(y), dtype=torch.float32)
74
+
75
+ engines = df['engine_id'].unique()
76
+ np.random.shuffle(engines)
77
+ split = int(len(engines)*0.85)
78
+ train_loader = DataLoader(TensorDataset(*create_dataset(df[df['engine_id'].isin(engines[:split])], 30)), batch_size=64, shuffle=True)
79
+ val_loader = DataLoader(TensorDataset(*create_dataset(df[df['engine_id'].isin(engines[split:])], 30)), batch_size=64)
80
+
81
+ # ─────────────────────────────────────────────────────────
82
+ # 3. TRAIN
83
+ # ─────────────────────────────────────────────────────────
84
+ model = CNNBiLSTMAttention(HP).to(DEVICE)
85
+ optimizer = optim.Adam(model.parameters(), lr=HP['lr'])
86
+ criterion = nn.MSELoss()
87
+
88
+ print("Training started...")
89
+ for epoch in range(HP['epochs']):
90
+ model.train()
91
+ for Xb, yb in train_loader:
92
+ optimizer.zero_grad()
93
+ loss = criterion(model(Xb.to(DEVICE)).squeeze(), yb.to(DEVICE))
94
+ loss.backward()
95
+ optimizer.step()
96
+
97
+ # Validation loop omitted for brevity, just verify output
98
+ if epoch % 10 == 0:
99
+ print(f"Epoch {epoch} finished.")
100
+
101
+ torch.save({'model_state': model.state_dict(), 'hp': HP}, MODEL_OUT)
102
+ print("Training complete and saved.")