Skip to content

Commit

Permalink
Handle weather safety alerts with is_measurament_safe function
Browse files Browse the repository at this point in the history
  • Loading branch information
albireox committed Oct 29, 2024
1 parent ca81432 commit 322d3e7
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 19 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Next version

### ✨ Improved

* Added function `is_measurament_safe()` to calculate if weather data values are safe.

### 🏷️ Changed

* Change `/overwatcher/status/allow_calibrations` to `/overwatcher/status/allow_calibrations`.
Expand Down
34 changes: 15 additions & 19 deletions src/lvmapi/routers/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@
from __future__ import annotations

import asyncio
import time
import warnings

import polars
from fastapi import APIRouter, Request
from pydantic import BaseModel

from lvmapi.tools.alerts import enclosure_alerts, spec_temperature_alerts
from lvmapi.tools.weather import get_weather_data
from lvmapi.tools.weather import get_weather_data, is_measurament_safe


class AlertsSummary(BaseModel):
Expand Down Expand Up @@ -73,24 +72,21 @@ async def summary(request: Request) -> AlertsSummary:
if not isinstance(weather_data, BaseException) and weather_data.height > 0:
last_weather = weather_data[-1]

humidity_alert = last_weather["relative_humidity"][0] > 80
dew_point_alert = last_weather["dew_point"][0] > last_weather["temperature"][0]
wind_alert = is_measurament_safe(
weather_data,
"wind_speed_avg",
threshold=35,
reopen_value=30,
)

humidity_alert = is_measurament_safe(
weather_data,
"relative_humidity",
threshold=80,
reopen_value=70,
)

now = time.time()
last_30m = weather_data.filter(polars.col.ts.dt.timestamp("ms") > (now - 1800))

# LCO rules are to close if wind speed is above 35 mph in the 30 minute
# average and reopen only if the average is below 30 mph.
wind_30m_last = last_weather["wind_speed_avg_30m"][0]
if wind_30m_last > 35:
wind_alert = True
elif wind_30m_last < 30:
wind_alert = False
else:
if (last_30m["wind_speed_avg_30m"] > 35).any():
wind_alert = True
else:
wind_alert = False
dew_point_alert = last_weather["dew_point"][0] > last_weather["temperature"][0]

o2_alerts = {
key: value for key, value in enclosure_alerts_response.items() if "o2_" in key
Expand Down
69 changes: 69 additions & 0 deletions src/lvmapi/tools/weather.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

from __future__ import annotations

import time

import httpx
import polars
from astropy.time import Time, TimeDelta
Expand Down Expand Up @@ -140,3 +142,70 @@ async def get_weather_data(
)

return df


def is_measurament_safe(
data: polars.DataFrame,
measurement: str,
threshold: float,
window: int = 30,
rolling_average_window: int = 30,
reopen_value: float | None = None,
):
"""Determines whether an alert should be raised for a given measurement.
An alert will be issued if the rolling average value of the ``measurement``
(a column in ``data``) over the last ``window`` seconds is above the
``threshold``. Once the alert has been raised the value of the ``measurement``
must fall below the ``reopen_value`` to close the alert (defaults to the same
``threshold`` value) in a rolling.
``window`` and ``rolling_average_window`` are in minutes.
Returns
-------
result
A boolean indicating whether the measurement is safe. `True` means the
measurement is in a valid, safe range.
"""

if measurement not in data.columns:
raise ValueError(f"Measurement {measurement} not found in data.")

reopen_value = reopen_value or threshold

data = data.filter(["ts", measurement])
data = data.with_columns(
average=polars.col(measurement).rolling_mean_by(
by="ts",
window_size=f"{rolling_average_window}m",
)
)

# Get data from the last window`.
now = time.time()
data_window = data.filter(polars.col.ts.dt.timestamp("ms") > (now - window * 60))

# If any of the values in the last "window" is above the threshold, it's unsafe.
if (data_window["average"] >= threshold).any():
return False

# If all the values in the last "window" are below the reopen threshold, it's safe.
if (data_window["average"] < reopen_value).all():
return True

# The last case is if the values in the last "window" are between the reopen and
# the threshold values. We want to avoid the returned value flipping from true
# to false in a quick manner. We check the previous "window" minutes to see if
# the alert was raised at any point. If so, we require the current window to
# be below the reopen value. Otherwise, we consider it's safe.

prev_window = data.filter(
polars.col.ts.dt.timestamp("ms") > (now - 2 * window * 60),
polars.col.ts.dt.timestamp("ms") < (now - window * 60),
)
if (prev_window["average"] >= threshold).any():
return (data_window["average"] < reopen_value).all()

return True

0 comments on commit 322d3e7

Please sign in to comment.