-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SFMS: Daily FFMC #4081
SFMS: Daily FFMC #4081
Changes from 8 commits
56a9cc9
9c0899d
b23f364
4f05b04
806bb85
2cfe911
fb1b53a
8e648de
b9a9b95
dc9e45c
2fbf45b
da48e99
ee731b0
ca2c182
1c85fb6
a9ebd57
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import os | ||
import logging | ||
import tempfile | ||
from typing import Callable, List, Iterator, cast | ||
from datetime import datetime, timedelta | ||
from app.auto_spatial_advisory.run_type import RunType | ||
from app.geospatial.wps_dataset import WPSDataset | ||
from app.sfms.fwi_processor import calculate_ffmc | ||
from app.sfms.raster_addresser import RasterKeyAddresser, WeatherParameter | ||
from app.utils.geospatial import GDALResamplingMethod | ||
from app.utils.s3_client import S3Client | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
DAILY_FFMC_DAYS = 2 | ||
|
||
MultiDatasetContext = Callable[[List[str]], Iterator[List["WPSDataset"]]] | ||
|
||
|
||
class DailyFFMCProcessor: | ||
""" | ||
Class for calculating/generating forecasted daily FFMC rasters for a date range | ||
""" | ||
|
||
def __init__(self, start_datetime: datetime, addresser: RasterKeyAddresser): | ||
self.start_datetime = start_datetime | ||
self.addresser = addresser | ||
|
||
async def process_daily_ffmc(self, s3_client: S3Client, input_dataset_context: MultiDatasetContext): | ||
for day in range(DAILY_FFMC_DAYS): | ||
current_day = self.start_datetime + timedelta(days=day) | ||
yesterday = current_day - timedelta(days=1) | ||
yesterday_ffmc_key = self.addresser.get_daily_ffmc(yesterday, RunType.ACTUAL) | ||
if await s3_client.all_objects_exist(yesterday_ffmc_key) == False: | ||
yesterday_ffmc_key = self.addresser.get_calculated_daily_ffmc(yesterday) | ||
|
||
if await s3_client.all_objects_exist(yesterday_ffmc_key) == False: | ||
logging.warning(f"No ffmc objects found for key: {yesterday_ffmc_key}") | ||
return | ||
|
||
temp_forecast_key = self.addresser.get_daily_model_data_key(current_day, RunType.FORECAST, WeatherParameter.TEMP) | ||
rh_forecast_key = self.addresser.get_daily_model_data_key(current_day, RunType.FORECAST, WeatherParameter.RH) | ||
precip_forecast_key = self.addresser.get_daily_model_data_key(current_day, RunType.FORECAST, WeatherParameter.PRECIP) | ||
wind_speed_forecast_key = self.addresser.get_daily_model_data_key(current_day, RunType.FORECAST, WeatherParameter.WIND_SPEED) | ||
|
||
with tempfile.TemporaryDirectory() as temp_dir: | ||
with input_dataset_context([yesterday_ffmc_key, temp_forecast_key, rh_forecast_key, precip_forecast_key, wind_speed_forecast_key]) as input_datasets: | ||
input_datasets = cast(List[WPSDataset], input_datasets) # Ensure correct type inference | ||
yesterday_ffmc_ds, temp_forecast_ds, rh_forecast_ds, precip_forecast_ds, wind_speed_forecast_ds = input_datasets | ||
|
||
# Warp weather datasets to match ffmc | ||
warped_temp_ds = temp_forecast_ds.warp_to_match(yesterday_ffmc_ds, f"{temp_dir}/{os.path.basename(temp_forecast_key)}", GDALResamplingMethod.BILINEAR) | ||
warped_rh_ds = rh_forecast_ds.warp_to_match(yesterday_ffmc_ds, f"{temp_dir}/{os.path.basename(rh_forecast_key)}", GDALResamplingMethod.BILINEAR) | ||
warped_precip_ds = precip_forecast_ds.warp_to_match(yesterday_ffmc_ds, f"{temp_dir}/{os.path.basename(precip_forecast_key)}", GDALResamplingMethod.BILINEAR) | ||
warped_wind_speed_ds = wind_speed_forecast_ds.warp_to_match( | ||
yesterday_ffmc_ds, f"{temp_dir}/{os.path.basename(wind_speed_forecast_key)}", GDALResamplingMethod.BILINEAR | ||
) | ||
|
||
temp_forecast_ds.close() | ||
rh_forecast_ds.close() | ||
precip_forecast_ds.close() | ||
wind_speed_forecast_ds.close() | ||
|
||
ffmc_values, ffmc_no_data_value = calculate_ffmc(yesterday_ffmc_ds, warped_temp_ds, warped_rh_ds, warped_precip_ds, warped_wind_speed_ds) | ||
|
||
today_ffmc_key = self.addresser.get_calculated_daily_ffmc(current_day) | ||
s3_client.persist_raster_data( | ||
temp_dir, today_ffmc_key, yesterday_ffmc_ds.as_gdal_ds().GetGeoTransform(), yesterday_ffmc_ds.as_gdal_ds().GetProjection(), ffmc_values, ffmc_no_data_value | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,9 @@ | ||
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.db.models.auto_spatial_advisory import RunTypeEnum | ||
from app.weather_models import ModelEnum | ||
from app.weather_models.rdps_filename_marshaller import compose_computed_precip_rdps_key, compose_rdps_key | ||
|
||
|
@@ -11,6 +12,11 @@ class WeatherParameter(enum.Enum): | |
TEMP = "temp" | ||
RH = "rh" | ||
WIND_SPEED = "wind_speed" | ||
PRECIP = "precipitation" | ||
|
||
@classmethod | ||
def non_precip(cls): | ||
return [cls.TEMP, cls.RH, cls.WIND_SPEED] | ||
|
||
|
||
class FWIParameter(enum.Enum): | ||
|
@@ -69,6 +75,18 @@ def get_model_data_key(self, start_time_utc: datetime, prediction_hour: int, wea | |
weather_model_date_prefix = f"{self.weather_model_prefix}/{start_time_utc.date().isoformat()}/" | ||
return os.path.join(weather_model_date_prefix, compose_rdps_key(start_time_utc, start_time_utc.hour, prediction_hour, weather_param.value)) | ||
|
||
def get_daily_model_data_key(self, timestamp: datetime, run_type: RunTypeEnum, weather_param: WeatherParameter): | ||
""" | ||
Generates the model data key that points to the associated raster artifact in the object store. | ||
The model is always assumed to be RDPS. | ||
|
||
:param timestamp: UTC date time when the model run started | ||
:param prediction_hour: the prediction hour offset from the start time | ||
""" | ||
assert_all_utc(timestamp) | ||
iso_date = timestamp.date().isoformat() | ||
return f"sfms/uploads/{run_type.value}/{iso_date}/{weather_param.value}{iso_date.replace('-', '')}.tif" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this meant to get RDPS weather model data? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope, same reasoning as: https://github.com/bcgov/wps/pull/4081/files#r1838823535 |
||
|
||
def get_calculated_precip_key(self, datetime_to_calculate_utc: datetime): | ||
""" | ||
Generates the calculated precip key that points to the associated raster artifact in the object store. | ||
|
@@ -81,6 +99,16 @@ def get_calculated_precip_key(self, datetime_to_calculate_utc: datetime): | |
calculated_weather_prefix = f"{self.weather_model_prefix}/{datetime_to_calculate_utc.date().isoformat()}/" | ||
return os.path.join(calculated_weather_prefix, compose_computed_precip_rdps_key(datetime_to_calculate_utc)) | ||
|
||
def get_daily_ffmc(self, timestamp: datetime, run_type: RunTypeEnum): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
assert_all_utc(timestamp) | ||
iso_date = timestamp.date().isoformat() | ||
return f"sfms/uploads/{run_type.value}/{iso_date}/fine_fuel_moisture_code{iso_date.replace('-', '')}.tif" | ||
|
||
def get_calculated_daily_ffmc(self, timestamp: datetime): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was using the generic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar reason to: https://github.com/bcgov/wps/pull/4081/files#r1838825805 |
||
assert_all_utc(timestamp) | ||
iso_date = timestamp.date().isoformat() | ||
return f"sfms/uploads/calculated/{iso_date}/fine_fuel_moisture_code{iso_date.replace('-', '')}.tif" | ||
|
||
def get_weather_data_keys(self, start_time_utc: datetime, datetime_to_calculate_utc: datetime, prediction_hour: int): | ||
""" | ||
Generates all model data keys that point to their associated raster artifacts in the object store. | ||
|
@@ -91,7 +119,7 @@ def get_weather_data_keys(self, start_time_utc: datetime, datetime_to_calculate_ | |
:return: temp, rh, wind speed and precip model data key | ||
""" | ||
assert_all_utc(start_time_utc, datetime_to_calculate_utc) | ||
non_precip_keys = tuple([self.get_model_data_key(start_time_utc, prediction_hour, param) for param in WeatherParameter]) | ||
non_precip_keys = tuple([self.get_model_data_key(start_time_utc, prediction_hour, param) for param in WeatherParameter.non_precip()]) | ||
precip_key = self.get_calculated_precip_key(datetime_to_calculate_utc) | ||
all_weather_data_keys = non_precip_keys + (precip_key,) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if we want to use weather_model data we need to keep track of a prediction_hour because we get model data for so many different hours, but maybe there's a better way to do it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opted to use all the daily SFMS model data since that's where we get the previous FFMC raster. Not sure if that's correct though. I guess that conflicts with our BUI calculations but the seed FFMC raster will either way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha, I could be wrong but I think the intention was to step forward in these calculations with weather model data (RDPS), since the weather data coming from SFMS is the coarsely interpolated forecasts from BC weather stations