"""Temporary Case 3 location fixture, checked against the current isolation model.""" from collections import defaultdict from datetime import datetime, timedelta from heapq import heappop, heappush from math import inf from app.algorithms.valve_isolation.topology_search import valve_isolation_analysis from app.infra.db.postgresql.network_assets import NetworkAssetRepository DEMO_PROJECTS = frozenset({"tjwater2_v2", "tjw_bench_tjwater2_20261105"}) EVENT_START = datetime.fromisoformat("2026-07-16T08:00:00+08:00") EVENT_END = EVENT_START + timedelta(hours=5) # Host-owned fixture truth; it is not returned to the Agent. REFERENCE_PIPE = "541725" REFERENCE_NODE = "170438" CANDIDATE_PIPE = "515870" CANDIDATE_SIMILARITY = 0.9708 def is_location_demo(project_code: str | None, args) -> bool: return bool( project_code in DEMO_PROJECTS and args.data_source == "monitoring" and EVENT_START - timedelta(minutes=15) <= args.scada_burst_start < EVENT_END and EVENT_START <= args.scada_burst_end <= EVENT_END ) def location_demo_context(project_code: str | None, replay_context: dict) -> dict: """Advertise an eligible frozen demo window without disclosing fixture truth.""" cutoff = datetime.fromisoformat(replay_context["knowledge_cutoff"]) if project_code not in DEMO_PROJECTS or not EVENT_START <= cutoff <= EVENT_END: return {"available": False} return { "available": True, "tool": "run_burst_location", "mode": "candidate_location", "burst_leakage_required": False, "scada_burst_start": (cutoff - timedelta(minutes=15)).isoformat(), "scada_burst_end": cutoff.isoformat(), "notice": "已开放候选定位;核验高嫌疑后调用 run_burst_location,漏量可省略。" "正常对照窗须从已冻结且核验有效的历史中明确选择,异常窗止于任务知识截止。" "取得候选管段与相似度后,继续管段展示、隔离边界与影响分析。", } def _checked_candidate(rows: list[dict]) -> dict | None: sources = [str(row["id"]) for row in rows if row["kind"] in {"reservoir", "tank"}] links = {str(row["id"]): row for row in rows if row["start_node_id"] is not None} if REFERENCE_PIPE not in links or CANDIDATE_PIPE not in links: return None entries = [f"{key}:{row['kind']}:{row['start_node_id']}:{row['end_node_id']}" for key, row in links.items()] closed = {key for key, row in links.items() if row["status"].upper() == "CLOSED"} if {REFERENCE_PIPE, CANDIDATE_PIPE} & closed: return None isolation = valve_isolation_analysis( entries, REFERENCE_PIPE, accident_element_type="link", water_source_nodes=sources, closed_links=closed, ) if not isolation["isolatable"]: return None zone = set(isolation["affected_nodes"]) candidate = links[CANDIDATE_PIPE] if not {candidate["start_node_id"], candidate["end_node_id"]} <= zone: return None candidate_isolation = valve_isolation_analysis( entries, CANDIDATE_PIPE, accident_element_type="link", water_source_nodes=sources, closed_links=closed, ) if (not candidate_isolation["isolatable"] or candidate_isolation["must_close_valves"] != isolation["must_close_valves"] or set(candidate_isolation["affected_nodes"]) != zone): return None # Follow the open DN600 line inside this valve zone, never cross a valve. diameter = links[REFERENCE_PIPE]["diameter"] if candidate["diameter"] != diameter: return None adjacency = defaultdict(list) for key, row in links.items(): a, b = row["start_node_id"], row["end_node_id"] if (row["kind"] == "pipe" and key not in closed and row["diameter"] == diameter and a in zone and b in zone and row["length"] is not None and row["length"] >= 0): adjacency[a].append((b, float(row["length"]))) adjacency[b].append((a, float(row["length"]))) distances = {REFERENCE_NODE: 0.0} queue = [(0.0, REFERENCE_NODE)] while queue: distance, node = heappop(queue) if distance != distances[node]: continue for neighbor, length in adjacency[node]: total = distance + length if total < distances.get(neighbor, inf): distances[neighbor] = total heappush(queue, (total, neighbor)) distance = min(distances.get(candidate["start_node_id"], inf), distances.get(candidate["end_node_id"], inf)) if not 200 <= distance <= 500: return None return { "pipe_id": CANDIDATE_PIPE, "distance_m": round(distance, 3), "distance_basis": "along_same_diameter_line_to_nearest_point_of_candidate_pipe", "boundary_valve_ids": isolation["must_close_valves"], "within_same_isolation_zone": True, } async def location_demo_result(connection, project_code: str, args) -> dict | None: if not is_location_demo(project_code, args): return None candidate = _checked_candidate(await NetworkAssetRepository.get_isolation_model(connection)) result = { "located_pipe": candidate["pipe_id"] if candidate else None, "top_candidates": [{"pipe_id": candidate["pipe_id"], "similarity": CANDIDATE_SIMILARITY, "similarity_percent": 97.08}] if candidate else [], "network": project_code, "data_source": "monitoring", "burst_leakage": args.burst_leakage, "exit_condition": "candidate_selected" if candidate else "no_candidate", "constraints": candidate, } if not candidate: result["notice"] = "当前范围内未找到同时满足同线路、200–500米及关阀范围约束的候选管段。" return result