浏览代码

Retrain TSPulse on rebalanced samples and switch tspluse_status to proportional distance

18922397810 2 周之前
父节点
当前提交
7f445ff7e3

+ 2 - 0
.gitignore

@@ -13,3 +13,5 @@ frontend/dist/
 web 账号.txt
 预训练之文件头.csv
 backend/LabelingPreTraining/inputdatas/
+*.log
+.backup_old_*/

二进制
backend/LabelingPreTraining/baseline/baseline.npz


+ 35 - 35
backend/LabelingPreTraining/baseline/baseline_summary.csv

@@ -1,36 +1,36 @@
 part,count,radius
-7号机组一缸压力盖侧_压力,84,0.126219
-7号机组一缸压力轴侧_压力,84,0.159425
-7号机组三缸压力盖侧_压力,82,0.164711
-7号机组三缸压力轴侧_压力,84,0.132081
-7号机组二缸压力盖侧_压力,82,0.137495
-7号机组二缸压力轴侧_压力,82,0.133279
-7号机组五缸压力盖侧_压力,84,0.261120
-7号机组五缸压力轴侧_压力,83,0.301723
-7号机组六缸压力盖侧_压力,83,0.124256
-7号机组六缸压力轴侧_压力,83,0.204148
-7号机组四缸压力盖侧_压力,84,0.129498
-7号机组四缸压力轴侧_压力,82,0.112543
-8号机组一缸压力盖侧_压力,84,0.299309
-8号机组一缸压力轴侧_压力,84,0.174872
-8号机组三缸压力盖侧_压力,82,0.089240
-8号机组三缸压力轴侧_压力,82,0.104497
-8号机组二缸压力盖侧_压力,82,0.085525
-8号机组二缸压力轴侧_压力,82,0.087815
-8号机组五缸压力盖侧_压力,82,0.106637
-8号机组五缸压力轴侧_压力,82,0.123400
-8号机组六缸压力轴侧_压力,83,0.171220
-8号机组四缸压力盖侧_压力,84,0.192413
-8号机组四缸压力轴侧_压力,82,0.153695
-9号机组一缸压力盖侧_压力,82,0.229602
-9号机组一缸压力轴侧_压力,84,0.304303
-9号机组三缸压力盖侧_压力,82,0.141863
-9号机组三缸压力轴侧_压力,82,0.149451
-9号机组二缸压力盖侧_压力,82,0.081894
-9号机组二缸压力轴侧_压力,82,0.119654
-9号机组五缸压力盖侧_压力,84,0.144385
-9号机组五缸压力轴侧_压力,82,0.145575
-9号机组六缸压力盖侧_压力,84,0.103386
-9号机组六缸压力轴侧_压力,83,0.111798
-9号机组四缸压力盖侧_压力,83,0.151684
-9号机组四缸压力轴侧_压力,84,0.117607
+7号机组一缸压力盖侧_压力,334,0.491949
+7号机组一缸压力轴侧_压力,333,0.567317
+7号机组三缸压力盖侧_压力,413,0.788923
+7号机组三缸压力轴侧_压力,333,0.481872
+7号机组二缸压力盖侧_压力,246,0.298719
+7号机组二缸压力轴侧_压力,249,0.424329
+7号机组五缸压力盖侧_压力,292,0.562654
+7号机组五缸压力轴侧_压力,249,0.823587
+7号机组六缸压力盖侧_压力,250,0.420239
+7号机组六缸压力轴侧_压力,332,0.611182
+7号机组四缸压力盖侧_压力,251,0.242862
+7号机组四缸压力轴侧_压力,249,0.399902
+8号机组一缸压力盖侧_压力,333,0.994161
+8号机组一缸压力轴侧_压力,331,0.835516
+8号机组三缸压力盖侧_压力,332,0.487719
+8号机组三缸压力轴侧_压力,332,0.893666
+8号机组二缸压力盖侧_压力,332,0.554903
+8号机组二缸压力轴侧_压力,331,0.635692
+8号机组五缸压力盖侧_压力,416,0.450255
+8号机组五缸压力轴侧_压力,247,0.955604
+8号机组六缸压力轴侧_压力,331,0.580575
+8号机组四缸压力盖侧_压力,249,0.497504
+8号机组四缸压力轴侧_压力,247,0.647852
+9号机组一缸压力盖侧_压力,328,1.274411
+9号机组一缸压力轴侧_压力,335,1.239706
+9号机组三缸压力盖侧_压力,332,1.036338
+9号机组三缸压力轴侧_压力,332,1.014872
+9号机组二缸压力盖侧_压力,330,0.420529
+9号机组二缸压力轴侧_压力,331,0.648495
+9号机组五缸压力盖侧_压力,333,1.140887
+9号机组五缸压力轴侧_压力,247,0.847419
+9号机组六缸压力盖侧_压力,332,0.826261
+9号机组六缸压力轴侧_压力,330,0.657405
+9号机组四缸压力盖侧_压力,332,0.670582
+9号机组四缸压力轴侧_压力,333,0.672981

+ 32 - 35
backend/LabelingPreTraining/baseline/validation_report.txt

@@ -1,49 +1,46 @@
 TSPulse 模型能力验证报告
 ========================================
 设备: Apple MPS
-评估集样本数: 140
+评估集样本数: 549
 
 [1] 损失收敛曲线 (epoch: train / val)
-  epoch   1: train 0.49377 / val 0.33041
-  epoch   2: train 0.35256 / val 0.32077
-  epoch   3: train 0.34754 / val 0.31910
-  epoch   4: train 0.34129 / val 0.31998
-  epoch   5: train 0.33505 / val 0.29960
-  epoch   6: train 0.29839 / val 0.26043
-  epoch   7: train 0.27106 / val 0.23832
-  epoch   8: train 0.24574 / val 0.21504
-  epoch   9: train 0.21309 / val 0.17202
-  epoch  10: train 0.17596 / val 0.14037
-  epoch  11: train 0.14785 / val 0.11632
-  epoch  12: train 0.11950 / val 0.09108
-  epoch  13: train 0.10143 / val 0.07853
-  epoch  14: train 0.09263 / val 0.07861
-  epoch  15: train 0.08870 / val 0.07181
-  epoch  16: train 0.08490 / val 0.07039
-  epoch  17: train 0.08113 / val 0.06641
-  epoch  18: train 0.07840 / val 0.06707
-  epoch  19: train 0.07561 / val 0.06043
-  epoch  20: train 0.07447 / val 0.06288
-  epoch  21: train 0.07330 / val 0.05902
-  epoch  22: train 0.07147 / val 0.06108
-  epoch  23: train 0.07110 / val 0.05982
-  epoch  24: train 0.06957 / val 0.05904
-  epoch  25: train 0.06943 / val 0.05856
-  epoch  26: train 0.06864 / val 0.05805
-  epoch  27: train 0.06786 / val 0.05800
-  epoch  28: train 0.06890 / val 0.05940
-  epoch  29: train 0.06792 / val 0.05725
+  epoch   1: train 0.37993 / val 0.31435
+  epoch   2: train 0.29011 / val 0.21095
+  epoch   3: train 0.16007 / val 0.08758
+  epoch   4: train 0.08549 / val 0.06635
+  epoch   5: train 0.07169 / val 0.06158
+  epoch   6: train 0.06394 / val 0.05309
+  epoch   7: train 0.05699 / val 0.04959
+  epoch   8: train 0.05335 / val 0.04846
+  epoch   9: train 0.05140 / val 0.04560
+  epoch  10: train 0.04896 / val 0.04545
+  epoch  11: train 0.04737 / val 0.04259
+  epoch  12: train 0.04618 / val 0.04317
+  epoch  13: train 0.04588 / val 0.04253
+  epoch  14: train 0.04480 / val 0.04094
+  epoch  15: train 0.04414 / val 0.04123
+  epoch  16: train 0.04333 / val 0.04159
+  epoch  17: train 0.04308 / val 0.04046
+  epoch  18: train 0.04237 / val 0.03925
+  epoch  19: train 0.04234 / val 0.03930
+  epoch  20: train 0.04203 / val 0.03951
+  epoch  21: train 0.04133 / val 0.03943
+  epoch  22: train 0.04157 / val 0.03885
+  epoch  23: train 0.04064 / val 0.03953
+  epoch  24: train 0.04047 / val 0.03890
+  epoch  25: train 0.04009 / val 0.03802
+  epoch  26: train 0.04013 / val 0.03752
 
-[3] 评估集重构 MSE: 0.05667
+[3] 评估集重构 MSE: 0.03724
 
 [4] 指纹健康检查
-  向量形状: (140, 128)
+  向量形状: (549, 128)
   全部有限值: True
   128 维中退化维数(std<1e-6): 0
 
 [2] 指纹区分度
-  平均同部位距离: 0.160397
-  平均跨部位距离: 4.646139
-  同部位/跨部位比值: 0.0345 (应明显 < 1)
+  平均同部位距离: 0.445900
+  平均跨部位距离: 4.307701
+  同部位/跨部位比值: 0.1035 (应明显 < 1)
 
 结论: 通过

二进制
backend/LabelingPreTraining/checkpoints/tspulse_frozen.pt


+ 1 - 1
backend/LabelingPreTraining/generate_samples.py

@@ -24,7 +24,7 @@ from app.algorithms.cycles import detect_cycles  # noqa: E402
 from app.db import get_connection  # noqa: E402
 
 HERE = Path(__file__).resolve().parent
-CSV_PATH = HERE / "预训练之文件头.csv"
+CSV_PATH = ROOT / "预训练之文件头.csv"
 OUTPUT_DIR = HERE / "inputdatas"
 MANIFEST_PATH = HERE / "manifest.csv"
 

+ 193 - 0
backend/LabelingPreTraining/manifest.csv

@@ -1,71 +1,264 @@
 group,point_name,measurement_type,wave_file_id,rpm,sample_time,cycle_count,total_samples,start_sample_index,end_sample_index,error
+7号机组一缸压力盖侧_压力,7号机组一缸压力盖侧,压力,97158,996.6,2026-03-20 18:15:00,42,64836,239,65075,
+7号机组一缸压力盖侧_压力,7号机组一缸压力盖侧,压力,97474,996.0,2026-04-03 19:15:04,41,63333,1186,64519,
+7号机组一缸压力盖侧_压力,7号机组一缸压力盖侧,压力,288174,995.7,2026-02-10 19:30:05,42,64896,92,64988,
+7号机组一缸压力盖侧_压力,7号机组一缸压力盖侧,压力,288217,995.8,2026-02-10 12:45:00,41,63347,1258,64605,
+7号机组一缸压力盖侧_压力,7号机组一缸压力盖侧,压力,288271,996.1,2026-02-10 12:00:00,42,64867,163,65030,
 7号机组一缸压力盖侧_压力,7号机组一缸压力盖侧,压力,288423,996.8,2026-02-10 10:45:04,42,64826,53,64879,
 7号机组一缸压力盖侧_压力,7号机组一缸压力盖侧,压力,288424,996.8,2026-02-10 11:00:04,42,64822,87,64909,
+7号机组一缸压力盖侧_压力,7号机组一缸压力盖侧,压力,288560,996.1,2026-02-10 14:15:02,42,64869,185,65054,
+7号机组一缸压力轴侧_压力,7号机组一缸压力轴侧,压力,100645,996.4,2026-03-27 13:00:03,41,63306,946,64252,
+7号机组一缸压力轴侧_压力,7号机组一缸压力轴侧,压力,100691,995.2,2026-03-31 21:45:00,41,63384,615,63999,
+7号机组一缸压力轴侧_压力,7号机组一缸压力轴侧,压力,100745,996.3,2026-03-27 13:45:00,42,64861,472,65333,
+7号机组一缸压力轴侧_压力,7号机组一缸压力轴侧,压力,100814,995.3,2026-03-30 06:00:03,41,63380,681,64061,
+7号机组一缸压力轴侧_压力,7号机组一缸压力轴侧,压力,100837,996.0,2026-03-30 06:45:00,42,64876,277,65153,
+7号机组一缸压力轴侧_压力,7号机组一缸压力轴侧,压力,100838,996.0,2026-03-31 21:15:04,42,64877,616,65493,
 7号机组一缸压力轴侧_压力,7号机组一缸压力轴侧,压力,292277,996.8,2026-02-10 10:45:04,42,64826,53,64879,
 7号机组一缸压力轴侧_压力,7号机组一缸压力轴侧,压力,292335,996.8,2026-02-10 11:00:04,42,64822,87,64909,
+7号机组三缸压力盖侧_压力,7号机组三缸压力盖侧,压力,18513,996.0,2025-07-27 15:30:02,41,63329,1145,64474,
+7号机组三缸压力盖侧_压力,7号机组三缸压力盖侧,压力,18551,995.9,2025-07-27 15:45:01,41,63337,1395,64732,
 7号机组三缸压力盖侧_压力,7号机组三缸压力盖侧,压力,19025,995.2,2025-04-29 18:30:04,41,63380,1151,64531,
 7号机组三缸压力盖侧_压力,7号机组三缸压力盖侧,压力,19070,995.3,2025-04-29 18:45:03,41,63378,697,64075,
+7号机组三缸压力盖侧_压力,7号机组三缸压力盖侧,压力,114956,995.2,2026-03-31 03:30:00,42,64930,432,65362,
+7号机组三缸压力盖侧_压力,7号机组三缸压力盖侧,压力,114983,996.4,2026-03-31 03:45:03,42,64850,657,65507,
+7号机组三缸压力盖侧_压力,7号机组三缸压力盖侧,压力,115050,996.2,2026-04-01 00:30:03,41,63319,1038,64357,
+7号机组三缸压力盖侧_压力,7号机组三缸压力盖侧,压力,115053,996.4,2026-03-31 23:45:00,42,64853,552,65405,
+7号机组三缸压力盖侧_压力,7号机组三缸压力盖侧,压力,115059,995.2,2026-03-31 21:45:00,41,63384,615,63999,
+7号机组三缸压力盖侧_压力,7号机组三缸压力盖侧,压力,115063,995.9,2026-03-31 21:00:04,41,63338,681,64019,
 7号机组三缸压力轴侧_压力,7号机组三缸压力轴侧,压力,20938,996.4,2025-04-29 15:45:00,42,64847,209,65056,
 7号机组三缸压力轴侧_压力,7号机组三缸压力轴侧,压力,20946,996.6,2025-04-29 16:00:02,42,64834,515,65349,
+7号机组三缸压力轴侧_压力,7号机组三缸压力轴侧,压力,117593,995.7,2026-03-17 07:30:04,41,63353,1407,64760,
+7号机组三缸压力轴侧_压力,7号机组三缸压力轴侧,压力,117627,996.2,2026-03-17 15:45:00,41,63320,800,64120,
+7号机组三缸压力轴侧_压力,7号机组三缸压力轴侧,压力,117703,995.1,2026-03-30 07:00:05,41,63392,584,63976,
+7号机组三缸压力轴侧_压力,7号机组三缸压力轴侧,压力,117708,995.3,2026-03-17 07:00:05,42,64922,380,65302,
+7号机组三缸压力轴侧_压力,7号机组三缸压力轴侧,压力,117718,995.5,2026-03-17 15:15:03,42,64912,146,65058,
+7号机组三缸压力轴侧_压力,7号机组三缸压力轴侧,压力,117769,995.1,2026-03-30 06:15:01,42,64935,564,65499,
+7号机组二缸压力盖侧_压力,7号机组二缸压力盖侧,压力,106279,996.6,2026-03-31 12:00:02,41,63292,1384,64676,
+7号机组二缸压力盖侧_压力,7号机组二缸压力盖侧,压力,106377,996.0,2026-03-31 12:15:00,41,63332,1039,64371,
+7号机组二缸压力盖侧_压力,7号机组二缸压力盖侧,压力,106402,995.9,2026-04-01 11:00:04,41,63343,1097,64440,
+7号机组二缸压力盖侧_压力,7号机组二缸压力盖侧,压力,106461,996.5,2026-04-01 11:30:01,41,63302,1230,64532,
 7号机组二缸压力盖侧_压力,7号机组二缸压力盖侧,压力,298530,995.9,2026-03-14 02:30:00,41,63342,1066,64408,
 7号机组二缸压力盖侧_压力,7号机组二缸压力盖侧,压力,298693,995.6,2026-03-14 02:15:01,41,63359,993,64352,
 7号机组二缸压力轴侧_压力,7号机组二缸压力轴侧,压力,16831,996.8,2025-04-30 01:15:00,41,63283,1165,64448,
 7号机组二缸压力轴侧_压力,7号机组二缸压力轴侧,压力,16871,995.6,2025-04-30 01:00:03,41,63355,1468,64823,
+7号机组二缸压力轴侧_压力,7号机组二缸压力轴侧,压力,109516,996.0,2026-03-31 21:15:04,42,64877,616,65493,
+7号机组二缸压力轴侧_压力,7号机组二缸压力轴侧,压力,109523,995.2,2026-03-31 21:45:00,41,63384,615,63999,
+7号机组二缸压力轴侧_压力,7号机组二缸压力轴侧,压力,302408,995.7,2026-03-14 17:00:02,42,64897,598,65495,
+7号机组二缸压力轴侧_压力,7号机组二缸压力轴侧,压力,302560,996.7,2026-03-14 17:15:04,42,64833,671,65504,
 7号机组五缸压力盖侧_压力,7号机组五缸压力盖侧,压力,26882,996.2,2025-04-29 15:15:00,42,64863,619,65482,
 7号机组五缸压力盖侧_压力,7号机组五缸压力盖侧,压力,26999,996.6,2025-04-29 15:30:00,42,64840,488,65328,
+7号机组五缸压力盖侧_压力,7号机组五缸压力盖侧,压力,132232,995.9,2026-03-30 13:45:01,42,64883,378,65261,
+7号机组五缸压力盖侧_压力,7号机组五缸压力盖侧,压力,132233,995.0,2026-03-30 14:15:01,42,64939,390,65329,
+7号机组五缸压力盖侧_压力,7号机组五缸压力盖侧,压力,330036,996.7,2026-02-13 07:45:03,41,63289,1360,64649,
+7号机组五缸压力盖侧_压力,7号机组五缸压力盖侧,压力,330038,995.8,2026-02-13 07:30:03,42,64889,491,65380,
+7号机组五缸压力盖侧_压力,7号机组五缸压力盖侧,压力,330041,995.4,2026-02-13 07:15:06,41,63369,1396,64765,
 7号机组五缸压力轴侧_压力,7号机组五缸压力轴侧,压力,28850,995.3,2025-04-29 16:15:01,42,64924,236,65160,
+7号机组五缸压力轴侧_压力,7号机组五缸压力轴侧,压力,135351,995.1,2026-03-31 17:45:00,42,64933,125,65058,
+7号机组五缸压力轴侧_压力,7号机组五缸压力轴侧,压力,135361,995.0,2026-03-31 18:15:02,41,63397,1334,64731,
+7号机组五缸压力轴侧_压力,7号机组五缸压力轴侧,压力,135383,995.0,2026-04-01 03:45:01,41,63396,1335,64731,
+7号机组五缸压力轴侧_压力,7号机组五缸压力轴侧,压力,135405,995.6,2026-04-01 03:30:01,42,64901,67,64968,
 7号机组五缸压力轴侧_压力,7号机组五缸压力轴侧,压力,333831,996.9,2026-03-14 07:45:03,41,63277,1246,64523,
+7号机组六缸压力盖侧_压力,7号机组六缸压力盖侧,压力,139404,996.2,2026-03-17 13:15:03,42,64863,0,64863,
+7号机组六缸压力盖侧_压力,7号机组六缸压力盖侧,压力,139515,995.8,2026-03-17 21:30:03,41,63346,1115,64461,
+7号机组六缸压力盖侧_压力,7号机组六缸压力盖侧,压力,139535,996.1,2026-03-17 12:45:04,42,64867,12,64879,
+7号机组六缸压力盖侧_压力,7号机组六缸压力盖侧,压力,139741,995.9,2026-03-17 20:45:06,42,64882,339,65221,
 7号机组六缸压力盖侧_压力,7号机组六缸压力盖侧,压力,338002,995.6,2026-03-13 14:16:25,41,63357,1314,64671,
 7号机组六缸压力盖侧_压力,7号机组六缸压力盖侧,压力,338272,996.8,2026-03-13 12:45:04,42,64829,97,64926,
+7号机组六缸压力轴侧_压力,7号机组六缸压力轴侧,压力,143020,996.4,2026-03-30 12:00:02,41,63309,669,63978,
+7号机组六缸压力轴侧_压力,7号机组六缸压力轴侧,压力,143060,996.1,2026-03-30 12:30:03,41,63323,1039,64362,
+7号机组六缸压力轴侧_压力,7号机组六缸压力轴侧,压力,143132,995.8,2026-04-01 06:15:01,41,63348,1403,64751,
+7号机组六缸压力轴侧_压力,7号机组六缸压力轴侧,压力,143139,996.3,2026-03-31 22:45:02,42,64860,251,65111,
+7号机组六缸压力轴侧_压力,7号机组六缸压力轴侧,压力,143146,996.4,2026-04-01 06:45:00,42,64851,452,65303,
+7号机组六缸压力轴侧_压力,7号机组六缸压力轴侧,压力,143152,995.2,2026-03-31 22:15:00,42,64928,30,64958,
 7号机组六缸压力轴侧_压力,7号机组六缸压力轴侧,压力,342667,997.2,2026-02-10 11:15:01,42,64800,383,65183,
 7号机组六缸压力轴侧_压力,7号机组六缸压力轴侧,压力,342757,998.5,2026-02-10 10:30:05,41,63177,971,64148,
+7号机组四缸压力盖侧_压力,7号机组四缸压力盖侧,压力,22467,996.3,2025-07-27 07:30:02,42,64860,154,65014,
+7号机组四缸压力盖侧_压力,7号机组四缸压力盖侧,压力,22500,996.0,2025-07-27 08:00:01,41,63331,1027,64358,
 7号机组四缸压力盖侧_压力,7号机组四缸压力盖侧,压力,22750,995.8,2025-04-30 00:00:04,42,64892,419,65311,
 7号机组四缸压力盖侧_压力,7号机组四缸压力盖侧,压力,22775,996.3,2025-04-30 00:45:03,42,64860,432,65292,
+7号机组四缸压力盖侧_压力,7号机组四缸压力盖侧,压力,23005,996.1,2025-06-12 01:00:01,42,64870,23,64893,
+7号机组四缸压力盖侧_压力,7号机组四缸压力盖侧,压力,23106,996.0,2025-06-12 00:30:40,42,64874,432,65306,
 7号机组四缸压力轴侧_压力,7号机组四缸压力轴侧,压力,24781,995.7,2025-04-30 00:15:04,41,63353,1042,64395,
 7号机组四缸压力轴侧_压力,7号机组四缸压力轴侧,压力,24849,996.8,2025-04-29 23:30:00,41,63283,1159,64442,
+7号机组四缸压力轴侧_压力,7号机组四缸压力轴侧,压力,126480,995.2,2026-03-31 03:30:00,42,64930,432,65362,
+7号机组四缸压力轴侧_压力,7号机组四缸压力轴侧,压力,126520,996.4,2026-03-31 03:45:03,42,64850,657,65507,
+7号机组四缸压力轴侧_压力,7号机组四缸压力轴侧,压力,126771,995.2,2026-03-31 21:45:00,41,63384,615,63999,
+7号机组四缸压力轴侧_压力,7号机组四缸压力轴侧,压力,126787,996.0,2026-03-31 21:15:04,42,64877,616,65493,
+8号机组一缸压力盖侧_压力,8号机组一缸压力盖侧,压力,159206,995.9,2026-03-17 20:15:00,41,63337,1273,64610,
+8号机组一缸压力盖侧_压力,8号机组一缸压力盖侧,压力,159241,997.3,2026-03-17 19:30:05,42,64792,144,64936,
+8号机组一缸压力盖侧_压力,8号机组一缸压力盖侧,压力,159261,996.9,2026-04-18 18:30:00,41,63275,1240,64515,
+8号机组一缸压力盖侧_压力,8号机组一缸压力盖侧,压力,159263,996.9,2026-04-18 19:00:02,42,64819,595,65414,
+8号机组一缸压力盖侧_压力,8号机组一缸压力盖侧,压力,159268,995.5,2026-04-18 23:30:04,41,63365,634,63999,
+8号机组一缸压力盖侧_压力,8号机组一缸压力盖侧,压力,159282,996.5,2026-04-18 23:00:02,42,64843,308,65151,
 8号机组一缸压力盖侧_压力,8号机组一缸压力盖侧,压力,360911,997.8,2026-03-15 11:00:01,42,64758,171,64929,
 8号机组一缸压力盖侧_压力,8号机组一缸压力盖侧,压力,360921,997.1,2026-03-15 10:45:06,42,64806,250,65056,
+8号机组一缸压力轴侧_压力,8号机组一缸压力轴侧,压力,162105,996.0,2026-03-18 14:45:04,41,63332,1025,64357,
+8号机组一缸压力轴侧_压力,8号机组一缸压力轴侧,压力,162119,997.4,2026-03-18 14:15:00,42,64788,271,65059,
+8号机组一缸压力轴侧_压力,8号机组一缸压力轴侧,压力,162283,997.2,2026-03-16 18:00:07,41,63259,1051,64310,
+8号机组一缸压力轴侧_压力,8号机组一缸压力轴侧,压力,162303,996.2,2026-03-16 17:15:03,41,63322,884,64206,
+8号机组一缸压力轴侧_压力,8号机组一缸压力轴侧,压力,162320,995.6,2026-04-18 21:15:02,41,63355,1168,64523,
+8号机组一缸压力轴侧_压力,8号机组一缸压力轴侧,压力,162327,996.6,2026-04-18 20:45:00,41,63293,767,64060,
 8号机组一缸压力轴侧_压力,8号机组一缸压力轴侧,压力,364619,997.1,2026-03-15 10:45:06,42,64806,250,65056,
 8号机组一缸压力轴侧_压力,8号机组一缸压力轴侧,压力,364620,997.8,2026-03-15 11:00:01,42,64758,171,64929,
+8号机组三缸压力盖侧_压力,8号机组三缸压力盖侧,压力,176089,996.6,2026-03-16 01:00:01,42,64842,124,64966,
+8号机组三缸压力盖侧_压力,8号机组三缸压力盖侧,压力,176172,996.8,2026-03-16 08:45:04,41,63279,1250,64529,
+8号机组三缸压力盖侧_压力,8号机组三缸压力盖侧,压力,176216,997.3,2026-03-20 19:30:01,41,63252,1049,64301,
+8号机组三缸压力盖侧_压力,8号机组三缸压力盖侧,压力,176218,997.3,2026-03-20 20:00:03,42,64796,75,64871,
+8号机组三缸压力盖侧_压力,8号机组三缸压力盖侧,压力,176225,995.8,2026-03-16 00:30:04,42,64890,36,64926,
+8号机组三缸压力盖侧_压力,8号机组三缸压力盖侧,压力,176304,997.3,2026-03-16 09:00:04,42,64794,663,65457,
 8号机组三缸压力盖侧_压力,8号机组三缸压力盖侧,压力,381256,997.5,2026-03-15 11:30:02,41,63239,945,64184,
 8号机组三缸压力盖侧_压力,8号机组三缸压力盖侧,压力,381257,997.6,2026-03-15 12:00:02,41,63230,1434,64664,
+8号机组三缸压力轴侧_压力,8号机组三缸压力轴侧,压力,179141,996.6,2026-03-19 04:15:03,41,63290,1173,64463,
+8号机组三缸压力轴侧_压力,8号机组三缸压力轴侧,压力,179188,997.0,2026-03-19 03:30:04,42,64815,275,65090,
+8号机组三缸压力轴侧_压力,8号机组三缸压力轴侧,压力,179514,996.9,2026-04-18 18:30:00,41,63275,1240,64515,
+8号机组三缸压力轴侧_压力,8号机组三缸压力轴侧,压力,179531,996.9,2026-04-18 19:00:02,42,64819,595,65414,
 8号机组三缸压力轴侧_压力,8号机组三缸压力轴侧,压力,384951,997.6,2026-03-15 12:00:02,41,63230,1434,64664,
 8号机组三缸压力轴侧_压力,8号机组三缸压力轴侧,压力,384959,997.8,2026-03-15 11:15:00,41,63221,804,64025,
+8号机组三缸压力轴侧_压力,8号机组三缸压力轴侧,压力,384969,996.4,2026-03-15 21:15:03,42,64850,116,64966,
+8号机组三缸压力轴侧_压力,8号机组三缸压力轴侧,压力,384976,996.3,2026-03-15 20:45:01,42,64860,152,65012,
+8号机组二缸压力盖侧_压力,8号机组二缸压力盖侧,压力,167787,996.4,2026-03-17 13:45:02,41,63303,1112,64415,
+8号机组二缸压力盖侧_压力,8号机组二缸压力盖侧,压力,167804,996.7,2026-03-17 13:15:03,42,64836,309,65145,
+8号机组二缸压力盖侧_压力,8号机组二缸压力盖侧,压力,167868,996.9,2026-04-18 19:00:02,42,64819,595,65414,
+8号机组二缸压力盖侧_压力,8号机组二缸压力盖侧,压力,167878,996.9,2026-04-18 18:30:00,41,63275,1240,64515,
 8号机组二缸压力盖侧_压力,8号机组二缸压力盖侧,压力,370923,997.6,2026-03-15 12:00:02,41,63230,1434,64664,
 8号机组二缸压力盖侧_压力,8号机组二缸压力盖侧,压力,370926,997.8,2026-03-15 11:15:00,41,63221,804,64025,
+8号机组二缸压力盖侧_压力,8号机组二缸压力盖侧,压力,370951,996.7,2026-03-15 19:45:02,42,64834,558,65392,
+8号机组二缸压力盖侧_压力,8号机组二缸压力盖侧,压力,370952,996.9,2026-03-15 20:15:04,42,64818,220,65038,
+8号机组二缸压力轴侧_压力,8号机组二缸压力轴侧,压力,170728,997.1,2026-03-18 16:30:04,41,63259,1420,64679,
+8号机组二缸压力轴侧_压力,8号机组二缸压力轴侧,压力,170738,995.6,2026-04-18 21:15:02,41,63355,1168,64523,
+8号机组二缸压力轴侧_压力,8号机组二缸压力轴侧,压力,170751,996.6,2026-04-18 20:45:00,41,63293,767,64060,
+8号机组二缸压力轴侧_压力,8号机组二缸压力轴侧,压力,170788,996.2,2026-03-18 16:00:00,42,64865,456,65321,
 8号机组二缸压力轴侧_压力,8号机组二缸压力轴侧,压力,374626,997.8,2026-03-15 11:15:00,41,63221,804,64025,
 8号机组二缸压力轴侧_压力,8号机组二缸压力轴侧,压力,374638,997.6,2026-03-15 12:00:02,41,63230,1434,64664,
+8号机组二缸压力轴侧_压力,8号机组二缸压力轴侧,压力,374650,996.7,2026-03-15 19:45:02,42,64834,558,65392,
+8号机组二缸压力轴侧_压力,8号机组二缸压力轴侧,压力,374660,996.9,2026-03-15 20:15:04,42,64818,220,65038,
+8号机组五缸压力盖侧_压力,8号机组五缸压力盖侧,压力,193321,995.9,2026-03-17 14:30:04,42,64882,344,65226,
+8号机组五缸压力盖侧_压力,8号机组五缸压力盖侧,压力,193426,997.3,2026-03-20 20:00:03,42,64796,75,64871,
+8号机组五缸压力盖侧_压力,8号机组五缸压力盖侧,压力,193544,996.1,2026-03-17 15:00:01,41,63325,911,64236,
+8号机组五缸压力盖侧_压力,8号机组五缸压力盖侧,压力,193554,996.3,2026-03-20 20:30:05,42,64861,369,65230,
+8号机组五缸压力盖侧_压力,8号机组五缸压力盖侧,压力,193628,996.5,2026-03-18 18:00:05,41,63304,1233,64537,
+8号机组五缸压力盖侧_压力,8号机组五缸压力盖侧,压力,193648,996.1,2026-03-18 17:30:03,42,64868,411,65279,
 8号机组五缸压力盖侧_压力,8号机组五缸压力盖侧,压力,401056,997.8,2026-03-15 11:15:00,41,63221,804,64025,
 8号机组五缸压力盖侧_压力,8号机组五缸压力盖侧,压力,401062,997.6,2026-03-15 12:00:02,41,63230,1434,64664,
+8号机组五缸压力盖侧_压力,8号机组五缸压力盖侧,压力,401074,996.7,2026-03-15 19:45:02,42,64834,558,65392,
+8号机组五缸压力盖侧_压力,8号机组五缸压力盖侧,压力,401091,996.9,2026-03-15 20:15:04,42,64818,220,65038,
+8号机组五缸压力轴侧_压力,8号机组五缸压力轴侧,压力,196239,997.5,2026-03-18 12:00:00,41,63238,816,64054,
+8号机组五缸压力轴侧_压力,8号机组五缸压力轴侧,压力,196254,997.4,2026-03-18 12:30:04,41,63241,740,63981,
 8号机组五缸压力轴侧_压力,8号机组五缸压力轴侧,压力,196528,997.3,2026-03-18 00:00:31,41,63252,775,64027,
 8号机组五缸压力轴侧_压力,8号机组五缸压力轴侧,压力,196618,997.2,2026-03-17 23:15:04,41,63258,752,64010,
+8号机组五缸压力轴侧_压力,8号机组五缸压力轴侧,压力,196716,996.9,2026-04-18 18:30:00,41,63275,1240,64515,
+8号机组五缸压力轴侧_压力,8号机组五缸压力轴侧,压力,196717,996.9,2026-04-18 19:00:02,42,64819,595,65414,
 8号机组六缸压力轴侧_压力,8号机组六缸压力轴侧,压力,204220,997.2,2026-03-17 05:15:07,41,63259,1387,64646,
 8号机组六缸压力轴侧_压力,8号机组六缸压力轴侧,压力,204282,995.9,2026-03-17 04:30:00,42,64884,614,65498,
+8号机组六缸压力轴侧_压力,8号机组六缸压力轴侧,压力,204457,995.4,2026-04-18 22:30:01,42,64914,218,65132,
+8号机组六缸压力轴侧_压力,8号机组六缸压力轴侧,压力,204458,995.5,2026-04-18 18:45:00,41,63365,1072,64437,
+8号机组六缸压力轴侧_压力,8号机组六缸压力轴侧,压力,204473,996.5,2026-04-18 23:00:05,41,63299,712,64011,
+8号机组六缸压力轴侧_压力,8号机组六缸压力轴侧,压力,204475,996.9,2026-04-18 18:30:01,41,63274,1328,64602,
+8号机组六缸压力轴侧_压力,8号机组六缸压力轴侧,压力,413797,996.2,2026-03-15 14:00:02,41,63320,715,64035,
+8号机组六缸压力轴侧_压力,8号机组六缸压力轴侧,压力,413809,996.2,2026-03-15 14:15:00,42,64866,415,65281,
 8号机组四缸压力盖侧_压力,8号机组四缸压力盖侧,压力,184949,996.0,2026-03-16 22:15:02,42,64877,28,64905,
 8号机组四缸压力盖侧_压力,8号机组四缸压力盖侧,压力,184980,997.2,2026-03-16 23:00:01,42,64797,358,65155,
+8号机组四缸压力盖侧_压力,8号机组四缸压力盖侧,压力,185051,995.5,2026-04-18 23:30:04,41,63365,634,63999,
+8号机组四缸压力盖侧_压力,8号机组四缸压力盖侧,压力,185052,995.6,2026-04-18 21:15:02,41,63355,1168,64523,
+8号机组四缸压力盖侧_压力,8号机组四缸压力盖侧,压力,185060,996.5,2026-04-18 23:00:02,42,64843,308,65151,
+8号机组四缸压力盖侧_压力,8号机组四缸压力盖侧,压力,185062,996.6,2026-04-18 20:45:00,41,63293,767,64060,
 8号机组四缸压力轴侧_压力,8号机组四缸压力轴侧,压力,188004,996.1,2026-03-17 13:30:00,41,63328,1502,64830,
 8号机组四缸压力轴侧_压力,8号机组四缸压力轴侧,压力,188054,996.9,2026-03-17 14:15:02,41,63274,706,63980,
+8号机组四缸压力轴侧_压力,8号机组四缸压力轴侧,压力,188129,996.9,2026-04-18 18:30:00,41,63275,1240,64515,
+8号机组四缸压力轴侧_压力,8号机组四缸压力轴侧,压力,188130,996.9,2026-04-18 19:00:02,42,64819,595,65414,
+8号机组四缸压力轴侧_压力,8号机组四缸压力轴侧,压力,394963,996.9,2026-03-15 14:45:04,41,63273,790,64063,
+8号机组四缸压力轴侧_压力,8号机组四缸压力轴侧,压力,394978,996.8,2026-03-15 15:00:03,41,63280,1248,64528,
+9号机组一缸压力盖侧_压力,9号机组一缸压力盖侧,压力,221469,995.4,2026-04-16 19:15:04,41,63371,982,64353,
+9号机组一缸压力盖侧_压力,9号机组一缸压力盖侧,压力,221564,996.6,2026-04-16 19:45:01,41,63297,702,63999,
+9号机组一缸压力盖侧_压力,9号机组一缸压力盖侧,压力,222391,995.9,2026-03-24 23:30:02,41,63336,893,64229,
+9号机组一缸压力盖侧_压力,9号机组一缸压力盖侧,压力,222420,996.6,2026-03-25 00:00:01,41,63298,1025,64323,
 9号机组一缸压力盖侧_压力,9号机组一缸压力盖侧,压力,434803,997.1,2026-01-30 13:30:06,41,63263,1434,64697,
 9号机组一缸压力盖侧_压力,9号机组一缸压力盖侧,压力,434810,997.3,2026-01-30 12:45:05,41,63251,1399,64650,
+9号机组一缸压力盖侧_压力,9号机组一缸压力盖侧,压力,435766,996.1,2026-02-04 17:15:01,41,63330,925,64255,
+9号机组一缸压力盖侧_压力,9号机组一缸压力盖侧,压力,435831,996.7,2026-02-04 17:45:03,41,63291,784,64075,
+9号机组一缸压力轴侧_压力,9号机组一缸压力轴侧,压力,224833,995.4,2026-04-14 23:30:02,41,63371,1249,64620,
+9号机组一缸压力轴侧_压力,9号机组一缸压力轴侧,压力,224929,996.5,2026-04-14 23:00:03,42,64845,351,65196,
+9号机组一缸压力轴侧_压力,9号机组一缸压力轴侧,压力,225792,995.6,2026-04-01 20:15:03,42,64905,348,65253,
+9号机组一缸压力轴侧_压力,9号机组一缸压力轴侧,压力,225845,996.8,2026-04-01 20:45:00,42,64830,103,64933,
 9号机组一缸压力轴侧_压力,9号机组一缸压力轴侧,压力,438740,997.1,2026-01-30 13:00:07,42,64806,216,65022,
 9号机组一缸压力轴侧_压力,9号机组一缸压力轴侧,压力,438871,996.2,2026-01-30 12:15:01,42,64869,364,65233,
+9号机组一缸压力轴侧_压力,9号机组一缸压力轴侧,压力,439978,997.1,2026-02-04 04:45:05,42,64806,321,65127,
+9号机组一缸压力轴侧_压力,9号机组一缸压力轴侧,压力,439984,997.2,2026-02-04 04:30:00,42,64802,197,64999,
 9号机组三缸压力盖侧_压力,9号机组三缸压力盖侧,压力,8828,996.7,2025-03-13 11:09:07,41,63292,1386,64678,
 9号机组三缸压力盖侧_压力,9号机组三缸压力盖侧,压力,8874,997.1,2025-03-13 11:45:04,41,63263,1254,64517,
+9号机组三缸压力盖侧_压力,9号机组三缸压力盖侧,压力,240909,995.7,2026-03-25 02:00:04,42,64895,354,65249,
+9号机组三缸压力盖侧_压力,9号机组三缸压力盖侧,压力,240995,996.5,2026-04-17 11:45:01,41,63300,942,64242,
+9号机组三缸压力盖侧_压力,9号机组三缸压力盖侧,压力,241036,995.3,2026-03-25 02:30:00,41,63381,1058,64439,
+9号机组三缸压力盖侧_压力,9号机组三缸压力盖侧,压力,241037,995.4,2026-04-17 12:15:00,42,64921,387,65308,
+9号机组三缸压力盖侧_压力,9号机组三缸压力盖侧,压力,457748,996.4,2026-02-07 00:45:04,42,64854,256,65110,
+9号机组三缸压力盖侧_压力,9号机组三缸压力盖侧,压力,457934,997.5,2026-02-07 00:01:02,42,64783,447,65230,
+9号机组三缸压力轴侧_压力,9号机组三缸压力轴侧,压力,11292,997.0,2025-03-14 01:15:04,42,64816,506,65322,
+9号机组三缸压力轴侧_压力,9号机组三缸压力轴侧,压力,11299,996.2,2025-03-14 01:45:01,42,64863,467,65330,
 9号机组三缸压力轴侧_压力,9号机组三缸压力轴侧,压力,11667,996.7,2025-03-13 11:09:07,41,63292,1386,64678,
 9号机组三缸压力轴侧_压力,9号机组三缸压力轴侧,压力,11677,997.1,2025-03-13 11:45:04,41,63263,1254,64517,
+9号机组三缸压力轴侧_压力,9号机组三缸压力轴侧,压力,242505,996.2,2026-04-15 08:45:04,41,63327,854,64181,
+9号机组三缸压力轴侧_压力,9号机组三缸压力轴侧,压力,243018,995.6,2026-04-15 08:15:02,42,64903,152,65055,
+9号机组三缸压力轴侧_压力,9号机组三缸压力轴侧,压力,461593,996.1,2026-02-08 00:30:04,41,63325,1301,64626,
+9号机组三缸压力轴侧_压力,9号机组三缸压力轴侧,压力,461693,997.4,2026-02-08 00:45:06,42,64789,49,64838,
+9号机组二缸压力盖侧_压力,9号机组二缸压力盖侧,压力,3825,996.3,2025-03-14 00:15:03,42,64862,31,64893,
+9号机组二缸压力盖侧_压力,9号机组二缸压力盖侧,压力,3862,996.0,2025-03-14 00:45:03,41,63334,839,64173,
 9号机组二缸压力盖侧_压力,9号机组二缸压力盖侧,压力,4335,996.7,2025-03-13 11:09:07,41,63292,1386,64678,
 9号机组二缸压力盖侧_压力,9号机组二缸压力盖侧,压力,4343,997.1,2025-03-13 11:45:04,41,63263,1254,64517,
+9号机组二缸压力盖侧_压力,9号机组二缸压力盖侧,压力,230840,996.3,2026-04-13 07:00:01,42,64859,244,65103,
+9号机组二缸压力盖侧_压力,9号机组二缸压力盖侧,压力,230884,996.6,2026-04-13 07:15:00,41,63297,1093,64390,
+9号机组二缸压力盖侧_压力,9号机组二缸压力盖侧,压力,446972,996.2,2026-02-08 16:30:01,41,63324,900,64224,
+9号机组二缸压力盖侧_压力,9号机组二缸压力盖侧,压力,447194,997.4,2026-02-08 17:00:03,41,63245,1289,64534,
+9号机组二缸压力轴侧_压力,9号机组二缸压力轴侧,压力,6400,996.3,2025-03-14 00:15:03,42,64862,31,64893,
+9号机组二缸压力轴侧_压力,9号机组二缸压力轴侧,压力,6432,996.0,2025-03-14 00:45:03,41,63334,839,64173,
 9号机组二缸压力轴侧_压力,9号机组二缸压力轴侧,压力,6778,997.1,2025-03-13 11:45:04,41,63263,1254,64517,
 9号机组二缸压力轴侧_压力,9号机组二缸压力轴侧,压力,6837,996.7,2025-03-13 11:09:07,41,63292,1386,64678,
+9号机组二缸压力轴侧_压力,9号机组二缸压力轴侧,压力,233820,996.5,2026-04-16 00:00:02,42,64844,135,64979,
+9号机组二缸压力轴侧_压力,9号机组二缸压力轴侧,压力,233908,996.7,2026-04-16 00:15:01,41,63291,855,64146,
+9号机组二缸压力轴侧_压力,9号机组二缸压力轴侧,压力,449869,997.6,2026-03-09 11:15:02,42,64776,234,65010,
+9号机组二缸压力轴侧_压力,9号机组二缸压力轴侧,压力,449892,997.4,2026-03-09 11:45:01,41,63243,981,64224,
+9号机组五缸压力盖侧_压力,9号机组五缸压力盖侧,压力,257639,996.7,2026-03-23 14:30:01,41,63293,1325,64618,
+9号机组五缸压力盖侧_压力,9号机组五缸压力盖侧,压力,257693,995.8,2026-03-23 15:00:00,42,64891,490,65381,
+9号机组五缸压力盖侧_压力,9号机组五缸压力盖侧,压力,259312,996.6,2026-04-17 11:30:03,41,63299,756,64055,
+9号机组五缸压力盖侧_压力,9号机组五缸压力盖侧,压力,259339,996.5,2026-04-17 11:45:01,41,63300,942,64242,
 9号机组五缸压力盖侧_压力,9号机组五缸压力盖侧,压力,480059,996.4,2026-01-30 17:45:06,42,64850,332,65182,
 9号机组五缸压力盖侧_压力,9号机组五缸压力盖侧,压力,480081,997.2,2026-01-30 17:00:02,42,64798,577,65375,
+9号机组五缸压力盖侧_压力,9号机组五缸压力盖侧,压力,481113,996.9,2026-02-03 21:00:04,42,64817,211,65028,
+9号机组五缸压力盖侧_压力,9号机组五缸压力盖侧,压力,481687,996.0,2026-02-03 20:30:00,42,64878,585,65463,
+9号机组五缸压力轴侧_压力,9号机组五缸压力轴侧,压力,261034,995.8,2026-04-17 05:45:00,41,63348,932,64280,
+9号机组五缸压力轴侧_压力,9号机组五缸压力轴侧,压力,261628,995.9,2026-04-17 06:00:00,42,64886,357,65243,
 9号机组五缸压力轴侧_压力,9号机组五缸压力轴侧,压力,483701,996.1,2026-01-30 16:45:00,41,63328,1109,64437,
 9号机组五缸压力轴侧_压力,9号机组五缸压力轴侧,压力,483741,997.3,2026-01-30 16:00:01,41,63251,1219,64470,
+9号机组五缸压力轴侧_压力,9号机组五缸压力轴侧,压力,484727,996.1,2026-02-04 08:45:00,41,63325,1213,64538,
+9号机组五缸压力轴侧_压力,9号机组五缸压力轴侧,压力,484847,996.2,2026-02-04 08:15:01,41,63322,691,64013,
+9号机组六缸压力盖侧_压力,9号机组六缸压力盖侧,压力,266547,995.5,2026-03-23 08:15:01,41,63361,1121,64482,
+9号机组六缸压力盖侧_压力,9号机组六缸压力盖侧,压力,266604,996.7,2026-03-23 07:30:02,42,64831,218,65049,
+9号机组六缸压力盖侧_压力,9号机组六缸压力盖侧,压力,266768,996.3,2026-04-05 04:15:00,42,64855,15,64870,
+9号机组六缸压力盖侧_压力,9号机组六缸压力盖侧,压力,266769,996.3,2026-04-05 04:45:04,41,63312,1349,64661,
+9号机组六缸压力盖侧_压力,9号机组六缸压力盖侧,压力,491305,998.6,2026-02-03 18:00:03,41,63168,1391,64559,
+9号机组六缸压力盖侧_压力,9号机组六缸压力盖侧,压力,491313,999.9,2026-02-03 08:30:00,41,63160,1422,64582,
 9号机组六缸压力盖侧_压力,9号机组六缸压力盖侧,压力,491684,996.3,2026-01-31 00:30:03,42,64857,341,65198,
 9号机组六缸压力盖侧_压力,9号机组六缸压力盖侧,压力,491873,997.2,2026-01-30 23:45:04,42,64798,469,65267,
+9号机组六缸压力轴侧_压力,9号机组六缸压力轴侧,压力,268358,995.5,2026-03-23 08:15:01,41,63361,1121,64482,
+9号机组六缸压力轴侧_压力,9号机组六缸压力轴侧,压力,268424,996.0,2026-03-23 08:30:03,41,63332,1330,64662,
+9号机组六缸压力轴侧_压力,9号机组六缸压力轴侧,压力,268935,996.7,2026-04-17 08:30:00,41,63289,1271,64560,
+9号机组六缸压力轴侧_压力,9号机组六缸压力轴侧,压力,268936,996.7,2026-04-17 09:00:02,41,63287,1226,64513,
+9号机组六缸压力轴侧_压力,9号机组六缸压力轴侧,压力,268996,995.2,2026-04-10 15:00:03,42,64928,238,65166,
+9号机组六缸压力轴侧_压力,9号机组六缸压力轴侧,压力,269205,996.6,2026-04-10 14:45:01,41,63295,1361,64656,
 9号机组六缸压力轴侧_压力,9号机组六缸压力轴侧,压力,495215,996.3,2026-01-31 00:30:03,42,64857,341,65198,
 9号机组六缸压力轴侧_压力,9号机组六缸压力轴侧,压力,495369,997.2,2026-01-31 01:15:04,41,63253,1183,64436,
+9号机组四缸压力盖侧_压力,9号机组四缸压力盖侧,压力,248306,996.5,2026-03-23 05:45:04,41,63304,906,64210,
+9号机组四缸压力盖侧_压力,9号机组四缸压力盖侧,压力,248400,996.3,2026-03-23 05:15:02,42,64858,206,65064,
+9号机组四缸压力盖侧_压力,9号机组四缸压力盖侧,压力,248587,995.6,2026-04-17 06:30:02,41,63359,883,64242,
+9号机组四缸压力盖侧_压力,9号机组四缸压力盖侧,压力,248657,995.9,2026-04-17 06:00:00,42,64886,357,65243,
+9号机组四缸压力盖侧_压力,9号机组四缸压力盖侧,压力,249740,995.4,2026-04-05 01:00:02,42,64916,572,65488,
+9号机组四缸压力盖侧_压力,9号机组四缸压力盖侧,压力,249860,996.5,2026-04-05 01:30:01,41,63304,1065,64369,
 9号机组四缸压力盖侧_压力,9号机组四缸压力盖侧,压力,468712,997.2,2026-01-31 00:15:06,42,64801,112,64913,
 9号机组四缸压力盖侧_压力,9号机组四缸压力盖侧,压力,468773,996.1,2026-01-31 01:00:02,41,63328,725,64053,
+9号机组四缸压力轴侧_压力,9号机组四缸压力轴侧,压力,251332,996.5,2026-03-23 05:45:04,41,63304,906,64210,
+9号机组四缸压力轴侧_压力,9号机组四缸压力轴侧,压力,251406,996.3,2026-03-23 05:15:02,42,64858,206,65064,
+9号机组四缸压力轴侧_压力,9号机组四缸压力轴侧,压力,252544,996.4,2026-04-17 19:00:01,41,63310,731,64041,
+9号机组四缸压力轴侧_压力,9号机组四缸压力轴侧,压力,252717,996.3,2026-04-17 18:30:02,42,64857,307,65164,
+9号机组四缸压力轴侧_压力,9号机组四缸压力轴侧,压力,252813,995.4,2026-04-07 16:15:04,41,63374,679,64053,
+9号机组四缸压力轴侧_压力,9号机组四缸压力轴侧,压力,253161,995.6,2026-04-07 15:30:00,42,64908,163,65071,
 9号机组四缸压力轴侧_压力,9号机组四缸压力轴侧,压力,472568,996.8,2026-01-31 03:45:01,42,64831,85,64916,
 9号机组四缸压力轴侧_压力,9号机组四缸压力轴侧,压力,472771,996.0,2026-01-31 04:30:03,42,64878,317,65195,

+ 420 - 0
backend/LabelingPreTraining/predict.py

@@ -0,0 +1,420 @@
+"""Step 3: long-running TSPulse anomaly detection poller.
+
+Polls the wave_file table for rows matching ``rpm > 0 AND measurement_type =
+'压力'``, reads only the first 3000 wave_sample rows per file, locates the
+first complete crankshaft cycle, and encodes it with the frozen TSPulse model
+into a 128-dim fingerprint. The fingerprint is compared against the part's
+centroid and radius from the baseline table and ``tspluse_status`` is written
+back as a proportional integer of the centroid distance:
+
+* 无周期 (no complete cycle found) -> ``-1``
+* 正常 (distance <= 2 x radius)   -> ``0``
+* 异常 (distance > 2 x radius)    -> ``min(127, round(10 x distance / radius))``
+
+Every processed file is written back, so a full re-run from an empty watermark
+cleanly replaces any previously stored levels.
+
+The samples of a whole batch of files are read with one ``IN (...) AND
+sample_index < 3000`` query and all candidate cycles are encoded in a single
+forward pass, so the network round trips and MPS kernel launches are amortised
+across the batch. Files are processed in ``(sample_time, id)`` ascending order
+and a tuple watermark on those two columns prevents re-processing files that
+have already been seen, so the poller first drains the backlog and then keeps
+watching for newly inserted rows.
+
+Usage:
+    conda activate tspulse
+    python predict.py [--start-time ""] [--start-id 0] [--interval 60]
+                      [--batch 100] [--limit 0] [--report-every 100]
+                      [--write-batch 10] [--dry-run]
+"""
+
+import argparse
+import sys
+import time
+from concurrent.futures import ThreadPoolExecutor
+from pathlib import Path
+
+import numpy as np
+import torch
+
+BACKEND = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(BACKEND))
+
+from app.algorithms.cycles import detect_cycles  # noqa: E402
+from app.db import get_connection  # noqa: E402
+from tspulse import TSPulse, describe, get_device  # noqa: E402
+from tspulse.dataset import N_CHANNELS, SEQ_LEN  # noqa: E402
+
+HERE = Path(__file__).resolve().parent
+CHECKPOINT_PATH = HERE / "checkpoints" / "tspulse_frozen.pt"
+BASELINE_PATH = HERE / "baseline" / "baseline.npz"
+
+SAMPLE_LIMIT = 3000
+MEASUREMENT_TYPE = "压力"
+
+ANOMALY_FACTOR = 2.0
+SCALE = 10.0
+STATUS_MAX = 127
+
+
+def load_model(device: torch.device) -> tuple[TSPulse, np.ndarray, np.ndarray]:
+    checkpoint = torch.load(CHECKPOINT_PATH, map_location="cpu", weights_only=False)
+    config = checkpoint["config"]
+    model = TSPulse(dim=config["dim"], depth=config["depth"], heads=config["heads"]).to(device)
+    model.load_state_dict(checkpoint["state_dict"])
+    model.eval()
+    return model, checkpoint["mean"], checkpoint["std"]
+
+
+def load_baseline() -> tuple[dict[str, int], np.ndarray, np.ndarray]:
+    data = np.load(BASELINE_PATH)
+    part_names = data["part_names"].astype(str).tolist()
+    part_index = {part: i for i, part in enumerate(part_names)}
+    return part_index, data["centroids"], data["radii"]
+
+
+def _fetch_chunk(connection, file_ids: list[int]) -> dict[int, np.ndarray]:
+    """Fetch the first SAMPLE_LIMIT rows of a chunk of files in one query."""
+    if not file_ids:
+        return {}
+    placeholders = ",".join(["%s"] * len(file_ids))
+    with connection.cursor() as cursor:
+        cursor.execute(
+            f"""
+            SELECT wave_file_id,
+                   CAST(signal_value AS FLOAT) AS sig,
+                   CAST(second_value AS FLOAT) AS sec
+            FROM wave_sample
+            WHERE wave_file_id IN ({placeholders}) AND sample_index < %s
+            ORDER BY wave_file_id, sample_index
+            """,
+            (*file_ids, SAMPLE_LIMIT),
+        )
+        rows = cursor.fetchall()
+    grouped: dict[int, tuple[list[float], list[float]]] = {}
+    for row in rows:
+        signal, second = grouped.setdefault(row["wave_file_id"], ([], []))
+        signal.append(float(row["sig"]))
+        second.append(float(row["sec"]) if row["sec"] is not None else np.nan)
+    out: dict[int, np.ndarray] = {}
+    for file_id, (signal, second) in grouped.items():
+        count = len(signal)
+        out[file_id] = np.column_stack(
+            [np.arange(count, dtype=float), np.asarray(signal), np.asarray(second)]
+        )
+    return out
+
+
+def read_batch_samples(
+    reader_connections: list,
+    file_ids: list[int],
+    workers: int,
+) -> dict[int, np.ndarray]:
+    """Fetch the first SAMPLE_LIMIT rows of every file in one round-trip.
+
+    The batch is split into ``workers`` chunks fetched on parallel persistent
+    connections. Returns {file_id: (n, 3) array} with columns
+    [sample_index(0..n-1), signal_value, second_value].
+    """
+    if not file_ids:
+        return {}
+    if workers <= 1 or len(file_ids) <= workers:
+        return _fetch_chunk(reader_connections[0], file_ids)
+    chunks = np.array_split(file_ids, min(workers, len(file_ids)))
+    with ThreadPoolExecutor(max_workers=len(chunks)) as executor:
+        futures = [
+            executor.submit(_fetch_chunk, reader_connections[i], chunk.tolist())
+            for i, chunk in enumerate(chunks)
+        ]
+        merged: dict[int, np.ndarray] = {}
+        for future in futures:
+            merged.update(future.result())
+    return merged
+
+
+def build_cycle_matrix(samples: np.ndarray, start: int, end: int) -> np.ndarray:
+    signal = samples[start:end, 1]
+    second = samples[start:end, 2]
+    count = end - start
+    return np.column_stack([signal, second, np.ones(count), np.ones(count)])
+
+
+def flush_status(connection, buffer: list[tuple[int, int]]) -> None:
+    """Write pending (file_id, status) pairs in a single UPDATE statement."""
+    if not buffer:
+        return
+    case_sql = " ".join("WHEN %s THEN %s" for _ in buffer)
+    placeholders = ",".join(["%s"] * len(buffer))
+    params: list[int] = []
+    for file_id, level in buffer:
+        params.extend([file_id, level])
+    with connection.cursor() as cursor:
+        cursor.execute(
+            f"UPDATE wave_file SET tspluse_status = CASE id {case_sql} END "
+            f"WHERE id IN ({placeholders})",
+            tuple(params + [file_id for file_id, _ in buffer]),
+        )
+    buffer.clear()
+
+
+def fetch_batch(
+    connection,
+    watermark_time: object | None,
+    watermark_id: int,
+    batch: int,
+) -> list[dict]:
+    with connection.cursor() as cursor:
+        if watermark_time is None:
+            cursor.execute(
+                """
+                SELECT id, point_name, measurement_type, sample_time
+                FROM wave_file
+                WHERE rpm > 0 AND measurement_type = %s
+                ORDER BY sample_time ASC, id ASC
+                LIMIT %s
+                """,
+                (MEASUREMENT_TYPE, batch),
+            )
+        else:
+            cursor.execute(
+                """
+                SELECT id, point_name, measurement_type, sample_time
+                FROM wave_file
+                WHERE rpm > 0 AND measurement_type = %s
+                  AND (sample_time > %s OR (sample_time = %s AND id > %s))
+                ORDER BY sample_time ASC, id ASC
+                LIMIT %s
+                """,
+                (MEASUREMENT_TYPE, watermark_time, watermark_time, watermark_id, batch),
+            )
+        return cursor.fetchall()
+
+
+def compute_status(distance: float, radius: float) -> int:
+    """Map a fingerprint distance to a proportional integer status.
+
+    Normal (distance <= ANOMALY_FACTOR x radius) returns 0; anomalies are
+    recorded as ``min(STATUS_MAX, round(SCALE x distance / radius))`` so the
+    stored tinyint is proportional to the centroid distance and can be queried
+    with ``tspluse_status >= N``. 无周期 files are handled separately as -1.
+    """
+    ratio = distance / radius
+    if ratio <= ANOMALY_FACTOR:
+        return 0
+    return min(STATUS_MAX, int(round(SCALE * ratio)))
+
+
+def prepare_cycle(
+    samples: np.ndarray,
+    part: str,
+    part_index: dict[str, int],
+) -> tuple[str, np.ndarray | None]:
+    """Locate the first complete cycle and build its [signal, second, 1, 1] matrix.
+
+    Returns (status, matrix); status is "ok" or a skip reason.
+    """
+    cycles, _ = detect_cycles(samples)
+    if not cycles:
+        return "无周期", None
+
+    cycle = cycles[0]
+    matrix = build_cycle_matrix(samples, cycle.start_offset, cycle.end_offset)
+    if len(matrix) > SEQ_LEN:
+        return "周期过长", None
+    if not np.all(np.isfinite(matrix[:, :2])):
+        return "坏数据", None
+    if part not in part_index:
+        return "无基准", None
+    return "ok", matrix
+
+
+def encode_batch(
+    model: TSPulse,
+    mean: np.ndarray,
+    std: np.ndarray,
+    device: torch.device,
+    matrices: list[np.ndarray],
+) -> np.ndarray:
+    """Encode many cycle matrices in a single forward pass -> (B, dim)."""
+    if not matrices:
+        return np.empty((0, model.dim), dtype=np.float32)
+    batch = np.zeros((len(matrices), SEQ_LEN, N_CHANNELS), dtype=np.float32)
+    for index, matrix in enumerate(matrices):
+        normalised = (matrix - mean) / std
+        batch[index, : len(normalised)] = normalised
+    tensor = torch.from_numpy(batch).to(device)
+    with torch.no_grad():
+        fingerprints, _ = model(tensor)
+    return fingerprints.cpu().numpy()
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description="TSPulse 长期异常预测轮询脚本")
+    parser.add_argument("--start-time", type=str, default="",
+                        help="起始水位时间(含),如 2026-01-01 00:00:00,留空从头开始")
+    parser.add_argument("--start-id", type=int, default=0,
+                        help="起始水位 id(含),同一 sample_time 时作为次序")
+    parser.add_argument("--interval", type=int, default=60, help="无新数据时的轮询间隔秒数")
+    parser.add_argument("--batch", type=int, default=100, help="每轮读取的文件数")
+    parser.add_argument("--read-workers", type=int, default=4, help="并行读样本的连接数(0=单连接)")
+    parser.add_argument("--limit", type=int, default=0, help="最多处理文件数,0 表示不限")
+    parser.add_argument("--report-every", type=int, default=100, help="每 N 个文件打印一次汇总")
+    parser.add_argument("--write-batch", type=int, default=100,
+                        help="回写攒满 N 条才批量写一次(每个文件都会回写)")
+    parser.add_argument("--dry-run", action="store_true", help="只预测不回写数据库")
+    args = parser.parse_args()
+
+    if not CHECKPOINT_PATH.exists():
+        print(f"未找到模型 {CHECKPOINT_PATH}, 请先运行 tspulse/train.py")
+        return 1
+    if not BASELINE_PATH.exists():
+        print(f"未找到基准表 {BASELINE_PATH}, 请先运行 build_baseline.py")
+        return 1
+
+    device = get_device()
+    print(f"设备: {describe(device)}")
+    model, mean, std = load_model(device)
+    part_index, centroids, radii = load_baseline()
+    print(f"基准表: {len(part_index)} 个部位, 异常判定: 距离 > {ANOMALY_FACTOR:g}×半径, "
+          f"回写 tspluse_status = min({STATUS_MAX}, round({SCALE:g} × 距离/半径)), 无周期=-1")
+
+    start_time = args.start_time.strip()
+    watermark_time: object | None = start_time or None
+    watermark_id = args.start_id
+    total = 0
+    stats = {"正常": 0, "异常": 0, "无周期": 0}
+    skipped = {"无基准": 0, "坏数据": 0, "周期过长": 0, "无样本": 0}
+    errors = 0
+    last_reported = 0
+    write_buffer: list[tuple[int, int]] = []
+
+    read_workers = max(1, min(args.read_workers, args.batch))
+    reader_connections = [get_connection() for _ in range(read_workers)]
+
+    def flush(connection) -> None:
+        if args.dry_run:
+            write_buffer.clear()
+        else:
+            flush_status(connection, write_buffer)
+
+    def summary_text() -> str:
+        return (
+            f"已处理 {total}, 正常 {stats['正常']}, 异常 {stats['异常']}, "
+            f"无周期 {stats['无周期']}, 跳过 {sum(skipped.values())}({dict(skipped)}), "
+            f"错误 {errors}, 水位 sample_time={watermark_time} id={watermark_id}"
+        )
+
+    print(f"水位起点: sample_time={start_time or '从头'}, id={args.start_id}, "
+          f"批大小: {args.batch}, 读并行: {read_workers} 连接, 间隔: {args.interval}s"
+          + (", 干跑(不回写)" if args.dry_run else ""))
+
+    try:
+        while True:
+            connection = get_connection()
+            try:
+                rows = fetch_batch(connection, watermark_time, watermark_id, args.batch)
+                if not rows:
+                    flush(connection)
+                    connection.close()
+                    print(
+                        f"无新数据(sample_time>{watermark_time}),等待 {args.interval}s 后继续... "
+                        f"[{summary_text()}]",
+                        flush=True,
+                    )
+                    time.sleep(args.interval)
+                    continue
+
+                file_ids = [int(row["id"]) for row in rows]
+                part_of = {int(row["id"]): f"{row['point_name']}_{row['measurement_type']}" for row in rows}
+                sample_map = read_batch_samples(reader_connections, file_ids, read_workers)
+
+                candidates: list[tuple[int, str, np.ndarray]] = []
+                for row in rows:
+                    file_id = int(row["id"])
+                    part = part_of[file_id]
+                    samples = sample_map.get(file_id)
+                    if samples is None or len(samples) == 0:
+                        status, matrix = "无样本", None
+                    else:
+                        try:
+                            status, matrix = prepare_cycle(samples, part, part_index)
+                        except Exception:
+                            errors += 1
+                            status, matrix = "错误", None
+
+                    total += 1
+
+                    if status == "ok":
+                        candidates.append((file_id, part, matrix))
+                    elif status == "无周期":
+                        stats["无周期"] += 1
+                        write_buffer.append((file_id, -1))
+                    else:
+                        if status in skipped:
+                            skipped[status] += 1
+                        write_buffer.append((file_id, 0))
+
+                if candidates:
+                    fingerprints = encode_batch(
+                        model, mean, std, device, [item[2] for item in candidates]
+                    )
+                    for (file_id, part, _matrix), fingerprint in zip(candidates, fingerprints):
+                        index = part_index[part]
+                        distance = float(np.linalg.norm(fingerprint - centroids[index]))
+                        radius = float(radii[index])
+                        status = compute_status(distance, radius)
+                        stats["异常" if status > 0 else "正常"] += 1
+                        write_buffer.append((file_id, status))
+                        if len(write_buffer) >= args.write_batch:
+                            flush(connection)
+
+                if total // args.report_every > last_reported:
+                    last_reported = total // args.report_every
+                    print(f"  汇总: {summary_text()}", flush=True)
+
+                if args.limit and total >= args.limit:
+                    flush(connection)
+                    watermark_time = rows[-1]["sample_time"]
+                    watermark_id = int(rows[-1]["id"])
+                    connection.close()
+                    print(f"已达 --limit={args.limit},退出")
+                    print(f"  汇总: {summary_text()}")
+                    return 0
+
+                flush(connection)
+                watermark_time = rows[-1]["sample_time"]
+                watermark_id = int(rows[-1]["id"])
+            finally:
+                try:
+                    connection.close()
+                except Exception:
+                    pass
+
+    except KeyboardInterrupt:
+        if write_buffer and not args.dry_run:
+            try:
+                with get_connection() as connection:
+                    flush_status(connection, write_buffer)
+            except Exception:
+                pass
+        for connection in reader_connections:
+            try:
+                connection.close()
+            except Exception:
+                pass
+        print(f"\n已中断。{summary_text()}")
+        if watermark_time is not None:
+            print(f"续跑命令: python predict.py --start-time \"{watermark_time}\" --start-id {watermark_id}")
+        return 0
+
+    for connection in reader_connections:
+        try:
+            connection.close()
+        except Exception:
+            pass
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

文件差异内容过多而无法显示
+ 8981 - 974
backend/LabelingPreTraining/split.csv


+ 21 - 2
backend/app/main.py

@@ -103,7 +103,9 @@ def health(_auth: str = Depends(require_auth)) -> dict[str, Any]:
 @app.get("/api/query-options")
 def query_options(_auth: str = Depends(require_auth)) -> dict[str, Any]:
     try:
-        return data_service.query_options()
+        result = data_service.query_options()
+        result["pretrainCounts"] = pretrain_service.recorded_counts()
+        return result
     except Exception as error:
         raise HTTPException(status_code=503, detail=str(error)) from error
 
@@ -114,16 +116,33 @@ def time_points(
     measurement_types: list[str] = Query(default=[]),
     min_time: str | None = None,
     max_time: str | None = None,
+    include_stopped: bool = False,
+    min_status: int | None = Query(default=None, ge=0),
     _auth: str = Depends(require_auth),
 ) -> dict[str, Any]:
     try:
-        return data_service.time_points(point_name, measurement_types, min_time, max_time)
+        return data_service.time_points(
+            point_name,
+            measurement_types,
+            min_time,
+            max_time,
+            include_stopped,
+            min_status,
+        )
     except ValueError as error:
         raise HTTPException(status_code=400, detail=str(error)) from error
     except Exception as error:
         raise HTTPException(status_code=503, detail=str(error)) from error
 
 
+@app.get("/api/tspluse-ruler")
+def tspluse_ruler(_auth: str = Depends(require_auth)) -> dict[str, Any]:
+    try:
+        return data_service.tspluse_ruler()
+    except Exception as error:
+        raise HTTPException(status_code=503, detail=str(error)) from error
+
+
 @app.post("/api/wave-window")
 def wave_window(payload: WaveWindowInput, _auth: str = Depends(require_auth)) -> dict[str, Any]:
     try:

+ 38 - 2
backend/app/services/data_service.py

@@ -180,12 +180,41 @@ class DataService:
             "notice": self._source_notice(source),
         }
 
+    def tspluse_ruler(self) -> dict[str, Any]:
+        """压力部位 tspluse_status 的全局标尺,进入页面时只查询一次。"""
+
+        def database_query():
+            with get_connection() as connection:
+                with connection.cursor() as cursor:
+                    cursor.execute(
+                        """
+                        SELECT MIN(tspluse_status) AS min_status,
+                               MAX(tspluse_status) AS max_status
+                        FROM wave_file
+                        WHERE rpm > 0 AND measurement_type = '压力'
+                        """,
+                    )
+                    row = cursor.fetchone()
+            return {
+                "min": int(row["min_status"]) if row and row["min_status"] is not None else 0,
+                "max": int(row["max_status"]) if row and row["max_status"] is not None else 0,
+            }
+
+        def demo_query():
+            return {"min": 0, "max": 0}
+
+        result, source = self._run_with_fallback(database_query, demo_query)
+        result["source"] = source
+        return result
+
     def time_points(
         self,
         point_name: str,
         measurement_types: list[str] | None,
         min_time: str | None,
         max_time: str | None,
+        include_stopped: bool = False,
+        min_status: int | None = None,
     ) -> dict[str, Any]:
         if not point_name.strip():
             raise ValueError("机组与部位不能为空")
@@ -202,6 +231,11 @@ class DataService:
                 f"measurement_type IN ({type_placeholders})",
             ]
             params: list[Any] = [point_name, *types]
+            if not include_stopped:
+                clauses.append("rpm > 0")
+            if min_status is not None and min_status > 0:
+                clauses.append("tspluse_status >= %s")
+                params.append(min_status)
             if start:
                 clauses.append("sample_time >= %s")
                 params.append(start)
@@ -213,7 +247,7 @@ class DataService:
                     cursor.execute(
                         f"""
                         SELECT id, point_name, measurement_type, sample_time,
-                               sample_count, sample_frequency_hz, rpm
+                               sample_count, sample_frequency_hz, rpm, tspluse_status
                         FROM wave_file
                         WHERE {' AND '.join(clauses)}
                         ORDER BY sample_time ASC, id ASC
@@ -253,6 +287,7 @@ class DataService:
                 "sampleCount": int(row["sample_count"] or 0),
                 "sampleFrequencyHz": int(row["sample_frequency_hz"] or 0),
                 "rpm": float(row["rpm"] or 0),
+                "status": int(row.get("tspluse_status") or 0),
             }
         return [
             {"index": index, **point}
@@ -725,6 +760,7 @@ class DataService:
                         "sampleFrequencyHz": int(metadata.get("sample_frequency_hz") or 0),
                         "pointName": str(metadata.get("point_name") or point_name),
                         "rpm": float(metadata.get("rpm") or 0),
+                        "status": int(metadata.get("tspluse_status") or 0),
                         "fileName": str(metadata.get("file_name") or ""),
                     },
                 )
@@ -921,7 +957,7 @@ class DataService:
                 cursor.execute(
                     """
                     SELECT id, point_name, measurement_type, sample_frequency_hz,
-                           sample_count, sample_time, rpm, file_name
+                           sample_count, sample_time, rpm, file_name, tspluse_status
                     FROM wave_file
                     WHERE id = %s
                     """,

+ 11 - 0
backend/app/services/pretrain_service.py

@@ -43,6 +43,17 @@ class PretrainService:
                 existing.add(int(raw))
         return sorted(existing & wanted)
 
+    def recorded_counts(self) -> dict[str, int]:
+        """每个 point_name 在预训练 CSV 中的记录数量。"""
+        with self._lock:
+            records = self._read_records()
+        counts: dict[str, int] = {}
+        for row in records:
+            name = str(row.get("point_name") or "").strip()
+            if name:
+                counts[name] = counts.get(name, 0) + 1
+        return counts
+
     def record(self, record: dict[str, object]) -> tuple[bool, bool]:
         row = {column: str(record.get(column) or "") for column in _CSV_COLUMNS}
         try:

+ 107 - 15
frontend/src/App.vue

@@ -1,10 +1,11 @@
 <script setup lang="ts">
 import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
 import { ElMessageBox } from 'element-plus'
-import { createAnnotation, deleteAnnotation, deletePretrain, fetchAnnotationConfig, fetchAnnotationsByIds, fetchPeriodDetail, fetchPretrainStatus, fetchQueryOptions, fetchTimePoints, fetchWaveWindow, getToken, login as apiLogin, logout as apiLogout, recordPretrain } from './api'
+import { createAnnotation, deleteAnnotation, deletePretrain, fetchAnnotationConfig, fetchAnnotationsByIds, fetchPeriodDetail, fetchPretrainStatus, fetchQueryOptions, fetchTimePoints, fetchTspluseRuler, fetchWaveWindow, getToken, login as apiLogin, logout as apiLogout, recordPretrain } from './api'
 import PeriodModal from './components/PeriodModal.vue'
 import TimePointStrip from './components/TimePointStrip.vue'
 import WaveChart from './components/WaveChart.vue'
+import { STOPPED_COLOR, statusGradientColor } from './statusColor'
 import { MEASUREMENT_TYPES, type Annotation, type AnnotationLabel, type Cycle, type MeasurementType, type PeriodDetail, type QueryOption, type TimePoint, type QueryOptionsResponse, type WaveWindowFile, type WaveWindowResponse } from './types'
 
 const queryMeta = ref<QueryOptionsResponse | null>(null)
@@ -15,10 +16,14 @@ const maxTime = ref('')
 const windowSize = ref(4)
 const maxPoints = ref(200000)
 const noSampling = ref(false)
+const includeStopped = ref(false)
+const minStatus = ref<number | null>(null)
+const ruler = ref<{ min: number; max: number } | null>(null)
 const timePoints = ref<TimePoint[]>([])
 const startIndex = ref(0)
 const waveData = ref<WaveWindowResponse | null>(null)
 const chartMode = ref<'split' | 'merge'>('split')
+const chartDirty = ref(false)
 const queryLoading = ref(false)
 const timePointsLoading = ref(false)
 const waveLoading = ref(false)
@@ -37,11 +42,15 @@ const loginForm = reactive({ username: '', password: '' })
 const waveChartRef = ref<InstanceType<typeof WaveChart> | null>(null)
 const recordedFileIds = ref<number[]>([])
 const recordingFileId = ref<number | null>(null)
-let waveDebounce: ReturnType<typeof setTimeout> | undefined
 let queryDebounce: ReturnType<typeof setTimeout> | undefined
 let waveRequestId = 0
 
 const pointNames = computed(() => queryMeta.value?.pointNames ?? [])
+
+function pointLabel(pointName: string): string {
+  const count = queryMeta.value?.pretrainCounts?.[pointName]
+  return count ? `${pointName} (${count})` : pointName
+}
 const selectedOptionRows = computed<QueryOption[]>(() => (
   queryMeta.value?.options.filter((row) => (
     row.pointName === selectedPointName.value && selectedTypes.value.includes(row.measurementType)
@@ -112,6 +121,14 @@ async function loadOptions() {
   }
 }
 
+async function loadTspluseRuler() {
+  try {
+    ruler.value = await fetchTspluseRuler()
+  } catch {
+    ruler.value = null
+  }
+}
+
 function firstRunningIndex(): number {
   const index = timePoints.value.findIndex((point) =>
     selectedTypes.value.some((type) => (point.files[type]?.rpm ?? 0) > 0),
@@ -130,6 +147,8 @@ async function loadTimePoints() {
       measurementTypes: selectedTypes.value,
       minTime: toApiTime(minTime.value),
       maxTime: toApiTime(maxTime.value),
+      includeStopped: includeStopped.value,
+      minStatus: minStatus.value,
     })
     timePoints.value = result.points
     timePointsSource.value = result.source
@@ -137,8 +156,8 @@ async function loadTimePoints() {
       0,
       Math.min(firstRunningIndex(), Math.max(0, timePoints.value.length - windowSize.value)),
     )
+    chartDirty.value = true
     void loadAnnotations()
-    await loadWaveWindowNow()
   } catch (error) {
     timePoints.value = []
     errorMessage.value = error instanceof Error ? error.message : '时间点读取失败'
@@ -151,6 +170,7 @@ async function loadWaveWindowNow() {
   const points = selectedWindowPoints.value
   if (!points.length || !selectedPointName.value) {
     waveData.value = null
+    chartDirty.value = false
     return
   }
   const requestId = ++waveRequestId
@@ -165,6 +185,7 @@ async function loadWaveWindowNow() {
     })
     if (requestId === waveRequestId) {
       waveData.value = result
+      chartDirty.value = false
     }
   } catch (error) {
     if (requestId === waveRequestId) errorMessage.value = error instanceof Error ? error.message : '波形读取失败'
@@ -173,9 +194,8 @@ async function loadWaveWindowNow() {
   }
 }
 
-function scheduleWaveWindow() {
-  if (waveDebounce) clearTimeout(waveDebounce)
-  waveDebounce = setTimeout(() => void loadWaveWindowNow(), 140)
+function markChartDirty() {
+  chartDirty.value = true
 }
 
 function scheduleTimePoints() {
@@ -208,24 +228,42 @@ function onTimeChange() {
 
 function onStartIndexChange(value: number) {
   startIndex.value = Math.max(0, Math.min(value, Math.max(0, timePoints.value.length - windowSize.value)))
-  scheduleWaveWindow()
+  markChartDirty()
 }
 
 function onWindowSizeChange(value: number) {
   windowSize.value = Math.max(1, Math.min(200, Math.round(value || 1)))
   startIndex.value = Math.min(startIndex.value, Math.max(0, timePoints.value.length - windowSize.value))
-  scheduleWaveWindow()
+  markChartDirty()
 }
 
 function resetWindow() {
   startIndex.value = 0
-  scheduleWaveWindow()
+  markChartDirty()
 }
 
 function pageWindow(direction: -1 | 1) {
   const next = startIndex.value + direction * windowSize.value
   startIndex.value = Math.max(0, Math.min(next, Math.max(0, timePoints.value.length - windowSize.value)))
-  scheduleWaveWindow()
+  markChartDirty()
+}
+
+function jumpToIndex(index: number) {
+  onStartIndexChange(Number(index))
+}
+
+const jumpOptions = computed(() => timePoints.value.map((point, index) => {
+  const file = point.files['压力']
+  const text = point.sampleTime.replace('T', ' ')
+  const label = file && file.status !== undefined ? `${text} (${file.status})` : text
+  const color = file ? (file.rpm <= 0 ? STOPPED_COLOR : statusGradientColor(file.status, ruler.value)) : undefined
+  return { value: index, label, color }
+}))
+
+function sampleTimeStyle(file: WaveWindowFile) {
+  if (file.measurementType !== '压力') return undefined
+  const color = file.rpm <= 0 ? STOPPED_COLOR : statusGradientColor(file.status, ruler.value)
+  return { color }
 }
 
 function refresh() {
@@ -401,7 +439,13 @@ watch(windowSize, () => {
   if (initialized.value) onWindowSizeChange(windowSize.value)
 })
 watch(noSampling, () => {
-  if (initialized.value) loadWaveWindowNow()
+  if (initialized.value && selectedWindowPoints.value.length) loadWaveWindowNow()
+})
+watch(includeStopped, () => {
+  if (initialized.value) scheduleTimePoints()
+})
+watch(minStatus, () => {
+  if (initialized.value) scheduleTimePoints()
 })
 watch(waveData, () => void refreshPretrainStatus(), { deep: false })
 
@@ -470,6 +514,7 @@ async function handleLogin() {
     loggedIn.value = true
     loginForm.password = ''
     await loadOptions()
+    void loadTspluseRuler()
     void loadAnnotationConfig()
   } catch (error) {
     loginError.value = error instanceof Error ? error.message : '登录失败'
@@ -498,12 +543,12 @@ onMounted(() => {
   loggedIn.value = getToken() != null
   if (loggedIn.value) {
     void loadOptions()
+    void loadTspluseRuler()
     void loadAnnotationConfig()
   }
 })
 onBeforeUnmount(() => {
   window.removeEventListener('auth-expired', onAuthExpired)
-  if (waveDebounce) clearTimeout(waveDebounce)
   if (queryDebounce) clearTimeout(queryDebounce)
 })
 </script>
@@ -564,7 +609,7 @@ onBeforeUnmount(() => {
               placeholder="输入机组、气缸或部位关键词"
               filter-placeholder="搜索采样点"
             >
-              <el-option v-for="pointName in pointNames" :key="pointName" :label="pointName" :value="pointName" />
+              <el-option v-for="pointName in pointNames" :key="pointName" :label="pointLabel(pointName)" :value="pointName" />
             </el-select>
           </label>
           <div class="field field-types">
@@ -625,6 +670,27 @@ onBeforeUnmount(() => {
           <el-button class="query-action query-button" type="primary" size="large" :loading="timePointsLoading" :disabled="!selectedPointName" @click="loadTimePoints">
             查询时间点
           </el-button>
+          <div class="query-row-2">
+            <label class="field field-check">
+              <span class="field-label">运行状态</span>
+              <el-checkbox v-model="includeStopped" class="query-checkbox" size="large">停机</el-checkbox>
+            </label>
+            <label class="field field-status">
+              <span class="field-label">质心距离 <em>异常级别 ≥ 输入值</em></span>
+              <el-input-number
+                v-model="minStatus"
+                class="query-control"
+                size="large"
+                :min="0"
+                :max="4"
+                :step="1"
+                :step-strictly="true"
+                controls-position="right"
+                clearable
+                placeholder="可空"
+              />
+            </label>
+          </div>
         </div>
         <el-alert v-if="errorMessage" class="data-alert" type="error" :closable="false" show-icon :title="errorMessage" />
       </section>
@@ -639,17 +705,41 @@ onBeforeUnmount(() => {
           :min-time="minTime"
           :max-time="maxTime"
           :annotated-file-ids="annotatedFileIds"
+          :ruler="ruler"
           @update:start-index="onStartIndexChange"
         />
         <div class="selection-controls">
           <div class="selection-readout">
             <span class="selection-accent"></span>
-            <span>当前窗口覆盖 <strong>{{ selectedWindowPoints.length }}</strong> 个时间点:{{ selectedWindowPoints.map((p) => p.sampleTime.replace('T', ' ')).join('、') }}</span>
+            <span>当前窗口覆盖 <strong>{{ selectedWindowPoints.length }}</strong> 个时间点</span>
           </div>
           <div class="selection-actions">
             <el-button class="ghost-button" plain :disabled="!startIndex" @click="resetWindow">回到起点</el-button>
             <el-button class="ghost-button" plain :disabled="startIndex <= 0" @click="pageWindow(-1)">上一页</el-button>
             <el-button class="ghost-button" plain :disabled="startIndex >= timePoints.length - windowSize" @click="pageWindow(1)">下一页</el-button>
+            <el-select-v2
+              class="jump-select"
+              :options="jumpOptions"
+              :model-value="startIndex"
+              :disabled="!timePoints.length"
+              size="small"
+              filterable
+              placeholder="跳转"
+              @update:model-value="jumpToIndex"
+            >
+              <template #default="{ item }">
+                <span :style="item.color ? { color: item.color } : undefined">{{ item.label }}</span>
+              </template>
+            </el-select-v2>
+            <el-button
+              class="ghost-button chart-query"
+              :class="{ 'is-dirty': chartDirty }"
+              type="primary"
+              plain
+              :loading="waveLoading"
+              :disabled="waveLoading || !selectedWindowPoints.length"
+              @click="loadWaveWindowNow"
+            >查询图表</el-button>
           </div>
         </div>
         <div v-if="waveData?.files.length" class="window-files">
@@ -662,6 +752,7 @@ onBeforeUnmount(() => {
                 <th>measurement_type</th>
                 <th>rpm</th>
                 <th>sample_time</th>
+                <th>tspluse_status</th>
                 <th>file_name</th>
                 <th>操作</th>
               </tr>
@@ -672,7 +763,8 @@ onBeforeUnmount(() => {
                 <td>{{ file.pointName }}</td>
                 <td>{{ file.measurementType }}</td>
                 <td class="mono">{{ file.rpm }}</td>
-                <td class="mono">{{ file.sampleTime.replace('T', ' ') }}</td>
+                <td class="mono" :style="sampleTimeStyle(file)">{{ file.sampleTime.replace('T', ' ') }}</td>
+                <td class="mono">{{ file.status ?? '' }}</td>
                 <td class="file-name" :title="file.fileName">{{ file.fileName || '—' }}</td>
                 <td class="actions">
                   <el-button class="file-action" size="small" plain :disabled="!waveData" @click="locateFile(file)">定位</el-button>

+ 9 - 0
frontend/src/api.ts

@@ -8,6 +8,7 @@ import type {
   QueryOptionsResponse,
   TimePoint,
   TimePointsResponse,
+  TspluseRulerResponse,
   WaveWindowResponse,
 } from './types'
 
@@ -70,14 +71,22 @@ export function fetchTimePoints(params: {
   measurementTypes: MeasurementType[]
   minTime?: string
   maxTime?: string
+  includeStopped?: boolean
+  minStatus?: number | null
 }) {
   const search = new URLSearchParams({ point_name: params.pointName })
   params.measurementTypes.forEach((value) => search.append('measurement_types', value))
   if (params.minTime) search.set('min_time', params.minTime)
   if (params.maxTime) search.set('max_time', params.maxTime)
+  if (params.includeStopped) search.set('include_stopped', 'true')
+  if (params.minStatus) search.set('min_status', String(params.minStatus))
   return request<TimePointsResponse>(`/api/time-points?${search.toString()}`)
 }
 
+export function fetchTspluseRuler() {
+  return request<TspluseRulerResponse>('/api/tspluse-ruler')
+}
+
 export function fetchWaveWindow(params: {
   pointName: string
   measurementTypes: MeasurementType[]

+ 11 - 3
frontend/src/components/TimePointStrip.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
 import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
 import type { MeasurementType, TimePoint } from '../types'
+import { RUNNING_COLOR, STOPPED_COLOR, statusGradientColor } from '../statusColor'
 
 const props = defineProps<{
   points: TimePoint[]
@@ -11,6 +12,7 @@ const props = defineProps<{
   minTime?: string
   maxTime?: string
   annotatedFileIds?: number[]
+  ruler?: { min: number; max: number } | null
 }>()
 
 const emit = defineEmits<{
@@ -29,6 +31,13 @@ function displayDate(value: string | undefined) {
   return value.replace('T', ' ').slice(0, 10)
 }
 
+function pointColor(measurementType: MeasurementType, fileInfo: { rpm: number; status?: number }): string {
+  const running = fileInfo.rpm > 0
+  if (!running) return STOPPED_COLOR
+  if (measurementType === '压力') return statusGradientColor(fileInfo.status, props.ruler ?? null)
+  return RUNNING_COLOR
+}
+
 function draw() {
   const element = canvas.value
   const host = canvasHost.value
@@ -83,8 +92,7 @@ function draw() {
       const x = xAt(pointIndex)
       if (x - lastX < 6 && pointIndex !== props.startIndex && pointIndex !== endIndex.value - 1) return
       lastX = x
-      const running = fileInfo.rpm > 0
-      context.fillStyle = running ? '#2e7d32' : '#c0c4cc'
+      context.fillStyle = pointColor(measurementType, fileInfo)
       context.beginPath()
       context.arc(x, y, pointIndex >= props.startIndex && pointIndex < endIndex.value ? 3.5 : 2.5, 0, Math.PI * 2)
       context.fill()
@@ -167,7 +175,7 @@ function updateSlider(value: number | number[]) {
 }
 
 watch(
-  () => [props.points, props.measurementTypes, props.startIndex, props.windowSize, props.annotatedFileIds],
+  () => [props.points, props.measurementTypes, props.startIndex, props.windowSize, props.annotatedFileIds, props.ruler],
   () => nextTick(draw),
   { deep: true },
 )

+ 1 - 0
frontend/src/components/WaveChart.vue

@@ -713,6 +713,7 @@ onBeforeUnmount(() => {
       </div>
     </div>
     <div v-if="loading" class="chart-loading">正在整理波形数据…</div>
+    <div v-else-if="!data" class="chart-empty-hint">请点击「查询图表」加载波形数据。</div>
     <div v-else-if="data && !hasPlottableData" class="chart-empty-hint">当前窗口没有可绘制的波形数据,请检查时间点和数据名称选择。</div>
     <div v-if="data && !data.cycles.length" class="chart-empty-hint">当前窗口未检测到完整周期,仍可查看原始波形。</div>
   </div>

+ 16 - 0
frontend/src/statusColor.ts

@@ -0,0 +1,16 @@
+const GREEN = { r: 46, g: 125, b: 50 }
+const RED = { r: 211, g: 47, b: 47 }
+
+export function statusGradientColor(status: number | undefined, ruler: { min: number; max: number } | null): string {
+  if (status === undefined || !ruler) return `rgb(${GREEN.r},${GREEN.g},${GREEN.b})`
+  const span = ruler.max - ruler.min
+  if (span <= 0) return `rgb(${GREEN.r},${GREEN.g},${GREEN.b})`
+  const t = Math.max(0, Math.min(1, ((status ?? 0) - ruler.min) / span))
+  const r = Math.round(GREEN.r + (RED.r - GREEN.r) * t)
+  const g = Math.round(GREEN.g + (RED.g - GREEN.g) * t)
+  const b = Math.round(GREEN.b + (RED.b - GREEN.b) * t)
+  return `rgb(${r},${g},${b})`
+}
+
+export const STOPPED_COLOR = '#c0c4cc'
+export const RUNNING_COLOR = `rgb(${GREEN.r},${GREEN.g},${GREEN.b})`

+ 16 - 1
frontend/src/styles.css

@@ -117,6 +117,12 @@ h2 { font-size: 18px; line-height: 1.35; font-weight: 600; }
 .query-control .el-tag { font-size: 13px; }
 .window-number .el-input__inner { text-align: center; }
 .query-button.el-button { width: 100%; height: 40px; margin: 0; border-radius: 4px; font-size: 14px; }
+.query-row-2 { display: flex; align-items: flex-end; gap: 16px; grid-column: 1 / -1; }
+.query-row-2 .field-check .field-label,
+.query-row-2 .field-status .field-label { white-space: nowrap; }
+.query-checkbox.el-checkbox { height: 40px; margin-right: 0; }
+.query-checkbox .el-checkbox__label { font-size: 14px; color: #303133; }
+.field-status { width: 240px; max-width: 100%; }
 .data-alert { margin-top: 16px; }
 
 .selection-panel { padding: 20px 24px 18px; }
@@ -141,11 +147,20 @@ h2 { font-size: 18px; line-height: 1.35; font-weight: 600; }
 .window-track { flex: 1; padding: 0 6px; }
 .window-track .el-slider { width: 100%; }
 .selection-controls { gap: 12px; margin-top: 14px; }
-.selection-readout { display: flex; align-items: flex-start; gap: 8px; color: #606266; font-size: 14px; flex: 1; min-width: 0; }
+.selection-readout { display: flex; align-items: center; gap: 8px; color: #606266; font-size: 14px; flex: 1; min-width: 0; }
 .selection-readout > span:last-child { line-height: 1.7; word-break: break-all; }
 .selection-actions { display: flex; gap: 8px; flex-shrink: 0; }
 .selection-readout strong { color: #303133; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: 500; }
 .selection-accent { width: 7px; height: 7px; background: #409eff; border-radius: 50%; }
+.jump-select { width: 220px; }
+.jump-select .el-select__wrapper { min-height: 32px; }
+.ghost-button.chart-query { font-weight: 500; }
+.ghost-button.chart-query.is-dirty {
+  border-color: #409eff;
+  background: #ecf5ff;
+  color: #409eff;
+  box-shadow: 0 0 0 1px #409eff inset;
+}
 .ghost-button.el-button { height: 32px; padding: 0 14px; font-size: 13px; }
 
 .window-files { margin-top: 12px; border: 1px solid #ebeef5; border-radius: 4px; overflow: hidden; background: #fff; }

+ 9 - 0
frontend/src/types.ts

@@ -14,6 +14,7 @@ export type FileInfo = {
   sampleCount: number
   sampleFrequencyHz: number
   rpm: number
+  status?: number
 }
 
 export type TimePoint = {
@@ -22,11 +23,18 @@ export type TimePoint = {
   files: Partial<Record<MeasurementType, FileInfo>>
 }
 
+export type TspluseRulerResponse = {
+  source: 'database' | 'demo'
+  min: number
+  max: number
+}
+
 export type QueryOptionsResponse = {
   source: 'database' | 'demo'
   measurementTypes: MeasurementType[]
   pointNames: string[]
   options: QueryOption[]
+  pretrainCounts?: Record<string, number>
   notice: string | null
 }
 
@@ -79,6 +87,7 @@ export type WaveWindowFile = {
   sampleFrequencyHz: number
   pointName: string
   rpm: number
+  status?: number
   fileName: string
 }