from datetime import datetime, date class OperatingModePredictor: """ 工况预测类(处理二维倒序数据,返回一维列表,仅保留各内嵌列表最新记录) 功能:根据输入的带时间序列的工况数据,推导每个监测点(point_id)的下一阶段工况 特性: 1. 输入为二维列表,每个内嵌列表对应一个point_id,且为倒序排列(最新记录在索引0) 2. 输出为一维列表,仅保留每个内嵌列表的最新记录,新增工况推导结果字段 3. 无需切换工况时,next_workinfo返回当前工况名称;需要切换时返回目标工况名称 4. 时间计算仅按日期(天)维度,忽略时分秒 5. 支持冬休场景:当前工况为冬休时,以冬休前上一个有效工况作为切换判断依据 """ def __init__(self): """初始化类,加载内置的工况配置、分组规则和切换触发规则""" # 基础工况配置(更新为最新的工况列表,重复key保留后面的值) self.base_periods = self._load_base_periods() # 工况分组(同步更新分组规则,匹配最新工况名称) self.condition_group = self._load_condition_group() # 工况切换触发规则(键:分组ID,值:触发天数+目标工况候选) self.group_transition_rules = self._load_group_transition_rules() # 冬休工况标识(仅保留“冬休”) self.winter_break_labels = self._load_winter_break_labels() def _load_base_periods(self): """加载基础工况配置(更新为最新工况列表,重复key自动保留后面的值)""" return { "路基或预压土填筑,连续填筑":1, "路基或预压土填筑,两次填筑间隔时间较长":7, "预压土或路基填筑完成,第1~3个月":7, "预压土或路基填筑完成,第4~6个月":14, "仰拱(底板)施工完成后,第1个月": 7, "预压土或路基填筑完成,6个月以后":30, "仰拱(底板)施工完成后,第2至3个月": 14, "仰拱(底板)施工完成后,3个月以后": 30, "仰拱(底板)施工完成后,第1个月": 7, # 重复key,保留此值 "仰拱(底板)施工完成后,第2至3个月": 14, # 重复key,保留此值 "仰拱(底板)施工完成后,3个月以后": 30, # 重复key,保留此值 "无砟轨道铺设后,第1至3个月": 30, # 重复key,保留此值 "无砟轨道铺设后,4至12个月": 90, # 重复key,保留此值 "无砟轨道铺设后,12个月以后": 180, # 重复key,保留此值 "墩台施工到一定高度": 30, "墩台混凝土施工": 30, "预制梁桥,架梁前": 30, # 重复key,保留此值 "预制梁桥,预制梁架设前": 1, # 重复key,保留此值 "预制梁桥,预制梁架设后": 7, "桥位施工桥梁,制梁前": 30, "桥位施工桥梁,上部结构施工中": 1, "架桥机(运梁车)通过": 2, # 重复key,保留此值(原7→2) "桥梁主体工程完工后,第1至3个月": 7, "桥梁主体工程完工后,第4至6个月": 14, # 重复key,保留此值 "桥梁主体工程完工后,6个月以后": 30, # 重复key,保留此值 "轨道铺设期间,前": 30, "轨道铺设期间,后": 14, "轨道铺设完成后,第1个月": 14, # 重复key,保留此值 "轨道铺设完成后,2至3个月": 30, # 重复key,保留此值 "轨道铺设完成后,4至12个月": 90, # 重复key,保留此值 "轨道铺设完成后,12个月以后": 180, # 重复key,保留此值 "铺路或堆载,一般情况": 1, "填筑或堆载,一般情况": 1, "铺路或堆载,沉降量突变情况": 1, "填筑或堆载,两次填筑间隔时间较长情况":3, "铺路或堆载,两次铺路间隔时间较长情况": 3, "堆载预压或路基填筑完成,第1至3个月":7, "堆载预压或路基填筑完成,第4至6个月": 14, "堆载预压或路基填筑完成,6个月以后": 30, "架桥机(运梁车) 首次通过前": 1, "架桥机(运梁车) 首次通过后,前3天": 1, "架桥机(运梁车) 首次通过后": 7, "轨道板(道床)铺设后,第1个月": 14, "轨道板(道床)铺设后,第2至3个月": 30, "轨道板(道床)铺设后,3个月以后": 90, "架桥机(运梁车)首次通过前": 1, "架桥机(运梁车)首次通过后,前3天": 1, "架桥机(运梁车)首次通过后": 7, "轨道板铺设前": 14, # 新增工况 "轨道板(道床)铺设后,第1至3个月": 14, # 新增工况 "轨道板(道床)铺设后,第4至6个月": 30, # 新增工况 "轨道板(道床)铺设后,6个月以后": 90, # 新增工况 "站场填方路基段填筑完成至静态验收": 14, # 新增工况 "桥墩(台)地面处拆模后": 30, # 新增工况 "敦身混凝土施工": 30, # 新增工况 "预制梁桥预制梁架设后": 7, # 新增工况 "现浇梁,浇筑前": 30, # 新增工况 "现浇梁上部结构施工中": 1, # 新增工况 "侨梁主体工程完工后,6个月以后": 30, # 新增工况(注意错别字) "轨道铺设,前": 30, # 新增工况 "轨道铺设,后": 14, # 新增工况 "仰拱(底板)施工完成后第,2至3个月": 14, # 新增工况(标点位置异常) "特殊地段隧道施工完成后至静态验收": 14, # 新增工况 "冬休": 0 # 保留冬休工况 } def _load_condition_group(self): """加载工况分组规则(同步更新,匹配最新工况名称)""" condition_group = { # 仰拱相关 "仰拱(底板)施工完成后,第1个月": "YG_DIBAN_1", "仰拱(底板)施工完成后,第1个月": "YG_DIBAN_1", "仰拱(底板)施工完成后,第2至3个月": "YG_DIBAN_2_3", "仰拱(底板)施工完成后,第2至3个月": "YG_DIBAN_2_3", "仰拱(底板)施工完成后,3个月以后": "YG_DIBAN_AFTER_3", "仰拱(底板)施工完成后,3个月以后": "YG_DIBAN_AFTER_3", "仰拱(底板)施工完成后第,2至3个月": "YG_DIBAN_2_3", # 新增异常格式工况分组 # 架桥机相关 "架桥机(运梁车) 首次通过前": "JQJ_FIRST_BEFORE", "架桥机(运梁车)首次通过前": "JQJ_FIRST_BEFORE", # 新增无空格版本 "架桥机(运梁车) 首次通过后,前3天": "JQJ_FIRST_AFTER_3D", "架桥机(运梁车)首次通过后,前3天": "JQJ_FIRST_AFTER_3D", # 新增无空格版本 "架桥机(运梁车) 首次通过后": "JQJ_FIRST_AFTER", "架桥机(运梁车)首次通过后": "JQJ_FIRST_AFTER", # 新增无空格版本 "架桥机(运梁车)通过": "STATIC", # 堆载预压/路基填筑相关 "堆载预压或路基填筑完成,第1至3个月": "DZYY_1_3", "堆载预压或路基填筑完成,第4至6个月": "DZYY_4_6", "堆载预压或路基填筑完成,6个月以后": "DZYY_AFTER_6", "路基或预压土填筑,连续填筑": "STATIC", "路基或预压土填筑,两次填筑间隔时间较长": "STATIC", "预压土或路基填筑完成,第1~3个月": "DZYY_1_3", "预压土或路基填筑完成,第4~6个月": "DZYY_4_6", "预压土或路基填筑完成,6个月以后": "DZYY_AFTER_6", # 轨道板相关 "轨道板(道床)铺设后,第1个月": "GDB_1", "轨道板(道床)铺设后,第2至3个月": "GDB_2_3", "轨道板(道床)铺设后,3个月以后": "GDB_AFTER_3", "轨道板铺设前": "STATIC", "轨道板(道床)铺设后,第1至3个月": "GDB_1", "轨道板(道床)铺设后,第4至6个月": "GDB_2_3", "轨道板(道床)铺设后,6个月以后": "GDB_AFTER_3", # 预制梁桥相关 "预制梁桥,架梁前": "STATIC", "预制梁桥,预制梁架设前": "YZLQ_BEFORE_JS", "预制梁桥,预制梁架设后": "YZLQ_AFTER_JS", "预制梁桥预制梁架设后": "YZLQ_AFTER_JS", # 新增工况分组 # 桥梁主体相关 "桥梁主体工程完工后,第1至3个月": "QL_ZHUTI_1_3", "桥梁主体工程完工后,第4至6个月": "QL_ZHUTI_4_6", "桥梁主体工程完工后,6个月以后": "QL_ZHUTI_AFTER_6", "侨梁主体工程完工后,6个月以后": "QL_ZHUTI_AFTER_6", # 错别字工况分组 # 轨道铺设相关 "轨道铺设完成后,第1个月": "GD_1", "轨道铺设完成后,2至3个月": "GD_2_3", "轨道铺设完成后,4至12个月": "GD_4_12", "轨道铺设完成后,12个月以后": "GD_AFTER_12", "轨道铺设期间,前": "STATIC", "轨道铺设期间,后": "STATIC", "轨道铺设,前": "STATIC", "轨道铺设,后": "STATIC", # 无砟轨道相关 "无砟轨道铺设后,第1至3个月": "WZGD_1_3", "无砟轨道铺设后,4至12个月": "WZGD_4_12", "无砟轨道铺设后,12个月以后": "WZGD_AFTER_12", # 静态工况(无切换规则) "墩台施工到一定高度": "STATIC", "墩台混凝土施工": "STATIC", "桥位施工桥梁,制梁前": "STATIC", "桥位施工桥梁,上部结构施工中": "STATIC", "铺路或堆载,一般情况": "STATIC", "填筑或堆载,一般情况": "STATIC", "铺路或堆载,沉降量突变情况": "STATIC", "填筑或堆载,两次填筑间隔时间较长情况": "STATIC", "铺路或堆载,两次铺路间隔时间较长情况": "STATIC", "站场填方路基段填筑完成至静态验收": "STATIC", "桥墩(台)地面处拆模后": "STATIC", "敦身混凝土施工": "STATIC", "现浇梁,浇筑前": "STATIC", "现浇梁上部结构施工中": "STATIC", "特殊地段隧道施工完成后至静态验收": "STATIC", "冬休": "STATIC" } return condition_group def _load_group_transition_rules(self): """加载工况切换触发规则(保持原有规则,适配新增工况)""" return { "YG_DIBAN_1": {"trigger_days": 30, "next_candidates": ["仰拱(底板)施工完成后,第2至3个月", "仰拱(底板)施工完成后,第2至3个月"]}, "YG_DIBAN_2_3": {"trigger_days": 60, "next_candidates": ["仰拱(底板)施工完成后,3个月以后", "仰拱(底板)施工完成后,3个月以后"]}, "YG_DIBAN_AFTER_3": {"trigger_days": None, "next_candidates": None}, "JQJ_FIRST_BEFORE": {"trigger_days": 1, "next_candidates": ["架桥机(运梁车) 首次通过后,前3天"]}, "JQJ_FIRST_AFTER_3D": {"trigger_days": 3, "next_candidates": ["架桥机(运梁车) 首次通过后"]}, "JQJ_FIRST_AFTER": {"trigger_days": None, "next_candidates": None}, "DZYY_1_3": {"trigger_days": 90, "next_candidates": ["堆载预压或路基填筑完成,第4至6个月"]}, "DZYY_4_6": {"trigger_days": 90, "next_candidates": ["堆载预压或路基填筑完成,6个月以后"]}, "DZYY_AFTER_6": {"trigger_days": None, "next_candidates": None}, "GDB_1": {"trigger_days": 30, "next_candidates": ["轨道板(道床)铺设后,第2至3个月"]}, "GDB_2_3": {"trigger_days": 30, "next_candidates": ["轨道板(道床)铺设后,3个月以后"]}, "GDB_AFTER_3": {"trigger_days": None, "next_candidates": None}, "YZLQ_BEFORE_JS": {"trigger_days": 1, "next_candidates": ["架桥机(运梁车)通过"]}, "YZLQ_AFTER_JS": {"trigger_days": 7, "next_candidates": ["桥梁主体工程完工后,第1至3个月"]}, "QL_ZHUTI_1_3": {"trigger_days": 90, "next_candidates": ["桥梁主体工程完工后,第4至6个月"]}, "QL_ZHUTI_4_6": {"trigger_days": 90, "next_candidates": ["桥梁主体工程完工后,6个月以后"]}, "QL_ZHUTI_AFTER_6": {"trigger_days": None, "next_candidates": None}, "GD_1": {"trigger_days": 30, "next_candidates": ["轨道铺设完成后,2至3个月"]}, "GD_2_3": {"trigger_days": 60, "next_candidates": ["轨道铺设完成后,4至12个月"]}, "GD_4_12": {"trigger_days": 240, "next_candidates": ["轨道铺设完成后,12个月以后"]}, "GD_AFTER_12": {"trigger_days": None, "next_candidates": None}, "WZGD_1_3": {"trigger_days": 90, "next_candidates": ["无砟轨道铺设后,4至12个月"]}, "WZGD_4_12": {"trigger_days": 240, "next_candidates": ["无砟轨道铺设后,12个月以后"]}, "WZGD_AFTER_12": {"trigger_days": None, "next_candidates": None}, "STATIC": {"trigger_days": None, "next_candidates": None} } def _load_winter_break_labels(self): """冬休标识(仅冬休)""" return {"冬休"} def _parse_to_date(self, time_str): """ 私有辅助方法:将时间字符串解析为日期对象,仅保留年月日,忽略时分秒 :param time_str: 时间字符串,格式为 "YYYY-MM-DD HH:MM:SS" :return: date对象 / None(解析失败时) """ if not time_str: return None try: dt = datetime.strptime(str(time_str).strip(), "%Y-%m-%d %H:%M:%S") return dt.date() except ValueError: return None def _get_time_statistics_from_reversed(self, data, workinfo): """ 私有辅助方法:从倒序数据中提取时间统计信息 :param data: 单个point_id的倒序数据列表 :param workinfo: 目标工况名称(冬休时为冬休前上一工况) :return: 元组 (首次测量日期date对象, 首次到末次持续天数, 今日与首次测量天数差) """ # 筛选目标工况的有效记录(忽略冬休) target_records = [ d for d in data if d.get("workinfoname") == workinfo and d.get("workinfoname") not in self.winter_break_labels ] if not target_records: return None, 0, 0 target_dates = [] for item in target_records: d = self._parse_to_date(item.get("MTIME_W")) if d: target_dates.append(d) if not target_dates: return None, 0, 0 last_date = target_dates[0] first_date = target_dates[-1] cumulative_days = (last_date - first_date).days today = date.today() days_to_today = (today - first_date).days if first_date else 0 return first_date, cumulative_days, days_to_today def _get_pre_winter_break_workinfo(self, inner_data_list): """ 私有辅助方法:从倒序数据中提取冬休前的上一个有效工况 :param inner_data_list: 单个point_id的倒序数据列表 :return: 冬休前工况名称 / None(未找到时) """ if not inner_data_list: return None # 倒序遍历,跳过冬休,找第一个有效工况 for record in inner_data_list: current_work = record.get("workinfoname") if current_work and current_work not in self.winter_break_labels and current_work in self.base_periods: return current_work return None def _match_next_condition(self, current_name, candidates): """ 私有辅助方法:按符号风格匹配下一个工况名称(全角/半角括号对应) :param current_name: 当前工况名称(冬休时为冬休前上一工况) :param candidates: 目标工况候选列表 :return: 匹配的工况名称 / 当前工况名称(无候选时) """ if not candidates: return current_name if "(" in current_name: for cand in candidates: if "(" in cand: return cand if "(" in current_name: for cand in candidates: if "(" in cand: return cand return candidates[0] def _validate_point_id(self, inner_list): """ 私有辅助方法:校验内嵌列表内所有元素的point_id是否一致 :param inner_list: 单个point_id的倒序数据列表 :return: point_id / None(校验失败或列表为空时) """ if not inner_list: return None base_point_id = inner_list[0].get("point_id") for item in inner_list: if item.get("point_id") != base_point_id: return None return base_point_id def predict(self, data_2d_list): """ 公有核心方法:执行工况预测,处理二维输入数据,返回一维结果列表 :param data_2d_list: 二维倒序数据列表,格式 [[{},{},{}], [{},{},{}]] :return: 一维结果列表,格式 [{}, {}, {}],每个元素为对应内嵌列表的最新记录+推导字段 """ final_result_1d = [] for inner_data_list in data_2d_list: if not isinstance(inner_data_list, list) or len(inner_data_list) == 0: continue latest_record = inner_data_list[0].copy() point_id = self._validate_point_id(inner_data_list) if not point_id: latest_record.update({ "status": "fail", "current_workinfo": None, "first_measure_date": None, "days_from_first_to_today": None, "next_workinfo": None, "judge_based_workinfo": None, "error_msg": "point_id不一致或缺失" }) final_result_1d.append(latest_record) continue current_workinfo = latest_record.get("workinfoname") if not current_workinfo or current_workinfo not in self.base_periods: latest_record.update({ "status": "fail", "current_workinfo": None, "first_measure_date": None, "days_from_first_to_today": None, "next_workinfo": None, "judge_based_workinfo": None, "error_msg": "工况无效或缺失" }) final_result_1d.append(latest_record) continue # 冬休逻辑:当前是冬休 → 取冬休前工况作为判断基准 if current_workinfo in self.winter_break_labels: judge_based_workinfo = self._get_pre_winter_break_workinfo(inner_data_list) if not judge_based_workinfo: latest_record.update({ "status": "fail", "current_workinfo": current_workinfo, "first_measure_date": None, "days_from_first_to_today": None, "next_workinfo": None, "judge_based_workinfo": None, "error_msg": "冬休前未找到有效工况" }) final_result_1d.append(latest_record) continue else: judge_based_workinfo = current_workinfo # 用判断基准工况计算时间 first_dt, cumulative_days, days_to_today = self._get_time_statistics_from_reversed( inner_data_list, judge_based_workinfo ) first_measure_date = first_dt.strftime("%Y-%m-%d") if first_dt else None # 推导下一工况 group_id = self.condition_group.get(judge_based_workinfo, "STATIC") rule = self.group_transition_rules.get(group_id, {}) trigger_days = rule.get("trigger_days") next_candidates = rule.get("next_candidates", []) if trigger_days is not None and cumulative_days >= trigger_days: next_workname = self._match_next_condition(judge_based_workinfo, next_candidates) else: next_workname = judge_based_workinfo # 组装结果 latest_record.update({ "status": "success", "current_workinfo": current_workinfo, "judge_based_workinfo": judge_based_workinfo, "first_measure_date": first_measure_date, "days_from_first_to_today": days_to_today, "next_workinfo": next_workname, "point_id": point_id, "error_msg": "" }) final_result_1d.append(latest_record) return final_result_1d