Skip to content

Commit 6f5e6db

Browse files
committed
feat: integrate XJTU-SY dataset and validate cross-domain reliability
1 parent 338effb commit 6f5e6db

4 files changed

Lines changed: 344 additions & 78 deletions

File tree

cnsd/datasets/xjtusy.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import glob
2+
import os
3+
4+
import numpy as np
5+
import pandas as pd
6+
7+
from cnsd.datasets.contract import Dataset
8+
from cnsd.physics.configs import XJTUSY_PHYSICS
9+
10+
# Known fault mappings per XJTU-SY paper
11+
# We map Bearing ID to CNSD fault class: 1=Outer, 2=Inner, 3=Cage
12+
XJTUSY_FAULTS = {
13+
'35Hz12kN': {
14+
'Bearing1_1': 1, # Outer
15+
'Bearing1_2': 1, # Outer
16+
'Bearing1_3': 1, # Outer
17+
},
18+
'37.5Hz11kN': {
19+
'Bearing2_1': 2, # Inner
20+
'Bearing2_2': 1, # Outer
21+
'Bearing2_4': 1, # Outer
22+
'Bearing2_5': 1, # Outer
23+
},
24+
'40Hz10kN': {
25+
'Bearing3_1': 1, # Outer
26+
'Bearing3_2': 2, # Inner
27+
'Bearing3_3': 2, # Inner
28+
'Bearing3_4': 2, # Inner
29+
'Bearing3_5': 1, # Outer
30+
},
31+
}
32+
33+
34+
def load_xjtusy_domain_split(
35+
data_dir=r'E:\301\CNSD\data\XJTU-SY\XJTU-SY_Bearing_Datasets',
36+
window_size=32768,
37+
train_cond='35Hz12kN',
38+
test_cond='37.5Hz11kN',
39+
):
40+
"""
41+
Loads authentic XJTU-SY dataset and strictly splits by condition (Domain Shift).
42+
Uses the run-to-failure nature to grab the first 15% as Healthy (0) and the
43+
last 15% as the Fault label.
44+
"""
45+
46+
def _load_condition(cond_folder, rpm_val):
47+
X, y, cond = [], [], []
48+
cond_path = os.path.join(data_dir, cond_folder)
49+
50+
if not os.path.exists(cond_path):
51+
return [], [], []
52+
53+
for bearing_folder in os.listdir(cond_path):
54+
bearing_path = os.path.join(cond_path, bearing_folder)
55+
if not os.path.isdir(bearing_path):
56+
continue
57+
58+
fault_label = XJTUSY_FAULTS.get(cond_folder, {}).get(bearing_folder, None)
59+
if (
60+
fault_label is None or bearing_folder == 'Bearing1_5'
61+
): # skip 1_5 to avoid mixed labels
62+
continue
63+
64+
csv_files = sorted(
65+
glob.glob(os.path.join(bearing_path, '*.csv')),
66+
key=lambda x: int(os.path.splitext(os.path.basename(x))[0]),
67+
)
68+
69+
total_files = len(csv_files)
70+
if total_files < 10:
71+
continue # ignore wildly corrupted directories
72+
73+
healthy_count = max(1, int(total_files * 0.20))
74+
fault_count = max(1, int(total_files * 0.20))
75+
76+
# Sliding window parameters
77+
step_size = 1024
78+
79+
# Extract Healthy
80+
for fpath in csv_files[:healthy_count]:
81+
df = pd.read_csv(fpath)
82+
sig = df.iloc[:, 0].values # Horizontal acceleration
83+
sig = (sig - np.mean(sig)) / (np.std(sig) + 1e-8)
84+
85+
# Slicing the 32768 array into overlapping 4096 windows
86+
for start_idx in range(0, len(sig) - window_size + 1, step_size):
87+
X.append(sig[start_idx : start_idx + window_size])
88+
y.append(0)
89+
cond.append(rpm_val)
90+
91+
# Extract Fault
92+
for fpath in csv_files[-fault_count:]:
93+
df = pd.read_csv(fpath)
94+
sig = df.iloc[:, 0].values
95+
sig = (sig - np.mean(sig)) / (np.std(sig) + 1e-8)
96+
97+
for start_idx in range(0, len(sig) - window_size + 1, step_size):
98+
X.append(sig[start_idx : start_idx + window_size])
99+
y.append(fault_label)
100+
cond.append(rpm_val)
101+
102+
return X, y, cond
103+
104+
X_train, y_train, c_train = _load_condition(train_cond, 2100.0)
105+
X_test, y_test, c_test = _load_condition(test_cond, 2250.0)
106+
107+
if not X_train or not X_test:
108+
raise FileNotFoundError(
109+
f'Could not load data. Ensure {data_dir} contains extracted {train_cond} and {test_cond} folders.'
110+
)
111+
112+
ds_train = Dataset.from_arrays(
113+
X=np.array(X_train, dtype=np.float32),
114+
y=np.array(y_train, dtype=np.int32),
115+
cond=np.array(c_train, dtype=np.float32),
116+
fs=25600,
117+
physics=XJTUSY_PHYSICS,
118+
taxonomy={0: ('Normal', 'None'), 1: ('Outer Race', 'Medium'), 2: ('Inner Race', 'High')},
119+
name='XJTUSY_Train',
120+
)
121+
122+
ds_test = Dataset.from_arrays(
123+
X=np.array(X_test, dtype=np.float32),
124+
y=np.array(y_test, dtype=np.int32),
125+
cond=np.array(c_test, dtype=np.float32),
126+
fs=25600,
127+
physics=XJTUSY_PHYSICS,
128+
taxonomy={0: ('Normal', 'None'), 1: ('Outer Race', 'Medium'), 2: ('Inner Race', 'High')},
129+
name='XJTUSY_Test',
130+
)
131+
132+
return ds_train, ds_test

cnsd/physics/configs.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,10 @@ class PhysicsConfig:
3131
fs=20000,
3232
name='SEU-Gearbox',
3333
)
34+
35+
XJTUSY_PHYSICS = PhysicsConfig(
36+
bearing={'n_balls': 8, 'd_ball': 7.94, 'd_pitch': 34.55, 'contact_angle': 0.0},
37+
cond_to_rpm={2100.0: 2100.0, 2250.0: 2250.0, 2400.0: 2400.0},
38+
fs=25600,
39+
name='XJTU-SY-LDK-UER204',
40+
)

evaluate_baselines.py

Lines changed: 79 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,84 @@
11
import os
22
import sys
3+
import argparse
34
import traceback
45

56
import numpy as np
67
import scipy.stats as stats
78

89
try:
910
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
10-
1111
import tensorflow as tf
1212

1313
from cnsd import Dataset
1414
from cnsd.diagnosis.system import CNSD
1515
from cnsd.perception.cnn import _train_cnn
16-
from cnsd.physics import PhysicsConfig
17-
from validate_pu import load_pu_domain_split
18-
19-
print('Loading Authentic PU dataset (Cross-Domain RPM Split)...')
20-
(X_train_full, y_train_full, cond_train_full), (X_target, y_target, cond_target) = (
21-
load_pu_domain_split()
22-
)
23-
24-
unique_rpm = set(cond_train_full).union(set(cond_target))
25-
rpm_map = {float(r): float(r) for r in unique_rpm}
26-
pu_physics = PhysicsConfig(
27-
bearing={'n_balls': 8, 'd_ball': 6.75, 'd_pitch': 28.5, 'contact_angle': 0.0},
28-
cond_to_rpm=rpm_map,
29-
fs=64000,
30-
name='PU-6203',
31-
)
32-
pu_taxonomy = {
33-
0: ('Normal', 'None'),
34-
1: ('Outer Race', 'Medium'),
35-
2: ('Inner Race', 'High'),
36-
}
16+
17+
# Argparse
18+
parser = argparse.ArgumentParser()
19+
parser.add_argument('--dataset', type=str, required=True, choices=['cwru', 'pu', 'xjtusy'])
20+
args = parser.parse_args()
21+
22+
print(f'Loading {args.dataset.upper()} dataset...')
23+
24+
if args.dataset == 'pu':
25+
from cnsd.physics import PhysicsConfig
26+
from validate_pu import load_pu_domain_split
27+
(X_train_full, y_train_full, cond_train_full), (X_target, y_target, cond_target) = load_pu_domain_split()
28+
unique_rpm = set(cond_train_full).union(set(cond_target))
29+
rpm_map = {float(r): float(r) for r in unique_rpm}
30+
physics = PhysicsConfig(
31+
bearing={'n_balls': 8, 'd_ball': 6.75, 'd_pitch': 28.5, 'contact_angle': 0.0},
32+
cond_to_rpm=rpm_map,
33+
fs=64000,
34+
name='PU-6203',
35+
)
36+
taxonomy = {0: ('Normal', 'None'), 1: ('Outer Race', 'Medium'), 2: ('Inner Race', 'High')}
37+
fs = 64000
38+
39+
elif args.dataset == 'cwru':
40+
from validate_run import load_cwru, CWRU, TAXONOMY
41+
X, y, cond = load_cwru()
42+
X = np.asarray(X, np.float32)
43+
y = np.asarray(y)
44+
cond = np.asarray(cond)
45+
46+
train_mask = cond < 3
47+
target_mask = cond == 3
48+
49+
X_train_full = X[train_mask]
50+
y_train_full = y[train_mask]
51+
cond_train_full = cond[train_mask]
52+
53+
X_target = X[target_mask]
54+
y_target = y[target_mask]
55+
cond_target = cond[target_mask]
56+
57+
physics = CWRU
58+
taxonomy = TAXONOMY
59+
fs = 12000
60+
61+
elif args.dataset == 'xjtusy':
62+
from cnsd.datasets.xjtusy import load_xjtusy_domain_split
63+
from cnsd.physics.configs import XJTUSY_PHYSICS
64+
train_ds, target_ds = load_xjtusy_domain_split(window_size=4096)
65+
66+
X_train_full = train_ds.X
67+
y_train_full = train_ds.y
68+
cond_train_full = train_ds.cond
69+
70+
X_target = target_ds.X
71+
y_target = target_ds.y
72+
cond_target = target_ds.cond
73+
74+
physics = target_ds.physics
75+
taxonomy = target_ds.taxonomy
76+
fs = target_ds.fs
3777

3878
def get_matched_coverage_gap(score, correct, target_n):
3979
if target_n == 0:
4080
return float('nan')
4181
# Score is higher for MORE confident
42-
# Sort descending by score
4382
sorted_indices = np.argsort(score)[::-1]
4483
hi_indices = sorted_indices[:target_n]
4584

@@ -73,13 +112,8 @@ def clone_for_mc(layer):
73112

74113
# Test data processing
75114
test_ds = Dataset.from_arrays(
76-
X_target,
77-
y_target,
78-
cond_target,
79-
fs=64000,
80-
physics=pu_physics,
81-
taxonomy=pu_taxonomy,
82-
name='PU_Test',
115+
X_target, y_target, cond_target,
116+
fs=fs, physics=physics, taxonomy=taxonomy, name=f'{args.dataset.upper()}_Test'
83117
)
84118
sig_te = np.stack([test_ds.X[i].reshape(-1) for i in range(len(test_ds.X))]).astype(np.float32)
85119
yte = test_ds.y
@@ -100,22 +134,14 @@ def clone_for_mc(layer):
100134
train_idx = indices[:split_idx]
101135
calib_idx = indices[split_idx:]
102136

103-
X_tr, y_tr, cond_tr = (
104-
X_train_full[train_idx],
105-
y_train_full[train_idx],
106-
cond_train_full[train_idx],
107-
)
108-
X_ca, y_ca, cond_ca = (
109-
X_train_full[calib_idx],
110-
y_train_full[calib_idx],
111-
cond_train_full[calib_idx],
112-
)
137+
X_tr, y_tr, cond_tr = X_train_full[train_idx], y_train_full[train_idx], cond_train_full[train_idx]
138+
X_ca, y_ca, cond_ca = X_train_full[calib_idx], y_train_full[calib_idx], cond_train_full[calib_idx]
113139

114140
train_ds = Dataset.from_arrays(
115-
X_tr, y_tr, cond_tr, fs=64000, physics=pu_physics, taxonomy=pu_taxonomy, name='PU_Train'
141+
X_tr, y_tr, cond_tr, fs=fs, physics=physics, taxonomy=taxonomy, name=f'{args.dataset.upper()}_Train'
116142
)
117143
calib_ds = Dataset.from_arrays(
118-
X_ca, y_ca, cond_ca, fs=64000, physics=pu_physics, taxonomy=pu_taxonomy, name='PU_Calib'
144+
X_ca, y_ca, cond_ca, fs=fs, physics=physics, taxonomy=taxonomy, name=f'{args.dataset.upper()}_Calib'
119145
)
120146

121147
# 2. Train Primary Model (Bypass SCM to prevent multiprocess deadlocks)
@@ -135,9 +161,7 @@ def clone_for_mc(layer):
135161
ens.append(m_cnn)
136162

137163
# 4. Calibrate Tau
138-
sig_ca = np.stack([calib_ds.X[i].reshape(-1) for i in range(len(calib_ds.X))]).astype(
139-
np.float32
140-
)
164+
sig_ca = np.stack([calib_ds.X[i].reshape(-1) for i in range(len(calib_ds.X))]).astype(np.float32)
141165
Xin_ca = sig_ca[..., None]
142166
probs_ca = model.cnn.predict(Xin_ca, batch_size=128, verbose=0)
143167
pred_ca = probs_ca.argmax(1)
@@ -148,10 +172,7 @@ def clone_for_mc(layer):
148172
for tau in [1.0, 1.5, 2.0, 2.5, 3.0]:
149173
model.symbolic.tau = float(tau)
150174
verds = np.array(
151-
[
152-
model.symbolic.diagnose(sig_ca[i], pred_ca[i], calib_ds.cond[i])['verdict']
153-
for i in range(len(sig_ca))
154-
]
175+
[model.symbolic.diagnose(sig_ca[i], pred_ca[i], calib_ds.cond[i])['verdict'] for i in range(len(sig_ca))]
155176
)
156177
conf = verds == 'CONFIRMED'
157178
cnfl = verds == 'CONFLICT'
@@ -172,10 +193,7 @@ def clone_for_mc(layer):
172193

173194
# Physics evaluation
174195
verds = np.array(
175-
[
176-
model.symbolic.diagnose(sig_te[i], pred_te[i], cond_te[i])['verdict']
177-
for i in range(len(sig_te))
178-
]
196+
[model.symbolic.diagnose(sig_te[i], pred_te[i], cond_te[i])['verdict'] for i in range(len(sig_te))]
179197
)
180198
conf = verds == 'CONFIRMED'
181199
cnfl = verds == 'CONFLICT'
@@ -209,20 +227,15 @@ def clone_for_mc(layer):
209227
results['mc_gap'].append(mc_gap)
210228

211229
# Ensemble at matched coverage
212-
ens_preds_probs = np.stack(
213-
[m.predict(Xin_te, batch_size=128, verbose=0) for m in ens]
214-
) # (3, n, c)
215-
ens_mean = ens_preds_probs.mean(0) # (n, c)
230+
ens_preds_probs = np.stack([m.predict(Xin_te, batch_size=128, verbose=0) for m in ens])
231+
ens_mean = ens_preds_probs.mean(0)
216232
ens_pred_class = ens_mean.argmax(1)
217233
ens_correct = ens_pred_class == yte
218-
# Score = negative entropy of ensemble mean
219234
ens_score = (ens_mean * np.log(ens_mean + eps)).sum(1)
220235
ens_gap = get_matched_coverage_gap(ens_score, ens_correct, target_n)
221236
results['ens_gap'].append(ens_gap)
222237

223-
print(
224-
f'Matched Coverage GAPs -> Softmax:{soft_gap:+.3f} | MC-Drop:{mc_gap:+.3f} | Ens:{ens_gap:+.3f}'
225-
)
238+
print(f'Matched Coverage GAPs -> Softmax:{soft_gap:+.3f} | MC-Drop:{mc_gap:+.3f} | Ens:{ens_gap:+.3f}')
226239

227240
# 6. Noise Test
228241
rng = np.random.RandomState(seed)
@@ -235,19 +248,13 @@ def clone_for_mc(layer):
235248
sig_n = sig_te + rng.randn(*sig_te.shape).astype(np.float32) * np.sqrt(npow)
236249

237250
Xin_n = sig_n[..., None]
238-
# Use ensemble mode vote to define 'unanimous' exactly like Abhi's template
239-
ep_class = np.stack(
240-
[m.predict(Xin_n, batch_size=128, verbose=0).argmax(1) for m in ens]
241-
)
251+
ep_class = np.stack([m.predict(Xin_n, batch_size=128, verbose=0).argmax(1) for m in ens])
242252
v = stats.mode(ep_class, axis=0, keepdims=False).mode
243253
unan = (ep_class == v).all(0)
244254
ok = v == yte
245255

246256
pv = np.array(
247-
[
248-
model.symbolic.diagnose(sig_n[i], v[i], cond_te[i])['verdict']
249-
for i in range(len(sig_n))
250-
]
257+
[model.symbolic.diagnose(sig_n[i], v[i], cond_te[i])['verdict'] for i in range(len(sig_n))]
251258
)
252259
pc = pv == 'CONFLICT'
253260
uw = unan & (~ok)
@@ -257,16 +264,10 @@ def clone_for_mc(layer):
257264
print(f' Noise={s:>5} | catch_rate={catch:.3f}')
258265

259266
print('\n' + '=' * 60 + f'\nFINAL AGGREGATED RESULTS ({len(seeds)} Seeds)\n' + '=' * 60)
260-
print(
261-
f'Physics GAP: {np.nanmean(results["phys_gap"]):+.3f} ± {np.nanstd(results["phys_gap"]):.3f}'
262-
)
263-
print(
264-
f'Softmax GAP: {np.nanmean(results["soft_gap"]):+.3f} ± {np.nanstd(results["soft_gap"]):.3f}'
265-
)
267+
print(f'Physics GAP: {np.nanmean(results["phys_gap"]):+.3f} ± {np.nanstd(results["phys_gap"]):.3f}')
268+
print(f'Softmax GAP: {np.nanmean(results["soft_gap"]):+.3f} ± {np.nanstd(results["soft_gap"]):.3f}')
266269
print(f'MC-Drop GAP: {np.nanmean(results["mc_gap"]):+.3f} ± {np.nanstd(results["mc_gap"]):.3f}')
267-
print(
268-
f'Ensemble GAP: {np.nanmean(results["ens_gap"]):+.3f} ± {np.nanstd(results["ens_gap"]):.3f}'
269-
)
270+
print(f'Ensemble GAP: {np.nanmean(results["ens_gap"]):+.3f} ± {np.nanstd(results["ens_gap"]):.3f}')
270271

271272
t_stat, p_val = stats.ttest_rel(results['phys_gap'], results['ens_gap'])
272273
avg_ncov = np.mean(results['ncov'])

0 commit comments

Comments
 (0)