Skip to content
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

MoreCast 2.0 - Simulated Indices #3200

Merged
merged 48 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
a37074f
adds more fwi functions from R library
brettedw Oct 17, 2023
d6faa20
adds lat/long to WeatherIndeterminate
brettedw Oct 17, 2023
95af97e
calculate forecasted fwi system values
brettedw Oct 17, 2023
ba327f0
adds FWI actuals/forecasts to Forecast Summary tab
brettedw Oct 19, 2023
e17027e
Morecast 2.0 frontend
brettedw Oct 25, 2023
784ab60
Morecast 2.0 backend
brettedw Oct 25, 2023
a4bd266
updates api and updates forecasts in redux store
brettedw Oct 26, 2023
264b8ff
updates tests
brettedw Oct 26, 2023
9b58a8f
docstring
brettedw Oct 26, 2023
233e84a
Merge branch 'main' into task/simulated-indices
brettedw Oct 26, 2023
e1b5dca
forecast choice labels
brettedw Oct 30, 2023
19c7057
moves api call to func, adds success/fail slice
brettedw Oct 30, 2023
8670144
backend
brettedw Oct 30, 2023
abae508
Get extra day of determinates for fwi calc
brettedw Oct 30, 2023
bbde898
Merge branch 'main' into task/simulated-indices
brettedw Oct 30, 2023
0684687
Fix router test, handle empty dates
conbrad Oct 31, 2023
a6cd4e6
Merge branch 'main' into task/simulated-indices
conbrad Oct 31, 2023
bfeb943
Merge branch 'main' into task/simulated-indices
brettedw Oct 31, 2023
12892bc
add backend tests for get_fwi_values
brettedw Oct 31, 2023
649ae55
code smell
brettedw Oct 31, 2023
7657c36
Add dataslice tests
conbrad Oct 31, 2023
4fc612c
Merge branch 'task/simulated-indices' of github.com:bcgov/wps into ta…
conbrad Oct 31, 2023
f6006cc
endpoint tests
brettedw Oct 31, 2023
f071f13
unused imports
brettedw Oct 31, 2023
d20bef9
processRowUpdate bug squasher?
brettedw Nov 1, 2023
cf624cd
smelly code
brettedw Nov 1, 2023
057d039
Merge branch 'main' into task/simulated-indices
brettedw Nov 1, 2023
8c1667a
Merge branch 'main' into task/simulated-indices
brettedw Nov 1, 2023
1a046f1
always be able to handle multiple stations
brettedw Nov 2, 2023
cb18c6b
clean up imports
brettedw Nov 2, 2023
3488ab1
Merge branch 'main' into task/simulated-indices
brettedw Nov 2, 2023
0159c1d
update test_forecasts test
brettedw Nov 6, 2023
f7354e1
deal with falsy 0's in backend
brettedw Nov 6, 2023
01dc387
adds util tests
brettedw Nov 6, 2023
400adaa
adds row filtering tests
brettedw Nov 6, 2023
3d410f1
Merge branch 'main' into task/simulated-indices
brettedw Nov 7, 2023
e9ec48d
row filter tests and rowFilters.ts
brettedw Nov 7, 2023
1b57b96
update rowFilters test
brettedw Nov 7, 2023
b4759bc
Merge branch 'main' into task/simulated-indices
brettedw Nov 7, 2023
776db25
rowFilters test refinement
brettedw Nov 7, 2023
708aecd
typos
brettedw Nov 7, 2023
a4fa721
camel to snake
brettedw Nov 8, 2023
983e4bf
add tests for mapForecastChoiceLabels
brettedw Nov 8, 2023
3c937f5
address some comments
brettedw Nov 8, 2023
59dc6f4
address more comments
brettedw Nov 8, 2023
f1472a4
Merge branch 'main' into task/simulated-indices
brettedw Nov 8, 2023
862f276
add tests for rowFilters returning undefined
brettedw Nov 8, 2023
5afe044
Merge branch 'main' into task/simulated-indices
brettedw Nov 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions api/app/fire_behaviour/cffdrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,73 @@
raise CFFDRSException("Failed to calculate ffmc")


def initial_spread_index(ffmc: float, wind_speed: float, fbpMod: bool = False):
def duff_moisture_code(dmc: float, temperature: float, relative_humidity: float,
precipitation: float, latitude: float = 55, month: int = 7,
latitude_adjust: bool = True):
"""
Computes Duff Moisture Code (DMC) by delegating to the cffdrs R package.

R function signature:
function (dmc_yda, temp, rh, prec, lat, mon, lat.adjust = TRUE)

:param dmc: The Duff Moisture Code (unitless) of the previous day
:type dmc: float
:param temperature: Temperature (centigrade)
:type temperature: float
:param relative_humidity: Relative humidity (%)
:type relative_humidity: float
:param precipitation: 24-hour rainfall (mm)
:type precipitation: float
:param latitude: Latitude (decimal degrees), defaults to 55
:type latitude: float
:param month: Month of the year (1-12), defaults to 7 (July)
:type month: int, optional
:param latitude_adjust: Options for whether day length adjustments should be applied to
the calculation, defaults to True
:type latitude_adjust: bool, optional
"""
if dmc is None:
dmc = NULL

Check warning on line 579 in api/app/fire_behaviour/cffdrs.py

View check run for this annotation

Codecov / codecov/patch

api/app/fire_behaviour/cffdrs.py#L579

Added line #L579 was not covered by tests
result = CFFDRS.instance().cffdrs._dmcCalc(dmc, temperature, relative_humidity, precipitation,
latitude, month, latitude_adjust)
if isinstance(result[0], float):
return result[0]
raise CFFDRSException("Failed to calculate dmc")

Check warning on line 584 in api/app/fire_behaviour/cffdrs.py

View check run for this annotation

Codecov / codecov/patch

api/app/fire_behaviour/cffdrs.py#L584

Added line #L584 was not covered by tests


def drought_code(dc: float, temperature: float, relative_humidity: float, precipitation: float,
latitude: float = 55, month: int = 7, latitude_adjust: bool = True) -> None:
"""
Computes Drought Code (DC) by delegating to the cffdrs R package.

:param dc: The Drought Code (unitless) of the previous day
:type dc: float
:param temperature: Temperature (centigrade)
:type temperature: float
:param relative_humidity: Relative humidity (%)
:type relative_humidity: float
:param precipitation: 24-hour rainfall (mm)
:type precipitation: float
:param latitude: Latitude (decimal degrees), defaults to 55
:type latitude: float
:param month: Month of the year (1-12), defaults to 7 (July)
:type month: int, optional
:param latitude_adjust: Options for whether day length adjustments should be applied to
the calculation, defaults to True
:type latitude_adjust: bool, optional
:raises CFFDRSException:
:return: None
"""
if dc is None:
dc = NULL

Check warning on line 611 in api/app/fire_behaviour/cffdrs.py

View check run for this annotation

Codecov / codecov/patch

api/app/fire_behaviour/cffdrs.py#L611

Added line #L611 was not covered by tests
result = CFFDRS.instance().cffdrs._dcCalc(dc, temperature, relative_humidity, precipitation,
latitude, month, latitude_adjust)
if isinstance(result[0], float):
return result[0]
raise CFFDRSException("Failed to calculate dmc")

Check warning on line 616 in api/app/fire_behaviour/cffdrs.py

View check run for this annotation

Codecov / codecov/patch

api/app/fire_behaviour/cffdrs.py#L616

Added line #L616 was not covered by tests


def initial_spread_index(ffmc: float, wind_speed: float, fbp_mod: bool = False):
""" Computes Initial Spread Index (ISI) by delegating to cffdrs R package.
This is necessary when recalculating ROS/HFI for modified FFMC values. Otherwise,
should be using the ISI value retrieved from WFWX.
Expand All @@ -565,7 +631,7 @@
"""
if ffmc is None:
ffmc = NULL
result = CFFDRS.instance().cffdrs._ISIcalc(ffmc=ffmc, ws=wind_speed, fbpMod=fbpMod)
result = CFFDRS.instance().cffdrs._ISIcalc(ffmc=ffmc, ws=wind_speed, fbpMod=fbp_mod)
if isinstance(result[0], float):
return result[0]
raise CFFDRSException("Failed to calculate ISI")
Expand Down
100 changes: 96 additions & 4 deletions api/app/morecast_v2/forecasts.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@

from datetime import datetime, time
from datetime import datetime, time, timedelta
from urllib.parse import urljoin
from app import config

from aiohttp import ClientSession
from collections import defaultdict

from app.utils.time import vancouver_tz
from typing import List, Optional
from typing import List, Optional, Tuple
from sqlalchemy.orm import Session
from app.db.crud.morecast_v2 import get_forecasts_in_range
from app.db.models.morecast_v2 import MorecastForecastRecord
from app.schemas.morecast_v2 import MoreCastForecastOutput, StationDailyFromWF1, WF1ForecastRecordType, WF1PostForecast, WeatherIndeterminate
from app.schemas.morecast_v2 import MoreCastForecastOutput, StationDailyFromWF1, WF1ForecastRecordType, WF1PostForecast, WeatherIndeterminate, WeatherDeterminate
from app.wildfire_one.schema_parsers import WFWXWeatherStation
from app.wildfire_one.wfwx_api import get_auth_header, get_forecasts_for_stations_by_date_range, get_wfwx_stations_from_station_codes
from app.fire_behaviour import cffdrs


def get_forecasts(db_session: Session, start_time: datetime, end_time: datetime, station_codes: List[int]) -> List[MoreCastForecastOutput]:
def get_forecasts(db_session: Session, start_time: Optional[datetime], end_time: Optional[datetime], station_codes: List[int]) -> List[MoreCastForecastOutput]:
if start_time is None or end_time is None:
return []

result = get_forecasts_in_range(db_session, start_time, end_time, station_codes)

forecasts: List[WeatherIndeterminate] = [MoreCastForecastOutput(station_code=forecast.station_code,
Expand Down Expand Up @@ -105,3 +109,91 @@ def filter_for_api_forecasts(forecasts: List[WeatherIndeterminate], actuals: Lis
if actual_exists(forecast, actuals):
filtered_forecasts.append(forecast)
return filtered_forecasts


def get_fwi_values(actuals: List[WeatherIndeterminate], forecasts: List[WeatherIndeterminate]) -> Tuple[List[WeatherIndeterminate], List[WeatherIndeterminate]]:
dgboss marked this conversation as resolved.
Show resolved Hide resolved
"""
Calculates actuals and forecasts with Fire Weather Index System values by calculating based off previous actuals and subsequent forecasts.

:param actuals: List of actual weather values
:type actuals: List[WeatherIndeterminate]
:param forecasts: List of existing forecasted values
:type forecasts: List[WeatherIndeterminate]
:return: Actuals and forecasts with calculated fire weather index values
:rtype: Tuple[List[WeatherIndeterminate], List[WeatherIndeterminate]
"""
all_indeterminates = actuals + forecasts
indeterminates_dict = defaultdict(dict)

# Shape indeterminates into nested dicts for quick and easy look ups by station code and date
for indeterminate in all_indeterminates:
indeterminates_dict[indeterminate.station_code][indeterminate.utc_timestamp.date()] = indeterminate

for idx, indeterminate in enumerate(all_indeterminates):
last_indeterminate = indeterminates_dict[indeterminate.station_code].get(
indeterminate.utc_timestamp.date() - timedelta(days=1), None)
if last_indeterminate is not None:
updated_forecast = calculate_fwi_values(last_indeterminate, indeterminate)
all_indeterminates[idx] = updated_forecast

updated_forecasts = [indeterminate for indeterminate in all_indeterminates if indeterminate.determinate ==
WeatherDeterminate.FORECAST]
updated_actuals = [
indeterminate for indeterminate in all_indeterminates if indeterminate.determinate == WeatherDeterminate.ACTUAL]

return updated_actuals, updated_forecasts


def calculate_fwi_values(yesterday: WeatherIndeterminate, today: WeatherIndeterminate) -> WeatherIndeterminate:
"""
Uses CFFDRS library to calculate Fire Weather Index System values

:param yesterday: The WeatherIndeterminate from the day before the date to calculate
:type yesterday: WeatherIndeterminate
:param today: The WeatherIndeterminate from the date to calculate
:type today: WeatherIndeterminate
:return: Updated WeatherIndeterminate
:rtype: WeatherIndeterminate
"""

# weather params for calculation date
month_to_calculate_for = int(today.utc_timestamp.strftime('%m'))
latitude = today.latitude
temp = today.temperature
rh = today.relative_humidity
precip = today.precipitation
wind_spd = today.wind_speed

if yesterday.fine_fuel_moisture_code is not None:
today.fine_fuel_moisture_code = cffdrs.fine_fuel_moisture_code(ffmc=yesterday.fine_fuel_moisture_code,
temperature=temp,
relative_humidity=rh,
precipitation=precip,
wind_speed=wind_spd)
if yesterday.duff_moisture_code is not None:
today.duff_moisture_code = cffdrs.duff_moisture_code(dmc=yesterday.duff_moisture_code,
temperature=temp,
relative_humidity=rh,
precipitation=precip,
latitude=latitude,
month=month_to_calculate_for,
latitude_adjust=True
)
if yesterday.drought_code is not None:
today.drought_code = cffdrs.drought_code(dc=yesterday.drought_code,
temperature=temp,
relative_humidity=rh,
precipitation=precip,
latitude=latitude,
month=month_to_calculate_for,
latitude_adjust=True
)
if today.fine_fuel_moisture_code is not None:
today.initial_spread_index = cffdrs.initial_spread_index(ffmc=today.fine_fuel_moisture_code,
wind_speed=today.wind_speed)
if today.duff_moisture_code is not None and today.drought_code is not None:
today.build_up_index = cffdrs.bui_calc(dmc=today.duff_moisture_code, dc=today.drought_code)
if today.initial_spread_index is not None and today.build_up_index is not None:
today.fire_weather_index = cffdrs.fire_weather_index(isi=today.initial_spread_index, bui=today.build_up_index)

return today
38 changes: 33 additions & 5 deletions api/app/routers/morecast_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@
from app.db.crud.morecast_v2 import get_forecasts_in_range, get_user_forecasts_for_date, save_all_forecasts
from app.db.database import get_read_session_scope, get_write_session_scope
from app.db.models.morecast_v2 import MorecastForecastRecord
from app.morecast_v2.forecasts import filter_for_api_forecasts, get_forecasts
from app.morecast_v2.forecasts import filter_for_api_forecasts, get_forecasts, get_fwi_values
from app.schemas.morecast_v2 import (IndeterminateDailiesResponse,
MoreCastForecastOutput,
MoreCastForecastRequest,
MorecastForecastResponse,
ObservedDailiesForStations,
StationDailiesResponse,
WeatherIndeterminate)
WeatherIndeterminate,
WeatherDeterminate,
SimulateIndeterminateIndices,
SimulatedWeatherIndeterminateResponse)
from app.schemas.shared import StationsRequest
from app.wildfire_one.schema_parsers import transform_morecastforecastoutput_to_weatherindeterminate
from app.utils.time import get_hour_20_from_date, get_utc_now
Expand Down Expand Up @@ -182,21 +185,29 @@ async def get_determinates_for_date_range(start_date: date,
end_time = vancouver_tz.localize(datetime.combine(end_date, time.max))
start_date_of_interest = get_hour_20_from_date(start_date)
end_date_of_interest = get_hour_20_from_date(end_date)
start_date_for_fwi_calc = start_date_of_interest - timedelta(days=1)

async with ClientSession() as session:
header = await get_auth_header(session)
# get station information from the wfwx api
wfwx_stations = await get_wfwx_stations_from_station_codes(session, header, unique_station_codes)
wf1_actuals, wf1_forecasts = await get_daily_determinates_for_stations_and_date(session, header,
start_date_of_interest,
start_date_for_fwi_calc,
end_date_of_interest,
unique_station_codes)

wf1_actuals, wf1_forecasts = get_fwi_values(wf1_actuals, wf1_forecasts)

# drop the days before the date of interest that were needed to calculate fwi values
wf1_actuals = [actual for actual in wf1_actuals if actual.utc_timestamp >= start_date_of_interest]
wf1_forecasts = [forecast for forecast in wf1_forecasts if forecast.utc_timestamp >= start_date_of_interest]

# Find the min and max dates for actuals from wf1. These define the range of dates for which
# we need to retrieve forecasts from our API database. Note that not all stations report actuals
# at the same time, so every station won't necessarily have an actual for each date in the range.
wf1_actuals_dates = [actual.utc_timestamp for actual in wf1_actuals]
min_wf1_actuals_date = min(wf1_actuals_dates)
max_wf1_actuals_date = max(wf1_actuals_dates)
min_wf1_actuals_date = min(wf1_actuals_dates, default=None)
dgboss marked this conversation as resolved.
Show resolved Hide resolved
max_wf1_actuals_date = max(wf1_actuals_dates, default=None)

with get_read_session_scope() as db_session:
forecasts_from_db: List[MoreCastForecastOutput] = get_forecasts(
Expand All @@ -220,3 +231,20 @@ async def get_determinates_for_date_range(start_date: date,
actuals=wf1_actuals,
predictions=predictions,
forecasts=wf1_forecasts)


@router.post('/simulate-indices/', response_model=SimulatedWeatherIndeterminateResponse)
async def calculate_forecasted_indices(simulate_records: SimulateIndeterminateIndices):
"""
Returns forecasts with all Fire Weather Index System values calculated using the CFFDRS R library
"""
indeterminates = simulate_records.simulate_records
logger.info(
f'/simulate-indices/ - simulating forecast records for stations: {set(indeterminate.station_name for indeterminate in indeterminates)}')

forecasts = [indeterminate for indeterminate in indeterminates if indeterminate.determinate ==
WeatherDeterminate.FORECAST]
actuals = [indeterminate for indeterminate in indeterminates if indeterminate.determinate == WeatherDeterminate.ACTUAL]

_, forecasts = get_fwi_values(actuals, forecasts)
return (SimulatedWeatherIndeterminateResponse(simulated_forecasts=forecasts))
10 changes: 10 additions & 0 deletions api/app/schemas/morecast_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ class WeatherIndeterminate(BaseModel):
station_name: str
determinate: WeatherDeterminate
utc_timestamp: datetime
latitude: Optional[float] = None
longitude: Optional[float] = None
temperature: Optional[float] = None
relative_humidity: Optional[float] = None
precipitation: Optional[float] = None
Expand All @@ -141,6 +143,14 @@ class IndeterminateDailiesResponse(BaseModel):
forecasts: List[WeatherIndeterminate]


class SimulateIndeterminateIndices(BaseModel):
simulate_records: List[WeatherIndeterminate]


class SimulatedWeatherIndeterminateResponse(BaseModel):
simulated_forecasts: List[WeatherIndeterminate]


class WF1ForecastRecordType(BaseModel):
id: str = "FORECAST"
displayLabel: str = "Forecast"
Expand Down
3 changes: 3 additions & 0 deletions api/app/tests/fixtures/wf1/lookup.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@
},
"{'size': '1000', 'page': 0, 'startingTimestamp': 1678910400000, 'endingTimestamp': 1679256000000, 'stationIds': ['bfe0a6e2-e269-0210-e053-259e228e58c7', 'bfe0a6e2-e26b-0210-e053-259e228e58c7', 'bfe0a6e2-e3bc-0210-e053-259e228e58c7']}": {
"None": "wfwx/v1/dailies/search__findDailiesByStationIdIsInAndWeatherTimestampBetweenOrderByStationIdAscWeatherTimestampAsc__size_1000_page_0_startingTimestamp_1678910400000_endingTimestamp_1679256000000_statio.json"
},
"{'size': '1000', 'page': 0, 'startingTimestamp': 1678824000000, 'endingTimestamp': 1679256000000, 'stationIds': ['bfe0a6e2-e269-0210-e053-259e228e58c7', 'bfe0a6e2-e26b-0210-e053-259e228e58c7', 'bfe0a6e2-e3bc-0210-e053-259e228e58c7']}": {
"None": "wfwx/v1/dailies/search__findDailiesByStationIdIsInAndWeatherTimestampBetweenOrderByStationIdAscWeatherTimestampAsc__size_1000_page_0_startingTimestamp_1678824000000_endingTimestamp_1679256000000_statio.json"
}
}
}
Expand Down
Loading
Loading