fix(simulation): use report step for schemes
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,154 @@
|
|||||||
|
import json
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.infra.db.timescaledb.repositories.scheme import SchemeRepository
|
||||||
|
from app.services.time_api import parse_utc_time
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_simulation_passes_report_step_for_extended_scheme(monkeypatch):
|
||||||
|
import app.services.simulation as simulation
|
||||||
|
|
||||||
|
time_updates: list[dict] = []
|
||||||
|
storage_calls: list[tuple] = []
|
||||||
|
|
||||||
|
monkeypatch.setattr(simulation, "open_project", lambda name: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
simulation,
|
||||||
|
"get_time",
|
||||||
|
lambda name: {
|
||||||
|
"HYDRAULIC TIMESTEP": "00:15:00",
|
||||||
|
"REPORT TIMESTEP": "1:00",
|
||||||
|
"DURATION": "0:00",
|
||||||
|
"PATTERN START": "0:00",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
simulation,
|
||||||
|
"set_time",
|
||||||
|
lambda name, changeset: time_updates.append(changeset.operations[0]),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(simulation, "run_project", lambda name: json.dumps({
|
||||||
|
"simulation_result": "successful",
|
||||||
|
"output": {
|
||||||
|
"times": {"num_periods": 21, "report_step": 900},
|
||||||
|
"node_results": _node_result(21),
|
||||||
|
"link_results": _link_result(21),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
simulation.TimescaleInternalStorage,
|
||||||
|
"store_scheme_simulation",
|
||||||
|
staticmethod(lambda *args, **kwargs: storage_calls.append((args, kwargs))),
|
||||||
|
)
|
||||||
|
|
||||||
|
simulation.run_simulation(
|
||||||
|
name="fengyang",
|
||||||
|
simulation_type="extended",
|
||||||
|
modify_pattern_start_time="2026-07-16T00:00:00+08:00",
|
||||||
|
modify_total_duration=18000,
|
||||||
|
scheme_type="burst_analysis",
|
||||||
|
scheme_name="five_hour_case",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert time_updates[0]["DURATION"] == "05:00:00"
|
||||||
|
assert time_updates[0]["REPORT TIMESTEP"] == "1:00"
|
||||||
|
assert storage_calls[0][0][5] == 21
|
||||||
|
assert storage_calls[0][0][6] == 900
|
||||||
Reference in New Issue
Block a user