Files
TJWaterServerBinary/tests/unit/test_burst_detection_service.py
T
jiang db6032bd84
Server CI/CD / docker-image (push) Successful in 24s
Server CI/CD / deploy-fallback-log (push) Has been cancelled
feat(burst-detection): update scada analysis flow
2026-07-17 16:31:46 +08:00

161 lines
4.6 KiB
Python

from __future__ import annotations
from datetime import datetime, timedelta, timezone
import numpy as np
import pandas as pd
from app.services import burst_detection
TARGET = datetime(2026, 6, 20, 5, 30, tzinfo=timezone.utc)
def _complete_records(*, target: datetime, offset: float = 0.0) -> list[dict]:
start = target - timedelta(days=15) + timedelta(minutes=15)
return [
{
"time": (start + timedelta(minutes=15 * index)).isoformat(),
"value": float(index % 96) + offset,
}
for index in range(15 * 96)
]
def test_build_target_pressure_aligns_timestamps_and_excludes_incomplete_sensor(
monkeypatch,
):
nodes = [f"J{index}" for index in range(6)]
mapping = {node: f"D{index}" for index, node in enumerate(nodes)}
scada_data = {
query_id: _complete_records(target=TARGET, offset=float(index))
for index, query_id in enumerate(mapping.values())
}
scada_data["D5"] = scada_data["D5"][:-1]
monkeypatch.setattr(
burst_detection,
"_get_pressure_sensor_mapping",
lambda _network: mapping,
)
monkeypatch.setattr(
burst_detection.InternalQueries,
"query_latest_scada_time",
lambda **_kwargs: TARGET,
)
monkeypatch.setattr(
burst_detection.InternalQueries,
"query_scada_by_ids_timerange",
lambda **_kwargs: scada_data,
)
frame, resolved_target, excluded = (
burst_detection._build_target_pressure_from_scada(
network="test",
sensor_nodes=nodes,
requested_target_time=TARGET,
sampling_interval_minutes=15,
points_per_day=96,
)
)
assert resolved_target == TARGET
assert frame.shape == (1440, 5)
assert frame.index[-1].to_pydatetime() == TARGET
assert excluded == [
{"sensor_node": "J5", "reason": "missing_or_invalid_samples"}
]
def test_target_mode_uses_fixed_parameters_and_only_classifies_target(monkeypatch):
index = pd.date_range(
start=TARGET - timedelta(days=15) + timedelta(minutes=15),
end=TARGET,
freq="15min",
)
values = np.tile(np.arange(96, dtype=float), 15)
frame = pd.DataFrame(
{f"J{sensor}": values + sensor for sensor in range(5)},
index=index,
)
monkeypatch.setattr(
burst_detection,
"_get_pressure_sensor_nodes",
lambda _network: list(frame.columns),
)
monkeypatch.setattr(
burst_detection,
"_build_target_pressure_from_scada",
lambda **_kwargs: (frame, TARGET, []),
)
payload = burst_detection.run_burst_detection(
network="test",
username="tester",
sampling_interval_minutes=15,
)
assert payload["target_time"] == TARGET.isoformat()
assert payload["sample_count"] == 1440
assert payload["points_per_day"] == 96
assert payload["algorithm_params"]["mu"] == 1
assert payload["summary"]["score_threshold"] == -0.04
assert [row["Role"] for row in payload["rows"]].count("target") == 1
assert all(not row["IsBurst"] for row in payload["rows"][:-1])
assert payload["reference_window"] == {
"start": (TARGET - timedelta(days=14)).isoformat(),
"end": (TARGET - timedelta(days=1)).isoformat(),
"day_count": 14,
}
def test_sampling_interval_uses_scada_frequency_and_can_be_overridden(monkeypatch):
monkeypatch.setattr(
burst_detection,
"get_all_scada_info",
lambda _network: [
{
"type": "pressure",
"associated_element_id": "J1",
"transmission_frequency": "0:15:00",
},
{
"type": "pressure",
"associated_element_id": "J2",
"transmission_frequency": "0:15:00",
},
],
)
assert (
burst_detection._resolve_sampling_interval_minutes(
network="test",
sensor_nodes=["J1", "J2"],
requested_interval=None,
)
== 15
)
assert (
burst_detection._resolve_sampling_interval_minutes(
network="test",
sensor_nodes=["J1", "J2"],
requested_interval=30,
)
== 30
)
def test_target_threshold_is_applied_only_to_latest_row():
result = pd.DataFrame(
{
"Day": [1, 2, 3],
"Score": [-0.3, -0.2, -0.04],
"Prediction": [-1, -1, 1],
"IsBurst": [True, True, False],
}
)
rows = burst_detection._serialize_result_rows(result, target_only=True)
assert [row["IsBurst"] for row in rows] == [False, False, True]