feat(burst-detection): update scada analysis flow
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
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]
|
||||
Reference in New Issue
Block a user