Skip to content

Commit

Permalink
Ported FMI "feels like" temperature calculation. (#52)
Browse files Browse the repository at this point in the history
* Ported FMI "feels like" temperature calculation.

Bumped version to 0.2.1

* Replaced short variable names with longer

* Updated README to mention "feels like" temperature
  • Loading branch information
jaenis authored Feb 27, 2024
1 parent e68fc2f commit 7e574e0
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ FMI provides the following commonly used information:
- Cloud coverage (%)
- Precipitation intensity (mm/h)
- Symbol [Documentation in Finnish](https://www.ilmatieteenlaitos.fi/latauspalvelun-pikaohje)
- Feels like temperature (°C), calculated from weather data [Documentation in Finnish](https://tietopyynto.fi/files/foi/2940/feels_like-1.pdf)


There are also other information available. Check [models.py](fmi_weather_client/models.py) and FMI documentation for
Expand Down
3 changes: 3 additions & 0 deletions fmi_weather_client/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ class WeatherData(NamedTuple):
geopotential_height: Value
land_sea_mask: Value

# Calculated "feels like" temperature
feels_like: Value


class Weather(NamedTuple):
"""Represents a weather"""
Expand Down
48 changes: 47 additions & 1 deletion fmi_weather_client/parsers/forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,56 @@ def to_value(vals: Dict[str, float], variable_name: str, unit: str) -> Value:
radiation_long_wave_surface_net_acc=to_value(values, 'RadiationNetSurfaceLWAccumulation', 'J/m²'),
radiation_short_wave_diff_surface_acc=to_value(values, 'RadiationDiffuseAccumulation', 'J/m²'), # Not supported
geopotential_height=to_value(values, 'GeopHeight', 'm'),
land_sea_mask=to_value(values, 'LandSeaMask', '') # Not supported
land_sea_mask=to_value(values, 'LandSeaMask', ''), # Not supported
feels_like=Value(_feels_like(values), '°C')
)


def _feels_like(vals: Dict[str, float]) -> float:
# Feels like temperature, ported from:
# https://github.com/fmidev/smartmet-library-newbase/blob/master/newbase/NFmiMetMath.cpp#L535
# For more documentation see:
# https://tietopyynto.fi/tietopyynto/ilmatieteen-laitoksen-kayttama-tuntuu-kuin-laskentakaava/
# https://tietopyynto.fi/files/foi/2940/feels_like-1.pdf
temperature = vals.get("Temperature", None)
wind_speed = vals.get("WindSpeedMS", None)
humidity = vals.get("Humidity", None)
radiation = vals.get("RadiationGlobal", None)

if temperature is None:
return None
if wind_speed is None or wind_speed < 0.0 or humidity is None:
return temperature

# Wind chilling factor
chill = 15 + (1-15/37)*temperature + 15/37*pow(wind_speed+1, 0.16)*(temperature-37)
# Heat index
heat = _summer_simmer(temperature, humidity)

# Add corrections together
feels = temperature + (chill - temperature) + (heat - temperature)

# Perform radiation correction only when radiation is available
if radiation is not None:
absorption = 0.07
feels += 0.7 * absorption * radiation / (wind_speed + 10) - 0.25

return feels


def _summer_simmer(temperature: float, humidity_percent: float):
if temperature <= 14.5:
return temperature

# Humidity value is expected to be on 0..1 scale
humidity = humidity_percent / 100.0
humidity_ref = 0.5

# Calculate the correction
return (1.8*temperature - 0.55*(1-humidity) * (1.8*temperature - 26) - 0.55*(1-humidity_ref)*26) \
/ (1.8*(1 - 0.55*(1-humidity_ref)))


def _float_or_none(value: Any) -> Optional[float]:
"""
Get value as float. None if conversion fails.
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="fmi-weather-client",
version="0.2.0",
version="0.2.1",
author="Mika Hiltunen",
author_email="[email protected]",
description="Library for fetching weather information from Finnish Meteorological Institute (FMI)",
Expand Down
26 changes: 26 additions & 0 deletions test/test_parsers_forecast.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import unittest
from fmi_weather_client.parsers.forecast import _float_or_none
from fmi_weather_client.parsers.forecast import _feels_like


class ForecastParserTest(unittest.TestCase):
Expand All @@ -11,3 +12,28 @@ def test_float_or_none(self):
self.assertEqual(_float_or_none("0"), 0.0)
self.assertEqual(_float_or_none(""), None)
self.assertEqual(_float_or_none("NaN"), None)

def test_feels_like(self):
# Partial data
self.assertEqual(_feels_like({}), None)
self.assertEqual(_feels_like({"Temperature": 10}), 10)
self.assertEqual(_feels_like({"Temperature": 10, "WindSpeedMS": 10}), 10)
self.assertEqual(_feels_like({"Temperature": 10, "Humidity": 10}), 10)
# Without radiation
self.assertAlmostEqual(_feels_like({"WindSpeedMS": 5, "Humidity": 50, "Temperature": 0}), -4.980, places=3)
self.assertAlmostEqual(_feels_like({"WindSpeedMS": 5, "Humidity": 50, "Temperature": -5}), -10.653, places=3)
self.assertAlmostEqual(_feels_like({"WindSpeedMS": 5, "Humidity": 50, "Temperature": 25}), 23.385, places=3)
self.assertAlmostEqual(_feels_like({"WindSpeedMS": 5, "Humidity": 90, "Temperature": 25}), 26.588, places=3)
# With radiation
self.assertAlmostEqual(
_feels_like({"WindSpeedMS": 0, "Humidity": 50, "Temperature": 0, "RadiationGlobal": 0}),
-0.250, places=3)
self.assertAlmostEqual(
_feels_like({"WindSpeedMS": 0, "Humidity": 50, "Temperature": 10, "RadiationGlobal": 50}),
9.995, places=3)
self.assertAlmostEqual(
_feels_like({"WindSpeedMS": 5, "Humidity": 50, "Temperature": 0, "RadiationGlobal": 800}),
-2.617, places=3)
self.assertAlmostEqual(
_feels_like({"WindSpeedMS": 5, "Humidity": 50, "Temperature": 25, "RadiationGlobal": 425}),
24.523, places=3)

0 comments on commit 7e574e0

Please sign in to comment.