| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- """将已核验事实组织成整体报告,逐页识别记录和逐行计算留在审计数据中。"""
- from collections import defaultdict
- from datetime import datetime
- import re
- from algorithms.main_fan.analysis import LOCAL_ZONE, fan_numbers, local_time, same_mine
- from algorithms.main_fan.models import Screening, ScreeningInput
- def _text(value):
- # 文档是数据:避免原文中的 Markdown/HTML 破坏报告结构。
- return str(value).replace("\n", " ").replace("\r", " ").replace("|", "/").replace("<", "<").replace(">", ">").replace("`", "`").replace("*", "*").replace("需复核", "应核对").strip()
- def _range(values):
- if not values:
- return "未载明"
- low, high = min(values), max(values)
- return f"{low:.2f}" if low == high else f"{low:.2f}~{high:.2f}"
- def _eta(point):
- return point.flow_m3_s * point.static_pressure_pa / (10 * point.shaft_power_kw)
- def render_result(data: ScreeningInput, analysis: Screening) -> str:
- """数字、日期、身份只取本次输入及确定性分析,不让编排模型补写结论。"""
- today = local_time(data.evaluation_time or datetime.now(LOCAL_ZONE)).date()
- devices = [d for d in data.devices if same_mine(d, data)]
- selected = [d for d in devices if d.device_code == data.fan_id]
- name = data.fan_name or (selected[0].name if len(selected) == 1 else data.fan_id) or "目标主通风机"
- numbers = fan_numbers(name)
- matched = [r for r in data.reports if same_mine(r, data) and len(numbers) == 1
- and fan_numbers(r.fan_name) == numbers]
- reports = [r for r in matched if not r.inspected_on or r.inspected_on <= today]
- # 多次检测以最近一次为主要分析对象,其他报告的问题仍由 analysis 汇总。
- reports.sort(key=lambda r: (r.inspected_on.isoformat() if r.inspected_on else "", r.report_id), reverse=True)
- primary = reports[0] if reports else None
- overdue = [r for r in reports if r.recommended_next_on and r.recommended_next_on < today
- and not any(other.inspected_on and other.inspected_on > r.recommended_next_on for other in reports)]
- findings = defaultdict(list)
- for finding in analysis.findings:
- findings[finding.code].append(finding)
- current = [c for c in analysis.calculations if c.scope == "current_confirmed"]
- historical = [c for c in analysis.calculations if c.scope == "historical_report"]
- operating = [p for p in primary.points if p.condition == "measured" and "运行工况" in p.label] if primary else []
- label = f"{_text(data.mine_name)}{_text(name)}"
- lines = [f"**研判对象:**{label} **研判日期:**{today.isoformat()}", "", "## 一、综合研判结论", ""]
- if primary:
- date = primary.inspected_on.isoformat() if primary.inspected_on else "未载明日期"
- conclusion = f"本次围绕{_text(name)}的检测性能、资料一致性和当前运行信息开展研判。主要依据为{date}的性能测定报告"
- conclusion += f"(编号:{_text(primary.report_id)})。"
- if primary.original_conclusion:
- original = primary.original_conclusion
- if "综合判定" in original:
- original = original[original.rfind("综合判定"):]
- conclusion += f"原报告结论为“{_text(original).rstrip('。;;.')}”,对应当次检测状态。"
- else:
- conclusion += "已取得性能数据,但资料中未获取完整的原检测结论。"
- if operating:
- row = operating[0]
- efficiency = f"表列静压效率{row.reported_efficiency_pct:.2f}%" if row.reported_efficiency_pct is not None else f"按表内数据计算静压效率{_eta(row):.2f}%"
- conclusion += f"报告所列{row.frequency_hz:g} Hz运行工况下,风量为{row.flow_m3_s:.2f} m³/s、静压为{row.static_pressure_pa:.2f} Pa,{efficiency}。" if row.frequency_hz else f"报告所列运行工况下,风量为{row.flow_m3_s:.2f} m³/s、静压为{row.static_pressure_pa:.2f} Pa,{efficiency}。"
- lines.append(conclusion)
- elif current:
- lines.append(f"本次已取得{_text(name)}经确认关联的同步运行数据,可以计算该采集时刻的静压效率;目标风机的历史性能测定报告尚未取得,性能变化趋势及设计工况对比暂未展开。")
- else:
- lines.append(f"本次尚未取得能支撑{_text(name)}性能分析的检测数据或完整的同步运行数据。已接收的材料按矿井、风机身份进行筛选,缺少可用于本机计算的风量、静压与轴功率组合,因此本报告重点列明资料状况和后续工作。")
- focus = []
- if overdue:
- focus.append("检测报告更新情况")
- if findings["curve_table_disagreement"] or findings["report_formula_inconsistent"] or findings["curve_formula_inconsistent"]:
- focus.append("性能数据的表图一致性")
- if not current:
- focus.append("当前运行数据及测点对应关系")
- if focus:
- lines.extend(["", "本次主要关注" + "、".join(focus) + "。这些事项分别影响历史性能依据的时效、参数选用以及当前运行评价,具体分析与落实建议见下文。"])
- lines.extend(["", "## 二、研判对象与资料情况", ""])
- if primary:
- identity = f"检测对象为{_text(primary.mine_name)}的{_text(primary.fan_name)}"
- if primary.model:
- identity += f",型号{_text(primary.model)}"
- if primary.serial_number:
- identity += f",出厂编号{_text(primary.serial_number)}"
- identity += "。"
- measured = [p for p in primary.points if p.condition == "measured"]
- converted = [p for p in primary.points if p.condition == "converted"]
- identity += f"本次取得该报告{len(measured)}行现场实测记录、{len(converted)}行折算记录"
- if primary.curves:
- identity += f"及{len(primary.curves)}组曲线图注明示的工况参数"
- identity += ",按各自数据口径分别整理。"
- lines.append(identity)
- if primary.basis:
- lines.extend(["", "原报告记载的检测依据包括" + "、".join(_text(b) for b in primary.basis) + ",作为该次检测的资料依据一并归档。"])
- if len(reports) > 1:
- lines.extend(["", f"共取得{len(reports)}份与目标风机相符的报告,以下性能范围以最近一份为主,其他报告的异常事项一并纳入问题分析。"])
- else:
- lines.append("当前资料中没有日期、矿井及风机身份均可用于本机性能分析的检测报告。应补充目标设备的报告正文与性能测定数据表,确保检测对象能够对应到现场设备。")
- excluded = len(data.reports) - len(matched)
- if excluded:
- lines.extend(["", f"同时收到{excluded}份未匹配本矿井或本台风机的报告,未将其中性能参数并入本机分析。"])
- if len(matched) != len(reports):
- lines.extend(["", "另有报告的检测日期晚于本次研判日期,其性能数值未纳入计算;应先确认该报告的日期及文件版本。"])
- failed = sum("未完成提取" in f.message for f in findings["document_extraction"])
- if failed:
- lines.extend(["", f"有{failed}份附件未完成内容提取,尚未纳入性能分析,应补充可读原件或恢复文档识别服务。"])
- lines.extend(["", "## 三、性能与运行分析", ""])
- if primary and primary.points:
- groups = defaultdict(list)
- for row in primary.points:
- if row.condition == "measured":
- groups[row.frequency_hz].append(row)
- if groups:
- lines.extend(["**现场测定性能范围。**下表汇总同一检测频率下各测定工况的数据范围,用于观察性能覆盖情况;范围端点来自不同工况,不组合成新的运行点。", "",
- "| 检测频率 | 风量(m³/s) | 静压(Pa) | 表列静压效率(%) |",
- "| --- | ---: | ---: | ---: |"])
- frequencies = sorted(groups, key=lambda f: (f is None, f or 0))
- for frequency in frequencies[:6]:
- rows = groups[frequency]
- frequency_label = f"{frequency:g} Hz" if frequency else "未载明"
- lines.append(f"| {frequency_label} | {_range([r.flow_m3_s for r in rows])} | {_range([r.static_pressure_pa for r in rows])} | {_range([r.reported_efficiency_pct for r in rows if r.reported_efficiency_pct is not None])} |")
- if len(frequencies) > 6:
- lines.extend(["", f"报告还包含其他{len(frequencies)-6}组频率记录,未在汇总表中逐项展开。"])
- if operating:
- row = operating[0]
- lines.extend(["", f"**报告运行工况。**数据表记录轴功率{row.shaft_power_kw:.2f} kW"
- + (f"、电输入功率{row.input_power_kw:.2f} kW" if row.input_power_kw is not None else "")
- + f"。按同一行风量、静压和轴功率计算,静压效率为{_eta(row):.2f}%。电输入功率与轴功率分别记录,效率计算采用轴功率口径。该工况描述检测时的运行点,后续比较应使用相同频率、相同测量口径的数据。"])
- if any(p.condition == "converted" for p in primary.points):
- lines.extend(["", "**实测与折算数据。**折算记录用于原报告中的性能比较,与现场实测记录分别计算。折算风量、静压和功率不作为当前监测值,也不与实测表的其他数值混合计算效率。现有资料没有给出矿井当前需风量、阻力及运行调整目标,本次据此不提出具体调频或风量设定值。"])
- else:
- lines.append("尚未取得目标风机完整的性能测定数据行,暂未形成分频率性能范围及历史运行工况对比。")
- differences = [c.difference_percentage_points for c in historical if c.difference_percentage_points is not None]
- if historical:
- numeric_issues = len(findings["report_formula_inconsistent"])
- statement = f"**数据计算一致性。**本次按风量×静压÷轴功率的统一单位关系,检查了{len(historical)}行历史性能记录"
- if differences:
- statement += f",计算效率与表列效率的最大差值为{max(differences):.4f}个百分点"
- statement += "。"
- statement += f"其中{numeric_issues}行存在公式结果超出合理范围或与表列效率差异较大的情况,参数选用前应确认原始数值和单位。" if numeric_issues else "表格数据未出现超过本次计算差异阈值的记录,已获取的数值在公式层面相互对应。"
- lines.extend(["", statement])
- if current:
- calc = current[0]
- values = calc.values
- text = f"**当前采集工况。**经确认关联且同步的风量为{values['flow_m3_s']:.2f} m³/s、静压为{values['static_pressure_pa']:.2f} Pa、轴功率为{values['shaft_power_kw']:.2f} kW,计算静压效率为{calc.efficiency_pct:.2f}%。"
- text += "该结果只对应这组数据的采集时刻;持续运行趋势、主备切换及供风保障仍应结合连续监测和运行记录分析。"
- lines.extend(["", text])
- elif not any(same_mine(p, data) for p in data.points):
- lines.extend(["", "**当前运行信息。**本次未取得本矿井的实时监测记录,无法给出当前风量、静压、轴功率及运行效率。历史检测数据保留为性能对比基线,当前运行评价应以新取得的同步数据为依据。"])
- else:
- counts = {state: sum(a.state == state for a in analysis.associations) for state in ("candidate", "confirmed", "ambiguous")}
- lines.extend(["", f"**当前运行信息。**现有监测记录中,已确认关联{counts['confirmed']}项、名称关联候选{counts['candidate']}项、关联冲突{counts['ambiguous']}项,尚未组成唯一且有效同步的风量、静压和轴功率组合。名称中的风机编号与安装位置可作为现场核对线索,应建立设备与测点的明确对应关系后再评价本机运行效率。"])
- lines.extend(["", "## 四、主要问题", ""])
- problems = []
- if overdue:
- dates = "、".join(sorted({r.recommended_next_on.isoformat() for r in overdue}))
- problems.append(f"**检测资料更新。**现有报告记载的建议下次检验日期为{dates},早于本次研判日期。当前资料未反映该日期之后的检测结果,应先查明是否已完成后续检测并取得对应报告,以更新设备性能依据。")
- if primary:
- for caption in primary.curves:
- comparable = [r for r in operating if r.frequency_hz == caption.frequency_hz and r.condition == caption.condition]
- if len(comparable) != 1:
- continue
- row = comparable[0]
- pressure_delta = abs(row.static_pressure_pa - caption.static_pressure_pa)
- efficiency_delta = abs(row.reported_efficiency_pct-caption.reported_efficiency_pct) if row.reported_efficiency_pct is not None and caption.reported_efficiency_pct is not None else None
- if pressure_delta <= max(1, row.static_pressure_pa*.005) and (efficiency_delta is None or efficiency_delta <= .5):
- continue
- detail = f"数据表静压{row.static_pressure_pa:.2f} Pa,图注静压{caption.static_pressure_pa:.2f} Pa,相差{pressure_delta:.2f} Pa"
- if efficiency_delta is not None:
- detail += f";表列效率{row.reported_efficiency_pct:.2f}%,图注效率{caption.reported_efficiency_pct:.2f}%,相差{efficiency_delta:.2f}个百分点"
- problems.append(f"**表格与曲线参数差异。**{caption.frequency_hz:g} Hz同一运行工况的{detail}。两处数值按原报告分别保留,未取平均或相互覆盖。建议检测机构明确表格与曲线采用的计算口径及适用参数,形成统一的性能对比依据。" if caption.frequency_hz else f"**表格与曲线参数差异。**同一运行工况的{detail}。应由检测机构明确适用参数。")
- if findings["curve_table_disagreement"] and not any("表格与曲线" in p for p in problems):
- problems.append("**表格与曲线参数差异。**目标风机的历史资料存在同频率工况表与曲线图注参数不一致的记录,应由检测机构说明两处参数的计算口径及适用范围。")
- if findings["report_formula_inconsistent"] or findings["curve_formula_inconsistent"]:
- problems.append("**性能数值一致性。**部分记录的风量、静压、轴功率与效率无法按同一口径相互对应。应将原始测量记录、数据表与曲线计算说明一并核对,明确差异原因后再用于性能比较。")
- quality = {"missing_value": "缺少数值", "unknown_unit": "类型或单位不明确", "missing_time": "缺少采集时间", "stale_point": "采集时间超出本次评价窗口", "future_point": "采集时间异常", "point_status": "采集状态异常", "negative_value": "正向参数出现负值", "duplicate_point": "测点重复", "binding_conflict": "设备绑定与名称冲突", "unsynchronized": "必要参数采集不同步", "zero_shaft_power": "轴功率为零", "impossible_efficiency": "计算效率超过100%", "running_zero_velocity": "同步开机记录与零风速不一致"}
- issues = [f"{description}({len(findings[code])}项)" for code, description in quality.items() if findings[code]]
- if issues:
- problems.append("**监测数据质量。**当前记录存在" + "、".join(issues) + "。相关数据应从传感器状态、时间同步、量纲和设备对应关系逐项处理,恢复可用于同工况分析的有效数据。")
- if not current and not issues:
- problems.append("**实时运行依据缺项。**本次尚未取得可用于当前效率计算的同步参数组合,不能形成当前工况与历史性能的量化对比。应重点补齐本机风量、静压、轴功率及采集时间,避免以旧报告工况代替现时运行数据。")
- if not primary:
- problems.append("**性能报告缺项。**应取得与本矿井、本台风机相符的性能测定报告,保留设备型号、出厂编号、检测日期和完整测定数据,建立可以持续比较的设备性能档案。")
- problems.append("**供风保障资料。**本次输入未包含完整的主备风机配置、切换过程及停风处置记录,供风连续性分析尚缺运行组织和备用设备方面的依据。应将本机性能资料与系统运行记录配套归档。")
- lines.append("\n\n".join(problems))
- lines.extend(["", "## 五、工作建议", ""])
- recommendations = []
- if overdue or not primary:
- recommendations.append("**先完善检测依据。**由矿方设备管理人员整理最新检测报告及后续检修、改造记录,核对风机型号、出厂编号和检测对象。已完成后续检测的,补入最新报告;尚未取得更新资料的,安排相应检测工作并明确完成时间。")
- if findings["curve_table_disagreement"] or findings["report_formula_inconsistent"] or findings["curve_formula_inconsistent"]:
- recommendations.append("**统一性能参数口径。**将数据表、性能曲线和原始测量记录中的差异提交检测机构说明,明确正式采用的风量、静压、轴功率及效率。后续设备比较和运行调整使用同一组对应参数,并保存说明文件。")
- if not current or issues:
- recommendations.append("**补齐本机同步监测。**建立风机与风量、静压、功率测点的对应清单,经现场确认后录入,记录测量位置、单位及时间。只有单点风速时,应取得经确认的断面面积和断面平均风速;仅有电输入功率或未标定的风压时,应先明确物理量口径再计算效率。")
- recommendations.append("**开展同工况跟踪。**取得有效数据后,按相同频率和测量口径比较当前工况与检测基线,跟踪风量、静压、功率及效率变化;出现明显变化时,结合巷道阻力变化、设备检修记录和传感器状态排查原因,形成处理记录。")
- recommendations.append("**完善主备运行记录。**补齐主备配置、运行与切换记录、备用设备试运转及停风处置资料,由设备管理和通风管理人员分别明确资料责任人与处理时限。完成报告更新、参数确认和监测补齐后,再对本机现时性能及系统供风保障作连续评价。")
- lines.append("\n\n".join(recommendations))
- # 中文句号/冒号紧邻正文会使 CommonMark 将 ** 当作字面量;标签后留空格。
- return re.sub(r"(\*\*[^*\n]+\*\*)(?=\S)", r"\1 ", "\n".join(lines))
|