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