fix(investigation): allow autonomous model baseline recovery
This commit is contained in:
@@ -116,6 +116,15 @@ async def calculate_replay_burst_location(
|
||||
)
|
||||
|
||||
|
||||
async def calculate_replay_hydraulic_scenario(name: str, args: BaseModel, *, network: str, username: str) -> dict[str, Any]:
|
||||
"""Evaluate model-based hydraulic alternatives without live SCADA substitution."""
|
||||
return await bounded_calculation(
|
||||
calculate_scenario, name,
|
||||
args.model_dump(exclude={"context_handle", "timeout_seconds"}, exclude_none=True),
|
||||
network=network, username=username, model_only=True,
|
||||
)
|
||||
|
||||
|
||||
async def calculate_replay_burst_simulation(args: BaseModel, *, network: str, username: str) -> dict[str, Any]:
|
||||
"""An independent current-model scenario, with no live SCADA substitution."""
|
||||
return await bounded_calculation(
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ class QueryTool:
|
||||
|
||||
_specs = [
|
||||
("run_contaminant_source_trace", "水污染候选溯源:提交由事件位置或监测映射取得的候选模型节点及事件窗,快速返回确定的已核验节点。当前为临时稳定选择,正式反演算法后续接入;不生成置信度。取得located_node后先展示节点、保存阶段结论并提现场核查/采样处置工单;工单与污染传播模拟、accident_element_type=node的隔离和影响分析并行,结果补入同一调查,不因算法来源停止", ContaminantSourceTraceInput, "simulation.run", "analysis", False, True),
|
||||
("run_simulation", "按水力步长在在线业务模型上执行指定时间段的实时模拟;更新该时段的模型条件及实时基线,返回指定节点和管段结果;duration单位为秒,输出为在线模型结果,正式分析成果通过方案工具保存", ScenarioInput, "simulation.run", "analysis", False, True),
|
||||
("run_simulation", "按水力步长在在线业务模型上执行指定时间段的实时模拟,更新全网实时基线,返回指定节点和管段结果;调查发现同刻基线缺失时可自主补算,等待成功并读取结果后重试依赖分析。duration单位为秒,补单帧可用60秒;回放按当前模型补算,不读取实时SCADA,不替代冻结观测;正式分析成果通过方案工具保存", 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),
|
||||
|
||||
@@ -15,7 +15,6 @@ from app.mcp.models import BindInput, BoundContext
|
||||
from app.mcp.scope import QueryScope
|
||||
from app.mcp.config import McpSettings
|
||||
from mcp.server.auth.provider import AccessToken
|
||||
from app.mcp.work_order_skills import skill_name_for_task
|
||||
|
||||
|
||||
def task_repository(scope: QueryScope) -> OperationalTaskRepository:
|
||||
@@ -32,8 +31,6 @@ async def verify_task(scope: QueryScope, binding: BoundContext) -> None:
|
||||
if not task:
|
||||
raise HTTPException(404, "Task is not in this project")
|
||||
binding.replay_context = task.get("source", {}).get("replay_context")
|
||||
if skill_name_for_task(task.get("workflow"), binding.replay_context) == "burst-event-handling":
|
||||
binding.tool_cap = [name for name in binding.tool_cap if name != "run_burst_location"]
|
||||
if binding.purpose == "task_collaboration":
|
||||
return
|
||||
if OPERATIONAL_TASK_EXECUTE not in permissions:
|
||||
|
||||
+24
-11
@@ -79,20 +79,20 @@ 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_flushing",
|
||||
"analyze_valve_isolation", "generate_node_coverage", *SCENARIO_TOOLS,
|
||||
"calculate_demand_allocation", "optimize_sensor_placement",
|
||||
"query_model_baseline_series", "get_model_baseline_at_time", "query_model_baseline_field",
|
||||
"get_calculation_job", "get_calculation_result", "list_calculation_jobs"}
|
||||
if tool.name not in allowed:
|
||||
asset_query = tool.semantics == "asset" and not tool.mutating and not tool.computation
|
||||
if tool.name not in allowed and not asset_query:
|
||||
raise HTTPException(403, "回放调查仅开放冻结 SCADA、资产、拓扑分析及指定模型情景;时序观测请使用 query_scada_readings")
|
||||
if tool.name == "run_burst_simulation":
|
||||
if not args.include_impact_coverage:
|
||||
raise HTTPException(422, "回放爆管情景必须启用影响覆盖,按当前模型估计并保存同刻在线基线")
|
||||
check_replay_window(replay_context, args.start_time, args.start_time)
|
||||
if tool.name == "run_contaminant_simulation":
|
||||
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":
|
||||
if tool.name in SCENARIO_TOOLS:
|
||||
check_replay_window(replay_context, args.start_time, args.start_time)
|
||||
if tool.semantics == "model_baseline":
|
||||
if tool.name == "query_model_baseline_series":
|
||||
check_replay_window(replay_context, args.start, args.end)
|
||||
else:
|
||||
check_replay_window(replay_context, args.time, args.time)
|
||||
if tool.name == "query_scada_readings":
|
||||
check_replay_window(replay_context, args.start, args.end)
|
||||
if tool.name == "run_contaminant_source_trace":
|
||||
@@ -213,6 +213,17 @@ async def execute_query(
|
||||
args.device_ids, args.start, args.end, args.limit + 1, args.offset)
|
||||
data, page, truncated = page_result(rows, args)
|
||||
origin = "synthetic"
|
||||
elif name in {"run_simulation", "analyze_valve_closure", "analyze_pressure_regulation"} and replay_context:
|
||||
from app.mcp.analysis_service import calculate_replay_hydraulic_scenario
|
||||
|
||||
await conn.commit()
|
||||
async with scope.simulation_routing():
|
||||
result = await calculate_replay_hydraulic_scenario(
|
||||
name, 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_simulation" and replay_context:
|
||||
from app.mcp.analysis_service import calculate_replay_burst_simulation
|
||||
|
||||
@@ -632,6 +643,8 @@ async def execute_query(
|
||||
truncated = snapshot.truncated
|
||||
else:
|
||||
raise ValueError("query handler is not registered")
|
||||
if replay_context and tool.semantics == "model_baseline":
|
||||
gaps.append("在线模型参考按本次查询时可用数据读取,非冻结SCADA观测;历史报告复用已保存证据。")
|
||||
window = (
|
||||
{"start": args.start, "end": args.end}
|
||||
if hasattr(args, "start") and args.start is not None
|
||||
|
||||
+13
-26
@@ -29,50 +29,36 @@ def calculate_scenario(name: str, parameters: dict[str, Any], *, network: str, u
|
||||
impact_threshold = parameters.pop("impact_threshold_mg_l", 0)
|
||||
common = dict(name=network, modify_pattern_start_time=start.isoformat(),
|
||||
modify_total_duration=duration, persist_results=False, scheme_name=None)
|
||||
boundary = {}
|
||||
if model_only:
|
||||
from app.infra.db.postgresql.scada import ScadaElementMappings
|
||||
# Model scenarios may support replay decisions without consuming mutable
|
||||
# live SCADA in place of the task's frozen observations.
|
||||
boundary["scada_mappings"] = ScadaElementMappings(
|
||||
reservoirs={}, tanks={}, fixed_pumps={}, variable_pumps={},
|
||||
pressure={}, demand={}, quality={},
|
||||
)
|
||||
if name == "run_simulation":
|
||||
from app.services.simulation import run_online_simulation
|
||||
|
||||
result = run_online_simulation(
|
||||
network, start, duration, output_node_ids=nodes, output_link_ids=links,
|
||||
network, start, duration, output_node_ids=nodes, output_link_ids=links, **boundary,
|
||||
)
|
||||
elif name == "run_burst_simulation":
|
||||
boundary = {}
|
||||
if model_only:
|
||||
from app.infra.db.postgresql.scada import ScadaElementMappings
|
||||
# Replay scenarios use current model assumptions, never mutable live
|
||||
# SCADA readings in place of the investigation's frozen observations.
|
||||
boundary["scada_mappings"] = ScadaElementMappings(
|
||||
reservoirs={}, tanks={}, fixed_pumps={}, variable_pumps={},
|
||||
pressure={}, demand={}, quality={},
|
||||
)
|
||||
result = scenarios.burst_analysis(**common, username=username,
|
||||
burst_ID=parameters["burst_pipe_ids"], burst_size=parameters["orifice_areas_cm2"], **boundary)
|
||||
elif name == "run_contaminant_simulation":
|
||||
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.contaminant_simulation(**common, username=username,
|
||||
source=parameters["source"], concentration=parameters["concentration"], source_pattern=parameters.get("pattern"), **boundary)
|
||||
elif name == "analyze_valve_closure":
|
||||
result = scenarios.valve_close_analysis(**common,
|
||||
modify_valve_opening={valve: 0.0 for valve in parameters["valves"]})
|
||||
modify_valve_opening={valve: 0.0 for valve in parameters["valves"]}, **boundary)
|
||||
elif name == "analyze_flushing":
|
||||
# The service consumes m³/h (the older REST label incorrectly says L/s).
|
||||
# Reject unsupported conversion rather than silently ignore the flow.
|
||||
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"],
|
||||
valve_control=flushing_valve_controls(parameters), **boundary)
|
||||
@@ -87,7 +73,8 @@ def calculate_scenario(name: str, parameters: dict[str, Any], *, network: str, u
|
||||
result = scenarios.pressure_regulation(**common,
|
||||
modify_fixed_pump_pattern={key: value for key, value in controls.items() if key in mappings.fixed_pumps},
|
||||
modify_variable_pump_pattern={key: value for key, value in controls.items() if key in mappings.variable_pumps},
|
||||
modify_tank_initial_level=parameters["tank_init_level"], scada_mappings=mappings)
|
||||
modify_tank_initial_level=parameters["tank_init_level"],
|
||||
scada_mappings=boundary.get("scada_mappings", mappings))
|
||||
else:
|
||||
raise ValueError("Scenario is not registered")
|
||||
if not isinstance(result, dict):
|
||||
|
||||
Reference in New Issue
Block a user