fix(simulation): use hydraulic timestep

This commit is contained in:
2026-07-16 14:16:06 +08:00
parent f72b56845f
commit 775ecb8a58
6 changed files with 121 additions and 20 deletions
+3 -12
View File
@@ -29,6 +29,7 @@ import pytz
import requests import requests
import time import time
import app.services.project_info as project_info import app.services.project_info as project_info
from app.services.time_api import parse_clock_duration_seconds
url_path = 'http://10.101.15.16:9000/loong' # 内网 url_path = 'http://10.101.15.16:9000/loong' # 内网
# url_path = 'http://183.64.62.100:9057/loong' # 外网 # url_path = 'http://183.64.62.100:9057/loong' # 外网
@@ -551,21 +552,11 @@ def from_clock_to_seconds (clock: str)->int:
return hr*3600+mnt*60+seconds return hr*3600+mnt*60+seconds
def from_clock_to_seconds_2 (clock: str)->int: def from_clock_to_seconds_2 (clock: str)->int:
str_format="%H:%M:%S" return parse_clock_duration_seconds(clock)
dt=datetime.strptime(clock,str_format)
hr=dt.hour
mnt=dt.minute
seconds=dt.second
return hr*3600+mnt*60+seconds
def from_clock_to_seconds_3 (clock: str)->int: def from_clock_to_seconds_3 (clock: str)->int:
str_format = "%H:%M" # 更新时间格式以适应 "小时:分钟" 格式 return parse_clock_duration_seconds(clock)
dt = datetime.strptime(clock,str_format)
hr = dt.hour
mnt = dt.minute
seconds = dt.second
return hr * 3600 + mnt * 60
###convert datetimestring ###convert datetimestring
+14 -2
View File
@@ -35,7 +35,11 @@ from app.services.simulation_ops import (
daily_scheduling_simulation, daily_scheduling_simulation,
) )
from app.services.valve_isolation import analyze_valve_isolation from app.services.valve_isolation import analyze_valve_isolation
from app.services.time_api import parse_aware_time, parse_utc_time from app.services.time_api import (
parse_aware_time,
parse_clock_duration_seconds,
parse_utc_time,
)
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
router = APIRouter() router = APIRouter()
@@ -118,6 +122,14 @@ def run_simulation_manually_by_date(
network_name: str, start_time: datetime, duration: int network_name: str, start_time: datetime, duration: int
) -> None: ) -> None:
end_datetime = start_time + timedelta(minutes=duration) end_datetime = start_time + timedelta(minutes=duration)
time_properties = simulation.get_time(network_name)
hydraulic_step_seconds = parse_clock_duration_seconds(
time_properties["HYDRAULIC TIMESTEP"],
field_name="HYDRAULIC TIMESTEP",
)
if hydraulic_step_seconds <= 0:
raise ValueError("HYDRAULIC TIMESTEP must be greater than 0.")
hydraulic_step = timedelta(seconds=hydraulic_step_seconds)
current_time = start_time current_time = start_time
while current_time < end_datetime: while current_time < end_datetime:
simulation.run_simulation( simulation.run_simulation(
@@ -125,7 +137,7 @@ def run_simulation_manually_by_date(
simulation_type="realtime", simulation_type="realtime",
modify_pattern_start_time=current_time.isoformat(timespec="seconds"), modify_pattern_start_time=current_time.isoformat(timespec="seconds"),
) )
current_time += timedelta(minutes=15) current_time += hydraulic_step
# 必须用这个PlainTextResponse,不然每个key都有引号 # 必须用这个PlainTextResponse,不然每个key都有引号
+8 -6
View File
@@ -34,7 +34,7 @@ import psycopg
import logging import logging
import app.services.globals as globals import app.services.globals as globals
import app.services.project_info as project_info import app.services.project_info as project_info
from app.services.time_api import parse_beijing_time from app.services.time_api import parse_beijing_time, parse_clock_duration_seconds
from app.core.config import get_pgconn_string from app.core.config import get_pgconn_string
from app.infra.db.timescaledb.internal_queries import ( from app.infra.db.timescaledb.internal_queries import (
InternalQueries as TimescaleInternalQueries, InternalQueries as TimescaleInternalQueries,
@@ -757,11 +757,13 @@ def run_simulation(
# 获取水力模拟步长,如’0:15:00‘ # 获取水力模拟步长,如’0:15:00‘
globals.hydraulic_timestep = dic_time["HYDRAULIC TIMESTEP"] globals.hydraulic_timestep = dic_time["HYDRAULIC TIMESTEP"]
# 将时间字符串转换为 timedelta 对象 # 转换为分钟浮点数,兼容 EPANET 的 H:MM 和 H:MM:SS 写法
time_obj = datetime.strptime(globals.hydraulic_timestep, "%H:%M:%S") globals.PATTERN_TIME_STEP = (
# 转换为分钟浮点数 parse_clock_duration_seconds(
globals.PATTERN_TIME_STEP = float( globals.hydraulic_timestep,
time_obj.hour * 60 + time_obj.minute + time_obj.second / 60 field_name="HYDRAULIC TIMESTEP",
)
/ 60
) )
# 对输入的时间参数进行处理 # 对输入的时间参数进行处理
pattern_start_time = convert_time_format(modify_pattern_start_time) pattern_start_time = convert_time_format(modify_pattern_start_time)
+35
View File
@@ -89,6 +89,41 @@ def to_time_range(dt: datetime, delta: float) -> tuple[datetime, datetime]:
return (start_time, end_time) return (start_time, end_time)
def parse_clock_duration_seconds(clock: str, field_name: str = "duration") -> int:
"""
Parse EPANET-style clock durations into seconds.
Accepted formats include H:MM, HH:MM, H:MM:SS, and HH:MM:SS.
"""
if not isinstance(clock, str):
raise ValueError(f"{field_name} must be a string clock duration.")
parts = clock.strip().split(":")
if len(parts) not in (2, 3):
raise ValueError(
f"{field_name} must use H:MM or H:MM:SS format, got {clock!r}."
)
try:
values = [int(part) for part in parts]
except ValueError as exc:
raise ValueError(
f"{field_name} must contain numeric clock parts, got {clock!r}."
) from exc
if any(value < 0 for value in values):
raise ValueError(f"{field_name} must not contain negative values.")
hours, minutes = values[0], values[1]
seconds = values[2] if len(values) == 3 else 0
if minutes >= 60 or seconds >= 60:
raise ValueError(
f"{field_name} minutes and seconds must be less than 60, got {clock!r}."
)
return hours * 3600 + minutes * 60 + seconds
def parse_beijing_date_range(query_date: str) -> tuple[datetime, datetime]: def parse_beijing_date_range(query_date: str) -> tuple[datetime, datetime]:
''' '''
将一个日期字符串,转换成 start/end 时间段,传进来的日期被认为是北京时间 将一个日期字符串,转换成 start/end 时间段,传进来的日期被认为是北京时间
+35
View File
@@ -19,11 +19,18 @@ def _load_simulation_module(monkeypatch):
timezone.utc timezone.utc
) )
def parse_clock_duration_seconds(value, field_name="duration"):
parts = [int(part) for part in value.split(":")]
hours, minutes = parts[0], parts[1]
seconds = parts[2] if len(parts) == 3 else 0
return hours * 3600 + minutes * 60 + seconds
install_stub( install_stub(
monkeypatch, monkeypatch,
"app.services.time_api", "app.services.time_api",
{ {
"parse_aware_time": parse_aware_time, "parse_aware_time": parse_aware_time,
"parse_clock_duration_seconds": parse_clock_duration_seconds,
"parse_utc_time": parse_utc_time, "parse_utc_time": parse_utc_time,
}, },
) )
@@ -31,6 +38,7 @@ def _load_simulation_module(monkeypatch):
monkeypatch, monkeypatch,
"app.services.simulation", "app.services.simulation",
{ {
"get_time": lambda name: {"HYDRAULIC TIMESTEP": "0:15:00"},
"run_simulation": lambda **kwargs: None, "run_simulation": lambda **kwargs: None,
"query_corresponding_element_id_and_query_id": lambda name: None, "query_corresponding_element_id_and_query_id": lambda name: None,
"query_corresponding_pattern_id_and_query_id": lambda name: None, "query_corresponding_pattern_id_and_query_id": lambda name: None,
@@ -227,6 +235,33 @@ def test_run_simulation_manually_by_date_uses_utc_aware_timestamps(monkeypatch):
] ]
def test_run_simulation_manually_by_date_uses_hydraulic_timestep(monkeypatch):
module = _load_simulation_module(monkeypatch)
captured_calls = []
monkeypatch.setattr(
module.simulation,
"get_time",
lambda name: {"HYDRAULIC TIMESTEP": "1:00"},
)
monkeypatch.setattr(
module.simulation,
"run_simulation",
lambda **kwargs: captured_calls.append(kwargs),
)
module.run_simulation_manually_by_date(
"demo",
datetime(2025, 1, 1, 16, 0, 0, tzinfo=timezone.utc),
120,
)
assert [call["modify_pattern_start_time"] for call in captured_calls] == [
"2025-01-01T16:00:00+00:00",
"2025-01-01T17:00:00+00:00",
]
def test_runsimulationmanuallybydate_endpoint_accepts_timezone_aware_start_time(monkeypatch): def test_runsimulationmanuallybydate_endpoint_accepts_timezone_aware_start_time(monkeypatch):
module = _load_simulation_module(monkeypatch) module = _load_simulation_module(monkeypatch)
captured = {} captured = {}
+26
View File
@@ -43,3 +43,29 @@ def test_utc_now_returns_timezone_aware_utc_datetime():
assert result.tzinfo == timezone.utc assert result.tzinfo == timezone.utc
assert result.utcoffset() == timedelta(0) assert result.utcoffset() == timedelta(0)
@pytest.mark.parametrize(
("clock", "expected_seconds"),
[
("1:00", 3600),
("01:00", 3600),
("0:05", 300),
("0:05:00", 300),
("24:00", 86400),
],
)
def test_parse_clock_duration_seconds_accepts_epanet_clock_formats(
clock, expected_seconds
):
module = _load_time_api_module()
assert module.parse_clock_duration_seconds(clock) == expected_seconds
@pytest.mark.parametrize("clock", ["bad", "1:60", "1:00:60", "-1:00"])
def test_parse_clock_duration_seconds_rejects_invalid_clock_formats(clock):
module = _load_time_api_module()
with pytest.raises(ValueError):
module.parse_clock_duration_seconds(clock)