Skip to content

Commit b4fad77

Browse files
committed
feat: integrate XJTU-SY dataset and validate cross-domain adaptation
1 parent 338effb commit b4fad77

3 files changed

Lines changed: 265 additions & 0 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+
)

validate_xjtusy.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import numpy as np
2+
import tensorflow as tf
3+
4+
from cnsd import Dataset
5+
from cnsd.datasets.xjtusy import load_xjtusy_domain_split
6+
from cnsd.diagnosis.system import CNSD
7+
8+
9+
def headline_accuracy_by_verdict(report, y_true):
10+
pred = np.array([r['predicted_class'] for r in report.records])
11+
correct = pred == np.asarray(y_true)
12+
verdicts = np.array([r['physics_verdict'] for r in report.records])
13+
out = {}
14+
for v in ('CONFIRMED', 'CONFLICT', 'INCONCLUSIVE'):
15+
m = verdicts == v
16+
if m.any():
17+
out[v] = {'n': int(m.sum()), 'cnn_accuracy': float(correct[m].mean())}
18+
return out
19+
20+
21+
if __name__ == '__main__':
22+
np.random.seed(42)
23+
tf.random.set_seed(42)
24+
25+
print('Loading XJTU-SY dataset (Cross-Domain RPM/Load Split)...')
26+
train_data, target_data = load_xjtusy_domain_split(window_size=4096)
27+
28+
# Split target domain into Calib (50%) and Test (50%)
29+
indices = np.arange(len(target_data.y))
30+
np.random.shuffle(indices)
31+
calib_size = len(indices) // 2
32+
33+
calib_idx = indices[:calib_size]
34+
test_idx = indices[calib_size:]
35+
36+
X_calib, y_calib, cond_calib = (
37+
target_data.X[calib_idx],
38+
target_data.y[calib_idx],
39+
target_data.cond[calib_idx],
40+
)
41+
X_test, y_test, cond_test = (
42+
target_data.X[test_idx],
43+
target_data.y[test_idx],
44+
target_data.cond[test_idx],
45+
)
46+
47+
calib_data = Dataset.from_arrays(
48+
X_calib,
49+
y_calib,
50+
cond_calib,
51+
fs=target_data.fs,
52+
physics=target_data.physics,
53+
taxonomy=target_data.taxonomy,
54+
name='XJTUSY_Calib',
55+
)
56+
test_data = Dataset.from_arrays(
57+
X_test,
58+
y_test,
59+
cond_test,
60+
fs=target_data.fs,
61+
physics=target_data.physics,
62+
taxonomy=target_data.taxonomy,
63+
name='XJTUSY_Test',
64+
)
65+
66+
print(
67+
f'Train (2100 RPM)={len(train_data.y)} | Calib (2250 RPM)={len(y_calib)} | Test (2250 RPM)={len(y_test)}'
68+
)
69+
70+
model = CNSD()
71+
72+
print('\n[1] Training Neural Network on 2100 RPM Source Data...')
73+
model.fit(train_data, epochs=20)
74+
75+
print('\n[2] Calibrating Tau threshold on 2250 RPM Target Data...')
76+
taus = np.arange(1.0, 5.1, 0.5)
77+
best_gap = -np.inf
78+
best_tau = 1.0
79+
80+
for tau in taus:
81+
model.symbolic.tau = float(tau)
82+
report = model.diagnose(calib_data)
83+
84+
hb = headline_accuracy_by_verdict(report, y_calib)
85+
86+
conf_acc = hb.get('CONFIRMED', {}).get('cnn_accuracy', 0.0)
87+
cnfl_acc = hb.get('CONFLICT', {}).get('cnn_accuracy', 0.0)
88+
gap = conf_acc - cnfl_acc if 'CONFIRMED' in hb and 'CONFLICT' in hb else 0.0
89+
90+
print(f'Calib tau={tau:.1f} | Conf={conf_acc:.3f} | Cnfl={cnfl_acc:.3f} | Gap={gap:+.3f}')
91+
if gap > best_gap:
92+
best_gap = gap
93+
best_tau = float(tau)
94+
95+
print(f'\n=> Selected optimal tau: {best_tau}')
96+
97+
print('\n[3] Evaluating on Test Set (2250 RPM)...')
98+
model.symbolic.tau = best_tau
99+
report = model.diagnose(test_data)
100+
pred = np.array([r['predicted_class'] for r in report.records])
101+
baseline_acc = float((pred == np.asarray(y_test)).mean())
102+
103+
print('\n--- FINAL TEST RESULTS (CROSS-DOMAIN XJTU-SY) ---')
104+
print(f'Baseline CNN Acc: {baseline_acc:.3f}')
105+
print('--------------------------------------------')
106+
107+
hb = headline_accuracy_by_verdict(report, y_test)
108+
if 'CONFIRMED' in hb:
109+
print(
110+
f' Physics-Confirmed Acc: {hb["CONFIRMED"]["cnn_accuracy"]:.3f} (n={hb["CONFIRMED"]["n"]})'
111+
)
112+
if 'CONFLICT' in hb:
113+
print(
114+
f' Physics-Conflict Acc: {hb["CONFLICT"]["cnn_accuracy"]:.3f} (n={hb["CONFLICT"]["n"]})'
115+
)
116+
if 'INCONCLUSIVE' in hb:
117+
inc_n = hb['INCONCLUSIVE']['n']
118+
inc_pct = (inc_n / len(y_test)) * 100
119+
print(
120+
f' Physics-Inconclusive Acc:{hb["INCONCLUSIVE"]["cnn_accuracy"]:.3f} (n={inc_n}, {inc_pct:.1f}%)'
121+
)
122+
123+
if 'CONFIRMED' in hb and 'CONFLICT' in hb:
124+
gap = hb['CONFIRMED']['cnn_accuracy'] - hb['CONFLICT']['cnn_accuracy']
125+
print(f' GAP (CONF - CNFL): {gap:+.3f}')
126+
print('--------------------------------------------')

0 commit comments

Comments
 (0)