mirror of
https://github.com/jdejaegh/python-irceline.git
synced 2025-06-27 03:35:56 +02:00
Merge pull request #3 from jdejaegh/belaqi_index_fix
Update computation of the BelAQI index
This commit is contained in:
commit
fc308021e8
5 changed files with 305 additions and 178 deletions
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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/m³)
|
|
||||||
:param pm25: PM2.5 daily mean (or running 24-hourly mean for real-time) (µg/m³)
|
Values taken from Table 1 of
|
||||||
:param o3: O3 max 1-hourly mean per day (or latest hourly mean for real-time) (µg/m³)
|
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/m³)
|
|
||||||
|
:param pm10: PM10 daily mean (µg/m³)
|
||||||
|
:param pm25: PM2.5 daily mean (µg/m³)
|
||||||
|
:param o3: O3 maximum running 8-hour mean (µg/m³)
|
||||||
|
:param no2: NO2 daily mean (µg/m³)
|
||||||
: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/m³)
|
||||||
|
:param pm25: PM2.5 hourly mean (µg/m³)
|
||||||
|
:param o3: O3 hourly mean (µg/m³)
|
||||||
|
:param no2: NO2 hourly mean (µg/m³)
|
||||||
|
: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
|
||||||
|
|
8
tests/fixtures/rio_wfs_for_belaqi.json
vendored
8
tests/fixtures/rio_wfs_for_belaqi.json
vendored
|
@ -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": [
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Reference in a new issue