main_fan_simulation.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. """从三个明确的测试输入组装本次主备风机任务,不预置报告事实或结论。"""
  2. from datetime import datetime, timedelta
  3. from math import ceil
  4. from typing import Literal
  5. from zoneinfo import ZoneInfo
  6. from pydantic import BaseModel, ConfigDict, Field, model_validator
  7. from algorithms.main_fan.models import ScreeningInput
  8. LOCAL_ZONE = ZoneInfo('Asia/Shanghai')
  9. FAN_IDS = ('mock-report-fan-1', 'mock-report-fan-2')
  10. class MainFanSimulation(BaseModel):
  11. model_config = ConfigDict(extra='forbid', allow_inf_nan=False)
  12. mine_code: str = Field(default='mock-zy', min_length=1, max_length=128)
  13. mine_name: str = Field(default='陕西众源煤业集团有限公司', min_length=1, max_length=256)
  14. direction: Literal['1_to_2', '2_to_1'] = '1_to_2'
  15. stopped_at: datetime = Field(default_factory=lambda: datetime.now(LOCAL_ZONE).replace(hour=10, minute=0, second=0, microsecond=0))
  16. switch_interval_minutes: float = Field(default=6, ge=0, le=120)
  17. before_flow_m3_min: float = Field(default=3800, gt=0, le=1_000_000)
  18. after_flow_m3_min: float = Field(default=3610, ge=0, le=1_000_000)
  19. switch_source: Literal['simulation', 'file'] = 'simulation'
  20. flow_source: Literal['simulation', 'file'] = 'simulation'
  21. switch_asset_id: str | None = Field(default=None, min_length=1, max_length=128)
  22. flow_asset_id: str | None = Field(default=None, min_length=1, max_length=128)
  23. @model_validator(mode='after')
  24. def timezone_required(self):
  25. if self.stopped_at.utcoffset() is None:
  26. raise ValueError('停机时间必须包含时区')
  27. for slot in ('switch', 'flow'):
  28. if (getattr(self, slot+'_source') == 'file') != bool(getattr(self, slot+'_asset_id')):
  29. raise ValueError('选择JSON输入时须指定该项文件,模拟输入时请移除对应文件')
  30. return self
  31. def build_input(settings: MainFanSimulation):
  32. at = settings.stopped_at
  33. started = at + timedelta(minutes=settings.switch_interval_minutes)
  34. common = {'mine_code': settings.mine_code, 'mine_name': settings.mine_name}
  35. evidence = {'source': 'test-lab://main-fan/three-inputs', 'method': 'synthetic',
  36. 'label': '测试页生成的运行时序', 'excerpt': '根据本次填写的切换间隔及前后风量生成,不含报告识别事实'}
  37. origin, destination = (0, 1) if settings.direction == '1_to_2' else (1, 0)
  38. data = {**common, 'fan_id': FAN_IDS[0], 'fan_name': '1号通风机', 'fan_pair': list(FAN_IDS),
  39. 'evaluation_time': (at + timedelta(minutes=ceil(settings.switch_interval_minutes)+90)).isoformat(),
  40. 'use_gateway': False, 'paired_assessment': True, 'document_cache_policy': 'off', 'reports': [],
  41. 'devices': [{'device_code': key, 'name': f'{i+1}号通风机', **common, 'evidence': evidence}
  42. for i, key in enumerate(FAN_IDS)], 'fan_state_records': [], 'operating_series': []}
  43. # 同步整机状态按分钟采样,并插入准确的启动时刻,保留小数分钟而不向上取整。
  44. times = {at+timedelta(minutes=m) for m in range(-40, ceil(settings.switch_interval_minutes)+61)}
  45. times.add(started)
  46. samples = [[], []]
  47. for time in sorted(times):
  48. for i, key in enumerate(FAN_IDS):
  49. running = time < at if i == origin else time >= started
  50. data['fan_state_records'].append({**common, 'fan_id': key, 'observed_at': time.isoformat(),
  51. 'state': 'running' if running else 'stopped', 'complete_fan_state': True, 'evidence': evidence})
  52. value = settings.before_flow_m3_min if i == origin else settings.after_flow_m3_min
  53. samples[i].append({'observed_at': time.isoformat(), 'value': value if running else 0})
  54. data['operating_series'] = [{**common, 'fan_id': key, 'metric': 'flow', 'unit': 'm3/min',
  55. 'sensor_id': f'test-flow-{i+1}', 'mapping_confirmed': True, 'samples': samples[i], 'evidence': evidence}
  56. for i, key in enumerate(FAN_IDS)]
  57. ScreeningInput.model_validate({k: v for k, v in data.items() if k in ScreeningInput.model_fields})
  58. return data