Files
TJWaterServerBinary/app/infra/epanet/epanet.py
T

500 lines
16 KiB
Python

import ctypes
import platform
import os
import sys
import json
import base64
from datetime import datetime
import subprocess
import logging
from typing import Any
import uuid
sys.path.append("..")
from app.native.wndb import project
from app.native.wndb import inp_out
def _verify_platform():
_platform = platform.system()
if _platform not in ["Windows", "Linux"]:
raise Exception(f"Platform {_platform} unsupported (not yet)")
if __name__ == "__main__":
_verify_platform()
class Output:
def __init__(self, path: str) -> None:
self._path = path
if platform.system() == "Windows":
self._lib = ctypes.CDLL(
os.path.join(os.path.dirname(__file__), "windows", "epanet-output.dll")
)
else:
self._lib = ctypes.CDLL(
os.path.join(os.path.dirname(__file__), "linux", "libepanet-output.so")
)
self._handle = ctypes.c_void_p()
self._check(self._lib.ENR_init(ctypes.byref(self._handle)))
self._check(
self._lib.ENR_open(self._handle, ctypes.c_char_p(self._path.encode()))
)
def __del__(self):
# throw exception in destructor ? :)
self._check(self._lib.ENR_close(ctypes.byref(self._handle)))
def _check(self, result):
if result != 0 and result != 10:
msg = ctypes.c_char_p()
code = self._lib.ENR_checkError(self._handle, ctypes.byref(msg))
assert code == result
error = f"Failed to read project [{self._path}] output, message [{msg.value.decode()}]"
self._lib.ENR_free(ctypes.byref(msg))
raise Exception(error)
def version(self) -> int:
v = ctypes.c_int()
self._check(self._lib.ENR_getVersion(self._handle, ctypes.byref(v)))
return v.value
def net_size(self) -> dict[str, int]:
element_count = ctypes.POINTER(ctypes.c_int)()
length = ctypes.c_int()
self._check(
self._lib.ENR_getNetSize(
self._handle, ctypes.byref(element_count), ctypes.byref(length)
)
)
assert length.value == 5
category = ["node", "tank", "link", "pump", "valve"]
sizes = {}
for i in range(length.value):
sizes[category[i]] = element_count[i]
self._lib.ENR_free(ctypes.byref(element_count))
return sizes
def units(self) -> dict[str, str]:
f_us = ["CFS", "GPM", "MGD", "IMGD", "AFD", "LPS", "LPM", "MLD", "CMH", "CMD"]
p_us = ["PSI", "MTR", "KPA"]
q_us = ["NONE", "MGL", "UGL", "HOURS", "PRCNT"]
f, p, q = ctypes.c_int(1), ctypes.c_int(2), ctypes.c_int(3)
f_u, p_u, q_u = ctypes.c_int(), ctypes.c_int(), ctypes.c_int()
self._check(self._lib.ENR_getUnits(self._handle, f, ctypes.byref(f_u)))
self._check(self._lib.ENR_getUnits(self._handle, p, ctypes.byref(p_u)))
self._check(self._lib.ENR_getUnits(self._handle, q, ctypes.byref(q_u)))
return {
"flow": f_us[f_u.value],
"pressure": p_us[p_u.value],
"quality": q_us[q_u.value],
}
def times(self) -> dict[str, int]:
ts = []
for i in range(1, 5):
t = ctypes.c_int(1)
self._check(
self._lib.ENR_getTimes(self._handle, ctypes.c_int(i), ctypes.byref(t))
)
ts.append(t.value)
d = {}
category = ["report_start", "report_step", "sim_duration", "num_periods"]
for i in range(4):
d[category[i]] = ts[i]
return d
def element_name(self) -> dict[str, list[str]]:
sizes = self.net_size()
node_type = ctypes.c_int(1)
nodes = []
for i in range(1, sizes["node"] + 1):
name = ctypes.c_char_p()
name_len = ctypes.c_int()
self._check(
self._lib.ENR_getElementName(
self._handle,
node_type,
ctypes.c_int(i),
ctypes.byref(name),
ctypes.byref(name_len),
)
)
nodes.append(name.value.decode())
self._lib.ENR_free(ctypes.byref(name))
link_type = ctypes.c_int(2)
links = []
for i in range(1, sizes["link"] + 1):
name = ctypes.c_char_p()
name_len = ctypes.c_int()
self._check(
self._lib.ENR_getElementName(
self._handle,
link_type,
ctypes.c_int(i),
ctypes.byref(name),
ctypes.byref(name_len),
)
)
links.append(name.value.decode())
self._lib.ENR_free(ctypes.byref(name))
return {"nodes": nodes, "links": links}
def energy_usage(self) -> list[dict[str, Any]]:
size = self.net_size()["pump"]
usages = []
category = [
"utilization",
"avg.efficiency",
"avg.kW/flow",
"avg.kwatts",
"max.kwatts",
"cost/day",
]
links = self.element_name()["links"]
for i in range(1, size + 1):
index = ctypes.c_int()
values = ctypes.POINTER(ctypes.c_float)()
length = ctypes.c_int()
self._check(
self._lib.ENR_getEnergyUsage(
self._handle,
ctypes.c_int(i),
ctypes.byref(index),
ctypes.byref(values),
ctypes.byref(length),
)
)
assert length.value == 6
d = {"pump": links[index.value - 1]}
for j in range(length.value):
d |= {category[j]: values[j]}
usages.append(d)
self._lib.ENR_free(ctypes.byref(values))
return usages
def reactions(self) -> dict[str, float]:
values = ctypes.POINTER(ctypes.c_float)()
length = ctypes.c_int()
self._check(
self._lib.ENR_getNetReacts(
self._handle, ctypes.byref(values), ctypes.byref(length)
)
)
assert length.value == 4
category = ["bulk", "wall", "tank", "source"]
d = {}
for i in range(4):
d[category[i]] = values[i]
self._lib.ENR_free(ctypes.byref(values))
return d
def node_results(self) -> list[dict[str, Any]]:
size = self.net_size()["node"]
num_periods = self.times()["num_periods"]
nodes = self.element_name()["nodes"]
category = ["demand", "head", "pressure", "quality"]
ds = []
for i in range(1, size + 1):
d = {"node": nodes[i - 1], "result": []}
for j in range(num_periods):
values = ctypes.POINTER(ctypes.c_float)()
length = ctypes.c_int()
self._check(
self._lib.ENR_getNodeResult(
self._handle, j, i, ctypes.byref(values), ctypes.byref(length)
)
)
assert length.value == len(category)
attributes = {}
for k in range(length.value):
attributes[category[k]] = values[k]
d["result"].append(attributes)
self._lib.ENR_free(ctypes.byref(values))
ds.append(d)
return ds
def link_results(self) -> list[dict[str, Any]]:
size = self.net_size()["link"]
num_periods = self.times()["num_periods"]
links = self.element_name()["links"]
category = [
"flow",
"velocity",
"headloss",
"quality",
"status",
"setting",
"reaction",
"friction",
]
ds = []
for i in range(1, size + 1):
d = {"link": links[i - 1], "result": []}
for j in range(num_periods):
values = ctypes.POINTER(ctypes.c_float)()
length = ctypes.c_int()
self._check(
self._lib.ENR_getLinkResult(
self._handle, j, i, ctypes.byref(values), ctypes.byref(length)
)
)
assert length.value == len(category)
attributes = {}
for k in range(length.value):
if category[k] == "status":
if values[k] == 2.0:
attributes[category[k]] = "CLOSED"
else:
attributes[category[k]] = "OPEN"
continue
attributes[category[k]] = values[k]
d["result"].append(attributes)
self._lib.ENR_free(ctypes.byref(values))
ds.append(d)
return ds
def dump(self) -> dict[str, Any]:
data = {}
data |= {"version": self.version()}
data |= {"net_size": self.net_size()}
data |= {"units": self.units()}
data |= {"times": self.times()}
data |= {"element_name": self.element_name()}
data |= {"energy_usage": self.energy_usage()}
data |= {"reactions": self.reactions()}
data |= {"node_results": self.node_results()}
data |= {"link_results": self.link_results()}
return data
def _dump_output(path: str) -> dict[str, Any]:
opt = Output(path)
data = opt.dump()
return data
def dump_output(path: str) -> str:
data = _dump_output(path)
return json.dumps(data)
def dump_report(path: str) -> str:
return open(path, "r").read()
def dump_output_binary(path: str) -> str:
with open(path, "rb") as f:
data = f.read()
bast64_data = base64.b64encode(data)
return str(bast64_data, "utf-8")
def _safe_remove(path: str) -> None:
try:
if os.path.exists(path):
os.remove(path)
except Exception:
logging.warning("failed to remove temp file: %s", path)
def _make_isolated_run_paths(base_name: str, cwd: str) -> tuple[str, str, str]:
# 确保保存临时文件的目录存在
db_inp_dir = os.path.join(cwd, "db_inp")
temp_dir = os.path.join(cwd, "temp")
os.makedirs(db_inp_dir, exist_ok=True)
os.makedirs(temp_dir, exist_ok=True)
# 进程号 + UUID 生成唯一后缀,避免并发进程互相覆盖临时文件。
token = f"{os.getpid()}_{uuid.uuid4().hex}"
inp = os.path.join(db_inp_dir, f"{base_name}.db.{token}.inp")
rpt = os.path.join(temp_dir, f"{base_name}.db.{token}.rpt")
opt = os.path.join(temp_dir, f"{base_name}.db.{token}.opt")
return inp, rpt, opt
# DingZQ, 2025-02-04, 返回dict[str, Any]
def run_project_return_dict(name: str, readable_output: bool = True) -> dict[str, Any]:
if not project.have_project(name):
raise Exception(f"Not found project [{name}]")
cwd = os.path.abspath(os.getcwd())
inp, rpt, opt = _make_isolated_run_paths(name, cwd)
inp_out.dump_inp(name, inp, "2")
if platform.system() == "Windows":
exe = os.path.join(os.path.dirname(__file__), "windows", "runepanet.exe")
else:
exe = os.path.join(os.path.dirname(__file__), "linux", "runepanet")
if platform.system() != "Windows":
if not os.access(exe, os.X_OK):
os.chmod(exe, 0o755)
data = {}
# 设置环境变量以包含库文件路径
env = os.environ.copy()
if platform.system() == "Linux":
lib_dir = os.path.dirname(exe)
env["LD_LIBRARY_PATH"] = f"{lib_dir}:{env.get('LD_LIBRARY_PATH', '')}"
process = subprocess.run([exe, inp, rpt, opt], env=env, capture_output=True, text=True)
result = process.returncode
if result != 0:
logging.error(f"EPANET failed with return code {result}")
logging.error(f"EPANET stdout: {process.stdout}")
logging.error(f"EPANET stderr: {process.stderr}")
data["simulation_result"] = "failed"
data["error_code"] = result
data["stdout"] = process.stdout
data["stderr"] = process.stderr
else:
data["simulation_result"] = "successful"
if readable_output:
data["output"] = _dump_output(opt)
else:
data["output"] = dump_output_binary(opt)
data["input_file"] = inp
data["report_file"] = rpt
data["output_file"] = opt
if os.path.exists(rpt):
data["report"] = dump_report(rpt)
else:
logging.error(f"EPANET report file not found: {rpt}")
data["report"] = f"Error: EPANET report file not found. Simulation return code: {result}. Check server logs for stdout/stderr."
# 返回内容后删除仿真临时文件,避免临时文件堆积。
_safe_remove(inp)
_safe_remove(rpt)
_safe_remove(opt)
return data
# original code
def run_project(name: str, readable_output: bool = True) -> str:
if not project.have_project(name):
raise Exception(f"Not found project [{name}]")
cwd = os.path.abspath(os.getcwd())
inp, rpt, opt = _make_isolated_run_paths(name, cwd)
inp_out.dump_inp(name, inp, "2")
if platform.system() == "Windows":
exe = os.path.join(os.path.dirname(__file__), "windows", "runepanet.exe")
else:
exe = os.path.join(os.path.dirname(__file__), "linux", "runepanet")
logging.info(f"Run simulation at {datetime.now()}")
logging.info("%s %s %s %s", exe, inp, rpt, opt)
if platform.system() != "Windows":
if not os.access(exe, os.X_OK):
os.chmod(exe, 0o755)
data = {}
# 设置环境变量以包含库文件路径
env = os.environ.copy()
if platform.system() == "Linux":
lib_dir = os.path.dirname(exe)
env["LD_LIBRARY_PATH"] = f"{lib_dir}:{env.get('LD_LIBRARY_PATH', '')}"
# DingZQ, 2025-06-02, 使用subprocess替代os.system
process = subprocess.run([exe, inp, rpt, opt], env=env)
result = process.returncode
# logging.info(f"Simulation result: {result}")
if result != 0:
data["simulation_result"] = "failed"
logging.error("simulation failed")
else:
data["simulation_result"] = "successful"
logging.info("simulation successful")
if readable_output:
data["output"] = _dump_output(opt)
else:
data["output"] = dump_output_binary(opt)
data["input_file"] = inp
data["report_file"] = rpt
data["output_file"] = opt
data["report"] = dump_report(rpt)
# 返回内容后删除仿真临时文件,避免临时文件堆积。
_safe_remove(inp)
_safe_remove(rpt)
_safe_remove(opt)
# logging.info(f"Report: {data['report']}")
return json.dumps(data)
def run_inp(name: str, readable_output: bool = True) -> str:
cwd = os.path.abspath(os.getcwd())
if platform.system() == "Windows":
exe = os.path.join(os.path.dirname(__file__), "windows", "runepanet.exe")
else:
exe = os.path.join(os.path.dirname(__file__), "linux", "runepanet")
source_inp = os.path.join(cwd, "inp", name + ".inp")
token = f"{os.getpid()}_{uuid.uuid4().hex}"
rpt = os.path.join(cwd, "temp", f"{name}.{token}.rpt")
opt = os.path.join(cwd, "temp", f"{name}.{token}.opt")
if platform.system() != "Windows":
if not os.access(exe, os.X_OK):
os.chmod(exe, 0o755)
data = {}
# 设置环境变量以包含库文件路径
env = os.environ.copy()
if platform.system() == "Linux":
lib_dir = os.path.dirname(exe)
env["LD_LIBRARY_PATH"] = f"{lib_dir}:{env.get('LD_LIBRARY_PATH', '')}"
process = subprocess.run([exe, source_inp, rpt, opt], env=env)
result = process.returncode
if result != 0:
data["simulation_result"] = "failed"
else:
data["simulation_result"] = "successful"
if readable_output:
data["output"] = _dump_output(opt)
else:
data["output"] = dump_output_binary(opt)
data["input_file"] = source_inp
data["report_file"] = rpt
data["output_file"] = opt
data["report"] = dump_report(rpt)
# 返回内容后删除仿真临时文件,避免临时文件堆积。
_safe_remove(source_inp)
_safe_remove(rpt)
_safe_remove(opt)
return json.dumps(data)