fix(simulation): use report step for schemes

This commit is contained in:
2026-07-16 15:47:26 +08:00
parent 6f9c94a4dd
commit b8f2f70013
4 changed files with 160 additions and 21 deletions
@@ -50,6 +50,7 @@ class InternalStorage:
link_result_list: List[dict], link_result_list: List[dict],
result_start_time: str, result_start_time: str,
num_periods: int = 1, num_periods: int = 1,
result_timestep_seconds: int | None = None,
db_name: str = None, db_name: str = None,
max_retries: int = 3, max_retries: int = 3,
): ):
@@ -70,6 +71,7 @@ class InternalStorage:
link_result_list, link_result_list,
result_start_time, result_start_time,
num_periods, num_periods,
result_timestep_seconds,
) )
break # 成功 break # 成功
except Exception as e: except Exception as e:
+31 -21
View File
@@ -3,10 +3,24 @@ from datetime import datetime, timedelta
from collections import defaultdict from collections import defaultdict
from psycopg import AsyncConnection, Connection, sql from psycopg import AsyncConnection, Connection, sql
import app.services.globals as globals import app.services.globals as globals
from app.services.time_api import parse_utc_time from app.services.time_api import parse_clock_duration_seconds, parse_utc_time
class SchemeRepository: class SchemeRepository:
@staticmethod
def _get_result_timestep(result_timestep_seconds: int | None) -> timedelta:
if result_timestep_seconds is not None:
if result_timestep_seconds <= 0:
raise ValueError("result_timestep_seconds must be greater than 0.")
return timedelta(seconds=result_timestep_seconds)
timestep_seconds = parse_clock_duration_seconds(
globals.hydraulic_timestep,
field_name="HYDRAULIC TIMESTEP",
)
if timestep_seconds <= 0:
raise ValueError("HYDRAULIC TIMESTEP must be greater than 0.")
return timedelta(seconds=timestep_seconds)
# --- Link Simulation --- # --- Link Simulation ---
@@ -452,6 +466,7 @@ class SchemeRepository:
link_result_list: List[Dict[str, any]], link_result_list: List[Dict[str, any]],
result_start_time: str, result_start_time: str,
num_periods: int = 1, num_periods: int = 1,
result_timestep_seconds: int | None = None,
): ):
""" """
Store scheme simulation results to TimescaleDB. Store scheme simulation results to TimescaleDB.
@@ -468,20 +483,16 @@ class SchemeRepository:
result_start_time, field_name="result_start_time" result_start_time, field_name="result_start_time"
) )
timestep_parts = globals.hydraulic_timestep.split(":") timestep = SchemeRepository._get_result_timestep(result_timestep_seconds)
timestep = timedelta(
hours=int(timestep_parts[0]),
minutes=int(timestep_parts[1]),
seconds=int(timestep_parts[2]),
)
# Prepare node data for batch insert # Prepare node data for batch insert
node_data = [] node_data = []
for node_result in node_result_list: for node_result in node_result_list:
node_id = node_result.get("node") node_id = node_result.get("node")
for period_index in range(num_periods): result_rows = node_result.get("result", [])
for period_index in range(min(num_periods, len(result_rows))):
current_time = simulation_time + (timestep * period_index) current_time = simulation_time + (timestep * period_index)
data = node_result.get("result", [])[period_index] data = result_rows[period_index]
node_data.append( node_data.append(
{ {
"time": current_time, "time": current_time,
@@ -499,9 +510,10 @@ class SchemeRepository:
link_data = [] link_data = []
for link_result in link_result_list: for link_result in link_result_list:
link_id = link_result.get("link") link_id = link_result.get("link")
for period_index in range(num_periods): result_rows = link_result.get("result", [])
for period_index in range(min(num_periods, len(result_rows))):
current_time = simulation_time + (timestep * period_index) current_time = simulation_time + (timestep * period_index)
data = link_result.get("result", [])[period_index] data = result_rows[period_index]
link_data.append( link_data.append(
{ {
"time": current_time, "time": current_time,
@@ -535,6 +547,7 @@ class SchemeRepository:
link_result_list: List[Dict[str, any]], link_result_list: List[Dict[str, any]],
result_start_time: str, result_start_time: str,
num_periods: int = 1, num_periods: int = 1,
result_timestep_seconds: int | None = None,
): ):
""" """
Store scheme simulation results to TimescaleDB (sync version). Store scheme simulation results to TimescaleDB (sync version).
@@ -551,20 +564,16 @@ class SchemeRepository:
result_start_time, field_name="result_start_time" result_start_time, field_name="result_start_time"
) )
timestep_parts = globals.hydraulic_timestep.split(":") timestep = SchemeRepository._get_result_timestep(result_timestep_seconds)
timestep = timedelta(
hours=int(timestep_parts[0]),
minutes=int(timestep_parts[1]),
seconds=int(timestep_parts[2]),
)
# Prepare node data for batch insert # Prepare node data for batch insert
node_data = [] node_data = []
for node_result in node_result_list: for node_result in node_result_list:
node_id = node_result.get("node") node_id = node_result.get("node")
for period_index in range(num_periods): result_rows = node_result.get("result", [])
for period_index in range(min(num_periods, len(result_rows))):
current_time = simulation_time + (timestep * period_index) current_time = simulation_time + (timestep * period_index)
data = node_result.get("result", [])[period_index] data = result_rows[period_index]
node_data.append( node_data.append(
{ {
"time": current_time, "time": current_time,
@@ -582,9 +591,10 @@ class SchemeRepository:
link_data = [] link_data = []
for link_result in link_result_list: for link_result in link_result_list:
link_id = link_result.get("link") link_id = link_result.get("link")
for period_index in range(num_periods): result_rows = link_result.get("result", [])
for period_index in range(min(num_periods, len(result_rows))):
current_time = simulation_time + (timestep * period_index) current_time = simulation_time + (timestep * period_index)
data = link_result.get("result", [])[period_index] data = result_rows[period_index]
link_data.append( link_data.append(
{ {
"time": current_time, "time": current_time,
+4
View File
@@ -1260,6 +1260,9 @@ def run_simulation(
node_result, link_result, modify_pattern_start_time, db_name=db_name node_result, link_result, modify_pattern_start_time, db_name=db_name
) )
elif simulation_type.upper() == "EXTENDED": 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")
TimescaleInternalStorage.store_scheme_simulation( TimescaleInternalStorage.store_scheme_simulation(
scheme_type, scheme_type,
scheme_name, scheme_name,
@@ -1267,6 +1270,7 @@ def run_simulation(
link_result, link_result,
modify_pattern_start_time, modify_pattern_start_time,
num_periods_result, num_periods_result,
result_timestep_seconds,
db_name=db_name, db_name=db_name,
) )
endtime = time.time() endtime = time.time()
@@ -0,0 +1,123 @@
import importlib.util
from datetime import timedelta
from pathlib import Path
from app.services.time_api import parse_utc_time
def _load_scheme_repository():
module_path = (
Path(__file__).resolve().parents[2]
/ "app"
/ "infra"
/ "db"
/ "timescaledb"
/ "repositories"
/ "scheme.py"
)
spec = importlib.util.spec_from_file_location(
"customer_scheme_repository_for_timestep_test", module_path
)
module = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(module)
return module.SchemeRepository
SchemeRepository = _load_scheme_repository()
def _node_result(periods: int) -> list[dict]:
return [
{
"node": "J1",
"result": [
{"demand": index, "head": index, "pressure": index, "quality": index}
for index in range(periods)
],
}
]
def _link_result(periods: int) -> list[dict]:
return [
{
"link": "P1",
"result": [
{
"flow": index,
"friction": index,
"headloss": index,
"quality": index,
"reaction": index,
"setting": index,
"status": index,
"velocity": index,
}
for index in range(periods)
],
}
]
def test_store_scheme_simulation_uses_15_minute_report_step(monkeypatch):
inserted: dict[str, list[dict]] = {}
monkeypatch.setattr(
SchemeRepository,
"insert_nodes_batch_sync",
staticmethod(lambda conn, data: inserted.setdefault("nodes", data)),
)
monkeypatch.setattr(
SchemeRepository,
"insert_links_batch_sync",
staticmethod(lambda conn, data: inserted.setdefault("links", data)),
)
SchemeRepository.store_scheme_simulation_result_sync(
conn=object(),
scheme_type="burst_analysis",
scheme_name="five_hour_case",
node_result_list=_node_result(21),
link_result_list=_link_result(21),
result_start_time="2026-07-16T00:00:00Z",
num_periods=21,
result_timestep_seconds=900,
)
start_time = parse_utc_time("2026-07-16T00:00:00Z")
assert len(inserted["nodes"]) == 21
assert inserted["nodes"][0]["time"] == start_time
assert inserted["nodes"][-1]["time"] == start_time + timedelta(hours=5)
assert inserted["links"][-1]["time"] == start_time + timedelta(hours=5)
def test_store_scheme_simulation_uses_hourly_report_step(monkeypatch):
inserted: dict[str, list[dict]] = {}
monkeypatch.setattr(
SchemeRepository,
"insert_nodes_batch_sync",
staticmethod(lambda conn, data: inserted.setdefault("nodes", data)),
)
monkeypatch.setattr(
SchemeRepository,
"insert_links_batch_sync",
staticmethod(lambda conn, data: inserted.setdefault("links", data)),
)
SchemeRepository.store_scheme_simulation_result_sync(
conn=object(),
scheme_type="burst_analysis",
scheme_name="hourly_case",
node_result_list=_node_result(6),
link_result_list=_link_result(6),
result_start_time="2026-07-16T00:00:00Z",
num_periods=6,
result_timestep_seconds=3600,
)
start_time = parse_utc_time("2026-07-16T00:00:00Z")
assert [item["time"] for item in inserted["nodes"]] == [
start_time + timedelta(hours=index) for index in range(6)
]