feat(investigations): refine report authoring and replay flushing
This commit is contained in:
@@ -171,9 +171,20 @@ class ValveClosureInput(ScenarioInput):
|
||||
|
||||
|
||||
class FlushingInput(ScenarioInput):
|
||||
drainage_node_id: Id
|
||||
drainage_node_id: Id = Field(description="已核实的冲洗排放节点;清洁水来源、流向及污染水处置须在方案中说明")
|
||||
flushing_flow_m3h: float = Field(gt=0, allow_inf_nan=False, description="明确的冲洗流量,m³/h;提供流量来源与估算依据")
|
||||
closed_valves: list[Id] = Field(default_factory=list, max_length=20)
|
||||
closed_valves: list[Id] = Field(default_factory=list, max_length=20,
|
||||
description="本次冲洗情景中保持关闭的完整阀门清单,用于隔离用户或约束流向")
|
||||
opened_valves: list[Id] = Field(default_factory=list, max_length=20,
|
||||
description="本次冲洗情景中明确打开的阀门,如清洁水进水和导流阀;不表示现场已开阀或恢复用户供水。未列阀门沿用当前模型状态,不继承上次独立模拟")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def distinct_valve_states(self):
|
||||
if any(len(values) != len(set(values)) for values in (self.closed_valves, self.opened_valves)):
|
||||
raise ValueError("flushing valve lists must contain distinct IDs")
|
||||
if set(self.closed_valves) & set(self.opened_valves):
|
||||
raise ValueError("a flushing valve cannot be both opened and closed")
|
||||
return self
|
||||
|
||||
|
||||
NonnegativeValue = Annotated[float, Field(ge=0, allow_inf_nan=False)]
|
||||
|
||||
@@ -132,3 +132,12 @@ async def calculate_replay_contaminant_simulation(args: BaseModel, *, network: s
|
||||
args.model_dump(exclude={"context_handle", "timeout_seconds"}, exclude_none=True),
|
||||
network=network, username=username, model_only=True,
|
||||
)
|
||||
|
||||
|
||||
async def calculate_replay_flushing(args: BaseModel, *, network: str, username: str) -> dict[str, Any]:
|
||||
"""Evaluate a model-only flushing plan without live SCADA or field execution."""
|
||||
return await bounded_calculation(
|
||||
calculate_scenario, "analyze_flushing",
|
||||
args.model_dump(exclude={"context_handle", "timeout_seconds"}, exclude_none=True),
|
||||
network=network, username=username, model_only=True,
|
||||
)
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ _specs = [
|
||||
("run_simulation", "按水力步长在在线业务模型上执行指定时间段的实时模拟;更新该时段的模型条件及实时基线,返回指定节点和管段结果;duration单位为秒,输出为在线模型结果,正式分析成果通过方案工具保存", ScenarioInput, "simulation.run", "analysis", False, True),
|
||||
("run_contaminant_simulation", "在隔离模型中模拟假设污染源的浓度传播;include_impact_coverage=true时汇总全网节点整个模拟时段超过impact_threshold_mg_l的累计范围、首个超阈值报告时刻和峰值,并保存地图证据。阈值为情景展示条件,非饮用水安全限值;回放按当前模型独立估计,不读取实时SCADA。定位后先提现场核查/采样处置工单,工单审核派发与本计算并行,结果补入同一调查", ContaminantSimulationInput, "simulation.run", "analysis", False, True),
|
||||
("analyze_valve_closure", "在隔离模型中模拟关阀的水力影响,返回指定节点/管段结果;现场阀门操作按业务审批执行", ValveClosureInput, "simulation.run", "analysis", False, True),
|
||||
("analyze_flushing", "在隔离模型中评估指定排放节点、冲洗流量和关阀组合的水力结果;现场效果由执行回执和复测核验", FlushingInput, "simulation.run", "analysis", False, True),
|
||||
("analyze_flushing", "在独立模型中评估指定排放节点、流量及明确开关阀组合的冲洗水力结果;未列阀门沿用当前模型,不继承上次模拟;不判定污染清除或恢复供水", FlushingInput, "simulation.run", "analysis", False, True),
|
||||
("analyze_pressure_regulation", "在隔离模型中评估给定调泵序列和水箱初始水位的压力影响;输出为给定方案评估,现场调度按审批执行", PressureRegulationInput, "simulation.run", "analysis", False, True),
|
||||
("optimize_sensor_placement", "使用灵敏度或K-means算法推荐压力监测点;返回候选节点,正式部署另行保存并核验观测能力", SensorPlacementInput, "optimization.run", "analysis", False, True),
|
||||
("run_leakage_identification", "基于SCADA压力与模型进行DMA漏损分配识别,需有依据的总漏损流量和时间对齐;返回待现场核验的区域估计。内部执行遗传算法与水力计算", LeakageIdentificationInput, "burst.run", "analysis", False, True),
|
||||
|
||||
@@ -189,11 +189,12 @@ def saved_scenario(network: str, username: str, args: BaseModel) -> Any:
|
||||
modify_valve_opening={valve: 0.0 for valve in parameters["valves"]})
|
||||
if args.scenario == "flushing":
|
||||
from app.native.wndb.model.options import get_option
|
||||
from app.mcp.scenario_service import flushing_valve_controls
|
||||
if get_option(network).get("UNITS") not in {"LPS", "CMH"}:
|
||||
raise ValueError("Flushing supports LPS or CMH models only")
|
||||
return scenarios.flushing_analysis(**common, username=username,
|
||||
drainage_node_ID=parameters["drainage_node_id"], flushing_flow=parameters["flushing_flow_m3h"],
|
||||
modify_valve_opening={valve: 0.0 for valve in parameters["closed_valves"]})
|
||||
valve_control=flushing_valve_controls(parameters))
|
||||
mappings = query_corresponding_element_id_and_query_id(network)
|
||||
controls = parameters["pump_control"]
|
||||
if set(controls) - (set(mappings.fixed_pumps) | set(mappings.variable_pumps)):
|
||||
|
||||
@@ -79,7 +79,7 @@ async def authorize_query(
|
||||
"get_element_properties", "list_elements", "query_nearby_elements", "get_network_options",
|
||||
"get_node_connections", "get_element_gis_properties", "query_network_extent",
|
||||
"list_regions", "get_region", "get_region_nodes",
|
||||
"analyze_valve_isolation", "generate_node_coverage", "run_burst_simulation", "run_contaminant_simulation",
|
||||
"analyze_valve_isolation", "generate_node_coverage", "run_burst_simulation", "run_contaminant_simulation", "analyze_flushing",
|
||||
"get_calculation_job", "get_calculation_result", "list_calculation_jobs"}
|
||||
if tool.name not in allowed:
|
||||
raise HTTPException(403, "回放调查仅开放冻结 SCADA、资产、拓扑分析及指定模型情景;时序观测请使用 query_scada_readings")
|
||||
@@ -91,6 +91,8 @@ async def authorize_query(
|
||||
if not args.include_impact_coverage:
|
||||
raise HTTPException(422, "回放污染情景必须启用影响覆盖,按当前模型估计传播范围")
|
||||
check_replay_window(replay_context, args.start_time, args.start_time)
|
||||
if tool.name == "analyze_flushing":
|
||||
check_replay_window(replay_context, args.start_time, args.start_time)
|
||||
if tool.name == "query_scada_readings":
|
||||
check_replay_window(replay_context, args.start, args.end)
|
||||
if tool.name == "run_contaminant_source_trace":
|
||||
@@ -233,6 +235,17 @@ async def execute_query(
|
||||
data = {"parameters": args.model_dump(mode="json", exclude={"context_handle"}), "result": result}
|
||||
origin = "synthetic"
|
||||
gaps = ["回放污染传播为当前模型的独立假设情景,不读取实时SCADA,不代表冻结观测或历史模型重建。未来模拟样本仅为预测;浓度阈值仅用于情景展示,不是饮用水安全限值。"]
|
||||
elif name == "analyze_flushing" and replay_context:
|
||||
from app.mcp.analysis_service import calculate_replay_flushing
|
||||
|
||||
await conn.commit()
|
||||
async with scope.simulation_routing():
|
||||
result = await calculate_replay_flushing(
|
||||
args, network=scope.project.project_code, username=scope.user.username,
|
||||
)
|
||||
data = {"parameters": args.model_dump(mode="json", exclude={"context_handle"}), "result": result}
|
||||
origin = "synthetic"
|
||||
gaps = ["回放冲洗为当前模型的独立水力情景,不读取实时SCADA,不继承上次模拟;未来模拟样本仅为预测,不验证污染清除、水质合格或现场操作。"]
|
||||
elif name == "run_burst_location" and replay_context:
|
||||
from app.mcp.analysis_service import calculate_replay_burst_location
|
||||
from app.services.scada_replay_queries import replay_burst_location_inputs
|
||||
@@ -540,7 +553,8 @@ async def execute_query(
|
||||
"algorithm_status": "provisional",
|
||||
"required_inputs": ["candidate_node_ids", "start", "end"],
|
||||
"next_steps": ["present_node", "interim_report", "propose_task_action", "run_contaminant_simulation",
|
||||
"analyze_valve_isolation", "generate_node_coverage", "report"],
|
||||
"source_control", "analyze_valve_isolation", "generate_node_coverage",
|
||||
"analyze_flushing", "field_feedback", "clearance_verification", "report"],
|
||||
"work_order_wait_blocks_analysis": False,
|
||||
"simulation_parameters": {"include_impact_coverage": True},
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@ SCENARIO_TOOLS = frozenset({
|
||||
})
|
||||
|
||||
|
||||
def flushing_valve_controls(parameters: dict[str, Any]) -> dict[str, dict[str, str]]:
|
||||
"""Apply explicit states without changing regulating-valve settings."""
|
||||
return {**{valve: {"status": "CLOSED"} for valve in parameters.get("closed_valves", [])},
|
||||
**{valve: {"status": "OPEN"} for valve in parameters.get("opened_valves", [])}}
|
||||
|
||||
|
||||
def calculate_scenario(name: str, parameters: dict[str, Any], *, network: str, username: str,
|
||||
model_only: bool = False) -> dict[str, Any]:
|
||||
from app.services import simulation_scenarios as scenarios
|
||||
@@ -60,9 +66,16 @@ def calculate_scenario(name: str, parameters: dict[str, Any], *, network: str, u
|
||||
from app.native.wndb.model.options import get_option
|
||||
if get_option(network).get("UNITS") not in {"LPS", "CMH"}:
|
||||
raise ValueError("Flushing currently supports LPS or CMH models only")
|
||||
boundary = {}
|
||||
if model_only:
|
||||
from app.infra.db.postgresql.scada import ScadaElementMappings
|
||||
boundary["scada_mappings"] = ScadaElementMappings(
|
||||
reservoirs={}, tanks={}, fixed_pumps={}, variable_pumps={},
|
||||
pressure={}, demand={}, quality={},
|
||||
)
|
||||
result = scenarios.flushing_analysis(**common, username=username,
|
||||
drainage_node_ID=parameters["drainage_node_id"], flushing_flow=parameters["flushing_flow_m3h"],
|
||||
modify_valve_opening={valve: 0.0 for valve in parameters["closed_valves"]})
|
||||
valve_control=flushing_valve_controls(parameters), **boundary)
|
||||
elif name == "analyze_pressure_regulation":
|
||||
from app.services.simulation import query_corresponding_element_id_and_query_id
|
||||
mappings = query_corresponding_element_id_and_query_id(network)
|
||||
@@ -102,4 +115,17 @@ def calculate_scenario(name: str, parameters: dict[str, Any], *, network: str, u
|
||||
threshold_mg_l=impact_threshold, model_only=model_only)
|
||||
impact["coverage"] = {**output["coverage"], **impact.get("coverage", {})}
|
||||
output.update(impact)
|
||||
if name == "analyze_flushing":
|
||||
output["flushing_plan"] = {
|
||||
"opened_valves": parameters.get("opened_valves", []),
|
||||
"closed_valves": parameters.get("closed_valves", []),
|
||||
"drainage_node_id": parameters["drainage_node_id"],
|
||||
"flushing_flow_m3h": parameters["flushing_flow_m3h"],
|
||||
"estimated_discharge_volume_m3": parameters["flushing_flow_m3h"] * duration / 3600,
|
||||
"state_basis": "independent_current_model_scenario",
|
||||
"boundary_source": "model_only_no_live_scada" if model_only else "model_with_start_time_scada",
|
||||
"result_scope": "hydraulic_only",
|
||||
"field_execution_verified": False,
|
||||
"water_quality_clearance_verified": False,
|
||||
}
|
||||
return output
|
||||
|
||||
Reference in New Issue
Block a user