import numpy as np from app.services.tjnetwork import ( ChangeSet, get_demand, get_option, get_pattern, get_pump, get_reservoir, get_status, get_tank, get_time, read_all, run_project, set_demand, set_pattern, set_status, set_tank, set_time, ) # from get_real_status import * from datetime import datetime, timedelta from math import modf import os import json import pytz import requests import time from typing import Optional, Tuple from uuid import UUID import typing import logging import app.services.globals as globals import app.services.project_info as project_info from app.services.time_api import parse_beijing_time, parse_clock_duration_seconds from app.native.wndb.core.connection import project_connection, project_transaction from app.native.wndb.core.database import refresh_materialized_views_after_commit from app.infra.db.timescaledb.internal_queries import ( InternalQueries as TimescaleInternalQueries, ) from app.services.scheme_management import create_analysis_run, update_analysis_run from app.infra.db.timescaledb.internal_queries import ( InternalStorage as TimescaleInternalStorage, ) logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) def _primary_demand(demand_set: dict) -> dict: """Return sequence-zero demand, creating it when a junction has none.""" demands = demand_set.setdefault("demands", []) if not demands: demands.append({"demand": 0.0, "pattern": None, "category": None}) return demands[0] def _primary_demand_pattern(demand_set: dict) -> str: """Return the first configured pattern in deterministic sequence order.""" pattern = next( ( demand.get("pattern") for demand in demand_set.get("demands", []) if demand.get("pattern") ), None, ) if pattern is None: raise ValueError( f"Junction {demand_set.get('junction')!r} has no demand pattern" ) return str(pattern) def query_corresponding_element_id_and_query_id(name: str) -> None: """Load realtime device-to-element mappings from the new asset schema.""" target_maps = { "reservoir_liquid_level": globals.reservoirs_id, "tank_liquid_level": globals.tanks_id, "fixed_pump": globals.fixed_pumps_id, "variable_pump": globals.variable_pumps_id, "pressure": globals.pressure_id, "demand": globals.demand_id, "quality": globals.quality_id, } for mapping in target_maps.values(): mapping.clear() with project_connection(name) as conn, conn.cursor() as cur: cur.execute( """ SELECT device_type, COALESCE(node_id, link_id) AS element_id, api_query_id FROM asset.scada_devices WHERE transmission_mode = 'realtime' AND api_query_id IS NOT NULL """ ) for record in cur.fetchall(): device_type = record["device_type"] element_id = record["element_id"] api_query_id = record["api_query_id"] mapping = target_maps.get(str(device_type).lower()) if mapping is not None: mapping[str(element_id)] = str(api_query_id) def get_pattern_index(cur_datetime: str) -> int: """ 根据给定的日期时间字符串,计算并返回对应的模式索引。 :param cur_datetime: str, 当前的日期时间字符串,格式为“YYYY-MM-DD HH:MM:SS”。 :return: int, 基于预定义的时间步长 PATTERN_TIME_STEP。 """ str_format = "%Y-%m-%d %H:%M:%S" dt = datetime.strptime(cur_datetime, str_format) hr = dt.hour mnt = dt.minute i = int((hr * 60 + mnt) / globals.PATTERN_TIME_STEP) return i def get_pattern_index_str(current_time: str) -> str: """ 根据当前时间获取时间步长的模式索引,并将其格式化为“HH:MM:00”字符串。 :param current_time: str, 当前时间,格式为"YYYY-MM-DD HH:MM:SS" :return: str, 以“HH:MM:00”格式返回 """ i = get_pattern_index(current_time) [minN, hrN] = modf(i * globals.PATTERN_TIME_STEP / 60) minN_str = str(int(minN * 60)) minN_str = minN_str.zfill(2) hrN_str = str(int(hrN)) hrN_str = hrN_str.zfill(2) str_i = "{}:{}:00".format(hrN_str, minN_str) return str_i def from_seconds_to_clock(secs: int) -> str: """ 从秒格式化为“HH:MM:00”字符串 :param secs: int,秒 :return: str, 以“HH:MM:00”格式返回 """ hrs = int(secs / 3600) minutes = int((secs - hrs * 3600) / 60) seconds = secs - hrs * 3600 - minutes * 60 hrs_str = str(hrs).zfill(2) minutes_str = str(minutes).zfill(2) seconds_str = str(seconds).zfill(2) str_clock = "{}:{}:{}".format(hrs_str, minutes_str, seconds_str) return str_clock def convert_time_format(original_time: str) -> str: """ 格式转换,将带时区的 ISO 8601 / RFC3339 时间转为北京时间的“YYYY-MM-DD HH:MM:SS” :param original_time: str,带显式时区的时间 :return: str,“2024-04-13 08:00:00”格式的时间 """ normalized_time = parse_beijing_time( original_time, field_name="modify_pattern_start_time" ) return normalized_time.replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S") def _apply_valve_control( project_name: str, valve_control: dict[str, dict] ) -> None: """Apply explicit valve status, setting, and opening controls.""" for valve_name, control in valve_control.items(): valve_status = get_status(project_name, valve_name) if "status" in control: valve_status["status"] = control["status"] if "setting" in control: valve_status["setting"] = control["setting"] if "k" in control: valve_k = control["k"] if valve_k == 0: valve_status["status"] = "CLOSED" else: valve_status["setting"] = 0.1036 * pow(valve_k, -3.105) cs = ChangeSet() cs.append(valve_status) set_status(project_name, cs) # 2025/01/11 def run_simulation( name: str, simulation_type: str, modify_pattern_start_time: str, modify_total_duration: int = 0, modify_reservoir_head_pattern: dict[str, list] = None, modify_tank_initial_level: dict[str, float] = None, modify_junction_base_demand: dict[str, float] = None, modify_junction_damand_pattern: dict[str, list] = None, modify_fixed_pump_pattern: dict[str, list] = None, modify_variable_pump_pattern: dict[str, list] = None, modify_valve_opening: dict[str, float] = None, scheme_type: str = None, scheme_name: str = None, result_db_name: str = None, valve_control: dict[str, dict] = None, scheme_username: str = "system", scheme_detail: dict | None = None, ) -> UUID | None: """ 传入需要修改的参数,改变数据库中对应位置的值,然后计算,返回结果 :param name: 模型名称,数据库中对应的名字 :param simulation_type: 模拟的类型,realtime为实时模拟,修改原数据库;extended为多步长模拟,需要复制数据库 :param modify_pattern_start_time: 模拟开始时间,格式为'2024-11-25T09:00:00+08:00' :param modify_total_duration: 模拟总历时,秒 :param modify_reservoir_head_pattern: dict中包含多个水库模式,str为水库head_pattern的id,list为修改后的head_pattern :param modify_tank_initial_level: dict中包含多个水塔,str为水塔的id,float为修改后的initial_level :param modify_junction_base_demand: dict中包含多个节点,str为节点的id,float为修改后的base_demand :param modify_junction_damand_pattern: dict中包含多个节点模式,str为节点demand_pattern的id,list为修改后的demand_pattern :param modify_fixed_pump_pattern: dict中包含多个水泵模式,str为工频水泵的id,list为修改后的pattern :param modify_variable_pump_pattern: dict中包含多个水泵模式,str为变频水泵的id,list为修改后的pattern :param modify_valve_opening: dict中包含多个阀门开启度,str为阀门的id,float为修改后的阀门开启度 :param valve_control: dict中可分别指定阀门的status、setting和k;存在时优先于modify_valve_opening :param scheme_type: 模拟方案类型 :param scheme_name:模拟方案名称 :return: """ # 记录开始时间 time_cost_start = time.perf_counter() print("name", name) print("simulation_type", simulation_type) print("modify_pattern_start_time", modify_pattern_start_time) print("modify_total_duration", modify_total_duration) print("modify_reservoir_head_pattern", modify_reservoir_head_pattern) print("modify_tank_initial_level", modify_tank_initial_level) print("modify_junction_base_demand", modify_junction_base_demand) print( "{} -- Hydraulic simulation started.".format( datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S") ) ) # 判断是实时模拟还是多步长模拟 # if simulation_type.upper() == 'REALTIME': # 实时模拟(修改原数据库) # name_c = name # elif simulation_type.upper() == 'EXTENDED': # 扩展模拟(复制数据库) # name_c = '_'.join([name, 'c']) # if have_project(name_c): # delete_project(name_c) # copy_project(name, name_c) # 备份项目 # else: # raise Exception('Incorrect simulation type, choose in (realtime, extended)') name_c = name with project_transaction(name_c): dic_time = get_time(name_c) print(dic_time) # 获取水力模拟步长,如’0:15:00‘ globals.hydraulic_timestep = dic_time["HYDRAULIC TIMESTEP"] # 转换为分钟浮点数,兼容 EPANET 的 H:MM 和 H:MM:SS 写法 globals.PATTERN_TIME_STEP = ( parse_clock_duration_seconds( globals.hydraulic_timestep, field_name="HYDRAULIC TIMESTEP", ) / 60 ) # 对输入的时间参数进行处理 pattern_start_time = convert_time_format(modify_pattern_start_time) # 获取模拟开始时间是对应pattern的第几个数 modify_index = get_pattern_index(pattern_start_time) # 遍历水泵的pattern_id,并根据输入的pump_pattern修改pattern的值 # for pump_pattern_id in pump_pattern_ids: # # 检查pump_pattern中pump_pattern_id对应的第一个频率值是否为有效数字(非空、非NaN)。如果该值有效,则继续执行代码块。 # if not np.isnan(modify_pump_pattern[pump_pattern_id][0]): # # 取出数据库中的pattern # pump_pattern = get_pattern(name_c, get_pump(name_c, pump_pattern_id)['pattern']) # # 替换数据库中的pattern为modify_pump_pattern # pump_pattern['factors'][modify_index: modify_index + len(modify_pump_pattern[pump_pattern_id])] \ # = modify_pump_pattern[pump_pattern_id] # cs = ChangeSet() # cs.append(pump_pattern) # set_pattern(name_c, cs) # 修改模拟开始的时间 str_pattern_start = get_pattern_index_str( convert_time_format(modify_pattern_start_time) ) dic_time = get_time(name_c) dic_time["PATTERN START"] = str_pattern_start dic_time["DURATION"] = from_seconds_to_clock(modify_total_duration) if simulation_type.upper() == "REALTIME": dic_time["DURATION"] = 0 cs = ChangeSet() cs.operations.append(dic_time) set_time(name_c, cs) # 根据SCADA实时数据进行修改,如果没有对应的SCADA数据,如未来的时间点,则不改变pg数据库的数据 if globals.reservoirs_id: # reservoirs_id = {'ZBBDJSCP000002': '2497', 'R00003': '2571'} # 1.获取reservoir的SCADA数据,形式如{'2497': '3.1231', '2571': '2.7387'} reservoir_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time( device_ids=list(globals.reservoirs_id.values()), query_time=modify_pattern_start_time, db_name=name, ) # 2.构建出新字典,形式如{'ZBBDJSCP000002': '3.1231', 'R00003': '2.7387'} reservoir_dict = { key: reservoir_SCADA_data_dict[value] for key, value in globals.reservoirs_id.items() } # 3.修改reservoir液位模式 for reservoir_name, value in reservoir_dict.items(): if value and float(value) != 0: # 先根据reservoir获取对应的pattern,再对pattern进行修改 reservoir_pattern = get_pattern( name_c, get_reservoir(name_c, reservoir_name)["pattern"] ) reservoir_pattern["factors"][modify_index] = ( float(value) + globals.RESERVOIR_BASIC_HEIGHT ) cs = ChangeSet() cs.append(reservoir_pattern) set_pattern(name_c, cs) if globals.tanks_id: # 修改tank初始液位 tank_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time( device_ids=list(globals.tanks_id.values()), query_time=modify_pattern_start_time, db_name=name, ) tank_dict = { key: tank_SCADA_data_dict[value] for key, value in globals.tanks_id.items() } for tank_name, value in tank_dict.items(): if value and float(value) != 0: tank = get_tank(name_c, tank_name) tank["init_level"] = float(value) cs = ChangeSet() cs.append(tank) set_tank(name_c, cs) if globals.fixed_pumps_id: # 修改工频泵的pattern fixed_pump_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time( device_ids=list(globals.fixed_pumps_id.values()), query_time=modify_pattern_start_time, db_name=name, ) # print(fixed_pump_SCADA_data_dict) fixed_pump_dict = { key: fixed_pump_SCADA_data_dict[value] for key, value in globals.fixed_pumps_id.items() } # print(fixed_pump_dict) for fixed_pump_name, value in fixed_pump_dict.items(): if value: pump_pattern = get_pattern( name_c, get_pump(name_c, fixed_pump_name)["pattern"] ) # print(pump_pattern) pump_pattern["factors"][modify_index] = float(value) # print(pump_pattern['factors'][modify_index]) cs = ChangeSet() cs.append(pump_pattern) set_pattern(name_c, cs) if globals.variable_pumps_id: # 修改变频泵的pattern variable_pump_SCADA_data_dict = ( TimescaleInternalQueries.query_scada_by_ids_time( device_ids=list(globals.variable_pumps_id.values()), query_time=modify_pattern_start_time, db_name=name, ) ) variable_pump_dict = { key: variable_pump_SCADA_data_dict[value] for key, value in globals.variable_pumps_id.items() } for variable_pump_name, value in variable_pump_dict.items(): if value: pump_pattern = get_pattern( name_c, get_pump(name_c, variable_pump_name)["pattern"] ) pump_pattern["factors"][modify_index] = float(value) / 50 cs = ChangeSet() cs.append(pump_pattern) set_pattern(name_c, cs) if globals.demand_id: # 基于实时数据,修改大用户节点的pattern demand_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time( device_ids=list(globals.demand_id.values()), query_time=modify_pattern_start_time, db_name=name, ) demand_dict = { key: demand_SCADA_data_dict[value] for key, value in globals.demand_id.items() } for demand_name, value in demand_dict.items(): if value is not None and not np.isnan(float(value)): demand_set = get_demand(name_c, demand_name) demand_pattern = get_pattern( name_c, _primary_demand_pattern(demand_set) ) if get_option(name_c)["UNITS"] == "LPS": demand_pattern["factors"][modify_index] = ( float(value) / 3.6 ) # 默认SCADA数据获取的是流量单位是m3/h, 转换为 L/s elif get_option(name_c)["UNITS"] == "CMH": demand_pattern["factors"][modify_index] = float(value) cs = ChangeSet() cs.append(demand_pattern) set_pattern(name_c, cs) # 水质、压力实时数据使用方法待补充 ############################# # 显式请求参数覆盖实时设备数据,用于扩展模拟。 # 修改清水池(reservoir)液位的pattern if modify_reservoir_head_pattern: for reservoir_name in modify_reservoir_head_pattern.keys(): # 这句代码的作用是判断modify_reservoir_head_pattern[reservoir_name][0]是否不是NaN。 # 如果modify_reservoir_head_pattern[reservoir_name][0]不是NaN,则条件成立,代码块会执行 if not np.isnan(modify_reservoir_head_pattern[reservoir_name][0]): # 给 list 中的所有元素加上 RESERVOIR_BASIC_HEIGHT modified_values = [ value + globals.RESERVOIR_BASIC_HEIGHT for value in modify_reservoir_head_pattern[reservoir_name] ] reservoir_pattern = get_pattern( name_c, get_reservoir(name_c, reservoir_name)["pattern"] ) reservoir_pattern["factors"][ modify_index : modify_index + len(modified_values) ] = modified_values cs = ChangeSet() cs.append(reservoir_pattern) set_pattern(name_c, cs) # 修改调节池(tank)初始液位 if modify_tank_initial_level: for tank_name in modify_tank_initial_level.keys(): if (not np.isnan(modify_tank_initial_level[tank_name])) and ( modify_tank_initial_level[tank_name] != 0 ): tank = get_tank(name_c, tank_name) tank["init_level"] = modify_tank_initial_level[tank_name] cs = ChangeSet() cs.append(tank) set_tank(name_c, cs) # 修改节点(junction)基础水量(demand) if modify_junction_base_demand: for junction_name in modify_junction_base_demand.keys(): if not np.isnan(modify_junction_base_demand[junction_name]): junction = get_demand(name_c, junction_name) _primary_demand(junction)["demand"] = ( modify_junction_base_demand[junction_name] ) cs = ChangeSet() cs.append(junction) set_demand(name_c, cs) # 修改节点(junction)的水量模式(pattern) if modify_junction_damand_pattern: for pattern_name in modify_junction_damand_pattern.keys(): if not np.isnan(modify_junction_damand_pattern[pattern_name][0]): junction_pattern = get_pattern(name_c, pattern_name) junction_pattern["factors"][ modify_index : modify_index + len(modify_junction_damand_pattern[pattern_name]) ] = modify_junction_damand_pattern[pattern_name] cs = ChangeSet() cs.append(junction_pattern) set_pattern(name_c, cs) # 修改工频水泵(fixed_pump)的pattern if modify_fixed_pump_pattern: for pump_name in modify_fixed_pump_pattern.keys(): if not np.isnan(modify_fixed_pump_pattern[pump_name][0]): pump_pattern = get_pattern( name_c, get_pump(name_c, pump_name)["pattern"] ) pump_pattern["factors"][ modify_index : modify_index + len(modify_fixed_pump_pattern[pump_name]) ] = modify_fixed_pump_pattern[pump_name] cs = ChangeSet() cs.append(pump_pattern) set_pattern(name_c, cs) # 修改变频水泵(variable_pump)的pattern if modify_variable_pump_pattern: for pump_name in modify_variable_pump_pattern.keys(): if not np.isnan(modify_variable_pump_pattern[pump_name][0]): # 给 list 中的所有元素除以 50Hz modified_values = [ value / 50 for value in modify_variable_pump_pattern[pump_name] ] pump_pattern = get_pattern( name_c, get_pump(name_c, pump_name)["pattern"] ) pump_pattern["factors"][ modify_index : modify_index + len(modified_values) ] = modified_values cs = ChangeSet() cs.append(pump_pattern) set_pattern(name_c, cs) # 显式阀门控制沿用 run_simulation_ex 的处理顺序和覆盖规则。 if valve_control is not None: _apply_valve_control(name_c, valve_control) # 保留原开度参数逻辑,兼容现有方案调用。 elif modify_valve_opening: for valve_name in modify_valve_opening.keys(): if not np.isnan(modify_valve_opening[valve_name]): valve_status = get_status(name_c, valve_name) if modify_valve_opening[valve_name] == 0: valve_status["status"] = "CLOSED" valve_status["setting"] = 0 elif modify_valve_opening[valve_name] < 1: valve_status["status"] = "OPEN" valve_status["setting"] = 0.1036 * pow( modify_valve_opening[valve_name], -3.105 ) elif modify_valve_opening[valve_name] == 1: valve_status["status"] = "OPEN" valve_status["setting"] = 0 cs = ChangeSet() cs.append(valve_status) set_status(name_c, cs) # 运行并返回结果 result_data = json.loads(run_project(name_c)) refresh_materialized_views_after_commit(name_c) time_cost_end = time.perf_counter() print( "{} -- Hydraulic simulation finished, cost time: {:.2f} s.".format( datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S"), time_cost_end - time_cost_start, ) ) output_data = result_data.get("output") if not isinstance(output_data, dict): raise RuntimeError("run_project did not return JSON output content") node_result = output_data.get("node_results") link_result = output_data.get("link_results") if node_result is None or link_result is None: raise RuntimeError("run_project output missing node_results or link_results") # link_flow = [] # for link in link_result: # link_flow.append(link['result'][-1]['flow']) # print(link_flow) times_info = output_data.get("times") or {} num_periods_result = times_info.get("num_periods") if num_periods_result is None: raise RuntimeError("run_project output missing times.num_periods") print("simulation_type", simulation_type) print("before store result") # print(num_periods_result) # print(node_result) # 存储 starttime = time.time() # 临时处理输入的name问题,后续需要优化,需要传递/获取iot数据库的名字 db_name = result_db_name or name if simulation_type.upper() == "REALTIME": TimescaleInternalStorage.store_realtime_simulation( node_result, link_result, modify_pattern_start_time, db_name=db_name ) elif simulation_type.upper() == "EXTENDED": result_timestep_seconds = times_info.get("report_step") if result_timestep_seconds is None: raise RuntimeError("run_project output missing times.report_step") if not scheme_type or not scheme_name: raise ValueError("extended simulation requires analysis run type and name") detail = scheme_detail or {} run_id = create_analysis_run( name=db_name, scheme_name=scheme_name, scheme_type=scheme_type, username=scheme_username, scheme_start_time=modify_pattern_start_time, scheme_detail=detail, ) try: TimescaleInternalStorage.store_analysis_simulation( run_id, node_result, link_result, modify_pattern_start_time, num_periods_result, result_timestep_seconds, db_name=db_name, ) except Exception: try: update_analysis_run( db_name, run_id, status="failed", username=scheme_username, scheme_detail=detail, ) except Exception: logging.exception("failed to mark analysis run %s as failed", run_id) raise update_analysis_run( db_name, run_id, status="completed", username=scheme_username, scheme_detail=detail, ) endtime = time.time() logging.info("store time: %f", endtime - starttime) # 暂不需要再次存储 SCADA 模拟信息 # TimescaleInternalQueries.fill_scheme_simulation_result_to_SCADA(scheme_type=scheme_type, scheme_name=scheme_name) print("after store result") return run_id if simulation_type.upper() == "EXTENDED" else None