Jelajahi Sumber

```
feat(DHU): 优化除湿机模型边界条件与约束逻辑

- 在 `optimize_dhu` 函数中增加 DHU_A/B 和 SDHU_A/B 的边界条件处理逻辑,引入加热盘管阀门开度判断,
动态调整再生温度上下限,提升优化过程的稳定性与合理性。
- 修改前后再生差值约束条件,当触发边界范围条件时忽略该约束,避免模型求解失败。
- 更新送风露点预测逻辑,支持基于 DHU 模型或实测送风露点两种模式,增强预测灵活性。
- 增加对回风口(前表冷后)配置的支持,完善模型输入结构和空气流量计算方法。
- 修复布尔类型转换未考虑 NaN 值的问题,确保数据类型转换的健壮性。
- 扩展训练数据时间窗口,并在设备模型加载失败时回退至基于实际送风露点的房间模型训练。
- 优化点位匹配逻辑,支持更复杂的设备名称解析方式,提高配置读取的准确性。
```

zhangshenhao 4 bulan lalu
induk
melakukan
42bb9e2088
6 mengubah file dengan 240 tambahan dan 99 penghapusan
  1. 14 4
      apps/DHU/config_reader.py
  2. 49 24
      apps/DHU/optimize.py
  3. 37 15
      apps/DHU/train.py
  4. 73 36
      apps/Room/predict.py
  5. 7 4
      model/DHU/DHU_AB.py
  6. 60 16
      model/DHU/SDHU_AB.py

+ 14 - 4
apps/DHU/config_reader.py

@@ -58,12 +58,19 @@ class ConfigReader:
                 raise Exception(f'点位{point_name}的点位ID缺失')
         
         # 根据配置情况更新全局点位
-        if equp_name in self.point.columns:
+        all_adjust_point = self.point.loc[:,'点位':].columns.to_list()
+        for each_adj in all_adjust_point:
+            all_equp = re.findall(r'\[(.*?)\]',each_adj)
+            if equp_name not in all_equp:
+                continue
             for point_name in point_map.keys():
-                new_point_id = self.point.loc[self.point.编号==point_name,equp_name].iat[0]
+                new_point_id = (
+                    point_info
+                    .loc[self.point.编号==point_name,each_adj].iat[0]
+                )
                 if isinstance(new_point_id,str):
                     point_map[point_name] = new_point_id
-        
+            
         # 根据房间数量重新调整点位
         if 'C' in equp_class:
             num_room = self.get_equp_info(equp_name,'房间数量',info_type='int')
@@ -128,7 +135,10 @@ def convert_info_type(info_value,info_type):
     elif info_type == 'float':
         info_value = float(info_value)
     elif info_type == 'bool':
-        info_value = bool(info_value)
+        if np.isnan(info_value):
+            info_value = False
+        else:
+            info_value = bool(info_value)
     elif info_type == 'datetime':
         if info_value == 'NOW':
             info_value = datetime.now()

+ 49 - 24
apps/DHU/optimize.py

@@ -284,15 +284,49 @@ def optimize_dhu(
     )
     print(TVP_data)
     
+    # 边界条件
+    if each_equp_type in ['DHU_A','DHU_B']:
+        wheel_1_TinR_cur       = data_cur.loc[:,'wheel_1_TinR'].iat[0]
+        wheel_2_TinR_cur       = data_cur.loc[:,'wheel_2_TinR'].iat[0]
+        wheel_1_TinR_uplim_set = config_reader.get_equp_info(each_eaup_name,key='前再生温度上限',info_type='float')
+        wheel_1_TinR_dwlim_set = config_reader.get_equp_info(each_eaup_name,key='前再生温度下限',info_type='float')
+        wheel_2_TinR_uplim_set = config_reader.get_equp_info(each_eaup_name,key='后再生温度上限',info_type='float')
+        wheel_2_TinR_dwlim_set = config_reader.get_equp_info(each_eaup_name,key='后再生温度下限',info_type='float')
+        heatingcoil_1_Val_cur  = data_cur.loc[:,'heatingcoil_1_Val'].iat[0]
+        heatingcoil_2_Val_cur  = data_cur.loc[:,'heatingcoil_2_Val'].iat[0]
+        def get_limit(cur,up_set,dw_set,Val):
+            adj_lim = False
+            if Val <= 5:
+                up_lim = up_set
+                dw_lim = cur
+                adj_lim = True
+            elif 5 < Val <= 95:
+                up_lim = up_set
+                dw_lim = dw_set
+            elif Val > 95:
+                up_lim = cur
+                dw_lim = dw_set
+                adj_lim = True
+            print(f'Val:{Val}, cur:{cur}, up_set:{up_set}, dw_set:{dw_set}, uplim:{up_lim}, dwlim:{dw_lim}')
+            if adj_lim:
+                print('触发边界范围条件,忽略前后再生差值的约束')
+            return min(up_lim,dw_lim),max(up_lim,dw_lim),adj_lim
+        
+        wheel_1_TinR_dwlim,wheel_1_TinR_uplim,wheel_1_TinR_adjlim = get_limit(wheel_1_TinR_cur,wheel_1_TinR_uplim_set,wheel_1_TinR_dwlim_set,heatingcoil_1_Val_cur)
+        wheel_2_TinR_dwlim,wheel_2_TinR_uplim,wheel_2_TinR_adjlim = get_limit(wheel_2_TinR_cur,wheel_2_TinR_uplim_set,wheel_2_TinR_dwlim_set,heatingcoil_2_Val_cur)
+    elif each_equp_type in ['SDHU_A','SDHU_B']:
+        wheel_1_TinR_uplim_set = config_reader.get_equp_info(each_eaup_name,key='前再生温度上限',info_type='float')
+        wheel_1_TinR_dwlim_set = config_reader.get_equp_info(each_eaup_name,key='前再生温度下限',info_type='float')
+
     # 约束条件
     constrains = []
     if each_equp_type in ['DHU_A','DHU_B']:
         constrains.append('#送风露点约束# coil_3_DoutA-[coil_3_DoutA]<0')
         Tdup = config_reader.get_equp_info(each_eaup_name,key='前后再生差值上限',info_type='float')
         Tddw = config_reader.get_equp_info(each_eaup_name,key='前后再生差值下限',info_type='float')
-        if not np.isnan(Tdup):
+        if not np.isnan(Tdup) and not wheel_1_TinR_adjlim and not wheel_2_TinR_adjlim:
             constrains.append(f'#前后再生差值上限# (wheel_1_TinR-wheel_2_TinR)-{Tdup}<0')
-        if not np.isnan(Tddw):
+        if not np.isnan(Tddw) and not wheel_1_TinR_adjlim and not wheel_2_TinR_adjlim:
             constrains.append(f'#前后再生差值下限# {Tddw}-(wheel_1_TinR-wheel_2_TinR)<0')
     elif each_equp_type in ['SDHU_A','SDHU_B']:
         constrains.append('#送风露点约束# wheel_1_DoutP-[wheel_1_DoutP]<0')
@@ -303,32 +337,23 @@ def optimize_dhu(
     if each_equp_type in ['DHU_A','DHU_B']:
         opt_res = MODEL.optimize(
             cur_input_data=data_cur,
-            wheel_1_TinR=(
-                config_reader.get_equp_info(each_eaup_name,key='前再生温度上限',info_type='float'),
-                config_reader.get_equp_info(each_eaup_name,key='前再生温度下限',info_type='float')
-            ),
-            wheel_2_TinR=(
-                config_reader.get_equp_info(each_eaup_name,key='后再生温度上限',info_type='float'),
-                config_reader.get_equp_info(each_eaup_name,key='后再生温度下限',info_type='float')
-            ),
-            fan_2_Hz   = None,
-            constrains = constrains,
-            logging    = False,
-            target     = 'summary_waste',
-            target_min = True
+            wheel_1_TinR = (wheel_1_TinR_uplim,wheel_1_TinR_dwlim),
+            wheel_2_TinR = (wheel_2_TinR_uplim,wheel_2_TinR_dwlim),
+            fan_2_Hz     = None,
+            constrains   = constrains,
+            logging      = False,
+            target       = 'summary_waste',
+            target_min   = True
         )
     elif each_equp_type in ['SDHU_A','SDHU_B']:
         opt_res = MODEL.optimize(
             cur_input_data = data_cur,
-            wheel_1_TinR   = (
-                config_reader.get_equp_info(each_eaup_name,key='前再生温度上限',info_type='float'),
-                config_reader.get_equp_info(each_eaup_name,key='前再生温度下限',info_type='float')
-            ),
-            fan_2_Hz   = (30,45),
-            constrains = constrains,
-            logging    = False,
-            target     = 'summary_waste',
-            target_min = True
+            wheel_1_TinR   = (wheel_1_TinR_uplim_set,wheel_1_TinR_dwlim_set),
+            fan_2_Hz       = (30,40),
+            constrains     = constrains,
+            logging        = False,
+            target         = 'summary_waste',
+            target_min     = True
         )
     opt_summary = opt_res['opt_summary']
     opt_var     = opt_res['opt_var']

+ 37 - 15
apps/DHU/train.py

@@ -49,6 +49,7 @@ def train(*inputs,config=None):
             )
         except Exception as E:
             ALL_RESULT['EXCEPTION']['Data'][each_eaup_name] = E
+            raise E
             continue
         
         # 训练模型 
@@ -56,7 +57,7 @@ def train(*inputs,config=None):
             equp_model,equp_data_clean = train_equp_model(
                 each_eaup_name=each_eaup_name,each_equp_type=equp_type,equp_data=equp_data,
                 config_reader=config_reader,config_reader_path=config_reader_path)
-            room_model = train_room_model(
+            train_room_model(
                 each_eaup_name=each_eaup_name,each_equp_type=equp_type,equp_data=equp_data,
                 config_reader=config_reader,config_reader_path=config_reader_path
             )
@@ -154,6 +155,7 @@ def train_equp_model(each_eaup_name,each_equp_type,equp_data,config_reader,confi
         equp_model = SDHU_AB(
             DHU_type   = each_equp_type,
             exist_Fa_H = config_reader.get_equp_info(each_eaup_name,'存在回风口','bool'),
+            exist_Fa_H0= config_reader.get_equp_info(each_eaup_name,'存在回风口(前表冷后)','bool'),
         )
     else:
         raise NotImplementedError
@@ -200,33 +202,53 @@ def train_room_model(each_eaup_name,each_equp_type,equp_data,config_reader:Confi
     if not config_reader.get_app_info(each_eaup_name,'模型训练','训练房间模型','bool'):
         return None
     
-    N_fit = 24 * 60
-    equp_model_path = f'{config_reader_path}/model/{each_eaup_name}.pkl'
+    N_fit = 24 * 60 * 3
+    
+    try:
+        equp_model_path = f'{config_reader_path}/model/{each_eaup_name}.pkl'
+        if each_equp_type in ['DHU_A','DHU_B']:
+            equp_model = DHU_AB.load(equp_model_path)
+            Dout       = equp_model.predict(equp_data.iloc[-N_fit:,:])['coil_3']['DoutA']
+        elif each_equp_type in ['SDHU_A','SDHU_B']:
+            equp_model = SDHU_AB.load(equp_model_path)
+            Dout       = equp_model.predict(equp_data.iloc[-N_fit:,:])['wheel_1']['DoutP']
+        else:
+            raise NotImplementedError
+    except Exception as E:
+        Dout = None
+        print(f'{each_eaup_name}设备模型加载失败,只训练基于实际送风露点的房间露点模型')
+    
+    # 实际送风露点
     if each_equp_type in ['DHU_A','DHU_B']:
-        equp_model = DHU_AB.load(equp_model_path)
-        Dout = equp_model.predict(equp_data.iloc[-N_fit:,:])['coil_3']['DoutA']
+        Dout_real  = equp_data.iloc[-N_fit:,:].loc[:,'wheel_2_DoutP'].values
     elif each_equp_type in ['SDHU_A','SDHU_B']:
-        equp_model = SDHU_AB.load(equp_model_path)
-        Dout = equp_model.predict(equp_data.iloc[-N_fit:,:])['wheel_1']['DoutP']
+        Dout_real  = equp_data.iloc[-N_fit:,:].loc[:,'coil_2_DoutA'].values
     else:
         raise NotImplementedError
+    
     N_room = config_reader.get_equp_info(each_eaup_name,'房间数量','int')
     
-    path_diffdata = f'{config_reader_path}/plot/plot_room_diffdata/'
-    path_lagcorr  = f'{config_reader_path}/plot/plot_room_lagcorr/'
+    path_diffdata    = f'{config_reader_path}/plot/plot_room_diffdata/'
+    path_lagcorr     = f'{config_reader_path}/plot/plot_room_lagcorr/'
+    path_diffdata_bk = f'{config_reader_path}/plot/plot_room_diffdata_bk/'
+    path_lagcorr_bk  = f'{config_reader_path}/plot/plot_room_lagcorr_bk/'
     Path(path_diffdata).mkdir(parents=True, exist_ok=True)
     Path(path_lagcorr).mkdir(parents=True, exist_ok=True)
+    Path(path_diffdata_bk).mkdir(parents=True, exist_ok=True)
+    Path(path_lagcorr_bk).mkdir(parents=True, exist_ok=True)
     
     for i in range(1,N_room+1):
         Droom = equp_data.iloc[-N_fit:,:].loc[:,f'room_{i}_Dpv'].values
-        room_model = RoomDewPredictor().fit_Droom(Dout=Dout,Droom=Droom)
-        room_model.save(f'{config_reader_path}/model/{each_eaup_name}_room_{i}_Dpv.pkl')
-        try:
+        if Dout is not None:
+            room_model = RoomDewPredictor().fit_Droom(Dout=Dout,Droom=Droom)
+            room_model.save(f'{config_reader_path}/model/{each_eaup_name}_room_{i}_Dpv.pkl')
             room_model.plot_diffdata(Dout,Droom).save(filename=f'{path_diffdata}/{each_eaup_name}_room_{i}_Dpv.png')
             room_model.plot_diffdata_lagcorr(Dout,Droom).save(filename=f'{path_lagcorr}/{each_eaup_name}_room_{i}_Dpv.png')
-        except:
-            pass
-    return room_model
+        room_model_bk = RoomDewPredictor().fit_Droom(Dout=Dout_real,Droom=Droom)
+        room_model_bk.save(f'{config_reader_path}/model/{each_eaup_name}_room_{i}_Dpv_bk.pkl')
+        room_model_bk.plot_diffdata(Dout_real,Droom).save(filename=f'{path_diffdata_bk}/{each_eaup_name}_room_{i}_Dpv.png')
+        room_model_bk.plot_diffdata_lagcorr(Dout_real,Droom).save(filename=f'{path_lagcorr_bk}/{each_eaup_name}_room_{i}_Dpv.png')
+    
 def save_train_info(equp_model,equp_data,config_reader_path,each_eaup_name):
     for plot_name,plot in equp_model.plot_check(equp_data).items():
         path = f'{config_reader_path}/plot/{plot_name}/'

+ 73 - 36
apps/Room/predict.py

@@ -34,6 +34,8 @@ def predict(*inputs,config=None):
         
         NOW = config_reader.get_app_info(each_eaup_name,'露点预测',key='开始时间',info_type='datetime')
         
+        use_DHU_model = config_reader.get_app_info(each_eaup_name,'露点预测',key='基于除湿机模型',info_type='bool')
+
         dhu_State = load_dhu_State(
             each_eaup_name     = each_eaup_name,
             config_reader      = config_reader,
@@ -46,45 +48,73 @@ def predict(*inputs,config=None):
             print('设备处于非运行状态,跳过')
             continue
         
-        equp_data = load_data(
-            cur_time           = NOW,
-            each_eaup_name     = each_eaup_name,
-            config_reader      = config_reader,
-            config_reader_path = config_reader_path,
-            data_URL           = data_URL,
-        )
-        equp_data_sm = equp_data.rolling(10,min_periods=0).mean()
-        equp_model = load_model(
-            each_eaup_name     = each_eaup_name,
-            each_equp_type     = each_equp_type,
-            config_reader      = config_reader,
-            config_reader_path = config_reader_path,
-            use_adj_name       = False
-        )
-        room_SP_point = config_reader.get_equp_point(
-            equp_name  = each_eaup_name,
-            equp_class = ['C','D']
-        )
-        
-        if each_equp_type in ['DHU_A','DHU_B']:
-            Dout_pred = equp_model.predict(equp_data_sm)['coil_3']['DoutA']
-        elif each_equp_type in ['SDHU_A','SDHU_B']:
-            Dout_pred = equp_model.predict(equp_data_sm)['wheel_1']['DoutP']
+        if use_DHU_model:
+            print('基于DHU模型进行预测')
+            equp_data = load_data(
+                cur_time           = NOW,
+                each_eaup_name     = each_eaup_name,
+                config_reader      = config_reader,
+                config_reader_path = config_reader_path,
+                data_URL           = data_URL,
+            )
+            equp_data_sm = equp_data.rolling(10,min_periods=0).mean()
+            equp_model = load_model(
+                each_eaup_name     = each_eaup_name,
+                each_equp_type     = each_equp_type,
+                config_reader      = config_reader,
+                config_reader_path = config_reader_path,
+                use_adj_name       = False
+            )
+            if each_equp_type in ['DHU_A','DHU_B']:
+                Dout_pred = equp_model.predict(equp_data_sm)['coil_3']['DoutA']
+            elif each_equp_type in ['SDHU_A','SDHU_B']:
+                Dout_pred = equp_model.predict(equp_data_sm)['wheel_1']['DoutP']
+            else:
+                raise NotImplementedError
         else:
-            raise NotImplementedError
-        
-        for each_room_num in range(
-            1,config_reader.get_equp_info(each_eaup_name,'房间数量','int') + 1
-        ):
-            room_model = RoomDewPredictor.load(
-                f'{config_reader_path}/model/{each_eaup_name}_room_{each_room_num}_Dpv.pkl'
+            print('基于实测送风露点进行预测')
+            point_B = config_reader.get_equp_point(each_eaup_name,equp_class=['B'])
+            point = config_reader.get_equp_point(each_eaup_name,equp_class=['C'])
+            if each_equp_type in ['DHU_A','DHU_B']:
+                Dout_name        = 'wheel_2_DoutP'
+                point[Dout_name] = point_B[Dout_name]
+            elif each_equp_type in ['SDHU_A','SDHU_B']:
+                Dout_name        = 'coil_2_DoutA'
+                point[Dout_name] = point_B[Dout_name]
+            else:
+                raise NotImplementedError
+            equp_data = (
+                DataLoader(
+                    path          = f'{config_reader_path}/data/room_predict/data_cur/',
+                    start_time    = NOW - timedelta(minutes=120),
+                    end_time      = NOW,
+                    print_process = False
+                )
+                .download_equp_data(
+                    equp_name   = each_eaup_name,
+                    point       = config_reader.get_equp_point(each_eaup_name,equp_class=['B','C']),
+                    url         = data_URL,
+                    clean_cache = True
+                )
+                .get_equp_data(
+                    equp_name = each_eaup_name,
+                )
             )
-            Droom_cur  = equp_data.loc[:,f'room_{each_room_num}_Dpv'].values[-1]
-            Droom_pred = room_model.predict_Droom(Dout=Dout_pred,Droom_cur=Droom_cur)
-            pred_lag   = room_model.model_info['model_Droom']['lag']
+            Dout = equp_data.loc[:,Dout_name].values
+        room_SP_point = config_reader.get_equp_point(equp_name = each_eaup_name,equp_class = ['C','D'])
+        
+        for each_room_num in range(1,config_reader.get_equp_info(each_eaup_name,'房间数量','int') + 1):
+            Droom_cur = equp_data.loc[:,f'room_{each_room_num}_Dpv'].values[-1]
+            if use_DHU_model:
+                room_model = RoomDewPredictor.load(f'{config_reader_path}/model/{each_eaup_name}_room_{each_room_num}_Dpv.pkl')
+                Droom_pred = room_model.predict_Droom(Dout=Dout_pred,Droom_cur=Droom_cur)
+            else:
+                room_model = RoomDewPredictor.load(f'{config_reader_path}/model/{each_eaup_name}_room_{each_room_num}_Dpv_bk.pkl')
+                Droom_pred = room_model.predict_Droom(Dout=Dout,Droom_cur=Droom_cur)
+            pred_lag = room_model.model_info['model_Droom']['lag']
+            
             print(f'{each_eaup_name}房间{each_room_num}在{NOW}的预测值')
             print(f'完整预测时序:{Droom_pred}')
-            print(f'{pred_lag}分钟后的预测值为{Droom_pred[-1]}')
             index      = pd.Index(
                 pd.date_range(
                     start = NOW+timedelta(minutes=1),
@@ -98,10 +128,17 @@ def predict(*inputs,config=None):
                 value    = Droom_pred,
                 ts       = index
             )
+            if config_reader.get_equp_info(each_eaup_name,'房间露点预测用当前值',info_type='bool'):
+                print(f'{each_eaup_name}房间{each_room_num}仅输出当前露点')
+                Droom_pred_last = Droom_cur
+                pred_lag = 1
+            else:
+                Droom_pred_last = Droom_pred[-1]
+            print(f'{pred_lag}分钟后的预测值为{Droom_pred_last}')
             # 最远预测点
             write_lag_predict(
                 point_id = f"{each_eaup_name}_{room_SP_point[f'room_{each_room_num}_Dpd']}",
-                value    = Droom_pred[-1]
+                value    = Droom_pred_last
             )
             # 最远预测步长
             write_lag_predict(

+ 7 - 4
model/DHU/DHU_AB.py

@@ -558,7 +558,8 @@ def model_A(
         heatingcoil_1_res = heatingcoil_1_res,
         heatingcoil_2_res = heatingcoil_2_res,
         wheel_1_TinR      = wheel_1_TinR,
-        wheel_2_TinR      = wheel_2_TinR
+        wheel_2_TinR      = wheel_2_TinR,
+        coil_3_Val        = coil_3_Val
     )
     return {
         'coil_2'       : coil_2_res,
@@ -727,7 +728,8 @@ def model_B(
         heatingcoil_1_res = heatingcoil_1_res,
         heatingcoil_2_res = heatingcoil_2_res,
         wheel_1_TinR      = wheel_1_TinR,
-        wheel_2_TinR      = wheel_2_TinR
+        wheel_2_TinR      = wheel_2_TinR,
+        coil_3_Val        = coil_3_Val
     )
     
     return {
@@ -873,7 +875,8 @@ def cal_Q_waste(
     heatingcoil_1_res,
     heatingcoil_2_res,
     wheel_1_TinR,
-    wheel_2_TinR
+    wheel_2_TinR,
+    coil_3_Val
 ) -> dict:
     def waste_cond_func1(TinR):
         waste = 0.15 + 0.0001 * (TinR-70)**3
@@ -883,7 +886,7 @@ def cal_Q_waste(
         return np.where(waste>0,waste,0)
     
     waste_Qsen1 = wheel_1_res['Qsen']
-    waste_Qsen2 = wheel_2_res['Qsen']
+    waste_Qsen2 = wheel_2_res['Qsen'] * np.where(coil_3_Val>0.01,1,0) # 阀门关闭时,认为热量没有浪费,用作加热用
     waste_cond1 = heatingcoil_1_res['Q'] * waste_cond_func1(wheel_1_TinR)
     waste_cond2 = heatingcoil_2_res['Q'] * waste_cond_func1(wheel_2_TinR)
     waste_out = (

+ 60 - 16
model/DHU/SDHU_AB.py

@@ -25,6 +25,7 @@ class SDHU_AB(BaseDevice):
         self,
         DHU_type      = 'A',
         exist_Fa_H    = True,
+        exist_Fa_H0   = False,
         wheel_1       = None,
         coolingcoil_2 = 'CoolingCoil2',
         heatingcoil_1 = 'SteamCoil',
@@ -49,11 +50,13 @@ class SDHU_AB(BaseDevice):
             'mixed_2'      : mixed_2
         }
         self.exist_Fa_H = exist_Fa_H
+        self.exist_Fa_H0 = exist_Fa_H0
         self.other_info = other_info if other_info is not None else {}
         self.record_load_info(
             components_str = self.components_str,
             DHU_type       = self.DHU_type,
             exist_Fa_H     = self.exist_Fa_H,
+            exist_Fa_H0    = self.exist_Fa_H0,
             other_info     = self.other_info
         )
     
@@ -79,8 +82,6 @@ class SDHU_AB(BaseDevice):
     @property
     def model_input_data_columns(self):
         columns = {
-            'Tin_F'       : 'coil_1_ToutA',
-            'Hin_F'       : 'coil_1_HoutA',
             'fan_1_Hz'    : 'fan_1_Hz',
             'fan_2_Hz'    : 'fan_2_Hz',
             'coil_2_TinW' : 'coil_2_TinW',
@@ -90,9 +91,21 @@ class SDHU_AB(BaseDevice):
         if self.exist_Fa_H:
             columns['mixed_1_TinM'] = 'mixed_1_TinM'
             columns['mixed_1_HinM'] = 'mixed_1_HinM'
-        if self.DHU_type == 'A':
+        
+        if self.exist_Fa_H0:
+            columns['coil_1_ToutA'] = 'mixed_0_ToutA'
+            columns['coil_1_HoutA'] = 'mixed_0_HoutA'
+        else:
             columns['coil_1_ToutA'] = 'coil_1_ToutA'
             columns['coil_1_HoutA'] = 'coil_1_HoutA'
+        
+        if self.DHU_type == 'A':
+            columns['Fa_TinA'] = 'Fa_TinA'
+            columns['Fa_HinA'] = 'Fa_HinA'
+        elif self.DHU_type == 'B':
+            columns['mixed_2_TinM'] = 'mixed_2_TinM'
+            columns['mixed_2_HinM'] = 'mixed_2_HinM'
+            
         return columns
     
     @property
@@ -131,7 +144,12 @@ class SDHU_AB(BaseDevice):
         
         with pm.Model() as self.MODEL_PYMC:
             param_prior = {name:comp.prior() for name,comp in self.components.items()}
-            param_prior['F_air'] = AirFlow_SDHU_A.prior(exist_Fa_H = self.exist_Fa_H)
+            if self.DHU_type == 'A':
+                param_prior['F_air'] = AirFlow_SDHU_A.prior(exist_Fa_H = self.exist_Fa_H)
+            elif self.DHU_type == 'B':
+                param_prior['F_air'] = AirFlow_SDHU_B.prior(exist_Fa_H = self.exist_Fa_H)
+            else:
+                raise NotImplementedError
             
             res = self.model(
                 **{k:input_data.loc[:,v].values for k,v in self.model_input_data_columns.items()},
@@ -199,6 +217,7 @@ class SDHU_AB(BaseDevice):
                 .rm_rule('State != 1')
                 .rm_rule('fan_1_Hz < 10').rm_rule('fan_2_Hz < 10')
                 .rm_outrange(method='raw',upper=140,lower=20,include_cols=['wheel_1_TinR'])
+                .rm_outrange(method='raw',upper=15,lower=-60,include_cols=['coil_2_DoutA'])
             )
         if 'observed' in data_type:
             filter_columns += list(self.model_observe_data_columns.values())
@@ -206,6 +225,7 @@ class SDHU_AB(BaseDevice):
             fill     = 0 if fill_zero else None,
             save_log = save_log
         )
+        clean_data = clean_data.loc[:,filter_columns]
         return clean_data
 
     
@@ -319,8 +339,8 @@ class SDHU_AB(BaseDevice):
         
         
 def model_A(
-    Tin_F,        # 前表冷后温度
-    Hin_F,        # 前表冷后湿度
+    Fa_TinA,        # 前表冷后温度
+    Fa_HinA,        # 前表冷后湿度
     coil_1_ToutA,
     coil_1_HoutA,
     fan_1_Hz,     # 处理侧风机频率 
@@ -348,8 +368,8 @@ def model_A(
         TinR   = wheel_1_TinR,
         HinR   = 0,
         FR     = air_flow['wheel_1_FaR'],
-        TinC   = Tin_F,
-        HinC   = Hin_F,
+        TinC   = Fa_TinA,
+        HinC   = Fa_HinA,
         FC     = air_flow['wheel_1_FaC'],
         engine = engine,
         param  = param['wheel_1']
@@ -385,8 +405,8 @@ def model_A(
         TinR   = wheel_1_TinR,
         HinR   = wheel_1_res['HoutC'],
         FR     = air_flow['wheel_1_FaR'],
-        TinC   = Tin_F,
-        HinC   = Hin_F,
+        TinC   = Fa_TinA,
+        HinC   = Fa_HinA,
         FC     = air_flow['wheel_1_FaC'],
         engine = engine,
         param  = param['wheel_1']
@@ -420,8 +440,8 @@ def model_A(
     }
         
 def model_B(
-    Tin_Fr,        # 再生侧入口温度
-    Hin_Fr,        # 再生侧入口湿度
+    mixed_2_TinM, # 再生侧入口温度
+    mixed_2_HinM, # 再生侧入口湿度
     coil_1_ToutA,
     coil_1_HoutA,
     fan_1_Hz,     # 处理侧风机频率 
@@ -446,7 +466,7 @@ def model_B(
         HinP   = coil_1_HoutA,
         FP     = air_flow['wheel_1_FaP'],
         TinR   = wheel_1_TinR,
-        HinR   = Hin_Fr,
+        HinR   = mixed_2_HinM,
         FR     = air_flow['wheel_1_FaR'],
         engine = engine,
         param  = param['wheel_1']
@@ -473,7 +493,7 @@ def model_B(
     )
     # 前再生加热盘管
     heatingcoil_1_res = components['heatingcoil_1'].model(
-        TinA   = Tin_Fr,
+        TinA   = mixed_2_TinM,
         ToutA  = wheel_1_TinR,
         FA     = air_flow['heatingcoil_1_Fa'],
         param  = param['heatingcoil_1'],
@@ -534,8 +554,32 @@ class AirFlow_SDHU_A:
 class AirFlow_SDHU_B:
     @classmethod
     def model(cls,fan_1_Hz,fan_2_Hz,param):
-        ...
-
+        F_air_X2_base = 1
+        F_air_H_base  = param['F_air'].get('H_base',0)
+        F_air_P_base  = param['F_air']['P_base']
+        
+        Fa_H  = F_air_H_base + (fan_1_Hz/50) * param['F_air'].get('HzP_H',0)
+        Fa_X2 = F_air_X2_base + (fan_1_Hz/50) * param['F_air']['HzP_X2']
+        Fa_S  = Fa_H + Fa_X2
+        Fa_P  = F_air_P_base + (fan_2_Hz/50) * param['F_air']['HzR_P']
+        return {
+            'wheel_1_FaP'     : Fa_X2,
+            'wheel_1_FaR'     : Fa_P,
+            'coil_2_FaA'      : Fa_S,
+            'mixed_1_FaM'     : Fa_H,
+            'mixed_1_FaA'     : Fa_X2,
+            'heatingcoil_1_Fa': Fa_P
+        }
+    @classmethod
+    def prior(cls,exist_Fa_H):
+        param = {}
+        param['HzP_X2'] = pm.HalfNormal('F_air_HzP_X2',sigma=1,initval=0.5)
+        param['HzR_P']  = pm.HalfNormal('F_air_HzR_P',sigma=1,initval=0.5)
+        param['P_base'] = pm.TruncatedNormal('F_air_P_base',mu=1,sigma=0.2,lower=0,initval=1)
+        if exist_Fa_H:
+            param['H_base'] = pm.TruncatedNormal('F_air_H_base',mu=1,sigma=0.2,lower=0,initval=1)
+            param['HzP_H']  = pm.HalfNormal('F_air_HzP_H',sigma=1,initval=0.5)
+        return param
         
 
 def cal_Q_waste(