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]