From 329178834c126864dc802fd87c07ac205b72d775 Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Thu, 16 May 2024 15:19:20 -0700 Subject: [PATCH] Handle none values more gracefully for FFMC calculations (#3629) Co-authored-by: Darren Boss --- api/app/fire_behaviour/cffdrs.py | 52 ++++++++++++-- api/app/tests/fire_behavior/test_cffdrs.py | 80 +++++++++++++++++----- 2 files changed, 108 insertions(+), 24 deletions(-) diff --git a/api/app/fire_behaviour/cffdrs.py b/api/app/fire_behaviour/cffdrs.py index 1b51346e3..1e452ca13 100644 --- a/api/app/fire_behaviour/cffdrs.py +++ b/api/app/fire_behaviour/cffdrs.py @@ -544,13 +544,28 @@ def fine_fuel_moisture_code(ffmc: float, temperature: float, relative_humidity: """ if ffmc is None: - ffmc = NULL + logger.error("Failed to calculate FFMC; initial FFMC is required.") + return None + if temperature is None: + temperature = NULL + if relative_humidity is None: + relative_humidity = NULL + if precipitation is None: + precipitation = NULL + if wind_speed is None: + # _ffmcCalc with throw if passed a NULL windspeed, so log a message and return None. + logger.error("Failed to calculate ffmc") + return None result = CFFDRS.instance().cffdrs._ffmcCalc(ffmc_yda=ffmc, temp=temperature, rh=relative_humidity, prec=precipitation, ws=wind_speed) + if len(result) == 0: + logger.error("Failed to calculate ffmc") + return None if isinstance(result[0], float): return result[0] - raise CFFDRSException("Failed to calculate ffmc") - + + logger.error("Failed to calculate ffmc") + return None def duff_moisture_code(dmc: float, temperature: float, relative_humidity: float, precipitation: float, latitude: float = 55, month: int = 7, @@ -578,12 +593,24 @@ def duff_moisture_code(dmc: float, temperature: float, relative_humidity: float, :type latitude_adjust: bool, optional """ if dmc is None: - dmc = NULL + logger.error("Failed to calculate DMC; initial DMC is required.") + return None + if temperature is None: + temperature = NULL + if relative_humidity is None: + relative_humidity = NULL + if precipitation is None: + precipitation = NULL result = CFFDRS.instance().cffdrs._dmcCalc(dmc, temperature, relative_humidity, precipitation, latitude, month, latitude_adjust) + + if len(result) == 0: + logger.error("Failed to calculate DMC") + return None if isinstance(result[0], float): return result[0] - raise CFFDRSException("Failed to calculate dmc") + logger.error("Failed to calculate DMC") + return None def drought_code(dc: float, temperature: float, relative_humidity: float, precipitation: float, @@ -610,12 +637,23 @@ def drought_code(dc: float, temperature: float, relative_humidity: float, precip :return: None """ if dc is None: - dc = NULL + logger.error("Failed to calculate DC; initial DC is required.") + return None + if temperature is None: + temperature = NULL + if relative_humidity is None: + relative_humidity = NULL + if precipitation is None: + precipitation = NULL result = CFFDRS.instance().cffdrs._dcCalc(dc, temperature, relative_humidity, precipitation, latitude, month, latitude_adjust) + if len(result) == 0: + logger.error("Failed to calculate DC") + return None if isinstance(result[0], float): return result[0] - raise CFFDRSException("Failed to calculate dmc") + logger.error("Failed to calculate DC") + return None def initial_spread_index(ffmc: float, wind_speed: float, fbp_mod: bool = False): diff --git a/api/app/tests/fire_behavior/test_cffdrs.py b/api/app/tests/fire_behavior/test_cffdrs.py index a38387b28..443b01ba3 100644 --- a/api/app/tests/fire_behavior/test_cffdrs.py +++ b/api/app/tests/fire_behavior/test_cffdrs.py @@ -6,7 +6,7 @@ import math from app.schemas.fba_calc import FuelTypeEnum from app.fire_behaviour import cffdrs -from app.fire_behaviour.cffdrs import (pandas_to_r_converter, hourly_fine_fuel_moisture_code, CFFDRSException) +from app.fire_behaviour.cffdrs import pandas_to_r_converter, hourly_fine_fuel_moisture_code, CFFDRSException start_date = datetime(2023, 8, 17) @@ -33,42 +33,88 @@ def test_pandas_to_r_converter(): def test_hourly_ffmc_calculates_values(): ffmc_old = 80.0 df = hourly_fine_fuel_moisture_code(df_hourly, ffmc_old) - + assert not df['hffmc'].isnull().any() def test_hourly_ffmc_no_temperature(): ffmc_old = 80.0 - df_hourly = pd.DataFrame({'datetime': [hourly_datetimes[0], hourly_datetimes[1]], 'celsius': [12, 1], 'precipitation': [0, 1], 'ws': [14, 12], 'rh':[50, 50]}) + df_hourly = pd.DataFrame( + { + 'datetime': [hourly_datetimes[0], hourly_datetimes[1]], + 'celsius': [12, 1], + 'precipitation': [0, 1], + 'ws': [14, 12], + 'rh': [50, 50], + } + ) with pytest.raises(CFFDRSException): hourly_fine_fuel_moisture_code(df_hourly, ffmc_old) - + def test_ros(): - """ ROS runs """ - ros =cffdrs.rate_of_spread(FuelTypeEnum.C7, 1, 1, 1, 1, pc=100, pdf=None, - cc=None, cbh=10) + """ROS runs""" + ros = cffdrs.rate_of_spread(FuelTypeEnum.C7, 1, 1, 1, 1, pc=100, pdf=None, cc=None, cbh=10) assert math.isclose(ros, 1.2966988409822604e-05) def test_ros_no_isi(): - """ ROS fails """ + """ROS fails""" with pytest.raises(cffdrs.CFFDRSException): - cffdrs.rate_of_spread(FuelTypeEnum.C7, None, 1, 1, 1, pc=100, pdf=None, - cc=None, cbh=10) + cffdrs.rate_of_spread(FuelTypeEnum.C7, None, 1, 1, 1, pc=100, pdf=None, cc=None, cbh=10) def test_ros_no_bui(): - """ ROS fails """ + """ROS fails""" with pytest.raises(cffdrs.CFFDRSException): - cffdrs.rate_of_spread(FuelTypeEnum.C7, 1, None, 1, 1, pc=100, pdf=None, - cc=None, cbh=10) + cffdrs.rate_of_spread(FuelTypeEnum.C7, 1, None, 1, 1, pc=100, pdf=None, cc=None, cbh=10) def test_ros_no_params(): - """ ROS fails """ + """ROS fails""" with pytest.raises(cffdrs.CFFDRSException): - cffdrs.rate_of_spread(FuelTypeEnum.C7, None, None, None, None, pc=100, pdf=None, - cc=None, cbh=10) - + cffdrs.rate_of_spread(FuelTypeEnum.C7, None, None, None, None, pc=100, pdf=None, cc=None, cbh=10) + + +@pytest.mark.parametrize( + "ffmc,temperature,precipitation,relative_humidity,wind_speed", + [ + (None, 10, 9, 8, 7), + (11, None, 9, 8, 7), + (11, 10, None, 8, 7), + (11, 10, 9, None, 7), + (11, 10, 9, 8, None), + (None, None, None, 8, 7), + (None, None, None, None, None), + ], +) +def test_failing_ffmc(ffmc, temperature, precipitation, relative_humidity, wind_speed): + """Test that we can handle None values when attempting to calculate ffmc""" + res = cffdrs.fine_fuel_moisture_code( + ffmc=ffmc, + temperature=temperature, + precipitation=precipitation, + relative_humidity=relative_humidity, + wind_speed=wind_speed, + ) + assert res is None + + +@pytest.mark.parametrize( + 'dmc,temperature,relative_humidity,precipitation', + [(None, 10, 90, 1), (100, None, 90, 1), (100, 10, None, 1), (100, 10, 90, None)], +) +def test_failing_dmc(dmc, temperature, relative_humidity, precipitation): + """Test that we can handle None values when attempting to calculate dmc""" + res = cffdrs.duff_moisture_code(dmc, temperature, relative_humidity, precipitation) + assert res is None + + +@pytest.mark.parametrize( + 'dc,temperature,relative_humidity,precipitation', [(None, 10, 90, 1), (100, None, 90, 1), (100, 10, 90, None)] +) +def test_failing_dc(dc, temperature, relative_humidity, precipitation): + """Test that we can handle None values when attempting to calculate dc""" + res = cffdrs.drought_code(dc, temperature, relative_humidity, precipitation) + assert res is None