| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- """只呈现本次资料支持的双机一致性分析,不把未开展项目写进报告。"""
- import base64
- from datetime import datetime
- from html import escape
- import re
- from algorithms.main_fan.analysis import LOCAL_ZONE, local_time
- from algorithms.main_fan.equivalence import three_check_statuses
- from extensions.main_fan.reporting import _text
- from extensions.main_fan.result_output import judgment_result
- def _value(field, value):
- if field == "rated_power":
- parts = value["components_kw"]
- expression = f"{len(parts)}×{parts[0]:g}" if len(set(parts)) == 1 and len(parts) > 1 else "+".join(f"{p:g}" for p in parts)
- return f"{expression} kW(合计{value['total_kw']:g} kW)" if len(parts) > 1 else f"{value['total_kw']:g} kW"
- if isinstance(value, list):
- value = "~".join(f"{v:g}" for v in value)
- units = {"rated_speed_rpm": " r/min", "impeller_diameter_m": " m", "rated_flow_range_m3_min": " m³/min", "rated_pressure_range_pa": " Pa"}
- if isinstance(value, (int, float)):
- value = f"{value:g}"
- return _text(value) + units.get(field, "")
- def performance_chart(curves, names):
- """按物理量分别画双机曲线;与差异计算共用插值点,不混用纵轴。"""
- charts = []
- metrics = ((0, "静压", "Pa", 0), (1, "轴功率", "kW", 1), (2, "静压效率", "%", 1))
- # 多于三个频率或角度组合时继续分页绘图,不丢弃后续曲线。
- for index, label, unit, decimals in metrics:
- for begin in range(0, len(curves), 3):
- panels = curves[begin:begin+3]
- charts.append(_performance_metric_chart(panels, names, index, label, unit, decimals))
- return "\n\n".join(charts)
- def _performance_metric_chart(panels, names, index, label, unit, decimals):
- width, height, panel_width = 1080, 365, 1080/len(panels)
- title = f"两台主通风机风量—{label}对比曲线"
- body = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}" role="img">',
- f'<title>{title}</title><rect width="{width}" height="{height}" rx="8" fill="#f8fafc"/>',
- '<g font-family="sans-serif" font-size="13" fill="#263445">',
- f'<text x="24" y="30" font-size="18" font-weight="600" fill="#173e65">风量—{label}对比</text>']
- styles = (("fan_1", "#2563eb", ""), ("fan_2", "#c26a08", ' stroke-dasharray="7 4"'))
- for i, (_, color, dash) in enumerate(styles):
- left = 24+i*width/2
- body.append(f'<path d="M{left:g},54h32" stroke="{color}" stroke-width="2.8"{dash}/>'
- f'<text x="{left+42:g}" y="58">{escape(_text(names[i]))}</text>')
- for panel, result in enumerate(panels):
- rows = result["curve_samples"]
- x0, top, plot_width, plot_height = panel*panel_width+68, 118, panel_width-92, 169
- xmin, xmax = rows[0]["flow_m3_min"], rows[-1]["flow_m3_min"]
- values = [r[key][index] for r in rows for key, _, _ in styles]
- ymin, ymax = min(values), max(values)
- padding = max((ymax-ymin)*.1, 1 if index == 0 else .5)
- ymin, ymax = max(0, ymin-padding), ymax+padding
- frequency = f"{result['frequency_hz']:g} Hz"
- angle = result.get("blade_angle_deg")
- if angle is not None:
- frequency += f" / 叶片角度 {angle:g}°"
- body.append(f'<text x="{x0+plot_width/2:.1f}" y="88" text-anchor="middle" font-size="15" font-weight="600">{frequency}</text>'
- f'<text x="{x0:.1f}" y="109">{label} ({unit})</text>')
- for tick in range(4):
- y = top+plot_height-tick*plot_height/3
- value = ymin+tick*(ymax-ymin)/3
- x = x0+tick*plot_width/3
- flow = xmin+tick*(xmax-xmin)/3
- body.append(f'<path d="M{x0:.1f},{y:.1f}h{plot_width:.1f} M{x:.1f},{top}v{plot_height}" stroke="#dce4ee"/>'
- f'<text x="{x0-8:.1f}" y="{y+4:.1f}" text-anchor="end">{value:.{decimals}f}</text>'
- f'<text x="{x:.1f}" y="309" text-anchor="middle">{flow:.0f}</text>')
- body.append(f'<path d="M{x0:.1f},{top}v{plot_height}h{plot_width:.1f}" fill="none" stroke="#64748b"/>'
- f'<text x="{x0+plot_width/2:.1f}" y="332" text-anchor="middle">风量 (m³/min)</text>')
- for key, color, dash in styles:
- points = " ".join(f"{x0+(r['flow_m3_min']-xmin)/(xmax-xmin)*plot_width:.2f},{top+plot_height-(r[key][index]-ymin)/(ymax-ymin)*plot_height:.2f}" for r in rows)
- body.append(f'<polyline points="{points}" fill="none" stroke="{color}" stroke-width="2.8" stroke-linejoin="round"{dash}/>')
- body.append('</g></svg>')
- encoded = base64.b64encode("".join(body).encode()).decode()
- return f""
- def switch_chart(event, names):
- """同一时间轴展示受控开停序列和风量,开停采用阶梯线。"""
- chart = event['chart_data']
- state_rows, flow_rows = chart['state_curves'], chart['flow_curves']
- rows = state_rows + flow_rows
- timestamps = [local_time(datetime.fromisoformat(p['observed_at'])).timestamp() for row in rows for p in row['samples']]
- if not timestamps or min(timestamps) == max(timestamps):
- return ''
- low, high = min(timestamps), max(timestamps)
- left, plot_width = 80, 770
- flow_top = 225 if state_rows else 45
- bottom = flow_top+145 if flow_rows else 175
- height = bottom+75
- x = lambda at: left+(at-low)/(high-low)*plot_width
- colors = {event['from_fan_id']:'#74b6ff',event['to_fan_id']:'#ffca76'}
- body = [f'<svg xmlns="http://www.w3.org/2000/svg" width="900" height="{height}" viewBox="0 0 900 {height}">',
- f'<rect width="900" height="{height}" fill="#151a23"/>',
- '<g font-family="sans-serif" font-size="12" fill="#dbe5f3">',
- f'<text x="80" y="22">{"模拟运行时序" if chart["synthetic"] else "运行监测时序"}</text>']
- for i,fan_id in enumerate((event['from_fan_id'],event['to_fan_id'])):
- body.append(f'<text x="{600+i*135}" y="22" fill="{colors[fan_id]}">{escape(names[fan_id])}</text>')
- timing = event['timing']
- if timing['interval_minutes'] is not None:
- stop = local_time(datetime.fromisoformat(timing['from_stopped_at'])).timestamp()
- start = local_time(datetime.fromisoformat(timing['to_started_at'])).timestamp()
- body.append(f'<rect x="{x(min(stop,start)):.2f}" y="35" width="{abs(x(start)-x(stop)):.2f}" height="{bottom-35}" fill="#e7aa50" opacity="0.12"/>')
- for at in (stop,start):
- body.append(f'<path d="M{x(at):.2f},35V{bottom}" stroke="#dba454" stroke-dasharray="4 4"/>')
- def draw(row, y, step=False):
- # 保留状态跳变;风量按分桶极值压缩,避免大量历史点撑大网页报告。
- samples = row['samples']
- if step:
- samples = [p for i,p in enumerate(samples) if i in {0,len(samples)-1} or p['value'] != samples[i-1]['value'] or
- (i+1<len(samples) and p['value'] != samples[i+1]['value'])]
- elif len(samples)>800:
- size = (len(samples)+399)//400
- selected = {0,len(samples)-1}
- for begin in range(0,len(samples),size):
- indices = range(begin,min(begin+size,len(samples)))
- selected.update((min(indices,key=lambda i:samples[i]['value']),max(indices,key=lambda i:samples[i]['value'])))
- samples = [samples[i] for i in sorted(selected)]
- commands, previous_at = [], None
- for point in samples:
- at = local_time(datetime.fromisoformat(point['observed_at'])).timestamp()
- px, py = x(at), y(point['value'])
- if previous_at is None:
- commands.append(f'M{px:.2f},{py:.2f}')
- else:
- commands.append(f'H{px:.2f}V{py:.2f}' if step else f'L{px:.2f},{py:.2f}')
- previous_at = at
- body.append(f'<path d="{" ".join(commands)}" fill="none" stroke="{colors[row["fan_id"]]}" stroke-width="2.4"/>')
- if state_rows:
- body.append('<text x="80" y="45">主备风机开停曲线(1=开,0=停)</text>')
- for i,row in enumerate(state_rows):
- top = 68+i*58
- for value in (0,1):
- y = top+(1-value)*28
- body.append(f'<path d="M80,{y}H850" stroke="#303b4a"/><text x="65" y="{y+4}" text-anchor="end">{value}</text>')
- body.append(f'<text x="65" y="{top+18}" text-anchor="end" font-size="10" fill="{colors[row["fan_id"]]}">{escape(names[row["fan_id"]])}</text>')
- draw(row,lambda value,top=top:top+(1-value)*28,step=True)
- if flow_rows:
- maximum = max((p['value'] for row in flow_rows for p in row['samples']),default=1)*1.15 or 1
- body.append(f'<text x="80" y="{flow_top-15}">主备风机风量曲线(m³/min)</text>')
- for tick in range(4):
- y = flow_top+145-tick*145/3
- body.append(f'<path d="M80,{y:.2f}H850" stroke="#303b4a"/><text x="65" y="{y+4:.2f}" text-anchor="end">{maximum*tick/3:.0f}</text>')
- for row in flow_rows:
- draw(row,lambda value:flow_top+145-value/maximum*145)
- for tick in range(6):
- at = low+(high-low)*tick/5
- label = datetime.fromtimestamp(at,LOCAL_ZONE).strftime('%H:%M')
- body.append(f'<text x="{x(at):.2f}" y="{bottom+23}" text-anchor="middle">{label}</text>')
- if timing['interval_minutes'] is not None:
- labels = [local_time(datetime.fromisoformat(timing[key])).strftime('%H:%M:%S') for key in ('from_stopped_at','to_started_at')]
- body.append(f'<text x="80" y="{height-15}">原机停机 {labels[0]} 备用启动 {labels[1]} 切换间隔 {timing["interval_minutes"]:g} 分钟</text>')
- body.append('</g></svg>')
- return '.encode()).decode()+')'
- def render_fan_assessment_report(dataset, assessment, analysis):
- fans, basic, curves, switches = dataset.fans, assessment["basic_comparison"], assessment["performance_comparison"], assessment["switch_comparison"]
- names = [f["fan_name"] for f in fans]
- reports = [f["report"] for f in fans if f["report"]]
- now = local_time(dataset.data.evaluation_time or datetime.now(LOCAL_ZONE)).date()
- lines = [f"**研判对象:** {_text(dataset.data.mine_name)} {'、'.join(_text(n) for n in names)}", f"**研判日期:** {now.isoformat()}", ""]
- section = 0
- def heading(title):
- nonlocal section
- section += 1
- lines.extend(["", f"## {'一二三四五六七八九十'[section-1]}、{title}", ""])
- heading("综合研判结论")
- code, short_result = judgment_result(assessment, analysis)
- lines.extend([f"**研判结果:{code}**。{short_result}", ""])
- items = three_check_statuses(basic, switches)
- summary = []
- if basic["rows"]:
- mismatches = [r["label"] for r in basic["rows"] if r["status"] == "mismatch"]
- summary.append(("两台风机参数一致性", "、".join(mismatches)+"存在差异" if mismatches else "已比较的基础参数一致", "parameters"))
- events = switches["events"]
- if events:
- intervals = [f"{e['metrics']['switch_interval_minutes']:g}分钟(上限{e['timing']['limit_minutes']:g}分钟)"
- for e in events if e['metrics'].get('switch_interval_minutes') is not None]
- flows = [f"{e['metrics']['flow_deviation_percent']:.2f}%(上限{e['limits']['flow_deviation_percent']:g}%)"
- for e in events if e['metrics'].get('flow_deviation_percent') is not None]
- if intervals:
- summary.append(("停机至启动间隔", ";".join(intervals), "switch_interval"))
- if flows:
- summary.append(("切换前后稳定风量差异", ";".join(flows), "flow_change"))
- if summary:
- lines.extend(["| 判识项目 | 本次结果 | 分项结论 |", "| --- | --- | --- |"])
- labels = {"pass": "通过", "fail": "不通过", "insufficient": "未形成有效对照"}
- for label, detail, key in summary:
- lines.append(f"| {label} | {detail} | {labels[items[key]]} |")
- lines.append("")
- compared = [r["label"] for r in basic["rows"]]
- if basic["rows"]:
- mismatches = [r["label"] for r in basic["rows"] if r["status"] == "mismatch"]
- sentence = f"本次对{'与'.join(_text(n) for n in names)}的检测报告进行联合研判,重点核对两台设备的报告参数及性能测定数据。"
- sentence += "基础参数比较发现"+"、".join(mismatches)+"存在差异。" if mismatches else "已比较的"+"、".join(compared)+"一致。"
- lines.append(sentence)
- elif reports:
- lines.append("本次依据已取得的主通风机检测报告整理设备身份及性能记录,研判范围以本次实际参与比较的数据为准。")
- elif switches["events"]:
- lines.append(f"本次依据{'、'.join(_text(n) for n in names)}的运行记录与同步监测序列,分析实际切换事件前后的稳定参数变化。")
- else:
- lines.append("本次已完成资料整理,尚未形成可发布的双机一致性比较结果。")
- if curves["results"]:
- frequencies = "、".join(f"{r['frequency_hz']:g} Hz" for r in curves["results"])
- max_pressure = max(r["metrics"]["maximum_pressure_deviation_percent"] for r in curves["results"])
- max_eta = max(r["metrics"]["efficiency_difference_points"] for r in curves["results"])
- lines.extend(["", f"两份报告共同记录了{frequencies}现场测定数据。本次在各自共同风量区间内比较,最大静压相对偏差为{max_pressure:.2f}%,最大静压效率差为{max_eta:.2f}个百分点。偏差均以表列数据为依据,计算结果及运行工况比较见下文。"])
- formal = [r for r in curves["results"] if r["comparability"] == "same_angle_frequency"]
- if formal:
- lines.extend(["", "相同叶片角度、相同频率下的可比曲线" + ("存在超过本次配置阈值的差异。" if any(r['status']=='fail' for r in formal) else "各项差异处于本次配置阈值内。")])
- if switches["events"]:
- synthetic = any(e['chart_data']['synthetic'] for e in switches['events'])
- sentence = f"本次分析{len(switches['events'])}次{'模拟' if synthetic else '运行'}切换事件,分别判断停机至启动间隔和前后稳定风量差异。"
- sentence += '两项均符合本次规则。' if all(items[k]=='pass' for k in ('switch_interval','flow_change')) else '具体分项结果见切换工况分析。'
- lines.extend(['',sentence])
- if reports:
- heading("研判资料与设备身份")
- lines.extend(["本次使用的检测资料按矿井和风机身份分别归集,两台设备的原始参数保持独立,比较过程中统一风量、静压和功率单位。", "",
- "| 设备 | 报告编号 | 出厂编号 | 检测日期 |", "| --- | --- | --- | --- |"])
- for fan in fans:
- report = fan["report"]
- if report:
- lines.append(f"| {_text(fan['fan_name'])} | {_text(report.report_id)} | {_text(report.serial_number)} | {report.inspected_on.isoformat() if report.inspected_on else ''} |")
- measured = sum(p.condition == "measured" for r in reports for p in r.points)
- converted = sum(p.condition == "converted" for r in reports for p in r.points)
- lines.extend(["", f"两份报告共整理现场实测记录{measured}行、折算记录{converted}行。性能比较使用现场实测记录;折算记录单独保留,未作为现场实测工况参与两机差异计算。检测日期和出厂编号用于确认资料对象及所属检测批次,出厂编号不同表明资料分别对应两台设备。"])
- if basic["rows"]:
- heading("报告基础参数一致性")
- lines.extend([f"| 比较项目 | {_text(names[0])} | {_text(names[1])} | 比较结果 |", "| --- | --- | --- | --- |"])
- for row in basic["rows"]:
- values = [_value(row["field"], value) for value in row["values"]]
- lines.append(f"| {row['label']} | {values[0]} | {values[1]} | {'一致' if row['status']=='match' else '不一致'} |")
- power = next((r for r in basic["rows"] if r["field"] == "rated_power"), None)
- if power:
- lines.extend(["", "额定功率比较采用报告基本参数表中的电动机铭牌配置,保留电动机组成并统一计算总额定功率。组合表达式由计算工具解析,运行工况下的轴功率及电输入功率另行用于性能分析。"])
- matching = [r["label"] for r in basic["rows"] if r["status"] == "match"]
- if matching:
- lines.extend(["", "上述一致项表明,两台设备在已载明的"+"、".join(matching)+"方面具有一致的报告参数。基础参数一致性以型号、额定功率及双方共同载明的设备参数为依据。"])
- if basic["backup_availability"]:
- lines.extend(["", "运行记录所列备用风机状态为" + ("可用。" if basic['backup_availability']['status']=='available' else "不可用。")])
- if curves["results"]:
- heading("报告性能测定数据比较")
- lines.extend(["性能测定数据作为补充分析,单独说明两机性能曲线差异,不增加综合判识的必过条件。本次按共同检测频率分别组织两台风机的风量—静压、风量—轴功率和风量—静压效率数据,在双方原始测量范围的交集内进行分段线性插值。比较范围覆盖率按共同风量跨度占各自测量跨度的比例计算,所有比较点均位于原始测量范围内。", "",
- "| 频率 | 共同风量区间(m³/min) | 平均静压偏差 | 最大静压偏差 | 最大效率差(百分点) |",
- "| --- | ---: | ---: | ---: | ---: |"])
- for result in curves["results"]:
- m, q = result["metrics"], result["common_flow_range_m3_min"]
- lines.append(f"| {result['frequency_hz']:g} Hz | {q[0]:.2f}~{q[1]:.2f} | {m['mean_pressure_deviation_percent']:.2f}% | {m['maximum_pressure_deviation_percent']:.2f}% | {m['efficiency_difference_points']:.2f} |")
- details = ";".join(f"{r['frequency_hz']:g} Hz共同区间覆盖率{r['common_flow_coverage']*100:.2f}%、静压均方根误差{r['metrics']['pressure_rmse_pa']:.2f} Pa、最大轴功率偏差{r['metrics']['maximum_power_deviation_percent']:.2f}%" for r in curves['results'])
- lines.extend(["", details+"。相对偏差以表格第一台设备为基准;效率差按百分点表达,避免与相对百分比混用。", "", performance_chart(curves['results'], names)])
- operating = [r for r in curves["results"] if r["operating_points"]]
- if operating:
- lines.extend(["", "**报告所列运行工况比较。** 以下为两份检测报告明示的运行工况,分别保留各自对应的风量、静压及功率。", "",
- "| 频率与设备 | 风量(m³/min) | 静压(Pa) | 轴功率(kW) | 静压效率(%) |",
- "| --- | ---: | ---: | ---: | ---: |"])
- operating_notes = []
- for result in operating:
- for name, point in zip(names, result['operating_points']):
- lines.append(f"| {result['frequency_hz']:g} Hz {_text(name)} | {point['flow_m3_min']:.2f} | {point['pressure_pa']:.2f} | {point['shaft_power_kw']:.2f} | {point['efficiency_pct']:.2f} |")
- m = result["metrics"]
- pieces = []
- if m.get('operating_flow_deviation_percent') is not None:
- pieces.append(f"风量偏差{m['operating_flow_deviation_percent']:.2f}%")
- if m.get('operating_pressure_deviation_percent') is not None:
- pieces.append(f"静压偏差{m['operating_pressure_deviation_percent']:.2f}%")
- if pieces:
- operating_notes.append(f"{result['frequency_hz']:g} Hz所列运行工况的"+"、".join(pieces)+"。这些参数描述各自检测时的运行点,与公共风量区间内的曲线数据差异分别计算。")
- for note in operating_notes:
- lines.extend(["", note])
- if switches["events"]:
- heading("切换前后稳定工况比较")
- lookup = {f['fan_id']: f['fan_name'] for f in fans}
- for event in switches['events']:
- if event['chart_data']['synthetic']:
- lines.extend(['本节采用明确标注的开停、风量模拟时序,验证切换间隔和风量差异两项业务规则。',''])
- lines.extend([f"**切换事件:** {event['switched_at']},{_text(lookup[event['from_fan_id']])}切换至{_text(lookup[event['to_fan_id']])}。", ""])
- timing = event['timing']
- if timing['interval_minutes'] is not None:
- sentence = f"原运行风机于{timing['from_stopped_at']}停机,另一台风机于{timing['to_started_at']}启动,切换间隔为**{timing['interval_minutes']:.2f}分钟**。"
- sentence += f"按不超过{timing['limit_minutes']:g}分钟的规则,判定为**{'通过' if timing['status']=='pass' else '超限'}**。"
- if timing['overlap_minutes']:
- sentence += f"两机运行重叠{timing['overlap_minutes']:.2f}分钟,停机等待间隔按0分钟计。"
- lines.extend([sentence,''])
- chart = switch_chart(event,lookup)
- if chart:
- lines.extend([chart,''])
- if event['changes']:
- lines.extend(["| 监测指标 | 切换前稳定均值 | 切换后稳定均值 | 数值变化 | 相对变化 |", "| --- | ---: | ---: | ---: | ---: |"])
- for metric, change in event['changes'].items():
- flow = metric == 'flow'
- label, factor, unit = ('风量', 60, 'm³/min') if flow else ('静压' if metric=='static_pressure' else '风压', 1, 'Pa')
- lines.append(f"| {label}({unit}) | {change['before']*factor:.2f} | {change['after']*factor:.2f} | {change['absolute_change']*factor:+.2f} | {change['relative_change_percent']:+.2f}% |")
- if event['changes']:
- window = next(iter(event['before_window'].values()))
- after = next(iter(event['after_window'].values()))
- lines.extend(["", f"前稳定窗口为{window['start']}至{window['end']},后稳定窗口为{after['start']}至{after['end']}。统计采用有效样本,窗口完整率分别为{window['completeness']*100:.2f}%和{after['completeness']*100:.2f}%。"])
- if 'flow' in event['changes']:
- change = event['changes']['flow']
- limit = event['limits']['flow_deviation_percent']
- lines.extend(['',f"风量差异按 |切换后均值-切换前均值| ÷ 切换前均值 × 100% 计算,即 |{change['after']*60:.2f}-{change['before']*60:.2f}| ÷ {change['before']*60:.2f} × 100%=**{event['metrics']['flow_deviation_percent']:.2f}%**。按不超过{limit:g}%的规则,判定为**{'通过' if event['checks']['flow_change']=='pass' else '超限'}**。"])
- if event['metrics']['recovery_time_minutes'] is not None:
- lines.extend(["", f"切换后在{event['metrics']['recovery_time_minutes']:.1f}分钟开始形成连续恢复窗口。"])
- if all(v == 'pass' for v in event['checks'].values()):
- lines.extend(['','本次切换间隔与稳定风量差异均符合配置规则,支持该次工况下的主备切换及供风衔接结论。'])
- consistency = [f for f in analysis.findings if f.code in {'curve_table_disagreement', 'report_formula_inconsistent', 'curve_formula_inconsistent'}]
- if consistency:
- heading("检测资料一致性事项")
- lines.extend(["以下事项记录检测资料自身的表图或公式差异,独立于基础参数、切换间隔和稳定风量三项判识,不作为综合结论的否决项。", ""])
- for fan in fans:
- report = fan['report']
- if not report:
- continue
- for caption in report.curves:
- points = [p for p in report.points if p.condition==caption.condition and p.frequency_hz==caption.frequency_hz and '运行工况' in p.label
- and abs(p.flow_m3_s-caption.flow_m3_s)<=max(.001,caption.flow_m3_s*.005)
- and abs(p.shaft_power_kw-caption.shaft_power_kw)<=max(.001,caption.shaft_power_kw*.005)]
- if len(points)!=1:
- continue
- row = points[0]
- pressure_diff = abs(row.static_pressure_pa-caption.static_pressure_pa)
- eta_diff = 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_diff <= max(1,row.static_pressure_pa*.005) and (eta_diff is None or eta_diff <= .5):
- continue
- text = f"{_text(fan['fan_name'])}{caption.frequency_hz:g} Hz报告的数据表静压为{row.static_pressure_pa:.2f} Pa,曲线图注为{caption.static_pressure_pa:.2f} Pa,相差{pressure_diff:.2f} Pa。"
- if eta_diff is not None:
- text += f"表列静压效率为{row.reported_efficiency_pct:.2f}%,图注为{caption.reported_efficiency_pct:.2f}%,相差{eta_diff:.2f}个百分点。"
- lines.extend([text+"本次两机性能数据比较采用数据表,图注数值独立保留。", ""])
- formula = [f for f in consistency if f.code in {'report_formula_inconsistent','curve_formula_inconsistent'}]
- if formula:
- lines.append(f"本次发现{len(formula)}项性能数值与同一组参数的效率公式不能一致对应,已在原始计算记录中保留具体数值和原文位置。")
- lines.extend(["", "建议将上述表图差异交由检测机构说明,统一正式采用的报表与曲线参数口径,避免在后续两机性能比较中交叉使用不同来源数值。"])
- historical = [c for c in analysis.calculations if c.scope=='historical_report']
- if historical:
- differences = [c.difference_percentage_points for c in historical if c.difference_percentage_points is not None]
- if differences:
- lines.extend(["", f"本次同时检查了两台风机共{len(historical)}行历史表格数据的静压效率公式,计算值与表列效率的最大差值为{max(differences):.4f}个百分点。逐行原始数值、公式及文件位置已与报告一并保存。"])
- return re.sub(r"(\*\*[^*\n]+\*\*)(?=\S)", r"\1 ", "\n".join(lines))
|