"""MCP online recalculation and isolated scenario adapters.""" from typing import Any SCENARIO_TOOLS = frozenset({ "run_simulation", "run_contaminant_simulation", "analyze_valve_closure", "analyze_flushing", "analyze_pressure_regulation", }) def calculate_scenario(name: str, parameters: dict[str, Any], *, network: str, username: str) -> dict[str, Any]: from app.services import simulation_scenarios as scenarios parameters = dict(parameters) nodes = parameters.pop("output_node_ids") links = parameters.pop("output_link_ids") start = parameters.pop("start_time") duration = parameters.pop("duration") common = dict(name=network, modify_pattern_start_time=start.isoformat(), modify_total_duration=duration, persist_results=False, scheme_name=None) 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, ) elif name == "run_contaminant_simulation": result = scenarios.contaminant_simulation(**common, username=username, source=parameters["source"], concentration=parameters["concentration"], source_pattern=parameters.get("pattern")) elif name == "analyze_valve_closure": result = scenarios.valve_close_analysis(**common, modify_valve_opening={valve: 0.0 for valve in parameters["valves"]}) 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") 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"]}) 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) controls = parameters["pump_control"] if set(controls) - (set(mappings.fixed_pumps) | set(mappings.variable_pumps)): raise ValueError("Pump controls must reference registered model pumps") if any(value not in (0, 1) for key, values in controls.items() if key in mappings.fixed_pumps for value in values): raise ValueError("Fixed-speed pump controls must be 0 or 1") 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) else: raise ValueError("Scenario is not registered") if not isinstance(result, dict): raise ValueError("Scenario did not return calculation results") selected_nodes = [row for row in result.get("node_results", []) if str(row.get("node")) in nodes] selected_links = [row for row in result.get("link_results", []) if str(row.get("link")) in links] return { "input_source": "online_model_simulation" if name == "run_simulation" else "isolated_model_simulation", "persisted_baseline": name == "run_simulation", "persisted_scheme": False, "start_time": start.isoformat(), "duration_seconds": duration, "units": result.get("units", {}), "times": result.get("times", {}), "node_results": selected_nodes, "link_results": selected_links, "coverage": {"requested_nodes": nodes, "requested_links": links, "missing_nodes": sorted(set(nodes) - {str(row["node"]) for row in selected_nodes}), "missing_links": sorted(set(links) - {str(row["link"]) for row in selected_links})}, }