fix(simulation): export ACTIVE valves correctly for EPANET 2
Generic Container CI/CD / test-build-publish (push) Successful in 2m12s
Server CI/CD v2 / build-test-publish-and-deploy (push) Successful in 2m12s

The legacy online simulation path still exported internal ACTIVE states as STATUS keywords, which EPANET 2 rejects. Preserve numeric settings and EPANET 3 output while passing the format through both exporters. Surface the original engine error inside the model transaction so failed runs roll back before result storage.

Regression guard: six failures reproduced on the deployed baseline, including native input validation. All 34 focused export, service and endpoint checks pass after the fix; the real project also returns native node and link results.
This commit is contained in:
2026-11-03 13:48:50 +08:00
parent 38a51ed65c
commit 9ab7b10e3c
5 changed files with 141 additions and 7 deletions
+2 -2
View File
@@ -120,7 +120,7 @@ def dump_inp(project: str, inp: str, version: str = '3'):
file.write('\n'.join(inp_out_demand(project)))
elif name == STATUS:
file.write('\n'.join(inp_out_status(project)))
file.write('\n'.join(inp_out_status(project, version=version)))
elif name == PATTERNS:
if version == '3':
@@ -232,7 +232,7 @@ def export_inp(project: str, version: str = '3') -> ChangeSet:
inp += '\n'.join(inp_out_demand(project))
elif name == STATUS:
inp += '\n'.join(inp_out_status(project))
inp += '\n'.join(inp_out_status(project, version=version))
elif name == PATTERNS:
if version == '3':
+4 -2
View File
@@ -93,14 +93,16 @@ def inp_in_status(line: str) -> str:
return str(f"insert into network.link_initial_settings (link_id, status, setting) values ({sql_literal(link)}, null, {sql_literal(float(value))});")
def inp_out_status(name: str) -> list[str]:
def inp_out_status(name: str, version: str = '2') -> list[str]:
lines = []
objs = read_all(name, 'select link_id as link, status, setting from network.link_initial_settings order by link_id')
for obj in objs:
link = obj['link']
status = obj['status'] if obj['status'] != None else ''
setting = obj['setting'] if obj['setting'] != None else ''
if status != '':
# EPANET 2 valves use their configured setting by default; ACTIVE is
# an internal state, not a valid keyword in its [STATUS] section.
if status != '' and not (version == '2' and status.upper() == LINK_STATUS_ACTIVE):
lines.append(f'{link} {status}')
if setting != '':
lines.append(f'{link} {setting}')
+17 -3
View File
@@ -490,6 +490,23 @@ def run_simulation(
set_status(name_c, cs)
# 运行并返回结果
result_data = json.loads(run_project(name_c))
if result_data.get("simulation_result") == "failed":
report = result_data.get("report") or ""
logging.error("EPANET failed for project %s:\n%s", name_c, report)
lines = report.splitlines()
error_index = next(
(i for i, line in enumerate(lines) if line.strip().startswith("Error ")),
None,
)
detail = (
" ".join(line.strip() for line in lines[error_index:error_index + 2])[:600]
if error_index is not None
else "引擎未返回错误详情,请查看服务日志"
)
raise RuntimeError(f"EPANET 计算失败:{detail}")
output_data = result_data.get("output")
if not isinstance(output_data, dict):
raise RuntimeError("run_project did not return JSON output content")
refresh_materialized_views_after_commit(name_c)
time_cost_end = time.perf_counter()
print(
@@ -498,9 +515,6 @@ def run_simulation(
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: