Files
xian_algorithm_new/app/api/rainfall.py
T
2026-06-28 09:26:13 +08:00

251 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
暴雨灾害链预测接口
"""
import asyncio
from datetime import datetime
from typing import List, Dict, Any, Optional
from fastapi import APIRouter, HTTPException
from app.schemas.api_schemas import (
RainfallPredictRequest, PredictResponse, PredictData,
UpdateMonitoringTimeRequest, DistrictSummaryRequest, DistrictSummaryItem
)
from app.utils.api_deps import get_rainfall_model, get_prediction_semaphore
from app.repositories.dbn_repository import dbn_repository
from app.repositories.rainfall_repository import rainfall_repository as rain_repo
from app.core.rainfall_manager import rainfall_manager
from app.config.paths import get_logger
from app.utils.db_helper import db_helper
from app.utils.time_converter import TimeConverter
router = APIRouter(prefix="/rainfall", tags=["暴雨灾害链"])
logger = get_logger("api.rainfall")
SOURCE_TYPE_MAP = {1: "隐患点", 2: "风险点"}
LEVEL_MAP = {"": "", "": "", "较高": "较高", "": ""}
def _build_prediction_map(results: List[Dict[str, Any]]) -> Dict[str, float]:
"""将模型原始结果转换为存储格式: {id_type: 概率百分比}"""
result_map = {}
for r in results:
probs = r.get("disaster_probabilities", {})
if not probs:
continue
source_id = r["source_id"]
source_type = r.get("source_type")
max_hazard = max(probs, key=probs.get)
key = f"{source_id}_{source_type}"
result_map[key] = round(probs[max_hazard] * 100, 2)
return result_map
def _build_prediction_map_with_location(results: List[Dict[str, Any]], threshold: float = 50.0) -> Dict[str, Dict[str, Any]]:
"""将模型原始结果转换为返回格式: {id_type: {probability, lon, lat}}"""
from config import settings
threshold = getattr(settings, 'PREDICT_PROBABILITY_THRESHOLD', threshold)
result_map = {}
for r in results:
probs = r.get("disaster_probabilities", {})
if not probs:
continue
source_id = r["source_id"]
source_type = r.get("source_type")
max_hazard = max(probs, key=probs.get)
prob_value = round(probs[max_hazard] * 100, 2)
# 低于阈值不返回
if prob_value < threshold:
continue
key = f"{source_id}_{source_type}"
result_map[key] = {
"probability": prob_value,
"lon": r.get("lon"),
"lat": r.get("lat")
}
return result_map
def _fetch_points(point_ids: Optional[List[int]], region_code: Optional[str]) -> List[Dict[str, Any]]:
"""获取点位列表"""
if point_ids:
return dbn_repository.get_points_by_ids(point_ids)
return dbn_repository.get_all_points(region_code)
def _predict_sync(point_ids: Optional[List[int]], region_code: Optional[str],
rainfall: Optional[float], duration: Optional[float],
operation_type: str, occurred_time: Optional[datetime] = None) -> tuple:
"""
同步执行暴雨预测(在线程池中运行)
Args:
occurred_time: 事件发生时间,用于查询降雨数据和DBN推理
Returns:
(存储用result_map, 返回用result_map_with_location, 传入的条件, 实际使用的事件时间)
"""
points = _fetch_points(point_ids, region_code)
if not points:
return {}, {}, {}, occurred_time or datetime.now()
# 使用传入的时间,如果没有传则使用 rainfall_manager 中的全局查询时间,最后才用当前时间
if occurred_time:
query_time = occurred_time
else:
from app.core.rainfall_manager import rainfall_manager
query_time = rainfall_manager.get_current_query_time() or datetime.now()
model = get_rainfall_model()
raw_results = model.predict_multiple_points(points, rainfall=rainfall, duration=duration, query_time=query_time)
result_map = _build_prediction_map(raw_results) # 用于存储
result_map_with_location = _build_prediction_map_with_location(raw_results) # 用于返回
# 存储传入的原始条件(降雨量和持续时间可能每个点不同,所以存储传入值)
condition = {
"point_ids": point_ids,
"region_code": region_code,
"rainfall": rainfall,
"duration": duration,
"occurred_time": query_time.isoformat() if hasattr(query_time, 'isoformat') else str(query_time)
}
return result_map, result_map_with_location, condition, query_time
@router.post("/update-monitoring-time", summary="更新降雨监测查询时间")
async def update_monitoring_time(req: UpdateMonitoringTimeRequest):
"""
更新降雨站点监测的查询时间,触发重新计算
- **query_time**: 新的查询时间,格式: YYYY-MM-DD HH:mm:ss
"""
try:
# 将字符串时间解析为 datetime 对象
new_time = TimeConverter.parse_input_time(req.query_time)
# 更新监测时间,触发重新计算
result = rainfall_manager.update_query_time(new_time)
logger.info(f"更新监测时间成功: {result}")
return {
"code": 200,
"message": "success",
"data": result
}
except ValueError as e:
logger.error(f"时间格式错误: {e}")
raise HTTPException(status_code=400, detail=f"时间格式错误: {e}")
except Exception as e:
logger.error(f"更新监测时间失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"更新监测时间失败: {e}")
@router.post("/predict", response_model=PredictResponse, summary="暴雨灾害链预测")
async def predict_rainfall(req: RainfallPredictRequest):
"""
批量预测隐患点/风险点的灾害概率。两种模式(二选一):
**自动推演模式**rainfall、duration、region_code 全部不传
→ 从气象表自动获取各点最近站降雨数据,不限区域
**指定条件模式**rainfall、duration、region_code 全部传入
→ 按指定降雨条件和区域预测
- **disaster_name**: 灾害名称
- **point_ids**: 点位ID列表(可选)
- **occurred_time**: 事件发生时间(可选,不传则为当前时间)
- **operation_type**: 操作类型(如 '实时监测', '情景模拟', '应急评估'
"""
semaphore = get_prediction_semaphore()
async with semaphore:
loop = asyncio.get_event_loop()
try:
result_map, result_map_with_location, condition, occurred_time = await loop.run_in_executor(
None, _predict_sync, req.point_ids, req.region_code,
req.rainfall, req.duration, req.operation_type, req.occurred_time
)
except Exception as e:
logger.error(f"暴雨预测失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"预测失败: {e}")
# 保存推理结果
record_id = None
if result_map:
try:
record_id = dbn_repository.save_inference_result(
disaster_name=req.disaster_name,
event_type="rainfall",
occurred_time=occurred_time,
operation_type=req.operation_type,
condition=condition,
result=result_map
)
logger.info(f"推理结果已保存,record_id={record_id}")
except Exception as e:
logger.error(f"保存推理结果失败: {e}", exc_info=True)
return PredictResponse(code=200, message="success", data=PredictData(record_id=record_id, list=result_map_with_location))
@router.post("/district-summary", summary="获取各区降雨概况")
async def district_summary(req: DistrictSummaryRequest):
"""
根据推理结果ID获取各行政区的降雨概况(用于报告生成)。
逻辑:
- 如果预测时传了 rainfall → 直接用 condition 中的值
- 如果预测时没传 rainfall → 从 xian_meteorology 按区聚合实际气象数据
- **inference_id**: 推理结果ID
"""
# 1. 查推理结果
record = dbn_repository.get_inference_result(req.inference_id)
if not record:
raise HTTPException(status_code=404, detail=f"推理结果不存在: {req.inference_id}")
condition = record.get('condition') or {}
occurred_time = record.get('occurred_time')
# 2. 如果传了 rainfall → 直接用 condition 中的值
if condition.get('rainfall') is not None:
region_code = condition.get('region_code')
rainfall_val = float(condition['rainfall'])
duration_val = float(condition.get('duration', 0))
if region_code:
# 查 district 名称
district_row = db_helper.execute_query_one(
"SELECT name FROM xian_district WHERE code = %s AND is_delete = 0", (region_code,)
)
district_name = district_row['name'] if district_row else region_code
items = [{
'district_name': district_name,
'district_code': region_code,
'rainfall': rainfall_val,
'duration_hours': duration_val
}]
else:
items = [{
'district_name': '全市',
'district_code': '',
'rainfall': rainfall_val,
'duration_hours': duration_val
}]
else:
# 3. 自动推演模式 → 从气象表按区聚合
items = rain_repo.get_district_rainfall_summary(occurred_time)
if not items:
return {"code": 200, "message": "success", "data": []}
# 转换为响应格式
result_items = [
DistrictSummaryItem(**item).model_dump()
for item in items
]
return {"code": 200, "message": "success", "data": result_items}