async def get_fwi_calc_outputs(request: FWIRequest): """ Returns FWI calculations for all inputs """ try: logger.info('/fwi_calc/') # we're interested in noon on the given day time_of_interest = get_hour_20_from_date(request.date) async with ClientSession() as session: yesterday_actual, actual = await calculate_actual(session, request, time_of_interest) adjusted = await calculate_adjusted(request) output = FWIOutput( datetime=get_hour_20_from_date(request.date), yesterday=yesterday_actual, actual=actual, adjusted=adjusted ) return FWIOutputResponse(fwi_outputs=[output]) except Exception as exception: logger.critical(exception, exc_info=True) raise
async def get_multi_fwi_calc_outputs(request: MultiFWIRequest): """ Returns FWI calculations for all inputs """ try: logger.info('/fwi_calc/multi') outputs: List[MultiFWIOutput] = [] async with ClientSession() as session: for fwi_input in request.inputs: time_of_interest = get_hour_20_from_date(fwi_input.datetime) output: MultiFWIOutput = await multi_calculate_actual( session, fwi_input, request.stationCode, time_of_interest) outputs.append(output) return MultiFWIOutputResponse(multi_fwi_outputs=outputs) except Exception as exception: logger.critical(exception, exc_info=True) raise
def calculate_fire_behaviour_advisory( station: FBACalculatorWeatherStation) -> FireBehaviourAdvisory: """ Transform from the raw daily json object returned by wf1, to our fba_calc.StationResponse object. """ # pylint: disable=too-many-locals # time of interest will be the same for all stations. time_of_interest = get_hour_20_from_date(station.time_of_interest) fmc = cffdrs.foliar_moisture_content(station.lat, station.long, station.elevation, get_julian_date(time_of_interest)) sfc = cffdrs.surface_fuel_consumption(station.fuel_type, station.bui, station.ffmc, station.percentage_conifer) lb_ratio = cffdrs.length_to_breadth_ratio(station.fuel_type, station.wind_speed) ros = cffdrs.rate_of_spread(station.fuel_type, isi=station.isi, bui=station.bui, fmc=fmc, sfc=sfc, pc=station.percentage_conifer, cc=station.grass_cure, pdf=station.percentage_dead_balsam_fir, cbh=station.crown_base_height) cfb = calculate_cfb(station.fuel_type, fmc, sfc, ros, station.crown_base_height) # Calculate rate of spread assuming 60 minutes since ignition. ros_t = cffdrs.rate_of_spread_t(fuel_type=station.fuel_type, ros_eq=ros, minutes_since_ignition=60, cfb=cfb) cfb_t = calculate_cfb(station.fuel_type, fmc, sfc, ros_t, station.crown_base_height) # Get the default crown fuel load, if none specified. if station.crown_fuel_load is None: cfl = FUEL_TYPE_DEFAULTS[station.fuel_type].get('CFL', None) else: cfl = station.crown_fuel_load hfi = cffdrs.head_fire_intensity( fuel_type=station.fuel_type, percentage_conifer=station.percentage_conifer, percentage_dead_balsam_fir=station.percentage_dead_balsam_fir, ros=ros, cfb=cfb, cfl=cfl, sfc=sfc) hfi_t = cffdrs.head_fire_intensity( fuel_type=station.fuel_type, percentage_conifer=station.percentage_conifer, percentage_dead_balsam_fir=station.percentage_dead_balsam_fir, ros=ros_t, cfb=cfb_t, cfl=cfl, sfc=sfc) critical_hours_4000 = get_critical_hours( 4000, station.fuel_type, station.percentage_conifer, station.percentage_dead_balsam_fir, station.bui, station.grass_cure, station.crown_base_height, station.ffmc, fmc, cfb, cfl, station.wind_speed, station.prev_day_daily_ffmc, station.last_observed_morning_rh_values) critical_hours_10000 = get_critical_hours( 10000, station.fuel_type, station.percentage_conifer, station.percentage_dead_balsam_fir, station.bui, station.grass_cure, station.crown_base_height, station.ffmc, fmc, cfb, cfl, station.wind_speed, station.prev_day_daily_ffmc, station.last_observed_morning_rh_values) fire_type = get_fire_type(fuel_type=station.fuel_type, crown_fraction_burned=cfb) flame_length = get_approx_flame_length(hfi) wsv = cffdrs.calculate_wind_speed(fuel_type=station.fuel_type, ffmc=station.ffmc, bui=station.bui, ws=station.wind_speed, fmc=fmc, sfc=sfc, pc=station.percentage_conifer, cc=station.grass_cure, pdf=station.percentage_dead_balsam_fir, cbh=station.crown_base_height, isi=station.isi) bros = cffdrs.back_rate_of_spread(fuel_type=station.fuel_type, ffmc=station.ffmc, bui=station.bui, wsv=wsv, fmc=fmc, sfc=sfc, pc=station.percentage_conifer, cc=station.grass_cure, pdf=station.percentage_dead_balsam_fir, cbh=station.crown_base_height) sixty_minute_fire_size = get_fire_size(station.fuel_type, ros, bros, 60, cfb, lb_ratio) sixty_minute_fire_size_t = get_fire_size(station.fuel_type, ros_t, bros, 60, cfb_t, lb_ratio) thirty_minute_fire_size = get_fire_size(station.fuel_type, ros, bros, 30, cfb, lb_ratio) return FireBehaviourAdvisory( hfi=hfi, ros=ros, fire_type=fire_type, cfb=cfb, flame_length=flame_length, sixty_minute_fire_size=sixty_minute_fire_size, thirty_minute_fire_size=thirty_minute_fire_size, critical_hours_hfi_4000=critical_hours_4000, critical_hours_hfi_10000=critical_hours_10000, hfi_t=hfi_t, ros_t=ros_t, cfb_t=cfb_t, sixty_minute_fire_size_t=sixty_minute_fire_size_t)
def given_input(fuel_type: str, percentage_conifer: float, percentage_dead_balsam_fir: float, grass_cure: float, crown_base_height: float, num_iterations: int): """ Take input and calculate actual and expected results """ # get python result: # seed = time() seed = 43 logger.info('using random seed: %s', seed) random.seed(seed) results = [] for index in range(num_iterations): # pylint: disable=invalid-name elevation = random.randint(0, 4019) latitude = random.uniform(45, 60) longitude = random.uniform(-118, -136) time_of_interest = _random_date() # NOTE: For high wind speeds, the difference between REDapp and FireBAT starts exceeding # tolerances. REDapp calculates it's own ISI (doesn't take the one provided by the system), # but uses a different formula from CFFDRS, so the results start getting more pronounced # with higher wind speeds. For that reason we limit our wind speeds to 40 km/h, since anything # above that starts failing the unit tests. wind_speed = random.uniform(0, 40) wind_direction = random.uniform(0, 360) temperature = random.uniform(0, 49.6) # Lytton, B.C., 2021 relative_humidity = random.uniform(0, 100) precipitation = random.uniform(0, 50) dc = random.uniform(0, 600) dmc = random.uniform(11, 205) bui = bui_calc(dmc, dc) ffmc = random.uniform(11, 100) isi = initial_spread_index(ffmc, wind_speed) message = ( f"""({index}) elevation:{elevation} ; lat: {latitude} ; lon: {longitude}; """ f"""toi: {time_of_interest}; ws: {wind_speed}; wd: {wind_direction}; """ f"""temperature: {temperature}; relative_humidity: {relative_humidity}; """ f"""precipitation: {precipitation}; dc: {dc}; dmc: {dmc}; bui: {bui}; """ f"""ffmc: {ffmc}; isi: {isi}""") logger.debug(message) test_entry = ( f"""({index}) | {fuel_type} | {elevation} | {latitude} | {longitude} | """ f"""{time_of_interest} | {wind_speed} | {wind_direction} | {percentage_conifer} | """ f"""{percentage_dead_balsam_fir} | {grass_cure} | {crown_base_height} | {isi} | """ f"""{bui} | {ffmc} | {dmc} | {dc} | 0.01 | 0.01 | 0.01 | 0.01 | None | 0.01 | """ f"""None | 0.01 | None | 0.01 | |""") logger.debug(test_entry) python_input = FBACalculatorWeatherStation( elevation=elevation, fuel_type=FuelTypeEnum[fuel_type], time_of_interest=time_of_interest, percentage_conifer=percentage_conifer, percentage_dead_balsam_fir=percentage_dead_balsam_fir, grass_cure=grass_cure, crown_base_height=crown_base_height, crown_fuel_load=None, lat=latitude, long=longitude, bui=bui, ffmc=ffmc, isi=isi, wind_speed=wind_speed, wind_direction=wind_direction, temperature=temperature, relative_humidity=relative_humidity, precipitation=precipitation, status='Forecasted', prev_day_daily_ffmc=None, last_observed_morning_rh_values=None) python_fba = calculate_fire_behaviour_advisory(python_input) # get REDapp result from java: java_fbp = FBPCalculateStatisticsCOM( elevation=elevation, latitude=latitude, longitude=longitude, time_of_interest=get_hour_20_from_date(time_of_interest), fuel_type=fuel_type, ffmc=ffmc, dmc=dmc, dc=dc, bui=bui, wind_speed=wind_speed, wind_direction=wind_direction, percentage_conifer=percentage_conifer, percentage_dead_balsam_fir=percentage_dead_balsam_fir, grass_cure=grass_cure, crown_base_height=crown_base_height) error_dict = {'fuel_type': fuel_type} results.append({ 'input': { 'isi': isi, 'bui': bui, 'wind_speed': wind_speed, 'ffmc': ffmc }, 'python': python_fba, 'java': java_fbp, 'fuel_type': fuel_type, 'error': error_dict }) return results
def get_prep_day_dailies(dailies_date: date, area_dailies: List[StationDaily]) -> List[StationDaily]: """ Return all the dailies (that's noon, or 20 hours UTC) for a given date """ dailies_date_time = get_hour_20_from_date(dailies_date) return list(filter(lambda daily: (daily.date == dailies_date_time), area_dailies))
def given_red_app_input(elevation: float, # pylint: disable=too-many-arguments, invalid-name, too-many-locals latitude: float, longitude: float, time_of_interest: date, wind_speed: float, wind_direction: float, percentage_conifer: float, percentage_dead_balsam_fir: float, grass_cure: float, crown_base_height: float, isi: float, bui: float, ffmc: float, dmc: float, dc: float, fuel_type: str): """ Take input and calculate actual and expected results """ # get python result: python_input = FBACalculatorWeatherStation(elevation=elevation, fuel_type=FuelTypeEnum[fuel_type], time_of_interest=time_of_interest, percentage_conifer=percentage_conifer, percentage_dead_balsam_fir=percentage_dead_balsam_fir, grass_cure=grass_cure, crown_base_height=crown_base_height, crown_fuel_load=None, lat=latitude, long=longitude, bui=bui, ffmc=ffmc, isi=isi, wind_speed=wind_speed, wind_direction=wind_direction, temperature=20.0, # temporary fix so tests don't break relative_humidity=20.0, precipitation=2.0, status='Forecasted', prev_day_daily_ffmc=90.0, last_observed_morning_rh_values={ 7.0: 61.0, 8.0: 54.0, 9.0: 43.0, 10.0: 38.0, 11.0: 34.0, 12.0: 23.0}) python_fba = calculate_fire_behaviour_advisory(python_input) # get REDapp result from java: java_fbp = FBPCalculateStatisticsCOM(elevation=elevation, latitude=latitude, longitude=longitude, time_of_interest=get_hour_20_from_date(time_of_interest), fuel_type=fuel_type, ffmc=ffmc, dmc=dmc, dc=dc, bui=bui, wind_speed=wind_speed, wind_direction=wind_direction, percentage_conifer=percentage_conifer, percentage_dead_balsam_fir=percentage_dead_balsam_fir, grass_cure=grass_cure, crown_base_height=crown_base_height) # NOTE: REDapp has a ros_eq and a ros_t ; # assumptions: # ros_eq == ROScalc # ros_t == ROStcalc expected = { 'ros': java_fbp.ros_eq, 'ros_t': java_fbp.ros_t, 'cfb': java_fbp.cfb / 100.0, # CFFDRS gives cfb as a fraction 'hfi': java_fbp.hfi, 'area': java_fbp.area } error_dict = { 'fuel_type': fuel_type } return { 'python': python_fba, 'expected': expected, 'fuel_type': fuel_type, 'error': error_dict }
async def get_stations_data( # pylint:disable=too-many-locals request: StationListRequest, _=Depends(authentication_required)): """ Returns per-station data for a list of requested stations """ logger.info('/fba-calc/stations') try: # build a list of station codes station_codes = [station.station_code for station in request.stations] # remove any duplicate station codes unique_station_codes = list(dict.fromkeys(station_codes)) # we're interested in noon on the given day time_of_interest = get_hour_20_from_date(request.date) async with ClientSession() as session: # authenticate against wfwx api 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) # get the dailies for all the stations dailies = await get_dailies(session, header, wfwx_stations, time_of_interest) # turn it into a dictionary so we can easily get at data using a station id dailies_by_station_id = { raw_daily.get('stationId'): raw_daily async for raw_daily in dailies } # must retrieve the previous day's observed/forecasted FFMC value from WFWX prev_day = time_of_interest - timedelta(days=1) # get the "daily" data for the station for the previous day yesterday_response = await get_dailies(session, header, wfwx_stations, prev_day) # turn it into a dictionary so we can easily get at data yesterday_dailies_by_station_id = { raw_daily.get('stationId'): raw_daily async for raw_daily in yesterday_response } # get hourly observation history from our API (used for calculating morning diurnal FFMC) hourly_observations = await get_hourly_readings_in_time_interval( unique_station_codes, time_of_interest - timedelta(days=4), time_of_interest) # also turn hourly obs data into a dict indexed by station id hourly_obs_by_station_code = \ {raw_hourly.station.code: raw_hourly for raw_hourly in hourly_observations} # we need a lookup from station code to station id # TODO: this is a bit silly, the call to get_wfwx_stations_from_station_codes repeats a lot of this! wfwx_station_lookup = { wfwx_station.code: wfwx_station for wfwx_station in wfwx_stations } stations_response = [] # for each station code in our station list. for requested_station in request.stations: # get the wfwx station wfwx_station = wfwx_station_lookup[requested_station.station_code] # get the raw daily response from wf1. if wfwx_station.wfwx_id in dailies_by_station_id and\ requested_station.station_code in hourly_obs_by_station_code: try: station_response = await process_request( dailies_by_station_id, yesterday_dailies_by_station_id, hourly_obs_by_station_code, wfwx_station, requested_station, time_of_interest) except Exception as exception: # pylint: disable=broad-except # If something goes wrong processing the request, then we return this station # with an error response. logger.error('request object: %s', request.__str__()) logger.critical(exception, exc_info=True) station_response = process_request_without_observation( requested_station, wfwx_station, request.date, 'ERROR') else: # if we can't get the daily (no forecast, or no observation) station_response = process_request_without_observation( requested_station, wfwx_station, request.date, 'N/A') # Add the response to our list of responses stations_response.append(station_response) return StationsListResponse(date=request.date, stations=stations_response) except Exception as exception: logger.error('request object: %s', request.__str__()) logger.critical(exception, exc_info=True) raise