monthly_roll.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. # -*- coding: utf-8 -*-
  2. """P3-c 月度滚动: 新数据 → L1 重跑 → 本体增量 → 沙盘刷新 → 变化摘要 (只报变化).
  3. 跑法: .venv/bin/python -m src.ontology.monthly_roll [--dry]
  4. --dry = 只检测与出摘要, 不重跑 L1 (数据未更新时的空转应零变化零动作)."""
  5. from src import paths as P
  6. import copy, json, pathlib, subprocess, sys
  7. import pandas as pd
  8. ONT = P.ont()
  9. ST = P.store()
  10. PY = P.python_exe() # 本安装目录的 venv (跨平台), 原写死 mac 路径
  11. def data_month():
  12. lub = pd.read_parquet(ST / 'mblub_monthly.parquet')
  13. return str(lub.month.max())
  14. def ont_month():
  15. o = json.loads((ONT / 'objects.json').read_text(encoding='utf-8'))
  16. mw = o.get('mwindow/rudong/climatology', {}).get('props', {})
  17. roll = o.get('meta/roll_state', {}).get('props', {})
  18. return roll.get('data_month')
  19. def diff_summary(before, after):
  20. """两版本体 diff → 变化摘要 (只报变化; 判级变化逐条, 计数类汇总)."""
  21. chg = []
  22. b_ids, a_ids = set(before), set(after)
  23. new_ids = a_ids - b_ids
  24. gone = b_ids - a_ids
  25. from collections import Counter
  26. if new_ids:
  27. chg.append(f"新增对象 {len(new_ids)}: " + str(dict(Counter(after[i]['type'] for i in new_ids))))
  28. if gone:
  29. chg.append(f"消失对象 {len(gone)}: {sorted(gone)[:8]}")
  30. for i in (a_ids & b_ids):
  31. if after[i]['type'] != 'Verdict':
  32. continue
  33. lb = before[i].get('props', {}).get('level')
  34. la = after[i].get('props', {}).get('level')
  35. if lb != la:
  36. chg.append(f"判级变化 {i}: {lb} → {la}")
  37. return chg
  38. def roll(dry=False):
  39. before = json.loads((ONT / 'objects.json').read_text(encoding='utf-8'))
  40. dm, om = data_month(), ont_month()
  41. actions = []
  42. if dm == om:
  43. actions.append(f'数据月 {dm} 无更新 — 空转 (L1 不重跑)')
  44. elif not dry:
  45. # L1 重跑清单 (仓建器) → 本体增量 → 状态机 → 沙盘
  46. subprocess.run([PY, 'scripts/windscada_watch_channels_build.py'], check=True, capture_output=True)
  47. subprocess.run([PY, '-m', 'src.ontology.populate'], check=True, capture_output=True)
  48. subprocess.run([PY, '-m', 'src.ontology.scenario_29'], check=True, capture_output=True)
  49. from . import acceptance_fsm
  50. acceptance_fsm.evaluate() # 修泵登记后此处会带 repair_done 参数
  51. actions.append(f'数据月 {om} → {dm}: L1仓重建 + populate增量 + 沙盘刷新 + 验收状态机')
  52. else:
  53. actions.append(f'[dry] 检测到新数据月 {dm} (本体记 {om}), 未执行重跑')
  54. after = json.loads((ONT / 'objects.json').read_text(encoding='utf-8'))
  55. chg = diff_summary(before, after)
  56. if not dry:
  57. # roll 状态只在真跑时落库 — dry 落状态会把随后的真跑骗成空转 (2026-08-28 实逮自修)
  58. from .store import Store
  59. s = Store(ONT / 'objects.json')
  60. # F8: 滚动状态演进走 force 通道, 逻辑时钟=数据月 (旧滚动记录进 history)
  61. s.put(dict(id='meta/roll_state', type='Decision',
  62. props=dict(name='月度滚动状态', data_month=dm, last_roll_actions=actions,
  63. last_changes=chg or ['零变化'])),
  64. force=True, when=dm)
  65. s.save()
  66. return dict(data_month=dm, actions=actions, changes=chg or ['零变化'])
  67. if __name__ == '__main__':
  68. r = roll(dry='--dry' in sys.argv)
  69. print(json.dumps(r, ensure_ascii=False, indent=1))