Merge pull request #3 from jdejaegh/belaqi_index_fix

Update computation of the BelAQI index
This commit is contained in:
Jules 2024-06-29 14:22:08 +02:00 committed by GitHub
commit fc308021e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 305 additions and 178 deletions

View file

@ -26,7 +26,7 @@ import aiohttp
import asyncio import asyncio
from datetime import datetime, date from datetime import datetime, date
from open_irceline import IrcelineRioClient, RioFeature, IrcelineForecastClient, ForecastFeature, belaqi_index_actual from open_irceline import IrcelineRioClient, RioFeature, IrcelineForecastClient, ForecastFeature, belaqi_index_rio_hourly
async def get_rio_interpolated_data(): async def get_rio_interpolated_data():
@ -61,7 +61,7 @@ async def get_current_belaqi():
"""Get current BelAQI index from RIO interpolated values""" """Get current BelAQI index from RIO interpolated values"""
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
client = IrcelineRioClient(session) client = IrcelineRioClient(session)
result = await belaqi_index_actual( result = await belaqi_index_rio_hourly(
rio_client=client, rio_client=client,
timestamp=datetime.utcnow(), # must be timezone aware timestamp=datetime.utcnow(), # must be timezone aware
position=(50.85, 4.35) # (lat, lon) for Brussels position=(50.85, 4.35) # (lat, lon) for Brussels

View file

@ -1,5 +1,5 @@
from .api import IrcelineRioClient, IrcelineForecastClient, IrcelineApiError from .api import IrcelineRioClient, IrcelineForecastClient, IrcelineApiError
from .belaqi import belaqi_index, belaqi_index_actual, belaqi_index_forecast from .belaqi import belaqi_index_rio_hourly, belaqi_index_forecast_daily, belaqi_index_daily, belaqi_index_hourly
from .data import RioFeature, ForecastFeature, FeatureValue, BelAqiIndex from .data import RioFeature, ForecastFeature, FeatureValue, BelAqiIndex
__version__ = '0.1.0' __version__ = '0.1.0'

View file

@ -1,26 +1,31 @@
""" """
Compute the BelAQI index from concentrations of PM10, PM2.5, O3 and NO2, based on Compute the BelAQI index from concentrations of PM10, PM2.5, O3 and NO2, based on
https://www.irceline.be/en/air-quality/measurements/belaqi-air-quality-index/information https://www.irceline.be/en/air-quality/measurements/air-quality-index-november-2022/info_nov2022
> to calculate the actual (hour per hour varying) sub-indexes and the global index, the concentration scales of Table 4
> are applied to the latest hourly mean O3 and NO2 concentrations and the running 24-hourly mean PM2.5 and PM10
> concentrations.
""" """
from datetime import datetime, date from datetime import datetime, date
from typing import Tuple, Dict from typing import Tuple, Dict, Final
from .api import IrcelineRioClient, IrcelineForecastClient from .api import IrcelineRioClient, IrcelineForecastClient
from .data import BelAqiIndex, RioFeature, ForecastFeature from .data import BelAqiIndex, RioFeature, ForecastFeature
# Ratio values from Figure 2 at
# https://www.irceline.be/en/air-quality/measurements/air-quality-index-november-2022/info_nov2022
NO2_MAX_HMEAN_TO_DMEAN: Final = 1.51
O3_MAX_HMEAN_TO_MAX8HMEAN: Final = 1.10
def belaqi_index(pm10: float, pm25: float, o3: float, no2: float) -> BelAqiIndex:
def belaqi_index_daily(pm10: float, pm25: float, o3: float, no2: float) -> BelAqiIndex:
""" """
Computes the BelAQI index based on the components Computes the daily BelAQI index based on the components
Raise ValueError if a component is < 0 Raise ValueError if a component is < 0
:param pm10: PM10 daily mean (or running 24-hourly mean for real-time) (µg/)
:param pm25: PM2.5 daily mean (or running 24-hourly mean for real-time) (µg/) Values taken from Table 1 of
:param o3: O3 max 1-hourly mean per day (or latest hourly mean for real-time) (µg/) https://www.irceline.be/en/air-quality/measurements/air-quality-index-november-2022/info_nov2022
:param no2: NO2 max 1-hourly mean per day (or latest hourly mean for real-time) (µg/)
:param pm10: PM10 daily mean (µg/)
:param pm25: PM2.5 daily mean (µg/)
:param o3: O3 maximum running 8-hour mean (µg/)
:param no2: NO2 daily mean (µg/)
:return: BelAQI index from 1 to 10 (Value of BelAqiIndex enum) :return: BelAQI index from 1 to 10 (Value of BelAqiIndex enum)
""" """
if pm10 is None or pm25 is None or o3 is None or no2 is None: if pm10 is None or pm25 is None or o3 is None or no2 is None:
@ -29,38 +34,89 @@ def belaqi_index(pm10: float, pm25: float, o3: float, no2: float) -> BelAqiIndex
if pm10 < 0 or pm25 < 0 or o3 < 0 or no2 < 0: if pm10 < 0 or pm25 < 0 or o3 < 0 or no2 < 0:
raise ValueError("All the components should have a positive value") raise ValueError("All the components should have a positive value")
elif pm10 > 100 or pm25 > 70 or o3 > 320 or no2 > 300: elif pm10 > 100 or pm25 > 50 or o3 > 220 or no2 > 50:
return BelAqiIndex.HORRIBLE return BelAqiIndex.HORRIBLE
elif pm10 > 80 or pm25 > 60 or o3 > 280 or no2 > 250: elif pm10 > 80 or pm25 > 40 or o3 > 190 or no2 > 40:
return BelAqiIndex.VERY_BAD return BelAqiIndex.VERY_BAD
elif pm10 > 70 or pm25 > 50 or o3 > 240 or no2 > 200: elif pm10 > 70 or pm25 > 35 or o3 > 160 or no2 > 35:
return BelAqiIndex.BAD return BelAqiIndex.BAD
elif pm10 > 60 or pm25 > 40 or o3 > 180 or no2 > 180: elif pm10 > 60 or pm25 > 25 or o3 > 130 or no2 > 30:
return BelAqiIndex.VERY_POOR return BelAqiIndex.VERY_POOR
elif pm10 > 50 or pm25 > 35 or o3 > 160 or no2 > 150: elif pm10 > 45 or pm25 > 15 or o3 > 100 or no2 > 25:
return BelAqiIndex.POOR return BelAqiIndex.POOR
elif pm10 > 40 or pm25 > 25 or o3 > 120 or no2 > 120: elif pm10 > 35 or pm25 > 10 or o3 > 80 or no2 > 20:
return BelAqiIndex.MODERATE return BelAqiIndex.MODERATE
elif pm10 > 30 or pm25 > 15 or o3 > 70 or no2 > 70: elif pm10 > 25 or pm25 > 7.5 or o3 > 70 or no2 > 15:
return BelAqiIndex.FAIRLY_GOOD return BelAqiIndex.FAIRLY_GOOD
elif pm10 > 20 or pm25 > 10 or o3 > 50 or no2 > 50: elif pm10 > 15 or pm25 > 5 or o3 > 60 or no2 > 10:
return BelAqiIndex.GOOD return BelAqiIndex.GOOD
elif pm10 > 10 or pm25 > 5 or o3 > 25 or no2 > 20: elif pm10 > 5 or pm25 > 2.5 or o3 > 30 or no2 > 5:
return BelAqiIndex.VERY_GOOD return BelAqiIndex.VERY_GOOD
elif pm10 >= 0 or pm25 >= 0 or o3 >= 0 or no2 >= 0: elif pm10 >= 0 or pm25 >= 0 or o3 >= 0 or no2 >= 0:
return BelAqiIndex.EXCELLENT return BelAqiIndex.EXCELLENT
async def belaqi_index_actual(rio_client: IrcelineRioClient, position: Tuple[float, float], def belaqi_index_hourly(pm10: float, pm25: float, o3: float, no2: float) -> BelAqiIndex:
"""
Computes the hourly BelAQI index based on the components
Raise ValueError if a component is < 0
Values taken from Table 2 of
https://www.irceline.be/en/air-quality/measurements/air-quality-index-november-2022/info_nov2022
:param pm10: PM10 hourly mean (µg/)
:param pm25: PM2.5 hourly mean (µg/)
:param o3: O3 hourly mean (µg/)
:param no2: NO2 hourly mean (µg/)
:return: BelAQI index from 1 to 10 (Value of BelAqiIndex enum)
"""
if pm10 is None or pm25 is None or o3 is None or no2 is None:
raise ValueError("All the components should be valued (at lest one is None here)")
if pm10 < 0 or pm25 < 0 or o3 < 0 or no2 < 0:
raise ValueError("All the components should have a positive value")
elif pm10 > 140 or pm25 > 75 or o3 > 240 or no2 > 75:
return BelAqiIndex.HORRIBLE
elif pm10 > 110 or pm25 > 60 or o3 > 210 or no2 > 60:
return BelAqiIndex.VERY_BAD
elif pm10 > 95 or pm25 > 50 or o3 > 180 or no2 > 50:
return BelAqiIndex.BAD
elif pm10 > 80 or pm25 > 35 or o3 > 150 or no2 > 45:
return BelAqiIndex.VERY_POOR
elif pm10 > 60 or pm25 > 20 or o3 > 110 or no2 > 40:
return BelAqiIndex.POOR
elif pm10 > 45 or pm25 > 15 or o3 > 90 or no2 > 30:
return BelAqiIndex.MODERATE
elif pm10 > 35 or pm25 > 10 or o3 > 75 or no2 > 20:
return BelAqiIndex.FAIRLY_GOOD
elif pm10 > 20 or pm25 > 7.5 or o3 > 65 or no2 > 15:
return BelAqiIndex.GOOD
elif pm10 > 10 or pm25 > 3.5 or o3 > 30 or no2 > 10:
return BelAqiIndex.VERY_GOOD
elif pm10 >= 0 or pm25 >= 0 or o3 >= 0 or no2 >= 0:
return BelAqiIndex.EXCELLENT
async def belaqi_index_rio_hourly(rio_client: IrcelineRioClient, position: Tuple[float, float],
timestamp: datetime | None = None) -> BelAqiIndex: timestamp: datetime | None = None) -> BelAqiIndex:
""" """
Get current BelAQI index value for the given position using the rio_client Get current BelAQI index value for the given position using the rio_client
@ -74,22 +130,22 @@ async def belaqi_index_actual(rio_client: IrcelineRioClient, position: Tuple[flo
timestamp = datetime.utcnow() timestamp = datetime.utcnow()
components = await rio_client.get_data( components = await rio_client.get_data(
timestamp=timestamp, timestamp=timestamp,
features=[RioFeature.PM10_24HMEAN, features=[RioFeature.PM10_HMEAN,
RioFeature.PM25_24HMEAN, RioFeature.PM25_HMEAN,
RioFeature.O3_HMEAN, RioFeature.O3_HMEAN,
RioFeature.NO2_HMEAN], RioFeature.NO2_HMEAN],
position=position position=position
) )
return belaqi_index( return belaqi_index_hourly(
components.get(RioFeature.PM10_24HMEAN, {}).get('value', -1), pm10=components.get(RioFeature.PM10_HMEAN, {}).get('value', -1),
components.get(RioFeature.PM25_24HMEAN, {}).get('value', -1), pm25=components.get(RioFeature.PM25_HMEAN, {}).get('value', -1),
components.get(RioFeature.O3_HMEAN, {}).get('value', -1), o3=components.get(RioFeature.O3_HMEAN, {}).get('value', -1),
components.get(RioFeature.NO2_HMEAN, {}).get('value', -1) no2=components.get(RioFeature.NO2_HMEAN, {}).get('value', -1)
) )
async def belaqi_index_forecast(forecast_client: IrcelineForecastClient, position: Tuple[float, float], async def belaqi_index_forecast_daily(forecast_client: IrcelineForecastClient, position: Tuple[float, float],
timestamp: date | None = None) -> Dict[date, BelAqiIndex | None]: timestamp: date | None = None) -> Dict[date, BelAqiIndex | None]:
""" """
Get forecasted BelAQI index value for the given position using the forecast_client. Get forecasted BelAQI index value for the given position using the forecast_client.
@ -115,13 +171,13 @@ async def belaqi_index_forecast(forecast_client: IrcelineForecastClient, positio
for _, day in components.keys(): for _, day in components.keys():
try: try:
result[day] = belaqi_index( result[day] = belaqi_index_daily(
components.get((ForecastFeature.PM10_DMEAN, day), {}).get('value', -1), pm10=components.get((ForecastFeature.PM10_DMEAN, day), {}).get('value', -1),
components.get((ForecastFeature.PM25_DMEAN, day), {}).get('value', -1), pm25=components.get((ForecastFeature.PM25_DMEAN, day), {}).get('value', -1),
components.get((ForecastFeature.O3_MAXHMEAN, day), {}).get('value', -1), o3=components.get((ForecastFeature.O3_MAXHMEAN, day), {}).get('value', -1) * O3_MAX_HMEAN_TO_MAX8HMEAN,
components.get((ForecastFeature.NO2_MAXHMEAN, day), {}).get('value', -1) no2=components.get((ForecastFeature.NO2_MAXHMEAN, day), {}).get('value', -1) * NO2_MAX_HMEAN_TO_DMEAN
) )
except ValueError: except (ValueError, TypeError):
result[day] = None result[day] = None
return result return result

View file

@ -3,7 +3,7 @@
"features": [ "features": [
{ {
"type": "Feature", "type": "Feature",
"id": "pm10_24hmean.fid--d1ce43_19045107e20_1893", "id": "pm10_hmean.fid--d1ce43_19045107e20_1893",
"geometry": { "geometry": {
"type": "Polygon", "type": "Polygon",
"coordinates": [ "coordinates": [
@ -41,7 +41,7 @@
}, },
{ {
"type": "Feature", "type": "Feature",
"id": "pm10_24hmean.fid--d1ce43_19045107e20_1894", "id": "pm10_hmean.fid--d1ce43_19045107e20_1894",
"geometry": { "geometry": {
"type": "Polygon", "type": "Polygon",
"coordinates": [ "coordinates": [
@ -79,7 +79,7 @@
}, },
{ {
"type": "Feature", "type": "Feature",
"id": "pm25_24hmean.fid--d1ce43_19045107e20_1895", "id": "pm25_hmean.fid--d1ce43_19045107e20_1895",
"geometry": { "geometry": {
"type": "Polygon", "type": "Polygon",
"coordinates": [ "coordinates": [
@ -117,7 +117,7 @@
}, },
{ {
"type": "Feature", "type": "Feature",
"id": "pm25_24hmean.fid--d1ce43_19045107e20_1896", "id": "pm25_hmean.fid--d1ce43_19045107e20_1896",
"geometry": { "geometry": {
"type": "Polygon", "type": "Polygon",
"coordinates": [ "coordinates": [

View file

@ -1,150 +1,222 @@
from datetime import date, timedelta, datetime from datetime import date, timedelta, datetime
from random import randint, seed
import pytest import pytest
from freezegun import freeze_time from freezegun import freeze_time
from src.open_irceline.api import IrcelineForecastClient, IrcelineRioClient from src.open_irceline.api import IrcelineForecastClient, IrcelineRioClient
from src.open_irceline.belaqi import belaqi_index, belaqi_index_forecast, belaqi_index_actual from src.open_irceline.belaqi import belaqi_index_forecast_daily, belaqi_index_rio_hourly, belaqi_index_hourly, \
belaqi_index_daily
from src.open_irceline.data import BelAqiIndex from src.open_irceline.data import BelAqiIndex
from tests.conftest import get_mock_session_many_csv, get_mock_session from tests.conftest import get_mock_session_many_csv, get_mock_session
def test_belaqi_index(): @pytest.mark.parametrize("pm10, pm25, o3, no2, expected", [
# Excellent (5, 2, 25, 5, BelAqiIndex.EXCELLENT),
assert belaqi_index(5, 2, 10, 10) == BelAqiIndex.EXCELLENT (15, 5, 50, 12, BelAqiIndex.VERY_GOOD),
assert belaqi_index(0, 0, 0, 0) == BelAqiIndex.EXCELLENT (30, 9, 70, 18, BelAqiIndex.GOOD),
assert belaqi_index(10, 5, 25, 20) == BelAqiIndex.EXCELLENT (40, 13, 80, 25, BelAqiIndex.FAIRLY_GOOD),
(55, 18, 100, 35, BelAqiIndex.MODERATE),
# Very good (70, 25, 130, 43, BelAqiIndex.POOR),
assert belaqi_index(15, 8, 40, 35) == BelAqiIndex.VERY_GOOD (90, 45, 160, 48, BelAqiIndex.VERY_POOR),
assert belaqi_index(11, 6, 26, 21) == BelAqiIndex.VERY_GOOD (100, 55, 200, 55, BelAqiIndex.BAD),
assert belaqi_index(20, 10, 50, 50) == BelAqiIndex.VERY_GOOD (130, 70, 230, 70, BelAqiIndex.VERY_BAD),
(150, 80, 250, 80, BelAqiIndex.HORRIBLE),
# Good (150, 80, 300, 80, BelAqiIndex.HORRIBLE),
assert belaqi_index(25, 12, 60, 60) == BelAqiIndex.GOOD (95, 5, 25, 5, BelAqiIndex.VERY_POOR),
assert belaqi_index(21, 11, 51, 51) == BelAqiIndex.GOOD (145, 5, 25, 5, BelAqiIndex.HORRIBLE),
assert belaqi_index(30, 15, 70, 70) == BelAqiIndex.GOOD (5, 55, 25, 5, BelAqiIndex.BAD),
(5, 85, 25, 5, BelAqiIndex.HORRIBLE),
# Fairly good (5, 5, 190, 5, BelAqiIndex.BAD),
assert belaqi_index(35, 20, 100, 90) == BelAqiIndex.FAIRLY_GOOD (5, 5, 260, 5, BelAqiIndex.HORRIBLE),
assert belaqi_index(31, 16, 71, 71) == BelAqiIndex.FAIRLY_GOOD (5, 5, 25, 65, BelAqiIndex.VERY_BAD),
assert belaqi_index(40, 25, 120, 120) == BelAqiIndex.FAIRLY_GOOD (5, 5, 25, 85, BelAqiIndex.HORRIBLE),
(45, 15, 150, 10, BelAqiIndex.POOR),
# Moderate (20, 25, 180, 15, BelAqiIndex.VERY_POOR),
assert belaqi_index(45, 30, 140, 130) == BelAqiIndex.MODERATE (10, 7, 250, 70, BelAqiIndex.HORRIBLE),
assert belaqi_index(41, 26, 121, 121) == BelAqiIndex.MODERATE (110, 3, 30, 25, BelAqiIndex.BAD),
assert belaqi_index(50, 35, 160, 150) == BelAqiIndex.MODERATE (5, 0, 0, 0, BelAqiIndex.EXCELLENT),
(15, 0, 0, 0, BelAqiIndex.VERY_GOOD),
# Poor (30, 0, 0, 0, BelAqiIndex.GOOD),
assert belaqi_index(55, 38, 170, 160) == BelAqiIndex.POOR (40, 0, 0, 0, BelAqiIndex.FAIRLY_GOOD),
assert belaqi_index(51, 36, 161, 151) == BelAqiIndex.POOR (55, 0, 0, 0, BelAqiIndex.MODERATE),
assert belaqi_index(60, 40, 180, 180) == BelAqiIndex.POOR (70, 0, 0, 0, BelAqiIndex.POOR),
(90, 0, 0, 0, BelAqiIndex.VERY_POOR),
# Very poor (100, 0, 0, 0, BelAqiIndex.BAD),
assert belaqi_index(65, 45, 200, 190) == BelAqiIndex.VERY_POOR (130, 0, 0, 0, BelAqiIndex.VERY_BAD),
assert belaqi_index(61, 41, 181, 181) == BelAqiIndex.VERY_POOR (150, 0, 0, 0, BelAqiIndex.HORRIBLE),
assert belaqi_index(70, 50, 240, 200) == BelAqiIndex.VERY_POOR (0, 2, 0, 0, BelAqiIndex.EXCELLENT),
(0, 5, 0, 0, BelAqiIndex.VERY_GOOD),
# Bad (0, 9, 0, 0, BelAqiIndex.GOOD),
assert belaqi_index(75, 55, 260, 220) == BelAqiIndex.BAD (0, 13, 0, 0, BelAqiIndex.FAIRLY_GOOD),
assert belaqi_index(71, 51, 241, 201) == BelAqiIndex.BAD (0, 18, 0, 0, BelAqiIndex.MODERATE),
assert belaqi_index(80, 60, 280, 250) == BelAqiIndex.BAD (0, 25, 0, 0, BelAqiIndex.POOR),
(0, 45, 0, 0, BelAqiIndex.VERY_POOR),
# Very bad (0, 55, 0, 0, BelAqiIndex.BAD),
assert belaqi_index(85, 65, 300, 270) == BelAqiIndex.VERY_BAD (0, 70, 0, 0, BelAqiIndex.VERY_BAD),
assert belaqi_index(81, 61, 281, 251) == BelAqiIndex.VERY_BAD (0, 80, 0, 0, BelAqiIndex.HORRIBLE),
assert belaqi_index(100, 70, 320, 300) == BelAqiIndex.VERY_BAD (0, 0, 25, 0, BelAqiIndex.EXCELLENT),
(0, 0, 50, 0, BelAqiIndex.VERY_GOOD),
# Horrible (0, 0, 70, 0, BelAqiIndex.GOOD),
assert belaqi_index(110, 75, 330, 310) == BelAqiIndex.HORRIBLE (0, 0, 80, 0, BelAqiIndex.FAIRLY_GOOD),
assert belaqi_index(101, 71, 321, 301) == BelAqiIndex.HORRIBLE (0, 0, 100, 0, BelAqiIndex.MODERATE),
assert belaqi_index(150, 100, 400, 400) == BelAqiIndex.HORRIBLE (0, 0, 130, 0, BelAqiIndex.POOR),
(0, 0, 160, 0, BelAqiIndex.VERY_POOR),
(0, 0, 200, 0, BelAqiIndex.BAD),
(0, 0, 230, 0, BelAqiIndex.VERY_BAD),
(0, 0, 250, 0, BelAqiIndex.HORRIBLE),
(0, 0, 0, 5, BelAqiIndex.EXCELLENT),
(0, 0, 0, 12, BelAqiIndex.VERY_GOOD),
(0, 0, 0, 18, BelAqiIndex.GOOD),
(0, 0, 0, 25, BelAqiIndex.FAIRLY_GOOD),
(0, 0, 0, 35, BelAqiIndex.MODERATE),
(0, 0, 0, 43, BelAqiIndex.POOR),
(0, 0, 0, 48, BelAqiIndex.VERY_POOR),
(0, 0, 0, 55, BelAqiIndex.BAD),
(0, 0, 0, 70, BelAqiIndex.VERY_BAD),
(0, 0, 0, 80, BelAqiIndex.HORRIBLE)
])
def test_belaqi_index_hourly(pm10, pm25, o3, no2, expected):
assert belaqi_index_hourly(pm10, pm25, o3, no2) == expected
def test_belaqi_single_component(): @pytest.mark.parametrize("pm10, pm25, o3, no2, expected_index", [
# Tests with only PM10 varying (5, 0, 0, 0, BelAqiIndex.EXCELLENT),
assert belaqi_index(5, 0, 0, 0) == BelAqiIndex.EXCELLENT (15, 0, 0, 0, BelAqiIndex.VERY_GOOD),
assert belaqi_index(15, 0, 0, 0) == BelAqiIndex.VERY_GOOD (25, 0, 0, 0, BelAqiIndex.GOOD),
assert belaqi_index(25, 0, 0, 0) == BelAqiIndex.GOOD (35, 0, 0, 0, BelAqiIndex.FAIRLY_GOOD),
assert belaqi_index(35, 0, 0, 0) == BelAqiIndex.FAIRLY_GOOD (45, 0, 0, 0, BelAqiIndex.MODERATE),
assert belaqi_index(45, 0, 0, 0) == BelAqiIndex.MODERATE (60, 0, 0, 0, BelAqiIndex.POOR),
assert belaqi_index(55, 0, 0, 0) == BelAqiIndex.POOR (70, 0, 0, 0, BelAqiIndex.VERY_POOR),
assert belaqi_index(65, 0, 0, 0) == BelAqiIndex.VERY_POOR (80, 0, 0, 0, BelAqiIndex.BAD),
assert belaqi_index(75, 0, 0, 0) == BelAqiIndex.BAD (100, 0, 0, 0, BelAqiIndex.VERY_BAD),
assert belaqi_index(85, 0, 0, 0) == BelAqiIndex.VERY_BAD (101, 0, 0, 0, BelAqiIndex.HORRIBLE),
assert belaqi_index(110, 0, 0, 0) == BelAqiIndex.HORRIBLE (0, 2.5, 0, 0, BelAqiIndex.EXCELLENT),
(0, 5, 0, 0, BelAqiIndex.VERY_GOOD),
# Tests with only PM2.5 varying (0, 7.5, 0, 0, BelAqiIndex.GOOD),
assert belaqi_index(0, 2, 0, 0) == BelAqiIndex.EXCELLENT (0, 10, 0, 0, BelAqiIndex.FAIRLY_GOOD),
assert belaqi_index(0, 8, 0, 0) == BelAqiIndex.VERY_GOOD (0, 15, 0, 0, BelAqiIndex.MODERATE),
assert belaqi_index(0, 12, 0, 0) == BelAqiIndex.GOOD (0, 25, 0, 0, BelAqiIndex.POOR),
assert belaqi_index(0, 20, 0, 0) == BelAqiIndex.FAIRLY_GOOD (0, 35, 0, 0, BelAqiIndex.VERY_POOR),
assert belaqi_index(0, 30, 0, 0) == BelAqiIndex.MODERATE (0, 40, 0, 0, BelAqiIndex.BAD),
assert belaqi_index(0, 38, 0, 0) == BelAqiIndex.POOR (0, 50, 0, 0, BelAqiIndex.VERY_BAD),
assert belaqi_index(0, 45, 0, 0) == BelAqiIndex.VERY_POOR (0, 51, 0, 0, BelAqiIndex.HORRIBLE),
assert belaqi_index(0, 55, 0, 0) == BelAqiIndex.BAD (0, 0, 30, 0, BelAqiIndex.EXCELLENT),
assert belaqi_index(0, 65, 0, 0) == BelAqiIndex.VERY_BAD (0, 0, 60, 0, BelAqiIndex.VERY_GOOD),
assert belaqi_index(0, 75, 0, 0) == BelAqiIndex.HORRIBLE (0, 0, 70, 0, BelAqiIndex.GOOD),
(0, 0, 80, 0, BelAqiIndex.FAIRLY_GOOD),
# Tests with only O3 varying (0, 0, 100, 0, BelAqiIndex.MODERATE),
assert belaqi_index(0, 0, 10, 0) == BelAqiIndex.EXCELLENT (0, 0, 130, 0, BelAqiIndex.POOR),
assert belaqi_index(0, 0, 40, 0) == BelAqiIndex.VERY_GOOD (0, 0, 160, 0, BelAqiIndex.VERY_POOR),
assert belaqi_index(0, 0, 60, 0) == BelAqiIndex.GOOD (0, 0, 190, 0, BelAqiIndex.BAD),
assert belaqi_index(0, 0, 100, 0) == BelAqiIndex.FAIRLY_GOOD (0, 0, 220, 0, BelAqiIndex.VERY_BAD),
assert belaqi_index(0, 0, 140, 0) == BelAqiIndex.MODERATE (0, 0, 221, 0, BelAqiIndex.HORRIBLE),
assert belaqi_index(0, 0, 170, 0) == BelAqiIndex.POOR (0, 0, 0, 5, BelAqiIndex.EXCELLENT),
assert belaqi_index(0, 0, 200, 0) == BelAqiIndex.VERY_POOR (0, 0, 0, 10, BelAqiIndex.VERY_GOOD),
assert belaqi_index(0, 0, 260, 0) == BelAqiIndex.BAD (0, 0, 0, 15, BelAqiIndex.GOOD),
assert belaqi_index(0, 0, 300, 0) == BelAqiIndex.VERY_BAD (0, 0, 0, 20, BelAqiIndex.FAIRLY_GOOD),
assert belaqi_index(0, 0, 330, 0) == BelAqiIndex.HORRIBLE (0, 0, 0, 25, BelAqiIndex.MODERATE),
(0, 0, 0, 30, BelAqiIndex.POOR),
# Tests with only NO2 varying (0, 0, 0, 35, BelAqiIndex.VERY_POOR),
assert belaqi_index(0, 0, 0, 10) == BelAqiIndex.EXCELLENT (0, 0, 0, 40, BelAqiIndex.BAD),
assert belaqi_index(0, 0, 0, 35) == BelAqiIndex.VERY_GOOD (0, 0, 0, 50, BelAqiIndex.VERY_BAD),
assert belaqi_index(0, 0, 0, 60) == BelAqiIndex.GOOD (0, 0, 0, 51, BelAqiIndex.HORRIBLE),
assert belaqi_index(0, 0, 0, 90) == BelAqiIndex.FAIRLY_GOOD (3, 1, 20, 4, BelAqiIndex.EXCELLENT),
assert belaqi_index(0, 0, 0, 130) == BelAqiIndex.MODERATE (10, 3, 50, 8, BelAqiIndex.VERY_GOOD),
assert belaqi_index(0, 0, 0, 160) == BelAqiIndex.POOR (20, 6, 65, 12, BelAqiIndex.GOOD),
assert belaqi_index(0, 0, 0, 190) == BelAqiIndex.VERY_POOR (30, 8, 75, 18, BelAqiIndex.FAIRLY_GOOD),
assert belaqi_index(0, 0, 0, 220) == BelAqiIndex.BAD (40, 12, 90, 22, BelAqiIndex.MODERATE),
assert belaqi_index(0, 0, 0, 270) == BelAqiIndex.VERY_BAD (50, 20, 110, 28, BelAqiIndex.POOR),
assert belaqi_index(0, 0, 0, 310) == BelAqiIndex.HORRIBLE (65, 30, 140, 33, BelAqiIndex.VERY_POOR),
(75, 38, 180, 38, BelAqiIndex.BAD),
(90, 45, 200, 45, BelAqiIndex.VERY_BAD),
(110, 55, 230, 55, BelAqiIndex.HORRIBLE),
(3, 30, 20, 8, BelAqiIndex.VERY_POOR),
(110, 6, 65, 12, BelAqiIndex.HORRIBLE),
(3, 6, 230, 12, BelAqiIndex.HORRIBLE),
(3, 6, 65, 55, BelAqiIndex.HORRIBLE),
(50, 5, 65, 12, BelAqiIndex.POOR),
(10, 20, 65, 12, BelAqiIndex.POOR),
(10, 5, 110, 12, BelAqiIndex.POOR),
(10, 5, 65, 28, BelAqiIndex.POOR),
(75, 5, 30, 8, BelAqiIndex.BAD),
(10, 38, 30, 8, BelAqiIndex.BAD),
(10, 5, 180, 8, BelAqiIndex.BAD),
(10, 5, 30, 38, BelAqiIndex.BAD),
(65, 3, 20, 22, BelAqiIndex.VERY_POOR),
(3, 30, 20, 22, BelAqiIndex.VERY_POOR),
(3, 3, 140, 22, BelAqiIndex.VERY_POOR),
(3, 3, 20, 33, BelAqiIndex.VERY_POOR),
(90, 6, 20, 22, BelAqiIndex.VERY_BAD),
(10, 45, 20, 22, BelAqiIndex.VERY_BAD),
(10, 6, 200, 22, BelAqiIndex.VERY_BAD),
(10, 6, 20, 45, BelAqiIndex.VERY_BAD),
(3, 30, 20, 4, BelAqiIndex.VERY_POOR),
(110, 1, 20, 4, BelAqiIndex.HORRIBLE),
(3, 1, 230, 4, BelAqiIndex.HORRIBLE),
(3, 1, 20, 55, BelAqiIndex.HORRIBLE),
(50, 3, 20, 4, BelAqiIndex.POOR),
(3, 20, 20, 4, BelAqiIndex.POOR),
(3, 1, 110, 4, BelAqiIndex.POOR),
(3, 1, 20, 28, BelAqiIndex.POOR),
])
def test_belaqi_index_daily(pm10, pm25, o3, no2, expected_index):
assert belaqi_index_daily(pm10, pm25, o3, no2) == expected_index
def test_belaqi_random(): def test_belaqi_hourly_value_error():
seed(42)
# Generate random test values and their expected indices
test_cases = [
(randint(0, 10), randint(0, 5), randint(0, 25), randint(0, 20), BelAqiIndex.EXCELLENT),
(randint(11, 20), randint(6, 10), randint(26, 50), randint(21, 50), BelAqiIndex.VERY_GOOD),
(randint(21, 30), randint(11, 15), randint(51, 70), randint(51, 70), BelAqiIndex.GOOD),
(randint(31, 40), randint(16, 25), randint(71, 120), randint(71, 120), BelAqiIndex.FAIRLY_GOOD),
(randint(41, 50), randint(26, 35), randint(121, 160), randint(121, 150), BelAqiIndex.MODERATE),
(randint(51, 60), randint(36, 40), randint(161, 180), randint(151, 180), BelAqiIndex.POOR),
(randint(61, 70), randint(41, 50), randint(181, 240), randint(181, 200), BelAqiIndex.VERY_POOR),
(randint(71, 80), randint(51, 60), randint(241, 280), randint(201, 250), BelAqiIndex.BAD),
(randint(81, 100), randint(61, 70), randint(281, 320), randint(251, 300), BelAqiIndex.VERY_BAD),
(randint(101, 150), randint(71, 100), randint(321, 400), randint(301, 400), BelAqiIndex.HORRIBLE)
]
# Test each case
for pm10, pm25, o3, no2, expected in test_cases:
assert belaqi_index(pm10, pm25, o3, no2) == expected
def test_belaqi_value_error():
with pytest.raises(ValueError): with pytest.raises(ValueError):
belaqi_index(-1, 0, 12, 8) belaqi_index_hourly(-1, 0, 12, 8)
with pytest.raises(ValueError): with pytest.raises(ValueError):
belaqi_index(1, -20, 12, 8) belaqi_index_hourly(1, -20, 12, 8)
with pytest.raises(ValueError): with pytest.raises(ValueError):
belaqi_index(1, 0, -12, 8) belaqi_index_hourly(1, 0, -12, 8)
with pytest.raises(ValueError): with pytest.raises(ValueError):
belaqi_index(1, 0, 12, -8888) belaqi_index_hourly(1, 0, 12, -8888)
def test_belaqi_daily_value_error():
with pytest.raises(ValueError):
belaqi_index_daily(-1, 0, 12, 8)
with pytest.raises(ValueError):
belaqi_index_daily(1, -20, 12, 8)
with pytest.raises(ValueError):
belaqi_index_daily(1, 0, -12, 8)
with pytest.raises(ValueError):
belaqi_index_daily(1, 0, 12, -8888)
def test_belaqi_hourly_value_error_none():
with pytest.raises(ValueError):
belaqi_index_hourly(None, 0, 12, 8)
with pytest.raises(ValueError):
belaqi_index_hourly(1, None, 12, 8)
with pytest.raises(ValueError):
belaqi_index_hourly(1, 0, None, 8)
with pytest.raises(ValueError):
belaqi_index_hourly(1, 0, 12, None)
def test_belaqi_daily_value_error_none():
with pytest.raises(ValueError):
belaqi_index_daily(None, 0, 12, 8)
with pytest.raises(ValueError):
belaqi_index_daily(1, None, 12, 8)
with pytest.raises(ValueError):
belaqi_index_daily(1, 0, None, 8)
with pytest.raises(ValueError):
belaqi_index_daily(1, 0, 12, None)
@freeze_time(datetime.fromisoformat("2024-06-19T19:30:09.581Z")) @freeze_time(datetime.fromisoformat("2024-06-19T19:30:09.581Z"))
@ -153,13 +225,13 @@ async def test_belaqi_index_forecast():
client = IrcelineForecastClient(session) client = IrcelineForecastClient(session)
pos = (50.55, 4.85) pos = (50.55, 4.85)
result = await belaqi_index_forecast(client, pos) result = await belaqi_index_forecast_daily(client, pos)
expected_days = {date(2024, 6, 19) + timedelta(days=i) for i in range(5)} expected_days = {date(2024, 6, 19) + timedelta(days=i) for i in range(5)}
assert set(result.keys()) == expected_days assert set(result.keys()) == expected_days
for v in result.values(): for v in result.values():
assert v == BelAqiIndex.GOOD assert v == BelAqiIndex.MODERATE
async def test_belaqi_index_forecast_missing_day(): async def test_belaqi_index_forecast_missing_day():
@ -167,7 +239,7 @@ async def test_belaqi_index_forecast_missing_day():
client = IrcelineForecastClient(session) client = IrcelineForecastClient(session)
pos = (50.55, 4.85) pos = (50.55, 4.85)
result = await belaqi_index_forecast(client, pos, date(2024, 6, 21)) result = await belaqi_index_forecast_daily(client, pos, date(2024, 6, 21))
expected_days = {date(2024, 6, 21) + timedelta(days=i) for i in range(5)} expected_days = {date(2024, 6, 21) + timedelta(days=i) for i in range(5)}
assert set(result.keys()) == expected_days assert set(result.keys()) == expected_days
@ -181,9 +253,8 @@ async def test_belaqi_index_actual():
client = IrcelineRioClient(session) client = IrcelineRioClient(session)
pos = (50.55, 4.85) pos = (50.55, 4.85)
result = await belaqi_index_actual(client, pos) result = await belaqi_index_rio_hourly(client, pos)
print(result) assert result == BelAqiIndex.GOOD
assert result == BelAqiIndex.FAIRLY_GOOD
@freeze_time(datetime.fromisoformat("2024-06-23T12:30:09.581Z")) @freeze_time(datetime.fromisoformat("2024-06-23T12:30:09.581Z"))
@ -193,4 +264,4 @@ async def test_belaqi_index_actual_missing_value():
pos = (50.55, 4.85) pos = (50.55, 4.85)
with pytest.raises(ValueError): with pytest.raises(ValueError):
_ = await belaqi_index_actual(client, pos) _ = await belaqi_index_rio_hourly(client, pos)