preview_main_fan_data.py 6.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. #!/usr/bin/env python3
  2. """用已有矿级原件回放固定发现及预处理,生成真实Agent输入;不访问模型或网关。"""
  3. import argparse
  4. import json
  5. from pathlib import Path
  6. import sys
  7. ROOT = Path(__file__).resolve().parents[1]
  8. sys.path.insert(0, str(ROOT))
  9. from extensions.main_fan import discovery, history_compaction, history_processing
  10. from extensions.main_fan.source_policy import source_policy
  11. def read(path):
  12. return json.loads(path.read_text(encoding='utf-8'))
  13. def write(path, value):
  14. path.write_text(json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False) + '\n', encoding='utf-8')
  15. def preview(capture, output):
  16. capture, output = Path(capture).resolve(), Path(output).resolve()
  17. source = read(capture / 'history-manifest.json')
  18. mine = {key: source[key] for key in ('dept_id', 'mine_id', 'mine_name')}
  19. def rows(name):
  20. body = read(capture / name)
  21. return body['rows'] if 'rows' in body else body['data']['rows']
  22. plan = discovery.plan_from_rows(mine, rows('safety-realtime.json'), rows('equipment-realtime.json'))
  23. points = [point.model_dump(mode='json') for shaft in plan.shafts for point in shaft.points]
  24. selected = {(point['system'], point['node_id']) for point in points}
  25. segments = [row for row in source['segments'] if (row['system'], row['node_id']) in selected]
  26. expected = len(points) * source['lookback_days']
  27. completed = sum(row['status'] == 'completed' for row in segments)
  28. manifest = {**source, 'points': points, 'segments': segments, 'point_count': len(points),
  29. 'expected_segments': expected, 'completed_segments': completed,
  30. 'failed_segments': len(segments) - completed, 'coverage_complete': completed == expected,
  31. 'source_policy': source_policy()}
  32. output.mkdir(parents=True, exist_ok=True)
  33. def resolve(virtual):
  34. if not virtual.startswith('/workspace/'):
  35. raise ValueError('证据路径必须位于/workspace/')
  36. path = (capture / virtual.removeprefix('/workspace/')).resolve()
  37. if not path.is_relative_to(capture):
  38. raise ValueError('证据路径越出回放目录')
  39. return path
  40. with history_compaction.RecordReader(resolve) as reader:
  41. details = history_compaction.summarize(manifest, reader)
  42. details.update(mine=mine, source_policy=source_policy(), storage=source['storage'])
  43. data = history_processing.prepare(details, manifest, reader,
  44. read(ROOT / 'examples/main_fan/equivalence_rules.json'), str(output / '开停与候选详情.json'))
  45. details['discovery'] = {'mode': 'fixed_code', 'shafts': [{key: shaft.model_dump(mode='json')[key]
  46. for key in ('shaft_id', 'shaft_name', 'identity_status', 'limitations')} for shaft in plan.shafts]}
  47. details['data_preparation'] = {'mode': 'fixed_code', 'validation': 'existing_real_capture_replay',
  48. 'agent_needs_monitoring_queries': False, 'new_gateway_requests': 0, 'model_calls': 0,
  49. 'note': '仅回放本矿已有40天原件;此示例为监测数据输入,PDF由正式批次另外提供。'}
  50. details['evidence'] = {'archive_local_path': str(capture / 'history-evidence.zip'),
  51. 'original_manifest_local_path': str(capture / 'history-manifest.json')}
  52. data = history_processing.agent_input(data)
  53. write(output / '固定测点计划.json', plan.model_dump(mode='json'))
  54. write(output / '开停与候选详情.json', details)
  55. write(output / '给Agent的数据.json', data)
  56. events = details['preprocessing']['switch_candidates']
  57. summary = ['# 固定代码给Agent的数据', '',
  58. '矿井:' + mine['mine_name'] + ';deptId:' + mine['dept_id'] + '。',
  59. '回查' + str(source['lookback_days']) + '天;' + str(len(points)) + '个测点;' + str(completed) + '/' + str(expected) + '个成功日段。',
  60. '本次使用已有真实原件验证,没有新增网关请求或模型调用。正式批次在调用Agent前自动执行相同取数及处理流程。', '',
  61. '固定程序完成:筛矿 → 分页获取1010开停点及30101风量点 → 按天回查 → 开停区间整理 → 切换候选 → 前后单点风量对比。',
  62. '原机以切换范围起点前1小时为目标,新机以终点后1小时为目标,分别取对应一侧最接近目标的一条采样;不求平均,不使用分钟覆盖率或CV门槛,零值保留。',
  63. 'Agent直接读取同目录的“给Agent的数据.json”。默认只保留矿井、时段、开停区间、风量概况、切换候选和必要限制。',
  64. '调试计数、测点ID、质量明细、规则及计算过程在“开停与候选详情.json”,原始采样仍在原目录的ZIP。', '',
  65. '## 本例切换候选', '']
  66. for event in events:
  67. summary.extend([event['event_id'] + ':' + event['from_fan'] + ' → ' + event['to_fan'],
  68. '采样定位范围:(' + event['after'] + ', ' + event['by'] + ']。该范围不是切换耗时。', '',
  69. '| 对象 | 目标时间 | 实际采样时间 | 风量 |', '|---|---|---|---:|'])
  70. for flow in event['flow_context']:
  71. side = 'before' if flow['fan_id'] == event['from_fan'] else 'after' if flow['fan_id'] == event['to_fan'] else None
  72. if side:
  73. sample = flow[side + '_reference']
  74. summary.append('| ' + flow['fan_id'] + ('切换前' if side == 'before' else '切换后') +
  75. ' | ' + sample['target_time'] + ' | ' + str(sample['time']) + ' | ' + str(sample['value']) + ' |')
  76. change = event['flow_comparison']['relative_change_percent']
  77. summary.extend(['', '按原编号的单点读数变化:' + (str(change) + '%' if change is not None else '不可计算') +
  78. '。该数值不证明连续稳定工况或实际故障;物理对应待确认的说明保留。', ''])
  79. (output / '说明.md').write_text('\n'.join(summary) + '\n', encoding='utf-8')
  80. return {'output': str(output), 'point_count': len(points), 'completed_segments': completed,
  81. 'switch_candidates': len(events), 'agent_data_bytes': (output / '给Agent的数据.json').stat().st_size,
  82. 'new_gateway_requests': 0, 'model_calls': 0}
  83. if __name__ == '__main__':
  84. parser = argparse.ArgumentParser(description=__doc__)
  85. parser.add_argument('--capture', type=Path, required=True)
  86. parser.add_argument('--output', type=Path, required=True)
  87. args = parser.parse_args()
  88. print(json.dumps(preview(args.capture, args.output), ensure_ascii=False))