From 6ef7b1cba81f5d3146f38f6055fe78df287d9e7a Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Wed, 13 Nov 2024 10:22:12 -0800 Subject: [PATCH] SFMS: Daily FFMC (#4081) - Adds daily FFMC logic for calculating daily FFMC rasters. - Renames bui job to account for ffmc and bui logic - Updates `coverage` and adds `pytest-cov` dev dependencies for running coverage in VSCode locally --- api/app/auto_spatial_advisory/sfms.py | 3 +- api/app/jobs/sfms_calculations.py | 14 +- ...ge_processor.py => daily_fwi_processor.py} | 36 +++-- api/app/sfms/fwi_processor.py | 20 ++- api/app/sfms/raster_addresser.py | 2 +- api/app/tests/jobs/test_sfms_calculations.py | 7 +- ...ocessor.py => test_daily_fwi_processor.py} | 56 +++++--- api/app/tests/sfms/test_fwi_processor.py | 70 +++++++-- api/poetry.lock | 136 +++++++++++------- api/pyproject.toml | 3 +- 10 files changed, 237 insertions(+), 110 deletions(-) rename api/app/sfms/{date_range_processor.py => daily_fwi_processor.py} (77%) rename api/app/tests/sfms/{test_bui_date_range_processor.py => test_daily_fwi_processor.py} (75%) diff --git a/api/app/auto_spatial_advisory/sfms.py b/api/app/auto_spatial_advisory/sfms.py index c5703701f..11905bd7a 100644 --- a/api/app/auto_spatial_advisory/sfms.py +++ b/api/app/auto_spatial_advisory/sfms.py @@ -1,6 +1,7 @@ -from cffdrs import bui, dc, dmc +from cffdrs import bui, dc, dmc, ffmc from numba import vectorize vectorized_bui = vectorize(bui) vectorized_dc = vectorize(dc) vectorized_dmc = vectorize(dmc) +vectorized_ffmc = vectorize(ffmc) diff --git a/api/app/jobs/sfms_calculations.py b/api/app/jobs/sfms_calculations.py index e7d87ab6e..1c957f31e 100644 --- a/api/app/jobs/sfms_calculations.py +++ b/api/app/jobs/sfms_calculations.py @@ -6,7 +6,7 @@ from app import configure_logging from app.rocketchat_notifications import send_rocketchat_notification -from app.sfms.date_range_processor import BUIDateRangeProcessor +from app.sfms.daily_fwi_processor import DailyFWIProcessor from app.sfms.raster_addresser import RasterKeyAddresser from app.utils.s3_client import S3Client from app.utils.time import get_utc_now @@ -19,7 +19,7 @@ class SFMSCalcJob: - async def calculate_bui(self, start_time: datetime): + async def calculate_daily_fwi(self, start_time: datetime): """ Entry point for processing SFMS DMC/DC/BUI rasters. To run from a specific date manually in openshift, see openshift/sfms-calculate/README.md @@ -28,17 +28,17 @@ async def calculate_bui(self, start_time: datetime): start_exec = get_utc_now() - bui_processor = BUIDateRangeProcessor(start_time, DAYS_TO_CALCULATE, RasterKeyAddresser()) + daily_processor = DailyFWIProcessor(start_time, DAYS_TO_CALCULATE, RasterKeyAddresser()) async with S3Client() as s3_client: - await bui_processor.process_bui(s3_client, multi_wps_dataset_context, multi_wps_dataset_context) + await daily_processor.process(s3_client, multi_wps_dataset_context, multi_wps_dataset_context) # calculate the execution time. execution_time = get_utc_now() - start_exec hours, remainder = divmod(execution_time.seconds, 3600) minutes, seconds = divmod(remainder, 60) - logger.info(f"BUI processing finished -- time elapsed {hours} hours, {minutes} minutes, {seconds:.2f} seconds") + logger.info(f"Daily FWI processing finished -- time elapsed {hours} hours, {minutes} minutes, {seconds:.2f} seconds") def main(): @@ -56,9 +56,9 @@ def main(): job = SFMSCalcJob() loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - loop.run_until_complete(job.calculate_bui(start_time)) + loop.run_until_complete(job.calculate_daily_fwi(start_time)) except Exception as e: - logger.error("An exception occurred while processing DMC/DC/BUI raster calculations", exc_info=e) + logger.error("An exception occurred while processing SFMS raster calculations", exc_info=e) rc_message = ":scream: Encountered an error while processing SFMS raster data." send_rocketchat_notification(rc_message, e) sys.exit(os.EX_SOFTWARE) diff --git a/api/app/sfms/date_range_processor.py b/api/app/sfms/daily_fwi_processor.py similarity index 77% rename from api/app/sfms/date_range_processor.py rename to api/app/sfms/daily_fwi_processor.py index 41cd2c4c4..f30dec525 100644 --- a/api/app/sfms/date_range_processor.py +++ b/api/app/sfms/daily_fwi_processor.py @@ -8,7 +8,7 @@ from app.geospatial.wps_dataset import WPSDataset from app.sfms.raster_addresser import FWIParameter, RasterKeyAddresser -from app.sfms.fwi_processor import calculate_bui, calculate_dc, calculate_dmc +from app.sfms.fwi_processor import calculate_bui, calculate_dc, calculate_dmc, calculate_ffmc from app.utils.geospatial import GDALResamplingMethod from app.utils.s3 import set_s3_gdal_config from app.utils.s3_client import S3Client @@ -20,9 +20,9 @@ MultiDatasetContext = Callable[[List[str]], Iterator[List["WPSDataset"]]] -class BUIDateRangeProcessor: +class DailyFWIProcessor: """ - Class for calculating/generating forecasted DMC/DC/BUI rasters for a date range + Class for calculating/generating forecasted daily FWI rasters for a date range """ def __init__(self, start_datetime: datetime, days: int, addresser: RasterKeyAddresser): @@ -30,7 +30,7 @@ def __init__(self, start_datetime: datetime, days: int, addresser: RasterKeyAddr self.days = days self.addresser = addresser - async def process_bui(self, s3_client: S3Client, input_dataset_context: MultiDatasetContext, new_dmc_dc_context: MultiDatasetContext): + async def process(self, s3_client: S3Client, input_dataset_context: MultiDatasetContext, new_dmc_dc_context: MultiDatasetContext): set_s3_gdal_config() for day in range(self.days): @@ -38,30 +38,31 @@ async def process_bui(self, s3_client: S3Client, input_dataset_context: MultiDat logger.info(f"Calculating DMC/DC/BUI for {datetime_to_calculate_utc.isoformat()}") # Get and check existence of weather s3 keys - temp_key, rh_key, _, precip_key = self.addresser.get_weather_data_keys(self.start_datetime, datetime_to_calculate_utc, prediction_hour) + temp_key, rh_key, wind_speed_key, precip_key = self.addresser.get_weather_data_keys(self.start_datetime, datetime_to_calculate_utc, prediction_hour) weather_keys_exist = await s3_client.all_objects_exist(temp_key, rh_key, precip_key) if not weather_keys_exist: logging.warning(f"No weather keys found for {model_run_for_hour(self.start_datetime.hour):02} model run") break # get and check existence of fwi s3 keys - dc_key, dmc_key = self._get_previous_fwi_keys(day, previous_fwi_datetime) + dc_key, dmc_key, ffmc_key = self._get_previous_fwi_keys(day, previous_fwi_datetime) fwi_keys_exist = await s3_client.all_objects_exist(dc_key, dmc_key) if not fwi_keys_exist: logging.warning(f"No previous DMC/DC keys found for {previous_fwi_datetime.date().isoformat()}") break - temp_key, rh_key, precip_key = self.addresser.gdal_prefix_keys(temp_key, rh_key, precip_key) + temp_key, rh_key, wind_speed_key, precip_key, ffmc_key = self.addresser.gdal_prefix_keys(temp_key, rh_key, wind_speed_key, precip_key, ffmc_key) dc_key, dmc_key = self.addresser.gdal_prefix_keys(dc_key, dmc_key) with tempfile.TemporaryDirectory() as temp_dir: - with input_dataset_context([temp_key, rh_key, precip_key, dc_key, dmc_key]) as input_datasets: + with input_dataset_context([temp_key, rh_key, wind_speed_key, precip_key, dc_key, dmc_key, ffmc_key]) as input_datasets: input_datasets = cast(List[WPSDataset], input_datasets) # Ensure correct type inference - temp_ds, rh_ds, precip_ds, dc_ds, dmc_ds = input_datasets + temp_ds, rh_ds, wind_speed_ds, precip_ds, dc_ds, dmc_ds, ffmc_ds = input_datasets # Warp weather datasets to match fwi warped_temp_ds = temp_ds.warp_to_match(dmc_ds, f"{temp_dir}/{os.path.basename(temp_key)}", GDALResamplingMethod.BILINEAR) warped_rh_ds = rh_ds.warp_to_match(dmc_ds, f"{temp_dir}/{os.path.basename(rh_key)}", GDALResamplingMethod.BILINEAR) + warped_wind_speed_ds = wind_speed_ds.warp_to_match(dmc_ds, f"{temp_dir}/{os.path.basename(wind_speed_key)}", GDALResamplingMethod.BILINEAR) warped_precip_ds = precip_ds.warp_to_match(dmc_ds, f"{temp_dir}/{os.path.basename(precip_key)}", GDALResamplingMethod.BILINEAR) # close unneeded datasets to reduce memory usage @@ -96,6 +97,18 @@ async def process_bui(self, s3_client: S3Client, input_dataset_context: MultiDat dc_nodata_value, ) + # Create and store FFMC dataset + ffmc_values, ffmc_no_data_value = calculate_ffmc(ffmc_ds, warped_temp_ds, warped_rh_ds, warped_wind_speed_ds, warped_precip_ds) + new_ffmc_key = self.addresser.get_calculated_index_key(datetime_to_calculate_utc, FWIParameter.FFMC) + await s3_client.persist_raster_data( + temp_dir, + new_ffmc_key, + dc_ds.as_gdal_ds().GetGeoTransform(), + dc_ds.as_gdal_ds().GetProjection(), + ffmc_values, + ffmc_no_data_value, + ) + # Open new DMC and DC datasets and calculate BUI new_bui_key = self.addresser.get_calculated_index_key(datetime_to_calculate_utc, FWIParameter.BUI) with new_dmc_dc_context([new_dmc_path, new_dc_path]) as new_dmc_dc_datasets: @@ -137,7 +150,10 @@ def _get_previous_fwi_keys(self, day_to_calculate: int, previous_fwi_datetime: d if day_to_calculate == 0: # if we're running the first day of the calculation, use previously uploaded actuals dc_key = self.addresser.get_uploaded_index_key(previous_fwi_datetime, FWIParameter.DC) dmc_key = self.addresser.get_uploaded_index_key(previous_fwi_datetime, FWIParameter.DMC) + ffmc_key = self.addresser.get_uploaded_index_key(previous_fwi_datetime, FWIParameter.FFMC) else: # otherwise use the last calculated key dc_key = self.addresser.get_calculated_index_key(previous_fwi_datetime, FWIParameter.DC) dmc_key = self.addresser.get_calculated_index_key(previous_fwi_datetime, FWIParameter.DMC) - return dc_key, dmc_key + ffmc_key = self.addresser.get_calculated_index_key(previous_fwi_datetime, FWIParameter.FFMC) + + return dc_key, dmc_key, ffmc_key diff --git a/api/app/sfms/fwi_processor.py b/api/app/sfms/fwi_processor.py index 23c9d74c0..08a86e261 100644 --- a/api/app/sfms/fwi_processor.py +++ b/api/app/sfms/fwi_processor.py @@ -3,7 +3,7 @@ import numpy as np from app.geospatial.wps_dataset import WPSDataset -from app.auto_spatial_advisory.sfms import vectorized_dmc, vectorized_dc, vectorized_bui +from app.auto_spatial_advisory.sfms import vectorized_dmc, vectorized_dc, vectorized_bui, vectorized_ffmc logger = logging.getLogger(__name__) @@ -55,3 +55,21 @@ def calculate_bui(dmc_ds: WPSDataset, dc_ds: WPSDataset): bui_values[nodata_mask] = nodata_value return bui_values, nodata_value + + +def calculate_ffmc(previous_ffmc_ds: WPSDataset, temp_ds: WPSDataset, rh_ds: WPSDataset, precip_ds: WPSDataset, wind_speed_ds: WPSDataset): + previous_ffmc_array, _ = previous_ffmc_ds.replace_nodata_with(0) + temp_array, _ = temp_ds.replace_nodata_with(0) + rh_array, _ = rh_ds.replace_nodata_with(0) + precip_array, _ = precip_ds.replace_nodata_with(0) + wind_speed_array, _ = wind_speed_ds.replace_nodata_with(0) + + start = perf_counter() + ffmc_values = vectorized_ffmc(previous_ffmc_array, temp_array, rh_array, precip_array, wind_speed_array) + logger.info("%f seconds to calculate vectorized ffmc", perf_counter() - start) + + nodata_mask, nodata_value = previous_ffmc_ds.get_nodata_mask() + if nodata_mask is not None: + ffmc_values[nodata_mask] = nodata_value + + return ffmc_values, nodata_value diff --git a/api/app/sfms/raster_addresser.py b/api/app/sfms/raster_addresser.py index 953707238..3a99bd90a 100644 --- a/api/app/sfms/raster_addresser.py +++ b/api/app/sfms/raster_addresser.py @@ -1,6 +1,6 @@ import os import enum -from datetime import datetime, timezone, timedelta +from datetime import datetime, timezone from zoneinfo import ZoneInfo from app import config from app.weather_models import ModelEnum diff --git a/api/app/tests/jobs/test_sfms_calculations.py b/api/app/tests/jobs/test_sfms_calculations.py index 29f4b97a3..478cf104d 100644 --- a/api/app/tests/jobs/test_sfms_calculations.py +++ b/api/app/tests/jobs/test_sfms_calculations.py @@ -12,7 +12,7 @@ def test_sfms_calc_job_fail_default(monkeypatch, mocker: MockerFixture): async def mock_job_error(): raise OSError("Error") - monkeypatch.setattr(SFMSCalcJob, "calculate_bui", mock_job_error) + monkeypatch.setattr(SFMSCalcJob, "calculate_daily_fwi", mock_job_error) monkeypatch.setattr("sys.argv", ["sfms_calculations.py"]) @@ -27,15 +27,14 @@ async def mock_job_error(): def test_sfms_calc_job_cli_arg(monkeypatch, mocker: MockerFixture): - calc_spy = mocker.patch.object(SFMSCalcJob, "calculate_bui", return_value=None) + daily_fwi_calc_spy = mocker.patch.object(SFMSCalcJob, "calculate_daily_fwi", return_value=None) test_datetime = "2024-10-10 5" monkeypatch.setattr("sys.argv", ["sfms_calculations.py", test_datetime]) sfms_calculations.main() - called_args, _ = calc_spy.call_args - assert called_args[0] == datetime.strptime(test_datetime, "%Y-%m-%d %H").replace(tzinfo=timezone.utc) + daily_fwi_calc_spy.assert_called_once_with(datetime.strptime(test_datetime, "%Y-%m-%d %H").replace(tzinfo=timezone.utc)) @pytest.mark.anyio diff --git a/api/app/tests/sfms/test_bui_date_range_processor.py b/api/app/tests/sfms/test_daily_fwi_processor.py similarity index 75% rename from api/app/tests/sfms/test_bui_date_range_processor.py rename to api/app/tests/sfms/test_daily_fwi_processor.py index e2ffa76f7..9e374dbcd 100644 --- a/api/app/tests/sfms/test_bui_date_range_processor.py +++ b/api/app/tests/sfms/test_daily_fwi_processor.py @@ -5,8 +5,8 @@ from datetime import datetime, timezone, timedelta from pytest_mock import MockerFixture from app.geospatial.wps_dataset import WPSDataset -from app.sfms import date_range_processor -from app.sfms.date_range_processor import BUIDateRangeProcessor +from app.sfms import daily_fwi_processor +from app.sfms.daily_fwi_processor import DailyFWIProcessor from app.sfms.raster_addresser import FWIParameter, RasterKeyAddresser from app.tests.dataset_common import create_mock_gdal_dataset, create_mock_wps_dataset from app.utils.geospatial import GDALResamplingMethod @@ -22,7 +22,7 @@ def create_mock_wps_datasets(num: int) -> List[WPSDataset]: def create_mock_input_dataset_context(): - input_datasets = create_mock_wps_datasets(5) + input_datasets = create_mock_wps_datasets(7) @contextmanager def mock_input_dataset_context(_: List[str]): @@ -56,20 +56,21 @@ def mock_new_dmc_dc_datasets_context(_: List[str]): @pytest.mark.anyio -async def test_bui_date_range_processor(mocker: MockerFixture): +async def test_daily_fwi_processor(mocker: MockerFixture): mock_key_addresser = RasterKeyAddresser() # key address spies get_weather_data_key_spy = mocker.spy(mock_key_addresser, "get_weather_data_keys") gdal_prefix_keys_spy = mocker.spy(mock_key_addresser, "gdal_prefix_keys") get_calculated_index_key_spy = mocker.spy(mock_key_addresser, "get_calculated_index_key") - bui_date_range_processor = BUIDateRangeProcessor(TEST_DATETIME, 2, mock_key_addresser) + fwi_processor = DailyFWIProcessor(TEST_DATETIME, 2, mock_key_addresser) # mock/spy dataset storage # mock weather index, param datasets used for calculations input_datasets, mock_input_dataset_context = create_mock_input_dataset_context() - mock_temp_ds, mock_rh_ds, mock_precip_ds, mock_dc_ds, mock_dmc_ds = input_datasets + mock_temp_ds, mock_rh_ds, mock_precip_ds, mock_wind_speed_ds, mock_dc_ds, mock_dmc_ds, mock_ffmc_ds = input_datasets temp_ds_spy = mocker.spy(mock_temp_ds, "warp_to_match") rh_ds_spy = mocker.spy(mock_rh_ds, "warp_to_match") + wind_speed_ds_spy = mocker.spy(mock_wind_speed_ds, "warp_to_match") precip_ds_spy = mocker.spy(mock_precip_ds, "warp_to_match") # mock new dmc and dc datasets @@ -80,9 +81,10 @@ async def test_bui_date_range_processor(mocker: MockerFixture): mocker.patch("osgeo.gdal.Open", return_value=create_mock_gdal_dataset()) # calculation spies - calculate_dmc_spy = mocker.spy(date_range_processor, "calculate_dmc") - calculate_dc_spy = mocker.spy(date_range_processor, "calculate_dc") - calculate_bui_spy = mocker.spy(date_range_processor, "calculate_bui") + calculate_dmc_spy = mocker.spy(daily_fwi_processor, "calculate_dmc") + calculate_dc_spy = mocker.spy(daily_fwi_processor, "calculate_dc") + calculate_bui_spy = mocker.spy(daily_fwi_processor, "calculate_bui") + calculate_ffmc_spy = mocker.spy(daily_fwi_processor, "calculate_ffmc") async with S3Client() as mock_s3_client: # mock s3 client @@ -90,7 +92,7 @@ async def test_bui_date_range_processor(mocker: MockerFixture): mocker.patch.object(mock_s3_client, "all_objects_exist", new=mock_all_objects_exist) persist_raster_spy = mocker.patch.object(mock_s3_client, "persist_raster_data", return_value="test_key.tif") - await bui_date_range_processor.process_bui(mock_s3_client, mock_input_dataset_context, mock_new_dmc_dc_datasets_context) + await fwi_processor.process(mock_s3_client, mock_input_dataset_context, mock_new_dmc_dc_datasets_context) # Verify weather model keys and actual keys are checked for both days assert mock_all_objects_exist.call_count == 4 @@ -107,7 +109,9 @@ async def test_bui_date_range_processor(mocker: MockerFixture): mocker.call( "weather_models/rdps/2024-10-10/00/temp/CMC_reg_TMP_TGL_2_ps10km_2024101000_P020.grib2", "weather_models/rdps/2024-10-10/00/rh/CMC_reg_RH_TGL_2_ps10km_2024101000_P020.grib2", + "weather_models/rdps/2024-10-10/00/wind_speed/CMC_reg_WIND_TGL_10_ps10km_2024101000_P020.grib2", "weather_models/rdps/2024-10-10/12/precip/COMPUTED_reg_APCP_SFC_0_ps10km_20241010_20z.tif", + "sfms/uploads/actual/2024-10-09/ffmc20241009.tif", ), # first day uploads mocker.call("sfms/uploads/actual/2024-10-09/dc20241009.tif", "sfms/uploads/actual/2024-10-09/dmc20241009.tif"), @@ -115,7 +119,9 @@ async def test_bui_date_range_processor(mocker: MockerFixture): mocker.call( "weather_models/rdps/2024-10-10/00/temp/CMC_reg_TMP_TGL_2_ps10km_2024101000_P044.grib2", "weather_models/rdps/2024-10-10/00/rh/CMC_reg_RH_TGL_2_ps10km_2024101000_P044.grib2", + "weather_models/rdps/2024-10-10/00/wind_speed/CMC_reg_WIND_TGL_10_ps10km_2024101000_P044.grib2", "weather_models/rdps/2024-10-11/12/precip/COMPUTED_reg_APCP_SFC_0_ps10km_20241011_20z.tif", + "sfms/calculated/forecast/2024-10-10/ffmc20241010.tif", ), # second day uploads mocker.call("sfms/calculated/forecast/2024-10-10/dc20241010.tif", "sfms/calculated/forecast/2024-10-10/dmc20241010.tif"), @@ -126,12 +132,15 @@ async def test_bui_date_range_processor(mocker: MockerFixture): # first day mocker.call(EXPECTED_FIRST_DAY, FWIParameter.DMC), mocker.call(EXPECTED_FIRST_DAY, FWIParameter.DC), + mocker.call(EXPECTED_FIRST_DAY, FWIParameter.FFMC), mocker.call(EXPECTED_FIRST_DAY, FWIParameter.BUI), # second day, previous days' dc and dmc are looked up first mocker.call(EXPECTED_FIRST_DAY, FWIParameter.DC), mocker.call(EXPECTED_FIRST_DAY, FWIParameter.DMC), + mocker.call(EXPECTED_FIRST_DAY, FWIParameter.FFMC), mocker.call(EXPECTED_SECOND_DAY, FWIParameter.DMC), mocker.call(EXPECTED_SECOND_DAY, FWIParameter.DC), + mocker.call(EXPECTED_SECOND_DAY, FWIParameter.FFMC), mocker.call(EXPECTED_SECOND_DAY, FWIParameter.BUI), ] @@ -146,6 +155,11 @@ async def test_bui_date_range_processor(mocker: MockerFixture): mocker.call(mock_dmc_ds, mocker.ANY, GDALResamplingMethod.BILINEAR), ] + assert wind_speed_ds_spy.call_args_list == [ + mocker.call(mock_dmc_ds, mocker.ANY, GDALResamplingMethod.BILINEAR), + mocker.call(mock_dmc_ds, mocker.ANY, GDALResamplingMethod.BILINEAR), + ] + assert precip_ds_spy.call_args_list == [ mocker.call(mock_dmc_ds, mocker.ANY, GDALResamplingMethod.BILINEAR), mocker.call(mock_dmc_ds, mocker.ANY, GDALResamplingMethod.BILINEAR), @@ -163,13 +177,19 @@ async def test_bui_date_range_processor(mocker: MockerFixture): wps_datasets = dc_calls[0][1:4] # Extract dataset arguments assert all(isinstance(ds, WPSDataset) for ds in wps_datasets) + for ffmc_calls in calculate_ffmc_spy.call_args_list: + ffmc_ds = ffmc_calls[0][0] + assert ffmc_ds == mock_ffmc_ds + wps_datasets = ffmc_calls[0][1:4] # Extract dataset arguments + assert all(isinstance(ds, WPSDataset) for ds in wps_datasets) + assert calculate_bui_spy.call_args_list == [ mocker.call(mock_new_dmc_ds, mock_new_dc_ds), mocker.call(mock_new_dmc_ds, mock_new_dc_ds), ] - # 3 each day, new dmc, dc and bui rasters - assert persist_raster_spy.call_count == 6 + # 4 each day, new dmc, dc and bui rasters + assert persist_raster_spy.call_count == 8 @pytest.mark.parametrize( @@ -191,14 +211,16 @@ async def test_no_weather_keys_exist(side_effect_1: bool, side_effect_2: bool, m _, mock_new_dmc_dc_datasets_context = create_mock_new_dmc_dc_context() # calculation spies - calculate_dmc_spy = mocker.spy(date_range_processor, "calculate_dmc") - calculate_dc_spy = mocker.spy(date_range_processor, "calculate_dc") - calculate_bui_spy = mocker.spy(date_range_processor, "calculate_bui") + calculate_dmc_spy = mocker.spy(daily_fwi_processor, "calculate_dmc") + calculate_dc_spy = mocker.spy(daily_fwi_processor, "calculate_dc") + calculate_bui_spy = mocker.spy(daily_fwi_processor, "calculate_bui") + calculate_ffmc_spy = mocker.spy(daily_fwi_processor, "calculate_ffmc") - bui_date_range_processor = BUIDateRangeProcessor(TEST_DATETIME, 1, RasterKeyAddresser()) + fwi_processor = DailyFWIProcessor(TEST_DATETIME, 1, RasterKeyAddresser()) - await bui_date_range_processor.process_bui(mock_s3_client, mock_input_dataset_context, mock_new_dmc_dc_datasets_context) + await fwi_processor.process(mock_s3_client, mock_input_dataset_context, mock_new_dmc_dc_datasets_context) calculate_dmc_spy.assert_not_called() calculate_dc_spy.assert_not_called() calculate_bui_spy.assert_not_called() + calculate_ffmc_spy.assert_not_called() diff --git a/api/app/tests/sfms/test_fwi_processor.py b/api/app/tests/sfms/test_fwi_processor.py index 4756f7226..b12f4eba0 100644 --- a/api/app/tests/sfms/test_fwi_processor.py +++ b/api/app/tests/sfms/test_fwi_processor.py @@ -2,18 +2,18 @@ import numpy as np import pytest -from cffdrs import bui, dc, dmc +from cffdrs import bui, dc, dmc, ffmc from osgeo import osr from app.geospatial.wps_dataset import WPSDataset -from app.sfms.fwi_processor import calculate_bui, calculate_dc, calculate_dmc +from app.sfms.fwi_processor import calculate_bui, calculate_dc, calculate_dmc, calculate_ffmc FWI_ARRAY = np.array([[12, 20], [-999, -999]]) TEST_ARRAY = np.array([[12, 20], [0, 0]]) @pytest.fixture -def sample_datasets(): +def sample_bui_input_datasets(): srs = osr.SpatialReference() srs.ImportFromEPSG(3005) transform = (-2, 1, 0, 2, 0, -1) @@ -34,8 +34,23 @@ def latitude_month(): return latitude, month -def test_calculate_dc_masked_correctly(sample_datasets, latitude_month): - dc_ds, _, temp_ds, rh_ds, precip_ds = sample_datasets +@pytest.fixture +def sample_daily_ffmc_input_datasets(): + srs = osr.SpatialReference() + srs.ImportFromEPSG(3005) + transform = (-2, 1, 0, 2, 0, -1) + + previous_ffmc_wps = WPSDataset.from_array(FWI_ARRAY, transform, srs.ExportToWkt(), nodata_value=-999) + temp_wps = WPSDataset.from_array(FWI_ARRAY, transform, srs.ExportToWkt(), nodata_value=-999) + rh_wps = WPSDataset.from_array(FWI_ARRAY, transform, srs.ExportToWkt(), nodata_value=-999) + wind_speed_wps = WPSDataset.from_array(FWI_ARRAY, transform, srs.ExportToWkt(), nodata_value=-999) + precip_wps = WPSDataset.from_array(FWI_ARRAY, transform, srs.ExportToWkt(), nodata_value=-999) + + return previous_ffmc_wps, temp_wps, rh_wps, precip_wps, wind_speed_wps + + +def test_calculate_dc_masked_correctly(sample_bui_input_datasets, latitude_month): + dc_ds, _, temp_ds, rh_ds, precip_ds = sample_bui_input_datasets latitude, month = latitude_month dc_values, nodata_value = calculate_dc(dc_ds, temp_ds, rh_ds, precip_ds, latitude, month) @@ -48,8 +63,8 @@ def test_calculate_dc_masked_correctly(sample_datasets, latitude_month): assert dc_values[0, 1] != nodata_value -def test_calculate_dmc_masked_correctly(sample_datasets, latitude_month): - _, dmc_ds, temp_ds, rh_ds, precip_ds = sample_datasets +def test_calculate_dmc_masked_correctly(sample_bui_input_datasets, latitude_month): + _, dmc_ds, temp_ds, rh_ds, precip_ds = sample_bui_input_datasets latitude, month = latitude_month dmc_values, nodata_value = calculate_dmc(dmc_ds, temp_ds, rh_ds, precip_ds, latitude, month) @@ -62,8 +77,8 @@ def test_calculate_dmc_masked_correctly(sample_datasets, latitude_month): assert dmc_values[0, 1] != nodata_value -def test_calculate_bui_masked_correctly(sample_datasets): - dc_ds, dmc_ds, _, _, _ = sample_datasets +def test_calculate_bui_masked_correctly(sample_bui_input_datasets): + dc_ds, dmc_ds, _, _, _ = sample_bui_input_datasets bui_values, nodata_value = calculate_bui(dmc_ds, dc_ds) @@ -75,8 +90,8 @@ def test_calculate_bui_masked_correctly(sample_datasets): assert bui_values[0, 1] != nodata_value -def test_calculate_dmc_values(sample_datasets, latitude_month): - _, dmc_ds, temp_ds, rh_ds, precip_ds = sample_datasets +def test_calculate_dmc_values(sample_bui_input_datasets, latitude_month): + _, dmc_ds, temp_ds, rh_ds, precip_ds = sample_bui_input_datasets latitude, month = latitude_month dmc_sample = FWI_ARRAY[0, 0] @@ -93,8 +108,8 @@ def test_calculate_dmc_values(sample_datasets, latitude_month): assert math.isclose(static_dmc, dmc_values[0, 0], abs_tol=0.01) -def test_calculate_dc_values(sample_datasets, latitude_month): - dc_ds, _, temp_ds, rh_ds, precip_ds = sample_datasets +def test_calculate_dc_values(sample_bui_input_datasets, latitude_month): + dc_ds, _, temp_ds, rh_ds, precip_ds = sample_bui_input_datasets latitude, month = latitude_month dc_sample = FWI_ARRAY[0, 0] @@ -111,8 +126,8 @@ def test_calculate_dc_values(sample_datasets, latitude_month): assert math.isclose(static_dmc, dc_values[0, 0], abs_tol=0.01) -def test_calculate_bui_values(sample_datasets): - dc_ds, dmc_ds, *_ = sample_datasets +def test_calculate_bui_values(sample_bui_input_datasets): + dc_ds, dmc_ds, *_ = sample_bui_input_datasets dc_sample = FWI_ARRAY[0, 0] dmc_sample = FWI_ARRAY[0, 0] @@ -122,3 +137,28 @@ def test_calculate_bui_values(sample_datasets): static_bui = bui(dmc_sample, dc_sample) assert math.isclose(static_bui, bui_values[0, 0], abs_tol=0.01) + + +def test_calculate_daily_values(sample_daily_ffmc_input_datasets): + previous_ffmc_wps, temp_wps, rh_wps, precip_wps, wind_speed_wps = sample_daily_ffmc_input_datasets + + previous_ffmc_sample = temp_sample = rh_sample = precip_sample = wind_speed_sample = FWI_ARRAY[0, 0] + + daily_ffmc_values, _ = calculate_ffmc(previous_ffmc_wps, temp_wps, rh_wps, precip_wps, wind_speed_wps) + + static_ffmc = ffmc(previous_ffmc_sample, temp_sample, rh_sample, wind_speed_sample, precip_sample) + + assert math.isclose(static_ffmc, daily_ffmc_values[0, 0], abs_tol=0.01) + + +def test_calculate_ffmc_masked_correctly(sample_daily_ffmc_input_datasets): + previous_ffmc_wps, temp_wps, rh_wps, precip_wps, wind_speed_wps = sample_daily_ffmc_input_datasets + + daily_ffmc_values, nodata_value = calculate_ffmc(previous_ffmc_wps, temp_wps, rh_wps, precip_wps, wind_speed_wps) + + # validate output shape and nodata masking + assert daily_ffmc_values.shape == (2, 2) + assert daily_ffmc_values[1, 0] == nodata_value + assert daily_ffmc_values[1, 1] == nodata_value + assert daily_ffmc_values[0, 0] != nodata_value + assert daily_ffmc_values[0, 1] != nodata_value diff --git a/api/poetry.lock b/api/poetry.lock index 9646886f3..093152ed2 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -866,61 +866,73 @@ test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist" [[package]] name = "coverage" -version = "6.5.0" +version = "7.6.4" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, - {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, - {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, - {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, - {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, - {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, - {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, - {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, - {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, - {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, - {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, - {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, - {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, - {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, - {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, - {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, - {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, - {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, - {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, - {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"}, + {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"}, + {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"}, + {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"}, + {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"}, + {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"}, + {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"}, + {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"}, + {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"}, + {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"}, + {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"}, + {file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"}, + {file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"}, + {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"}, + {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"}, ] [package.extras] @@ -3857,6 +3869,24 @@ parse-type = "*" py = "*" pytest = ">=4.3" +[[package]] +name = "pytest-cov" +version = "6.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + [[package]] name = "pytest-mock" version = "3.14.0" @@ -5422,4 +5452,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = ">=3.12.3" -content-hash = "6d9a525df5a8a57a4972aa4f2dc10b047b7de3b6bbb03e57064e5253209059a7" +content-hash = "7e39ed285da1c29436dd04eb5a1505fe4f3cbf93424afdc52b82a142863a51c1" diff --git a/api/pyproject.toml b/api/pyproject.toml index 6a3b8671d..4f1c482c9 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -53,7 +53,7 @@ pytest = "^7.2.1" jupyter = "^1" notebook = "^7.0.7" pytest_bdd = "^5" -coverage = "^6" +coverage = "^7.6.4" pycodestyle = "^2" autopep8 = "^2" matplotlib = "^3" @@ -64,6 +64,7 @@ jsonpickle = "^3.0.0" pytest-watch = "^4.2.0" pytest-testmon = "^2.0.0" ruff = "^0.4.0" +pytest-cov = "^6.0.0" [build-system] requires = ["poetry>=1.1.11"]