diff --git a/backend/packages/wps-api/Dockerfile b/backend/packages/wps-api/Dockerfile index 7ed8b5271e..c244da1dc4 100644 --- a/backend/packages/wps-api/Dockerfile +++ b/backend/packages/wps-api/Dockerfile @@ -12,12 +12,6 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get -y update RUN apt-get -y install unixodbc-dev libgdal-dev libudunits2-dev -# Install R -RUN apt-get update --fix-missing && apt-get -y install r-base - -# Install cffdrs -RUN R -e "install.packages('cffdrs')" - # Install other dependencies RUN apt-get -y install git build-essential python3 python3-dev python3-pip curl vim diff --git a/backend/packages/wps-api/pyproject.toml b/backend/packages/wps-api/pyproject.toml index d49d54945d..ac703f0fbc 100644 --- a/backend/packages/wps-api/pyproject.toml +++ b/backend/packages/wps-api/pyproject.toml @@ -27,7 +27,6 @@ dependencies = [ "jinja2>=3,<4", "numpy==2.3.*", "aiobotocore==2.15.1", - "rpy2>=3.4.5,<4", "redis>=7.0.0,<8", "pyjnius>=1.3.0,<2", "hiredis>=3.0.0,<4", diff --git a/backend/packages/wps-api/src/app/auto_spatial_advisory/critical_hours.py b/backend/packages/wps-api/src/app/auto_spatial_advisory/critical_hours.py index f03cf9630d..5f7c6fcc74 100644 --- a/backend/packages/wps-api/src/app/auto_spatial_advisory/critical_hours.py +++ b/backend/packages/wps-api/src/app/auto_spatial_advisory/critical_hours.py @@ -233,7 +233,7 @@ def calculate_critical_hours_for_station_by_fuel_type( pdf=percentage_dead_balsam_fir, cbh=crown_base_height, ) - cfb = calculate_cfb(fuel_type, fmc, sfc, ros, crown_base_height) + cfb = calculate_cfb(fuel_type, fmc, sfc, ros, crown_base_height, isi=isi, bui=bui) critical_hours = get_critical_hours( 4000, diff --git a/backend/packages/wps-api/src/app/fire_behaviour/advisory.py b/backend/packages/wps-api/src/app/fire_behaviour/advisory.py index 48e565562f..973ac3eb92 100644 --- a/backend/packages/wps-api/src/app/fire_behaviour/advisory.py +++ b/backend/packages/wps-api/src/app/fire_behaviour/advisory.py @@ -147,13 +147,13 @@ def calculate_fire_behaviour_advisory( pdf=station.percentage_dead_balsam_fir, cbh=station.crown_base_height, ) - cfb = calculate_cfb(station.fuel_type, fmc, sfc, ros, station.crown_base_height) + cfb = calculate_cfb(station.fuel_type, fmc, sfc, ros, station.crown_base_height, isi=station.isi, bui=station.bui) # 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) + cfb_t = calculate_cfb(station.fuel_type, fmc, sfc, ros_t, station.crown_base_height, isi=station.isi, bui=station.bui) # Get the default crown fuel load, if none specified. if station.crown_fuel_load is None: diff --git a/backend/packages/wps-api/src/app/fire_behaviour/cffdrs.py b/backend/packages/wps-api/src/app/fire_behaviour/cffdrs.py index 68e67aa467..484fa3bf4b 100644 --- a/backend/packages/wps-api/src/app/fire_behaviour/cffdrs.py +++ b/backend/packages/wps-api/src/app/fire_behaviour/cffdrs.py @@ -1,52 +1,92 @@ -""" This module contains functions for computing fire weather metrics. -""" +"""This module contains functions for computing fire weather metrics.""" + import logging import math from typing import Optional -import rpy2 -import rpy2.robjects as robjs -from rpy2.robjects import pandas2ri -from rpy2.rinterface import NULL + import pandas as pd -import app.utils.r_importer -from app.utils.singleton import Singleton +from cffdrs import ( + buildup_index, +) +from cffdrs import ( + drought_code as _drought_code, +) +from cffdrs import ( + duff_moisture_code as _duff_moisture_code, +) +from cffdrs import ( + fine_fuel_moisture_code as _fine_fuel_moisture_code, +) +from cffdrs import ( + fire_weather_index as _fire_weather_index, +) +from cffdrs import ( + initial_spread_index as _initial_spread_index, +) +from cffdrs.back_rate_of_spread import back_rate_of_spread as _back_rate_of_spread +from cffdrs.c6_calc import ( + crown_fraction_burned_c6, + crown_rate_of_spread_c6, + intermediate_surface_rate_of_spread_c6, + surface_rate_of_spread_c6, +) +from cffdrs.cfb_calc import critical_surface_intensity, surface_fire_rate_of_spread +from cffdrs.cfb_calc import crown_fraction_burned as _crown_fraction_burned +from cffdrs.distance_at_time import distance_at_time +from cffdrs.fire_intensity import fire_intensity +from cffdrs.foliar_moisture_content import foliar_moisture_content as _foliar_moisture_content +from cffdrs.hourly_fine_fuel_moisture_code import hourly_fine_fuel_moisture_code as _hourly_ffmc +from cffdrs.length_to_breadth import length_to_breadth +from cffdrs.length_to_breadth_at_time import length_to_breadth_at_time +from cffdrs.rate_of_spread import rate_of_spread as _rate_of_spread +from cffdrs.rate_of_spread_at_time import rate_of_spread_at_time +from cffdrs.slope_calc import slope_adjustment +from cffdrs.surface_fuel_consumption import surface_fuel_consumption as _surface_fuel_consumption +from cffdrs.total_fuel_consumption import total_fuel_consumption as _total_fuel_consumption from wps_shared.fuel_types import FuelTypeEnum - logger = logging.getLogger(__name__) -def _none2null(_): - """ Turn None values into null """ - return robjs.r("NULL") - - -none_converter = robjs.conversion.Converter("None converter") -none_converter.py2rpy.register(type(None), _none2null) - - -@Singleton -class CFFDRS(): - """ Singleton that loads CFFDRS R lib once in memory for reuse.""" +# Computable: SFC, FMC +# To store in DB: PC, PDF, CC, CBH (attached to fuel type, red book) +PARAMS_ERROR_MESSAGE = "One or more params passed to cffdrs call is None." - def __init__(self): - self.cffdrs = app.utils.r_importer.import_cffsdrs() +# Fuel types that require specific optional parameters. +_FUEL_TYPES_REQUIRING_PC = {FuelTypeEnum.M1, FuelTypeEnum.M2} +_FUEL_TYPES_REQUIRING_PDF = {FuelTypeEnum.M3, FuelTypeEnum.M4} +_FUEL_TYPES_REQUIRING_CC = {FuelTypeEnum.O1A, FuelTypeEnum.O1B} class CFFDRSException(Exception): - """ CFFDRS contextual exception """ + """CFFDRS contextual exception""" -# Computable: SFC, FMC -# To store in DB: PC, PDF, CC, CBH (attached to fuel type, red book) -PARAMS_ERROR_MESSAGE = "One or more params passed to R call is None." +def _validate_fuel_type_params( + fuel_type: FuelTypeEnum, pc: float, pdf: float, cc: float, cbh: float +): + """Raise CFFDRSException if a parameter required for the given fuel type is None. + + pc, pdf, and cc are only required for the fuel types that use them. + cbh is always required because cffdrs_py unconditionally computes critical surface intensity. + Callers should resolve cbh from FUEL_TYPE_DEFAULTS before calling cffdrs functions. + """ + if cbh is None: + raise CFFDRSException( + f"cbh is required for fuel_type {fuel_type.value}; pass crown_base_height or use FUEL_TYPE_DEFAULTS" + ) + if fuel_type in _FUEL_TYPES_REQUIRING_PC and pc is None: + raise CFFDRSException(f"pc is required for fuel_type {fuel_type.value}") + if fuel_type in _FUEL_TYPES_REQUIRING_PDF and pdf is None: + raise CFFDRSException(f"pdf is required for fuel_type {fuel_type.value}") + if fuel_type in _FUEL_TYPES_REQUIRING_CC and cc is None: + raise CFFDRSException(f"cc is required for fuel_type {fuel_type.value}") def correct_wind_azimuth(wind_direction: float): """ - #Corrections to reorient Wind Azimuth(WAZ) - WAZ <- WD + pi - WAZ <- ifelse(WAZ > 2 * pi, WAZ - 2 * pi, WAZ) + Corrections to reorient Wind Azimuth (WAZ). + WAZ = WD + pi; if WAZ > 2*pi: WAZ -= 2*pi """ if wind_direction is None: return None @@ -56,870 +96,544 @@ def correct_wind_azimuth(wind_direction: float): return waz -def calculate_wind_speed(fuel_type: FuelTypeEnum, - ffmc: float, - bui: float, - ws: float, - fmc: float, - sfc: float, - pc: float, - cc: float, - pdf: float, - cbh: float, - isi: float): - """ - Wind azimuth, slope azimuth, ground slope, net effective windspeed - """ +def calculate_wind_speed( + fuel_type: FuelTypeEnum, + ffmc: float, + bui: float, + ws: float, + fmc: float, + sfc: float, + pc: float, + cc: float, + pdf: float, + cbh: float, + isi: float, +): + """ + Wind azimuth, slope azimuth, ground slope, net effective windspeed + """ + if fuel_type is None or ffmc is None or bui is None or ws is None or fmc is None or sfc is None or isi is None: + raise CFFDRSException( + PARAMS_ERROR_MESSAGE + + f"calculate_wind_speed; fuel_type: {fuel_type}, ffmc: {ffmc}, bui: {bui}, ws: {ws}, fmc: {fmc}, sfc: {sfc}, isi: {isi}" + ) wind_azimuth = correct_wind_azimuth(ws) slope_azimuth = None # a.k.a. SAZ ground_slope = 0 # right now we're not taking slope into account - wsv = calculate_net_effective_windspeed(fuel_type=fuel_type, - ffmc=ffmc, - bui=bui, - ws=ws, - waz=wind_azimuth, - gs=ground_slope, - saz=slope_azimuth, - fmc=fmc, - sfc=sfc, - pc=pc, - cc=cc, - pdf=pdf, - cbh=cbh, - isi=isi) + wsv = calculate_net_effective_windspeed( + fuel_type=fuel_type, + ffmc=ffmc, + bui=bui, + ws=ws, + waz=wind_azimuth, + gs=ground_slope, + saz=slope_azimuth, + fmc=fmc, + sfc=sfc, + pc=pc, + cc=cc, + pdf=pdf, + cbh=cbh, + isi=isi, + ) return wsv -def calculate_net_effective_windspeed(fuel_type: FuelTypeEnum, - ffmc: float, - bui: float, - ws: float, - waz: float, - gs: float, - saz: Optional[float], - fmc: float, - sfc: float, - pc: float, - cc: float, - pdf: float, - cbh: float, - isi: float): - """ - #Calculate the net effective windspeed (WSV) - WSV0 <- .Slopecalc(FUELTYPE, FFMC, BUI, WS, WAZ, GS, SAZ, - FMC, SFC, PC, PDF, CC, CBH, ISI, output = "WSV") - WSV <- ifelse(GS > 0 & FFMC > 0, WSV0, WS) - """ +def calculate_net_effective_windspeed( + fuel_type: FuelTypeEnum, + ffmc: float, + bui: float, + ws: float, + waz: float, + gs: float, + saz: Optional[float], + fmc: float, + sfc: float, + pc: float, + cc: float, + pdf: float, + cbh: float, + isi: float, +): + """ + Calculate the net effective windspeed (WSV). + """ + if fuel_type is None or ffmc is None or bui is None or ws is None or gs is None or fmc is None or sfc is None or isi is None: + raise CFFDRSException( + PARAMS_ERROR_MESSAGE + + f"calculate_net_effective_windspeed; fuel_type: {fuel_type}, ffmc: {ffmc}, bui: {bui}, ws: {ws}, gs: {gs}, fmc: {fmc}, sfc: {sfc}, isi: {isi}" + ) if gs > 0 and ffmc > 0: - # Description: - # Calculate the net effective windspeed (WSV), the net effective wind - # direction (RAZ) or the wind azimuth (WAZ). - # - # All variables names are laid out in the same manner as FCFDG (1992) and - # Wotton (2009). - # - # - # Forestry Canada Fire Danger Group (FCFDG) (1992). "Development and - # Structure of the Canadian Forest Fire Behavior Prediction System." - # Technical Report ST-X-3, Forestry Canada, Ottawa, Ontario. - # - # Wotton, B.M., Alexander, M.E., Taylor, S.W. 2009. Updates and revisions to - # the 1992 Canadian forest fire behavior prediction system. Nat. Resour. - # Can., Can. For. Serv., Great Lakes For. Cent., Sault Ste. Marie, Ontario, - # Canada. Information Report GLC-X-10, 45p. - # - # Args: - # FUELTYPE: The Fire Behaviour Prediction FuelType - # FFMC: Fine Fuel Moisture Code - # BUI: The Buildup Index value - # WS: Windspeed (km/h) - # WAZ: Wind Azimuth - # GS: Ground Slope (%) - # SAZ: Slope Azimuth - # FMC: Foliar Moisture Content - # SFC: Surface Fuel Consumption (kg/m^2) - # PC: Percent Conifer (%) - # PDF: Percent Dead Balsam Fir (%) - # CC: Constant - # CBH: Crown Base Height (m) - # ISI: Initial Spread Index - # output: Type of variable to output (RAZ/WSV, default=RAZ) - # Returns: - # BE: The Buildup Effect - result = CFFDRS.instance().cffdrs._Slopecalc(FUELTYPE=fuel_type.value, - FFMC=ffmc, - BUI=bui, - WS=ws, - WAZ=waz, - GS=gs, - SAZ=saz, - FMC=fmc, - SFC=sfc, - PC=pc, - PDF=pdf, - CC=cc, - CBH=cbh, - ISI=isi, - output="WSV") - if isinstance(result[0], float): - return result[0] - raise CFFDRSException("Failed to calculate Slope") + _validate_fuel_type_params(fuel_type, pc, pdf, cc, cbh) + result = slope_adjustment( + fuel_type=fuel_type.value, + ffmc=ffmc, + bui=bui, + ws=ws, + waz=waz, + gs=gs, + saz=saz, + fmc=fmc, + sfc=sfc, + pc=pc, + pdf=pdf, + cc=cc, + cbh=cbh, + isi=isi, + ) + return result.wsv return ws -def flank_rate_of_spread(ros: float, bros: float, lb: float): - """ - # Description: - # Calculate the Flank Fire Spread Rate. - # - # All variables names are laid out in the same manner as Forestry Canada - # Fire Danger Group (FCFDG) (1992). Development and Structure of the - # Canadian Forest Fire Behavior Prediction System." Technical Report - # ST-X-3, Forestry Canada, Ottawa, Ontario. - # - # Args: - # ROS: Fire Rate of Spread (m/min) - # BROS: Back Fire Rate of Spread (m/min) - # LB: Length to breadth ratio - # - - # Returns: - # FROS: Flank Fire Spread Rate (m/min) - # - """ - result = CFFDRS.instance().cffdrs._FROScalc(ROS=ros, BROS=bros, LB=lb) - if isinstance(result[0], float): - return result[0] - raise CFFDRSException("Failed to calculate FROS") - - -def back_rate_of_spread(fuel_type: FuelTypeEnum, - ffmc: float, - bui: float, - wsv: float, - fmc: float, - sfc: float, - pc: float, - cc: float, - pdf: float, - cbh: float): - """ - # Description: - # Calculate the Back Fire Spread Rate. - # - # All variables names are laid out in the same manner as Forestry Canada - # Fire Danger Group (FCFDG) (1992). Development and Structure of the - # Canadian Forest Fire Behavior Prediction System." Technical Report - # ST-X-3, Forestry Canada, Ottawa, Ontario. - # - # Args: - # FUELTYPE: The Fire Behaviour Prediction FuelType - # FFMC: Fine Fuel Moisture Code - # BUI: Buildup Index - # WSV: Wind Speed Vector - # FMC: Foliar Moisture Content - # SFC: Surface Fuel Consumption - # PC: Percent Conifer - # PDF: Percent Dead Balsam Fir - # CC: Degree of Curing (just "C" in FCFDG 1992) - # CBH: Crown Base Height - - # Returns: - # BROS: Back Fire Spread Rate - # - """ - - if fuel_type is None or ffmc is None or bui is None or fmc is None or sfc is None: - message = PARAMS_ERROR_MESSAGE + \ - f"_BROScalc ; fuel_type: {fuel_type.value}, ffmc: {ffmc}, bui: {bui}, fmc: {fmc}, sfc: {sfc}" +def back_rate_of_spread( + fuel_type: FuelTypeEnum, + ffmc: float, + bui: float, + wsv: float, + fmc: float, + sfc: float, + pc: float, + cc: float, + pdf: float, + cbh: float, +): + """Calculate the Back Fire Spread Rate.""" + if ( + fuel_type is None + or ffmc is None + or bui is None + or wsv is None + or fmc is None + or sfc is None + ): + message = ( + PARAMS_ERROR_MESSAGE + + f"_BROScalc ; fuel_type: {fuel_type.value}, ffmc: {ffmc}, bui: {bui}, wsv: {wsv}, fmc: {fmc}, sfc: {sfc}" + ) raise CFFDRSException(message) - - if pc is None: - pc = NULL - if cc is None: - cc = NULL - if pdf is None: - pdf = NULL - if cbh is None: - cbh = NULL - if wsv is None: - wsv = NULL - result = CFFDRS.instance().cffdrs._BROScalc(FUELTYPE=fuel_type.value, - FFMC=ffmc, - BUI=bui, - WSV=wsv, - FMC=fmc, - SFC=sfc, - PC=pc, - PDF=pdf, - CC=cc, - CBH=cbh) - if isinstance(result[0], float): - return result[0] - raise CFFDRSException("Failed to calculate BROS") + _validate_fuel_type_params(fuel_type, pc, pdf, cc, cbh) + + return _back_rate_of_spread( + fuel_type=fuel_type.value, + ffmc=ffmc, + bui=bui, + wsv=wsv, + fmc=fmc, + sfc=sfc, + pc=pc, + pdf=pdf, + cc=cc, + cbh=cbh, + ) def bui_calc(dmc: float, dc: float): - """ - # Description: Buildup Index Calculation. All code - # is based on a C code library that was written by Canadian - # Forest Service Employees, which was originally based on - # the Fortran code listed in the reference below. All equations - # in this code refer to that document. - # - # Equations and FORTRAN program for the Canadian Forest Fire - # Weather Index System. 1985. Van Wagner, C.E.; Pickett, T.L. - # Canadian Forestry Service, Petawawa National Forestry - # Institute, Chalk River, Ontario. Forestry Technical Report 33. - # 18 p. - # - # Additional reference on FWI system - # - # Development and structure of the Canadian Forest Fire Weather - # Index System. 1987. Van Wagner, C.E. Canadian Forestry Service, - # Headquarters, Ottawa. Forestry Technical Report 35. 35 p. - # - # - # Args: dc: Drought Code - # dmc: Duff Moisture Code - # - # Returns: A single bui value - """ - result = CFFDRS.instance().cffdrs._buiCalc(dmc=dmc, dc=dc) - if isinstance(result[0], float): - return result[0] - raise CFFDRSException("Failed to calculate bui") - + """Buildup Index calculation. -def rate_of_spread_t(fuel_type: FuelTypeEnum, - ros_eq: float, - minutes_since_ignition: float, - cfb: float): - """ - # Description: - # Computes the Rate of Spread prediction based on fuel type and FWI - # conditions at elapsed time since ignition. Equations are from listed - # FCFDG (1992). - # - # All variables names are laid out in the same manner as Forestry Canada - # Fire Danger Group (FCFDG) (1992). Development and Structure of the - # Canadian Forest Fire Behavior Prediction System." Technical Report - # ST-X-3, Forestry Canada, Ottawa, Ontario. - # - # Args: - # FUELTYPE: The Fire Behaviour Prediction FuelType - # ROSeq: Equilibrium Rate of Spread (m/min) - # HR: Time since ignition (hours) - # CFB: Crown Fraction Burned - # Returns: - # ROSt: Rate of Spread at time since ignition - # - """ - # NOTE: CFFDRS documentation incorrectly states that HR is hours since ignition, it's actually - # minutes. - result = CFFDRS.instance().cffdrs._ROStcalc(FUELTYPE=fuel_type.value, - ROSeq=ros_eq, - HR=minutes_since_ignition, - CFB=cfb) - if isinstance(result[0], float): - return result[0] - raise CFFDRSException("Failed to calculate ROSt") - - -def rate_of_spread(fuel_type: FuelTypeEnum, - isi: float, - bui: float, - fmc: float, - sfc: float, - pc: float, - cc: float, - pdf: float, - cbh: float): - """ Computes ROS by delegating to cffdrs R package. - pdf: Percent Dead Balsam Fir (%) - - # From cffdrs R package comments: - # FUELTYPE: The Fire Behaviour Prediction FuelType - # ISI: Initial Spread Index - # BUI: Buildup Index - # FMC: Foliar Moisture Content - # SFC: Surface Fuel Consumption (kg/m^2) - # PC: Percent Conifer (%) - # PDF: Percent Dead Balsam Fir (%) - # CC: Constant (we think this is grass cure.) - # CBH: Crown to base height(m) - - # Returns: - # ROS: Rate of spread (m/min) - # - NOTE: For C1, only ISI and BUI is used to calculate ROS. All other inputs are ignored. + Returns None if any input is None. See fine_fuel_moisture_code for rationale. """ + if dmc is None or dc is None: + return None + return buildup_index(dmc, dc) + + +def rate_of_spread_t( + fuel_type: FuelTypeEnum, ros_eq: float, minutes_since_ignition: float, cfb: float +): + """ + Computes the Rate of Spread prediction at elapsed time since ignition. + NOTE: HR is minutes since ignition (not hours, despite the documentation). + """ + if fuel_type is None or ros_eq is None or minutes_since_ignition is None or cfb is None: + raise CFFDRSException( + PARAMS_ERROR_MESSAGE + + f"rate_of_spread_t; fuel_type: {fuel_type}, ros_eq: {ros_eq}, minutes_since_ignition: {minutes_since_ignition}, cfb: {cfb}" + ) + return rate_of_spread_at_time(fuel_type.value, ros_eq, minutes_since_ignition, cfb) + + +def rate_of_spread( + fuel_type: FuelTypeEnum, + isi: float, + bui: float, + fmc: float, + sfc: float, + pc: float, + cc: float, + pdf: float, + cbh: float, +): + """Computes Rate of Spread (m/min).""" if fuel_type is None or isi is None or bui is None or sfc is None: - message = PARAMS_ERROR_MESSAGE + \ - f"_ROScalc ; fuel_type: {fuel_type.value}, isi: {isi}, bui: {bui}, fmc: {fmc}, sfc: {sfc}" + message = ( + PARAMS_ERROR_MESSAGE + + f"_ROScalc ; fuel_type: {fuel_type.value}, isi: {isi}, bui: {bui}, fmc: {fmc}, sfc: {sfc}" + ) raise CFFDRSException(message) - - # For some reason, the registered converter can't turn a None to a NULL, but we need to - # set these to NULL, despite setting a converter for None to NULL, because it it can only - # convert a NULL to NULL. Doesn't make sense? Exactly. - # https://www.google.com/url?sa=i&url=https%3A%2F%2Fwww.deviantart.com%2Ffirefox2014%2Fart%2FJackie-Chan-Meme-525778492&psig=AOvVaw3WsEdtu_OswdactmBuGmtH&ust=1625962534127000&source=images&cd=vfe&ved=0CAoQjRxqFwoTCNCc0dac1_ECFQAAAAAdAAAAABAD - if pc is None: - pc = NULL - if cc is None: - cc = NULL - if pdf is None: - pdf = NULL - if cbh is None: - cbh = NULL - result = CFFDRS.instance().cffdrs._ROScalc(FUELTYPE=fuel_type.value, - ISI=isi, - BUI=bui, - FMC=fmc, - SFC=sfc, - PC=pc, - PDF=pdf, - CC=cc, - CBH=cbh) - if isinstance(result[0], float): - return result[0] - raise CFFDRSException("Failed to calculate ROS") - - -def surface_fuel_consumption( - fuel_type: FuelTypeEnum, - bui: float, - ffmc: float, - pc: float): - """ Computes SFC by delegating to cffdrs R package - Assumes a standard GFL of 0.35 kg/m ^ 2. - - # Args: - # FUELTYPE: The Fire Behaviour Prediction FuelType - # BUI: Buildup Index - # FFMC: Fine Fuel Moisture Code - # PC: Percent Conifer (%) - # GFL: Grass Fuel Load (kg/m^2) (0.35 kg/m^2) - # Returns: - # SFC: Surface Fuel Consumption (kg/m^2) - """ + _validate_fuel_type_params(fuel_type, pc, pdf, cc, cbh) + + return _rate_of_spread( + fuel_type=fuel_type.value, + isi=isi, + bui=bui, + fmc=fmc, + sfc=sfc, + pc=pc, + pdf=pdf, + cc=cc, + cbh=cbh, + ) + + +def surface_fuel_consumption(fuel_type: FuelTypeEnum, bui: float, ffmc: float, pc: float): + """Computes SFC. Assumes a standard GFL of 0.35 kg/m^2.""" if fuel_type is None or bui is None or ffmc is None: - message = PARAMS_ERROR_MESSAGE + \ - f"_SFCcalc; fuel_type: {fuel_type.value}, bui: {bui}, ffmc: {ffmc}" + message = ( + PARAMS_ERROR_MESSAGE + + f"_SFCcalc; fuel_type: {fuel_type.value}, bui: {bui}, ffmc: {ffmc}" + ) raise CFFDRSException(message) - if pc is None: - pc = NULL - result = CFFDRS.instance().cffdrs._SFCcalc(FUELTYPE=fuel_type.value, - BUI=bui, - FFMC=ffmc, - PC=pc, - GFL=0.35) - if isinstance(result[0], float): - return result[0] - raise CFFDRSException("Failed to calculate SFC") + if fuel_type in _FUEL_TYPES_REQUIRING_PC and pc is None: + raise CFFDRSException(f"pc is required for fuel_type {fuel_type.value}") + + # Note: cffdrs_py signature is (fuel_type, ffmc, bui, pc, gfl) — ffmc before bui + return _surface_fuel_consumption( + fuel_type=fuel_type.value, + ffmc=ffmc, + bui=bui, + pc=pc, + gfl=0.35, + ) def fire_distance(fuel_type: FuelTypeEnum, ros_eq: float, hr: int, cfb: float): - """ - # Description: - # Calculate the Head fire spread distance at time t. In the documentation - # this variable is just "D". - # - # All variables names are laid out in the same manner as Forestry Canada - # Fire Danger Group (FCFDG) (1992). Development and Structure of the - # Canadian Forest Fire Behavior Prediction System." Technical Report - # ST-X-3, Forestry Canada, Ottawa, Ontario. - # - # Args: - # FUELTYPE: The Fire Behaviour Prediction FuelType - # ROSeq: The predicted equilibrium rate of spread (m/min) - # HR (t): The elapsed time (min) - # CFB: Crown Fraction Burned - # - # Returns: - # DISTt: Head fire spread distance at time t - """ - result = CFFDRS.instance().cffdrs._DISTtcalc(fuel_type.value, ros_eq, hr, cfb) - if isinstance(result[0], float): - return result[0] - raise CFFDRSException("Failed to calculate DISTt") - - -def foliar_moisture_content(lat: int, long: int, elv: float, day_of_year: int, - date_of_minimum_foliar_moisture_content: int = 0): - """ Computes FMC by delegating to cffdrs R package - TODO: Find out the minimum fmc date that is passed as D0, for now it's 0. Passing 0 makes FFMCcalc - calculate it. - - # Args: - # LAT: Latitude (decimal degrees) - # LONG: Longitude (decimal degrees) - # ELV: Elevation (metres) - # DJ: Day of year (often referred to as julian date) - # D0: Date of minimum foliar moisture content - # (constant date, set by geography across province, 5 different dates) - # - # Returns: - # FMC: Foliar Moisture Content - """ - logger.debug('calling _FMCcalc(LAT=%s, LONG=%s, ELV=%s, DJ=%s, D0=%s)', lat, - long, elv, day_of_year, date_of_minimum_foliar_moisture_content) + """Calculate the Head fire spread distance at time t.""" + if fuel_type is None or ros_eq is None or hr is None or cfb is None: + raise CFFDRSException( + PARAMS_ERROR_MESSAGE + + f"fire_distance; fuel_type: {fuel_type}, ros_eq: {ros_eq}, hr: {hr}, cfb: {cfb}" + ) + return distance_at_time(fuel_type.value, ros_eq, hr, cfb) + + +def foliar_moisture_content( + lat: int, + long: int, + elv: float, + day_of_year: int, + date_of_minimum_foliar_moisture_content: int = 0, +): + """Computes FMC.""" + if lat is None or long is None or elv is None or day_of_year is None: + raise CFFDRSException( + PARAMS_ERROR_MESSAGE + + f"foliar_moisture_content; lat: {lat}, long: {long}, elv: {elv}, day_of_year: {day_of_year}" + ) + logger.debug( + "calling FMCcalc(LAT=%s, LONG=%s, ELV=%s, DJ=%s, D0=%s)", + lat, + long, + elv, + day_of_year, + date_of_minimum_foliar_moisture_content, + ) # FMCcalc expects longitude to always be a positive number. if long < 0: long = -long - result = CFFDRS.instance().cffdrs._FMCcalc(LAT=lat, LONG=long, ELV=elv, - DJ=day_of_year, D0=date_of_minimum_foliar_moisture_content) - if isinstance(result[0], float): - return result[0] - raise CFFDRSException("Failed to calculate FMC") + return _foliar_moisture_content( + lat=lat, long=long, elv=elv, dj=day_of_year, d0=date_of_minimum_foliar_moisture_content + ) def length_to_breadth_ratio(fuel_type: FuelTypeEnum, wind_speed: float): - """ Computes L/B ratio by delegating to cffdrs R package - - # Args: - # FUELTYPE: The Fire Behaviour Prediction FuelType - # WSV: The Wind Speed (km/h) - # Returns: - # LB: Length to Breadth ratio - """ + """Computes L/B ratio.""" if wind_speed is None or fuel_type is None: - return CFFDRSException() - result = CFFDRS.instance().cffdrs._LBcalc(FUELTYPE=fuel_type.value, WSV=wind_speed) - if isinstance(result[0], float): - return result[0] - raise CFFDRSException("Failed to calculate LB") - - -def length_to_breadth_ratio_t(fuel_type: FuelTypeEnum, - lb: float, - time_since_ignition: float, - cfb: float): - """ Computes L/B ratio by delegating to cffdrs R package - - # Description: - # Computes the Length to Breadth ratio of an elliptically shaped fire at - # elapsed time since ignition. Equations are from listed FCFDG (1992) and - # Wotton et. al. (2009), and are marked as such. - # - # All variables names are laid out in the same manner as Forestry Canada - # Fire Danger Group (FCFDG) (1992). Development and Structure of the - # Canadian Forest Fire Behavior Prediction System." Technical Report - # ST-X-3, Forestry Canada, Ottawa, Ontario. - # - # Wotton, B.M., Alexander, M.E., Taylor, S.W. 2009. Updates and revisions to - # the 1992 Canadian forest fire behavior prediction system. Nat. Resour. - # Can., Can. For. Serv., Great Lakes For. Cent., Sault Ste. Marie, Ontario, - # Canada. Information Report GLC-X-10, 45p. - # - # Args: - # FUELTYPE: The Fire Behaviour Prediction FuelType - # LB: Length to Breadth ratio - # HR: Time since ignition (hours) - # CFB: Crown Fraction Burned - # Returns: - # LBt: Length to Breadth ratio at time since ignition - # + raise CFFDRSException( + PARAMS_ERROR_MESSAGE + + f"length_to_breadth_ratio; fuel_type: {fuel_type}, wind_speed: {wind_speed}" + ) + return length_to_breadth(fuel_type.value, wind_speed) + + +def length_to_breadth_ratio_t( + fuel_type: FuelTypeEnum, lb: float, time_since_ignition: float, cfb: float +): + """Computes L/B ratio at elapsed time since ignition.""" + if fuel_type is None or lb is None or time_since_ignition is None or cfb is None: + raise CFFDRSException( + PARAMS_ERROR_MESSAGE + + f"length_to_breadth_ratio_t; fuel_type: {fuel_type}, lb: {lb}, time_since_ignition: {time_since_ignition}, cfb: {cfb}" + ) + return length_to_breadth_at_time(fuel_type.value, lb, time_since_ignition, cfb) + + +def fine_fuel_moisture_code( + ffmc: float, + temperature: float, + relative_humidity: float, + precipitation: float, + wind_speed: float, +): + """Computes Fine Fuel Moisture Code (FFMC). + + Returns None if any input is None. This is intentional: callers chain FWI calculations + (ffmc → isi → fwi) and rely on None propagating through the chain rather than raising. """ - result = CFFDRS.instance().cffdrs._LBtcalc(FUELTYPE=fuel_type.value, LB=lb, - HR=time_since_ignition, CFB=cfb) - if isinstance(result[0], float): - return result[0] - raise CFFDRSException("Failed to calculate LBt") - - -def fine_fuel_moisture_code(ffmc: float, temperature: float, relative_humidity: float, - precipitation: float, wind_speed: float): - """ Computes Fine Fuel Moisture Code (FFMC) by delegating to cffdrs R package. - This is necessary when recalculating certain fire weather indices based on - user-defined input for wind speed. - - # Args: ffmc_yda: The Fine Fuel Moisture Code from previous iteration - # temp: Temperature (centigrade) - # rh: Relative Humidity (%) - # prec: Precipitation (mm) - # ws: Wind speed (km/h) - # - # - # Returns: A single ffmc value - """ - if ffmc is None: 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") + if temperature is None or relative_humidity is None or precipitation is None: return None - if isinstance(result[0], float): - return result[0] - - 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, - 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 + return _fine_fuel_moisture_code( + ffmc_yda=ffmc, temp=temperature, rh=relative_humidity, ws=wind_speed, prec=precipitation + ) + + +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). + + Returns None if any required input is None. See fine_fuel_moisture_code for rationale. """ if dmc is None: 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 + if temperature is None or relative_humidity is None or precipitation is None: + return None if latitude is None: latitude = 55 if month is None: month = 7 - 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] - logger.error("Failed to calculate DMC") - return None - - -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 + return _duff_moisture_code( + dmc_yda=dmc, + temp=temperature, + rh=relative_humidity, + prec=precipitation, + lat=latitude, + mon=month, + lat_adjust=latitude_adjust, + ) + + +def drought_code( + dc: float, + temperature: float, + relative_humidity: float, + precipitation: float, + latitude: float = 55, + month: int = 7, + latitude_adjust: bool = True, +): + """Computes Drought Code (DC). + + Returns None if any required input is None. See fine_fuel_moisture_code for rationale. """ if dc is None: 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 + if temperature is None or relative_humidity is None or precipitation is None: + return None if latitude is None: latitude = 55 if month is None: month = 7 - 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] - logger.error("Failed to calculate DC") - return None + return _drought_code( + dc_yda=dc, + temp=temperature, + rh=relative_humidity, + prec=precipitation, + lat=latitude, + mon=month, + lat_adjust=latitude_adjust, + ) 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. - - # Args: - # ffmc: Fine Fuel Moisture Code - # ws: Wind Speed (km/h) - # fbpMod: TRUE/FALSE if using the fbp modification at the extreme end - # - # Returns: - # ISI: Intial Spread Index - """ - if ffmc is None: - ffmc = NULL - 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") - - -def fire_weather_index(isi: float, bui: float): - """ Computes Fire Weather Index (FWI) by delegating to cffdrs R package. - - Args: isi: Initial Spread Index - bui: Buildup Index + """Computes Initial Spread Index (ISI). - Returns: A single fwi value + Returns None if any input is None. See fine_fuel_moisture_code for rationale. """ + if ffmc is None or wind_speed is None: + return None + return _initial_spread_index(ffmc=ffmc, ws=wind_speed, fbp_mod=fbp_mod) - result = CFFDRS.instance().cffdrs._fwiCalc(isi=isi, bui=bui) - if isinstance(result[0], float): - return result[0] - raise CFFDRSException("Failed to calculate fwi") - - -def crown_fraction_burned(fuel_type: FuelTypeEnum, fmc: float, sfc: float, - ros: float, cbh: float) -> float: - """ Computes Crown Fraction Burned (CFB) by delegating to cffdrs R package. - Value returned will be between 0-1. - # Args: - # FUELTYPE: The Fire Behaviour Prediction FuelType - # FMC: Foliar Moisture Content - # SFC: Surface Fuel Consumption - # CBH: Crown Base Height - # ROS: Rate of Spread - # option: Which variable to calculate(ROS, CFB, RSC, or RSI) +def fire_weather_index(isi: float, bui: float): + """Computes Fire Weather Index (FWI). - # Returns: - # CFB, CSI, RSO depending on which option was selected. + Returns None if any input is None. See fine_fuel_moisture_code for rationale. """ - if cbh is None: - cbh = NULL - if cbh is None or fmc is None: - message = PARAMS_ERROR_MESSAGE + \ - f"_CFBcalc; fuel_type: {fuel_type.value}, cbh: {cbh}, fmc: {fmc}" + if isi is None or bui is None: + return None + return _fire_weather_index(isi=isi, bui=bui) + + +def crown_fraction_burned( + fuel_type: FuelTypeEnum, + fmc: float, + sfc: float, + ros: float, + cbh: float, + isi: Optional[float] = None, + bui: Optional[float] = None, +) -> float: + """Computes Crown Fraction Burned (CFB). Value returned will be between 0-1. + + C6 requires isi and bui to compute the crown and surface rates of spread separately, + which are inputs to the C6-specific CFB formula. + """ + if cbh is None or fmc is None or sfc is None or ros is None: + message = ( + PARAMS_ERROR_MESSAGE + + f"_CFBcalc; fuel_type: {fuel_type.value}, cbh: {cbh}, fmc: {fmc}, sfc: {sfc}, ros: {ros}" + ) raise CFFDRSException(message) - result = CFFDRS.instance().cffdrs._CFBcalc(FUELTYPE=fuel_type.value, FMC=fmc, SFC=sfc, - ROS=ros, CBH=cbh) - if isinstance(result[0], float): - return result[0] - raise CFFDRSException("Failed to calculate CFB") + csi = critical_surface_intensity(fmc, cbh) + rso = surface_fire_rate_of_spread(csi, sfc) + if fuel_type == FuelTypeEnum.C6: + if isi is None or bui is None: + raise CFFDRSException("isi and bui are required for C6 crown_fraction_burned") + rsi = intermediate_surface_rate_of_spread_c6(isi) + rss = surface_rate_of_spread_c6(rsi, bui) + rsc = crown_rate_of_spread_c6(isi, fmc) + return crown_fraction_burned_c6(rsc, rss, rso) + return _crown_fraction_burned(ros, rso) def total_fuel_consumption( - fuel_type: FuelTypeEnum, cfb: float, sfc: float, pc: float, pdf: float, cfl: float): - """ Computes Total Fuel Consumption (TFC), which is a required input to calculate Head Fire Intensity. - TFC is calculated by delegating to cffdrs R package. - - # Args: - # FUELTYPE: The Fire Behaviour Prediction FuelType - # CFL: Crown Fuel Load (kg/m^2) - # CFB: Crown Fraction Burned (0-1) - # SFC: Surface Fuel Consumption (kg/m^2) - # PC: Percent Conifer (%) - # PDF: Percent Dead Balsam Fir (%) - # option: Type of output (TFC, CFC, default=TFC) - # Returns: - # TFC: Total (Surface + Crown) Fuel Consumption (kg/m^2) - # OR - # CFC: Crown Fuel Consumption (kg/m^2) - """ + fuel_type: FuelTypeEnum, cfb: float, sfc: float, pc: float, pdf: float, cfl: float +): + """Computes Total Fuel Consumption (TFC).""" if cfb is None or cfl is None: - message = PARAMS_ERROR_MESSAGE + \ - f"_TFCcalc; fuel_type: {fuel_type.value}, cfb: {cfb}, cfl: {cfl}" + message = ( + PARAMS_ERROR_MESSAGE + f"_TFCcalc; fuel_type: {fuel_type.value}, cfb: {cfb}, cfl: {cfl}" + ) raise CFFDRSException(message) - # According to fbp.Rd in cffdrs R package, Crown Fuel Load (CFL) can use default value of 1.0 - # without causing major impacts on final output. - if pc is None: - pc = NULL - if pdf is None: - pdf = NULL - result = CFFDRS.instance().cffdrs._TFCcalc(FUELTYPE=fuel_type.value, CFL=cfl, CFB=cfb, SFC=sfc, - PC=pc, - PDF=pdf) - if isinstance(result[0], float): - return result[0] - raise CFFDRSException("Failed to calculate TFC") - - -def head_fire_intensity(fuel_type: FuelTypeEnum, - percentage_conifer: float, - percentage_dead_balsam_fir: float, - ros: float, - cfb: float, - cfl: float, - sfc: float): - """ Computes Head Fire Intensity (HFI) by delegating to cffdrs R package. - Calculating HFI requires a number of inputs that must be calculated first. This function - first makes method calls to calculate the necessary intermediary values. - """ + if fuel_type in _FUEL_TYPES_REQUIRING_PC and pc is None: + raise CFFDRSException(f"pc is required for fuel_type {fuel_type.value}") + if fuel_type in _FUEL_TYPES_REQUIRING_PDF and pdf is None: + raise CFFDRSException(f"pdf is required for fuel_type {fuel_type.value}") + return _total_fuel_consumption( + fuel_type=fuel_type.value, + cfl=cfl, + cfb=cfb, + sfc=sfc, + pc=pc, + pdf=pdf, + ) + + +def head_fire_intensity( + fuel_type: FuelTypeEnum, + percentage_conifer: float, + percentage_dead_balsam_fir: float, + ros: float, + cfb: float, + cfl: float, + sfc: float, +): + """Computes Head Fire Intensity (HFI).""" + tfc = total_fuel_consumption( + fuel_type, cfb, sfc, percentage_conifer, percentage_dead_balsam_fir, cfl + ) + return fire_intensity(fc=tfc, ros=ros) - tfc = total_fuel_consumption(fuel_type, cfb, sfc, - percentage_conifer, percentage_dead_balsam_fir, cfl) - # Args: - # FC: Fuel Consumption (kg/m^2) - # ROS: Rate of Spread (m/min) - # - # Returns: - # FI: Fire Intensity (kW/m) - - result = CFFDRS.instance().cffdrs._FIcalc(FC=tfc, ROS=ros) - if isinstance(result[0], float): - return result[0] - raise CFFDRSException("Failed to calculate FI") - - -def pandas_to_r_converter(df: pd.DataFrame) -> robjs.vectors.DataFrame: - """ - Convert pandas dataframe to an R data.frame object - - :param df: Pandas dataframe - :type df: pd.DataFrame - :return: R data.frame object - :rtype: robjs.vectors.DataFrame - """ - with (robjs.default_converter + pandas2ri.converter).context(): - r_df = robjs.conversion.get_conversion().py2rpy(df) - - return r_df - - -def hourly_fine_fuel_moisture_code(weatherstream: pd.DataFrame, ffmc_old: float, - time_step: int = 1, calc_step: bool = False, batch: bool = True, - hourly_fwi: bool = False) -> pd.DataFrame: - """ Computes hourly FFMC based on noon FFMC using diurnal curve for approximation. - Delegates the calculation to cffdrs R package. - https://rdrr.io/rforge/cffdrs/man/hffmc.html - - Args: weatherstream: Input weather stream data.frame which includes - temperature, relative humidity, wind speed, - precipitation, hourly value, and bui. More specific - info can be found in the hffmc.Rd help file. - ffmc_old: ffmc from previous timestep - time_step: The time (hours) between previous FFMC and current - time. - calc_step: Optional for whether time step between two observations is calculated. Default is FALSE, - no calculations. This is used when time intervals are not uniform in the input. - (optional) - batch: Single step or iterative (default=TRUE). If multiple weather stations are processed, - an additional "id" column is required in the input weatherstream to label different - stations, and the data needs to be sorted by date/time and "id". - hourlyFWI: calculate hourly ISI, FWI, and DSR. Daily BUI is required. - (TRUE/FALSE, default=FALSE) - - Returns: A single or multiple hourly ffmc value(s) - - From hffmc.Rd: - weatherstream (required) - A dataframe containing input variables of hourly weather observations. - It is important that variable names have to be the same as in the following list, but they - are case insensitive. The order in which the input variables are entered is not important. - - Typically this dataframe also contains date and hour fields so outputs can be associated - with a specific day and time, however these fields are not used in the calculations. If - multiple weather stations are being used, a weather station ID field is typically included as well, - though this is simply for bookkeeping purposes and does not affect the calculation. - - temp (required) Temperature (centigrade) - rh (required) Relative humidity (%) - ws (required) 10-m height wind speed (km/h) - prec (required) 1-hour rainfall (mm) - hr (optional) Hourly value to calculate sub-hourly ffmc - bui (optional) Daily BUI value for the computation of hourly FWI. It is - required when hourlyFWI=TRUE - """ - - # We have to change field names to exactly what the CFFDRS lib expects. - # This may need to be adjusted depending on the future data input model, which is currently unknown - column_name_map = {'temperature':'temp', 'relative_humidity': 'rh', 'wind_speed': 'ws', 'precipitation': 'prec', 'datetime': 'hr'} - weatherstream = weatherstream.rename(columns=column_name_map) - - r_weatherstream = pandas_to_r_converter(weatherstream) - try: - result = CFFDRS.instance().cffdrs.hffmc(r_weatherstream, - ffmc_old=ffmc_old, time_step=time_step, calc_step=calc_step, - batch=batch, hourlyFWI=hourly_fwi) - - if isinstance(result, robjs.vectors.FloatVector): - weatherstream['hffmc'] = list(result) - return weatherstream - except rpy2.rinterface_lib.embedded.RRuntimeError as e: - logger.error(f"An error occurred when calculating hourly ffmc: {e}") - raise CFFDRSException("Failed to calculate hffmc") - def get_ffmc_for_target_hfi( - fuel_type: FuelTypeEnum, - percentage_conifer: float, - percentage_dead_balsam_fir: float, - bui: float, - wind_speed: float, - grass_cure: int, - crown_base_height: float, - ffmc: float, fmc: float, cfb: float, cfl: float, target_hfi: float): - """ Returns a floating point value for minimum FFMC required (holding all other values constant) - before HFI reaches the target_hfi (in kW/m). - """ + fuel_type: FuelTypeEnum, + percentage_conifer: float, + percentage_dead_balsam_fir: float, + bui: float, + wind_speed: float, + grass_cure: int, + crown_base_height: float, + ffmc: float, + fmc: float, + cfb: float, + cfl: float, + target_hfi: float, +): + """Returns a floating point value for minimum FFMC required (holding all other values constant) + before HFI reaches the target_hfi (in kW/m). + """ # start off using the actual FFMC value experimental_ffmc = ffmc - experimental_sfc = surface_fuel_consumption(fuel_type, bui, experimental_ffmc, percentage_conifer) + experimental_sfc = surface_fuel_consumption( + fuel_type, bui, experimental_ffmc, percentage_conifer + ) experimental_isi = initial_spread_index(experimental_ffmc, wind_speed) - experimental_ros = rate_of_spread(fuel_type, experimental_isi, bui, fmc, experimental_sfc, - percentage_conifer, - grass_cure, percentage_dead_balsam_fir, crown_base_height) - experimental_hfi = head_fire_intensity(fuel_type, - percentage_conifer, - percentage_dead_balsam_fir, - experimental_ros, cfb, - cfl, experimental_sfc) + experimental_ros = rate_of_spread( + fuel_type, + experimental_isi, + bui, + fmc, + experimental_sfc, + percentage_conifer, + grass_cure, + percentage_dead_balsam_fir, + crown_base_height, + ) + experimental_hfi = head_fire_intensity( + fuel_type, + percentage_conifer, + percentage_dead_balsam_fir, + experimental_ros, + cfb, + cfl, + experimental_sfc, + ) error_hfi = (target_hfi - experimental_hfi) / target_hfi - # FFMC has upper bound 101 - # exit condition 1: FFMC of 101 still causes HFI < target_hfi - # exit condition 2: FFMC of 0 still causes HFI > target_hfi - # exit condition 3: relative error within 1% + # FFMC has upper bound 101 + # exit condition 1: FFMC of 101 still causes HFI < target_hfi + # exit condition 2: FFMC of 0 still causes HFI > target_hfi + # exit condition 3: relative error within 1% while abs(error_hfi) > 0.01: if experimental_ffmc >= 100.9 and experimental_hfi < target_hfi: break if experimental_ffmc <= 0.1: break - if error_hfi > 0: # if the error value is a positive number, make experimental FFMC value bigger + if ( + error_hfi > 0 + ): # if the error value is a positive number, make experimental FFMC value bigger experimental_ffmc = min(101, experimental_ffmc + ((101 - experimental_ffmc) / 2)) else: # if the error value is a negative number, need to make experimental FFMC value smaller experimental_ffmc = max(0, experimental_ffmc - ((101 - experimental_ffmc) / 2)) experimental_isi = initial_spread_index(experimental_ffmc, wind_speed) - experimental_sfc = surface_fuel_consumption(fuel_type, bui, experimental_ffmc, percentage_conifer) - experimental_ros = rate_of_spread(fuel_type, experimental_isi, bui, fmc, - experimental_sfc, percentage_conifer, - grass_cure, percentage_dead_balsam_fir, crown_base_height) - experimental_hfi = head_fire_intensity(fuel_type, - percentage_conifer, - percentage_dead_balsam_fir, experimental_ros, - cfb, cfl, experimental_sfc) + experimental_sfc = surface_fuel_consumption( + fuel_type, bui, experimental_ffmc, percentage_conifer + ) + experimental_ros = rate_of_spread( + fuel_type, + experimental_isi, + bui, + fmc, + experimental_sfc, + percentage_conifer, + grass_cure, + percentage_dead_balsam_fir, + crown_base_height, + ) + experimental_hfi = head_fire_intensity( + fuel_type, + percentage_conifer, + percentage_dead_balsam_fir, + experimental_ros, + cfb, + cfl, + experimental_sfc, + ) error_hfi = (target_hfi - experimental_hfi) / target_hfi return (experimental_ffmc, experimental_hfi) diff --git a/backend/packages/wps-api/src/app/fire_behaviour/prediction.py b/backend/packages/wps-api/src/app/fire_behaviour/prediction.py index 05b962477e..fa7e7d86a1 100644 --- a/backend/packages/wps-api/src/app/fire_behaviour/prediction.py +++ b/backend/packages/wps-api/src/app/fire_behaviour/prediction.py @@ -5,7 +5,7 @@ import os from datetime import datetime from enum import Enum -from typing import List +from typing import List, Optional import pandas as pd from app.fire_behaviour import c7b, cffdrs @@ -80,7 +80,15 @@ def __init__(self): self.morning_df = morning_df -def calculate_cfb(fuel_type: FuelTypeEnum, fmc: float, sfc: float, ros: float, cbh: float): +def calculate_cfb( + fuel_type: FuelTypeEnum, + fmc: float, + sfc: float, + ros: float, + cbh: float, + isi: Optional[float] = None, + bui: Optional[float] = None, +): """Calculate the crown fraction burned (returning 0 for fuel types without crowns to burn)""" if fuel_type in [ FuelTypeEnum.D1, @@ -97,7 +105,7 @@ def calculate_cfb(fuel_type: FuelTypeEnum, fmc: float, sfc: float, ros: float, c # We can't calculate cfb without a crown base height! cfb = None else: - cfb = cffdrs.crown_fraction_burned(fuel_type, fmc=fmc, sfc=sfc, ros=ros, cbh=cbh) + cfb = cffdrs.crown_fraction_burned(fuel_type, fmc=fmc, sfc=sfc, ros=ros, cbh=cbh, isi=isi, bui=bui) return cfb @@ -428,7 +436,7 @@ def calculate_fire_behaviour_prediction_using_cffdrs( FuelTypeEnum[fuel_type], isi, bui, fmc, sfc, pc=pc, cc=cc, pdf=pdf, cbh=cbh ) if sfc is not None: - cfb = calculate_cfb(FuelTypeEnum[fuel_type], fmc, sfc, ros, cbh) + cfb = calculate_cfb(FuelTypeEnum[fuel_type], fmc, sfc, ros, cbh, isi=isi, bui=bui) if ros is not None and cfb is not None and cfl is not None: hfi = cffdrs.head_fire_intensity( diff --git a/backend/packages/wps-api/src/app/main.py b/backend/packages/wps-api/src/app/main.py index 7c1055b277..a66e73cbb7 100644 --- a/backend/packages/wps-api/src/app/main.py +++ b/backend/packages/wps-api/src/app/main.py @@ -4,7 +4,6 @@ """ import logging -from time import perf_counter from urllib.request import Request from fastapi import FastAPI, Depends, Response from fastapi.middleware.cors import CORSMiddleware @@ -35,7 +34,6 @@ fire_watch, fcm, ) -from app.fire_behaviour.cffdrs import CFFDRS configure_logging() @@ -160,15 +158,6 @@ async def get_health(): "/health - healthy: %s. %s", health_check.get("healthy"), health_check.get("message") ) - # Instantiate the CFFDRS singleton. Binding to R can take quite some time... - cffdrs_start = perf_counter() - CFFDRS.instance() - cffdrs_end = perf_counter() - delta = cffdrs_end - cffdrs_start - # Any delta below 100 milliseconds is just noise in the logs. - if delta > 0.1: - logger.info("%f seconds added by CFFDRS startup", delta) - return health_check except Exception as exception: logger.error(exception, exc_info=True) diff --git a/backend/packages/wps-api/src/app/routers/fba_calc.py b/backend/packages/wps-api/src/app/routers/fba_calc.py index 2d9485e549..ef8c26d098 100644 --- a/backend/packages/wps-api/src/app/routers/fba_calc.py +++ b/backend/packages/wps-api/src/app/routers/fba_calc.py @@ -5,9 +5,9 @@ from aiohttp.client import ClientSession from fastapi import APIRouter, Depends -from wps_wf1.wfwx_api import WfwxApi from wps_shared.auth import audit, authentication_required from wps_shared.db.crud.hfi_calc import get_fire_centre_station_codes +from wps_shared.fuel_types import FUEL_TYPE_DEFAULTS from wps_shared.schemas.fba_calc import ( StationListRequest, StationRequest, @@ -16,6 +16,7 @@ ) from wps_shared.schemas.stations import WFWXWeatherStation from wps_shared.utils.time import get_hour_20_from_date +from wps_wf1.wfwx_api import WfwxApi from app.fire_behaviour.advisory import ( FBACalculatorWeatherStation, @@ -132,7 +133,9 @@ async def process_request( percentage_conifer=requested_station.percentage_conifer, percentage_dead_balsam_fir=requested_station.percentage_dead_balsam_fir, grass_cure=requested_station.grass_cure, - crown_base_height=requested_station.crown_base_height, + crown_base_height=requested_station.crown_base_height + if requested_station.crown_base_height is not None + else FUEL_TYPE_DEFAULTS[requested_station.fuel_type]["CBH"], crown_fuel_load=requested_station.crown_fuel_load, lat=wfwx_station.lat, long=wfwx_station.long, diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_01a_0curing_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_01a_0curing_firebat.py index 9054b44544..0a1eed9d14 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_01a_0curing_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_01a_0curing_firebat.py @@ -3,13 +3,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = '/api/fba-calc/stations' -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_01a_30curing_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_01a_30curing_firebat.py index 827fc02fd2..0369d2ec43 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_01a_30curing_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_01a_30curing_firebat.py @@ -3,13 +3,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = '/api/fba-calc/stations' -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_01a_90curing_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_01a_90curing_firebat.py index 5b259d7298..36d55f3e35 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_01a_90curing_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_01a_90curing_firebat.py @@ -3,13 +3,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = '/api/fba-calc/stations' -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_01b_0curing_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_01b_0curing_firebat.py index c47db9402a..af6ba86410 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_01b_0curing_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_01b_0curing_firebat.py @@ -3,13 +3,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = '/api/fba-calc/stations' -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_01b_30curing_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_01b_30curing_firebat.py index ae6fa2833b..0d6312abc8 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_01b_30curing_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_01b_30curing_firebat.py @@ -3,13 +3,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = '/api/fba-calc/stations' -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_01b_90curing_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_01b_90curing_firebat.py index d3feed68e3..6b1b08dcd4 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_01b_90curing_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_01b_90curing_firebat.py @@ -3,13 +3,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = '/api/fba-calc/stations' -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_c1_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_c1_firebat.py index 3db764cbe2..3dbe792e80 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_c1_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_c1_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_c2_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_c2_firebat.py index c69229aceb..549ad83041 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_c2_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_c2_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_c3_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_c3_firebat.py index 6f19b5c9fd..7bb6c6bf40 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_c3_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_c3_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_c4_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_c4_firebat.py index 5bdd9d7328..66b69f09a6 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_c4_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_c4_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_c5_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_c5_firebat.py index 047f9f689b..3933b2438e 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_c5_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_c5_firebat.py @@ -3,13 +3,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = '/api/fba-calc/stations' -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_c6_2mcbh_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_c6_2mcbh_firebat.py index 89abe5d1b6..06b30c2161 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_c6_2mcbh_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_c6_2mcbh_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() @@ -66,23 +64,20 @@ async def test_c6_2mcbh_request_response( ) assert math.isclose(response.json()["stations"][0]["fire_weather_index"], 27.792, abs_tol=0.001) assert math.isclose( - response.json()["stations"][0]["head_fire_intensity"], 10390.163, abs_tol=0.01 + response.json()["stations"][0]["head_fire_intensity"], 8835.000, abs_tol=0.01 ) assert math.isclose(response.json()["stations"][0]["rate_of_spread"], 7.588, abs_tol=0.001) assert math.isclose( - response.json()["stations"][0]["percentage_crown_fraction_burned"], 0.800, abs_tol=0.001 + response.json()["stations"][0]["percentage_crown_fraction_burned"], 0.420, abs_tol=0.001 ) - assert math.isclose(response.json()["stations"][0]["flame_length"], 5.885, abs_tol=0.001) + assert math.isclose(response.json()["stations"][0]["flame_length"], 5.427, abs_tol=0.001) assert math.isclose( - response.json()["stations"][0]["sixty_minute_fire_size"], 8.603, abs_tol=0.001 + response.json()["stations"][0]["sixty_minute_fire_size"], 4.995, abs_tol=0.001 ) assert math.isclose( - response.json()["stations"][0]["thirty_minute_fire_size"], 1.449, abs_tol=0.001 + response.json()["stations"][0]["thirty_minute_fire_size"], 0.604, abs_tol=0.001 ) assert response.json()["stations"][0]["fire_type"] == "IC" assert response.json()["stations"][0]["critical_hours_hfi_4000"] == {"start": 13.0, "end": 20.0} - assert response.json()["stations"][0]["critical_hours_hfi_10000"] == { - "start": 16.0, - "end": 18.0, - } + assert response.json()["stations"][0]["critical_hours_hfi_10000"] is None diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_c6_7mcbh_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_c6_7mcbh_firebat.py index ba9f5e38ff..09b7c84e46 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_c6_7mcbh_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_c6_7mcbh_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_c7_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_c7_firebat.py index 528ba686bb..820ae8b0c5 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_c7_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_c7_firebat.py @@ -3,13 +3,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = '/api/fba-calc/stations' -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_d1_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_d1_firebat.py index 0019d5c3a5..78744419cb 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_d1_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_d1_firebat.py @@ -3,13 +3,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = '/api/fba-calc/stations' -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_m1_25conifer_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_m1_25conifer_firebat.py index 140103b6ac..4cb4e92bab 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_m1_25conifer_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_m1_25conifer_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_m1_50conifer_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_m1_50conifer_firebat.py index e3e46931c1..9a454ed479 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_m1_50conifer_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_m1_50conifer_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_m1_75conifer_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_m1_75conifer_firebat.py index 90af82e5d3..2354d1062f 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_m1_75conifer_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_m1_75conifer_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_m2_25conifer_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_m2_25conifer_firebat.py index ae6597c590..8c9f7688d8 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_m2_25conifer_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_m2_25conifer_firebat.py @@ -3,13 +3,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = '/api/fba-calc/stations' -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_m2_50conifer_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_m2_50conifer_firebat.py index 779811609b..97ee6612f7 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_m2_50conifer_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_m2_50conifer_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_m2_75conifer_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_m2_75conifer_firebat.py index bf2a2d3fad..7b74d759a9 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_m2_75conifer_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_m2_75conifer_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_m3_100deadfir_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_m3_100deadfir_firebat.py index d82a777097..d4d0defd1d 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_m3_100deadfir_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_m3_100deadfir_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_m3_30deadfir_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_m3_30deadfir_firebat.py index 08150ea122..b181a1e949 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_m3_30deadfir_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_m3_30deadfir_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_m3_60deadfir_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_m3_60deadfir_firebat.py index 483f897734..1f3c803d87 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_m3_60deadfir_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_m3_60deadfir_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_m4_100deadfir_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_m4_100deadfir_firebat.py index 0895bf4b0d..b37220210d 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_m4_100deadfir_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_m4_100deadfir_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_m4_30deadfir_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_m4_30deadfir_firebat.py index d844c4d623..43d00f9930 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_m4_30deadfir_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_m4_30deadfir_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_m4_60deadfir_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_m4_60deadfir_firebat.py index 2b82b594f3..2cdf232edd 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_m4_60deadfir_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_m4_60deadfir_firebat.py @@ -2,13 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() @pytest.fixture() diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_r.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_r.py deleted file mode 100644 index 7f899e2048..0000000000 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_r.py +++ /dev/null @@ -1,25 +0,0 @@ -""" Test the we have a good version of R """ -import logging -from packaging import version -from rpy2 import robjects - -logger = logging.getLogger(__name__) - - -def test_r_version(): - """ Test the we have a good version of R """ - response = robjects.r("version") - # The response we get from R is pretty gross. It's a list of strings. - # We're looking for somethin like: "['R version 4fb .1.2 (2021-11-01)']" - # Would be nice to fix to a particular version, but each developers has a different - # version of R! - for item in response: - # If it contains "version", it's what we're looking for. - if 'version' in str(item): - # Look for the version number. - r_version = str(item).split(' ')[3] - logger.info('R version: %s', r_version) - # If the major version is >= 4.1.2, we're good. - assert version.parse(r_version) >= version.parse('4.1.2') - return - assert False diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_s1_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_s1_firebat.py index c3e2e80d84..c31dd2649e 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_s1_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_s1_firebat.py @@ -2,14 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() - @pytest.fixture() async def async_client(): diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_s2_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_s2_firebat.py index 4b75028e0a..b1576cc4be 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_s2_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_s2_firebat.py @@ -2,14 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() - @pytest.fixture() async def async_client(): diff --git a/backend/packages/wps-api/src/app/tests/fba_calc/test_s3_firebat.py b/backend/packages/wps-api/src/app/tests/fba_calc/test_s3_firebat.py index 5afbb831d6..b4677a707c 100644 --- a/backend/packages/wps-api/src/app/tests/fba_calc/test_s3_firebat.py +++ b/backend/packages/wps-api/src/app/tests/fba_calc/test_s3_firebat.py @@ -2,14 +2,11 @@ from aiohttp import ClientSession import pytest import math -from app.fire_behaviour.cffdrs import CFFDRS from wps_shared.tests.common import default_mock_client_get firebat_url = "/api/fba-calc/stations" -CFFDRS.instance() - @pytest.fixture() async def async_client(): diff --git a/backend/packages/wps-api/src/app/tests/fire_behavior/test_cffdrs.py b/backend/packages/wps-api/src/app/tests/fire_behavior/test_cffdrs.py index e6e0ffda86..ed52158110 100644 --- a/backend/packages/wps-api/src/app/tests/fire_behavior/test_cffdrs.py +++ b/backend/packages/wps-api/src/app/tests/fire_behavior/test_cffdrs.py @@ -1,58 +1,29 @@ +import math from datetime import datetime -import pandas as pd + import numpy as np -import rpy2.robjects as robjs +import pandas as pd import pytest -import math -from wps_shared.fuel_types 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.prediction import calculate_cfb +from wps_shared.fuel_types import FuelTypeEnum start_date = datetime(2023, 8, 17) end_date = datetime(2023, 8, 18) -hourly_datetimes = pd.date_range(start=start_date, end=end_date, freq='H') +hourly_datetimes = pd.date_range(start=start_date, end=end_date, freq="H") hourly_data = { - 'datetime': hourly_datetimes, - 'temp': np.random.default_rng(111).uniform(20.0, 30.0, size=len(hourly_datetimes)), - 'rh': np.random.default_rng(111).uniform(40.0, 100.0, size=len(hourly_datetimes)), - 'precipitation': np.random.default_rng(111).uniform(0.0, 1.0, size=len(hourly_datetimes)), - 'ws': np.random.default_rng(111).uniform(0.0, 30.0, size=len(hourly_datetimes)), + "temperature": np.random.default_rng(111).uniform(20.0, 30.0, size=len(hourly_datetimes)), + "relative_humidity": np.random.default_rng(111).uniform( + 40.0, 100.0, size=len(hourly_datetimes) + ), + "precipitation": np.random.default_rng(111).uniform(0.0, 1.0, size=len(hourly_datetimes)), + "wind_speed": np.random.default_rng(111).uniform(0.0, 30.0, size=len(hourly_datetimes)), } df_hourly = pd.DataFrame(hourly_data) -def test_pandas_to_r_converter(): - r_df = pandas_to_r_converter(df_hourly) - - assert isinstance(r_df, robjs.vectors.DataFrame) - - -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], - } - ) - - 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) @@ -74,7 +45,9 @@ def test_ros_no_bui(): def test_ros_no_params(): """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( @@ -102,7 +75,7 @@ def test_failing_ffmc(ffmc, temperature, precipitation, relative_humidity, wind_ @pytest.mark.parametrize( - 'dmc,temperature,relative_humidity,precipitation', + "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): @@ -112,7 +85,8 @@ def test_failing_dmc(dmc, temperature, relative_humidity, precipitation): @pytest.mark.parametrize( - 'dc,temperature,relative_humidity,precipitation', [(None, 10, 90, 1), (100, None, 90, 1), (100, 10, 90, None)] + "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""" @@ -120,6 +94,33 @@ def test_failing_dc(dc, temperature, relative_humidity, precipitation): assert res is None +@pytest.mark.parametrize( + "ffmc,wind_speed", + [(None, 10), (11, None), (None, None)], +) +def test_failing_isi(ffmc, wind_speed): + """ISI returns None when any input is None.""" + assert cffdrs.initial_spread_index(ffmc, wind_speed) is None + + +@pytest.mark.parametrize( + "isi,bui", + [(None, 50), (20, None), (None, None)], +) +def test_failing_fwi(isi, bui): + """FWI returns None when any input is None.""" + assert cffdrs.fire_weather_index(isi, bui) is None + + +@pytest.mark.parametrize( + "dmc,dc", + [(None, 100), (50, None), (None, None)], +) +def test_failing_bui(dmc, dc): + """BUI returns None when any input is None.""" + assert cffdrs.bui_calc(dmc, dc) is None + + def test_none_latitude_dmc(): res = cffdrs.duff_moisture_code(100, 10, 90, 0, latitude=None) assert res is not None @@ -138,3 +139,73 @@ def test_none_latitude_dc(): def test_none_month_dc(): res = cffdrs.drought_code(100, 10, 90, 0, month=None) assert res is not None + + +def test_dmc_temp_below_threshold_gives_zero_drying(): + """cffdrs_py clamps temp to -1.1 when temp < 1.1°C (Eq. 16), making drying rate rk = 0. + With no precipitation and a low initial DMC, the result stays at the initial value.""" + # temp=1.0 is below the 1.1°C threshold — rk = 1.894 * (-1.1 + 1.1) * ... = 0 + res_below = cffdrs.duff_moisture_code(10, 1.0, 50, 0) + # temp=1.2 is above the threshold — rk > 0, so DMC increases slightly + res_above = cffdrs.duff_moisture_code(10, 1.2, 50, 0) + assert res_below < res_above + + +def test_dmc_temp_at_threshold_gives_zero_drying(): + """At exactly 1.1°C, temp is not clamped (condition is temp < 1.1), so rk is a small positive value.""" + res_at = cffdrs.duff_moisture_code(10, 1.1, 50, 0) + res_below = cffdrs.duff_moisture_code(10, 1.0, 50, 0) + assert res_at >= res_below + + +def test_back_rate_of_spread_requires_wsv(): + with pytest.raises(cffdrs.CFFDRSException): + cffdrs.back_rate_of_spread(FuelTypeEnum.C7, ffmc=85.0, bui=50.0, wsv=None, fmc=100.0, sfc=1.5, pc=None, cc=None, pdf=None, cbh=7.0) + + +@pytest.mark.parametrize( + "kwargs", + [ + {"bui": 80.0}, # missing isi + {"isi": 20.0}, # missing bui + {}, # missing both + ], +) +def test_crown_fraction_burned_c6_requires_isi_and_bui(kwargs): + with pytest.raises(cffdrs.CFFDRSException): + cffdrs.crown_fraction_burned(FuelTypeEnum.C6, fmc=100.0, sfc=0.5, ros=15.0, cbh=7.0, **kwargs) + + +def test_crown_fraction_burned_c6_returns_valid_cfb(): + """C6 CFB is between 0 and 1 with crown-burning conditions.""" + cfb = cffdrs.crown_fraction_burned(FuelTypeEnum.C6, fmc=100.0, sfc=0.5, ros=15.0, cbh=7.0, isi=20.0, bui=80.0) + assert 0.0 <= cfb <= 1.0 + + + +def test_crown_fraction_burned_non_c6_does_not_require_isi_bui(): + """Non-C6 fuel types work without isi/bui.""" + cfb = cffdrs.crown_fraction_burned(FuelTypeEnum.C7, fmc=100.0, sfc=0.5, ros=15.0, cbh=7.0) + assert 0.0 <= cfb <= 1.0 + + +def test_calculate_cfb_c6_threads_isi_bui(): + """calculate_cfb passes isi and bui through to crown_fraction_burned for C6.""" + cfb = calculate_cfb(FuelTypeEnum.C6, fmc=100.0, sfc=0.5, ros=15.0, cbh=7.0, isi=20.0, bui=80.0) + assert cfb is not None + assert 0.0 <= cfb <= 1.0 + + +def test_calculate_cfb_c6_missing_isi_bui_raises(): + """calculate_cfb raises for C6 when isi/bui not provided.""" + with pytest.raises(cffdrs.CFFDRSException): + calculate_cfb(FuelTypeEnum.C6, fmc=100.0, sfc=0.5, ros=15.0, cbh=7.0) + + +@pytest.mark.parametrize( + "fuel_type", + [FuelTypeEnum.D1, FuelTypeEnum.O1A, FuelTypeEnum.O1B, FuelTypeEnum.S1, FuelTypeEnum.S2, FuelTypeEnum.S3], +) +def test_calculate_cfb_returns_zero_for_no_crown_fuel_types(fuel_type): + """Fuel types without crowns return 0 regardless of inputs.""" + assert calculate_cfb(fuel_type, fmc=100.0, sfc=0.5, ros=15.0, cbh=7.0) == 0 diff --git a/backend/packages/wps-api/src/app/tests/fire_behavior/test_fwi_adjust.py b/backend/packages/wps-api/src/app/tests/fire_behavior/test_fwi_adjust.py index 08085848d3..b459f2e270 100644 --- a/backend/packages/wps-api/src/app/tests/fire_behavior/test_fwi_adjust.py +++ b/backend/packages/wps-api/src/app/tests/fire_behavior/test_fwi_adjust.py @@ -82,7 +82,11 @@ def test_adjusted_fwi_result_with_precipitation(): }, raw_daily=raw_daily, ) - assert math.isclose(adjusted_fwi_result.dmc, 0.256, abs_tol=0.001) + # dmc=0.0 because temperature=1 is below the 1.1°C threshold in cffdrs_py's DMC calculation + # (Eq. 16): temp is clamped to -1.1, making the drying rate rk = 1.894*(−1.1+1.1)*... = 0. + # The precipitation component (25mm, dmc_yda=1) also drives pr to 0 after clamping. + # The previous R-based implementation did not apply this clamp and returned ~0.256. + assert math.isclose(adjusted_fwi_result.dmc, 0.0, abs_tol=0.001) assert math.isclose(adjusted_fwi_result.dc, 0.0, abs_tol=0.001) assert math.isclose(adjusted_fwi_result.bui, 0.0, abs_tol=0.001) assert math.isclose(adjusted_fwi_result.ffmc, 26.757, abs_tol=0.001) diff --git a/backend/packages/wps-api/src/app/utils/r_importer.py b/backend/packages/wps-api/src/app/utils/r_importer.py deleted file mode 100644 index f344bd7b91..0000000000 --- a/backend/packages/wps-api/src/app/utils/r_importer.py +++ /dev/null @@ -1,7 +0,0 @@ -""" Imports R libs, easier for mocking""" -from rpy2.robjects.packages import importr - - -def import_cffsdrs(): - """ Import cffdrs """ - return importr('cffdrs') diff --git a/backend/uv.lock b/backend/uv.lock index 70a61ff32d..d6b60b6200 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -3734,51 +3734,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, ] -[[package]] -name = "rpy2" -version = "3.6.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging", marker = "sys_platform == 'win32'" }, - { name = "rpy2-rinterface" }, - { name = "rpy2-robjects" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/ba/393d5aaf21204d7678e59f7e7cd54d9a929d5f7ad8218f172100c8c7a6c8/rpy2-3.6.4.tar.gz", hash = "sha256:a24e8dda5c5ff8cbd2b8ebd1ccf6f1a5a0a576623700cf91e2cce98d41a79fd3", size = 53247, upload-time = "2025-09-26T00:31:37.986Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/09/93/d49eccd6662ff5cec439f9de716e6520c990cec031fcdd8d08e7fd530681/rpy2-3.6.4-py3-none-any.whl", hash = "sha256:edf437d6637b89311f860fb3e44144ea5eff286a65c48645805833baff090621", size = 9895, upload-time = "2025-09-26T00:31:36.841Z" }, -] - -[[package]] -name = "rpy2-rinterface" -version = "3.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, - { name = "packaging", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/2a/6ced3b62a9cbfd1f3c24f7005f6eca492d906a4b6a6d56d097a116f14539/rpy2_rinterface-3.6.3.tar.gz", hash = "sha256:477bc2f51d007ad1b8567c5d4ba1af0f59951686ee7f10576a06d0834de5776f", size = 79406, upload-time = "2025-09-04T22:50:50.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/69/a24661f9ebdcd1d20ab7dd30a7d360437f19eb2446cbdb1bb3e651a9ff8d/rpy2_rinterface-3.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:830c6a636d1ad42abcd8f8ba096975d42bfcf05b396dfaf4ff3c9e59ae182881", size = 173483, upload-time = "2025-09-04T22:52:33.545Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d4/d06dea263670ba7938bd01c00de2ace3da4e9730b8a0becf08c7c8574e69/rpy2_rinterface-3.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:c79572bb205f4eb19d51b81395f0370f7ea793e74352da1ed63b25d9f9970fd8", size = 174533, upload-time = "2025-09-04T22:53:31.954Z" }, - { url = "https://files.pythonhosted.org/packages/72/ee/0c5774b75862b148ae9e0f97feed2b6abf899c652f0df8cc5556c149f6dc/rpy2_rinterface-3.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62c6bc4a4566d4cd227a377c95c2a2be0e30bfb7b9ed3d5bea58e1c4cdfdf024", size = 173477, upload-time = "2025-09-04T22:52:34.57Z" }, - { url = "https://files.pythonhosted.org/packages/d8/0e/f0463f33815875e3b7da58c6f13df1d4e416d4abe57aa9261a31910ef6ca/rpy2_rinterface-3.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:b6eb511a07e30d7253b6fc401c14dbc9cc94efd70abdaa421f5101654c012d68", size = 174531, upload-time = "2025-09-04T22:53:32.97Z" }, -] - -[[package]] -name = "rpy2-robjects" -version = "3.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "packaging", marker = "sys_platform == 'win32'" }, - { name = "rpy2-rinterface" }, - { name = "tzlocal" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/58/107252dab2d342eb77bd84d672abc6ecb57428758e251cfccf841c4b6a69/rpy2_robjects-3.6.3.tar.gz", hash = "sha256:731aa1a4905c4b25c0564d72cbfd85f9230102f989e7a22515885e91d4df3d40", size = 105849, upload-time = "2025-09-26T00:31:27.792Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/af/a794c935ddd3dd8b47a8931e0c8cee1b756ff983db288284786ad29a8bf8/rpy2_robjects-3.6.3-py3-none-any.whl", hash = "sha256:de58c7126dbcd66c4692e7aef91d3cc57a96134828d37a268669f78ee64b974d", size = 125862, upload-time = "2025-09-26T00:31:26.623Z" }, -] - [[package]] name = "ruff" version = "0.14.9" @@ -4297,18 +4252,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] -[[package]] -name = "tzlocal" -version = "5.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tzdata", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, -] - [[package]] name = "uri-template" version = "1.3.0" @@ -4448,7 +4391,6 @@ dependencies = [ { name = "redis" }, { name = "requests" }, { name = "requests-ntlm" }, - { name = "rpy2" }, { name = "scikit-learn" }, { name = "scipy" }, { name = "sentry-sdk", extra = ["fastapi"] }, @@ -4515,7 +4457,6 @@ requires-dist = [ { name = "requests", specifier = ">=2,<3" }, { name = "requests-ntlm", specifier = ">=1,<2" }, { name = "rope", marker = "extra == 'dev'", specifier = ">=1,<2" }, - { name = "rpy2", specifier = ">=3.4.5,<4" }, { name = "scikit-learn", specifier = ">=1.1.3,<2" }, { name = "scipy", specifier = ">=1,<2" }, { name = "sentry-sdk", extras = ["fastapi"], specifier = ">=2.0.1,<3" }, diff --git a/openshift/wps-api-base/docker/Dockerfile b/openshift/wps-api-base/docker/Dockerfile index 4d004dd123..8752be5f26 100644 --- a/openshift/wps-api-base/docker/Dockerfile +++ b/openshift/wps-api-base/docker/Dockerfile @@ -7,18 +7,14 @@ ARG USERNAME=worker ARG USER_UID=1010 ARG USER_GID=1000 -# Tell r-base not to wait for interactive input. ENV DEBIAN_FRONTEND=noninteractive # Install pre-requisites # - python (we want python!) # - gdal (for geospatial) -# - R (for cffdrs) # - xfonts-75dpi, xfonts-base (for wkhtmltopdf) # - tippecanoe for generating pmtiles -# - Additional libraries for R spatial packages (s2, sf) -# - cmake and libabsl-dev for s2 package compilation -RUN apt-get update --fix-missing && apt-get -y install python3 python3-pip python3-dev python-is-python3 libudunits2-dev r-base-dev xfonts-base xfonts-75dpi curl git build-essential libproj-dev libgeos-dev libsqlite3-dev libpq-dev libtirpc-dev libssl-dev libcurl4-openssl-dev libxml2-dev cmake libabsl-dev +RUN apt-get update --fix-missing && apt-get -y install python3 python3-pip python3-dev python-is-python3 libudunits2-dev xfonts-base xfonts-75dpi curl git build-essential libproj-dev libgeos-dev libsqlite3-dev libpq-dev libtirpc-dev libssl-dev libcurl4-openssl-dev libxml2-dev cmake libabsl-dev RUN curl -sSL https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb > /tmp/wkhtmltox_0.12.6.1-2.jammy_amd64.deb # Enable for mac M1 @@ -34,9 +30,6 @@ RUN dpkg -i /tmp/wkhtmltox_0.12.6.1-2.jammy_amd64.deb # Install tippecanoe for pmtiles RUN git clone https://github.com/felt/tippecanoe.git --branch 2.62.5 && cd tippecanoe && make -j && make install && cd .. -# Install cffdrs -RUN R -e "install.packages('cffdrs')" - # Add the worker user with UID 1010 and assign them to the existing group with GID 1000 RUN useradd --uid $USER_UID --gid $USER_GID -m $USERNAME diff --git a/openshift/wps-api-base/openshift/build.yaml b/openshift/wps-api-base/openshift/build.yaml index 3f0255abc3..60f0267326 100644 --- a/openshift/wps-api-base/openshift/build.yaml +++ b/openshift/wps-api-base/openshift/build.yaml @@ -66,18 +66,14 @@ objects: ARG USER_UID=1010 ARG USER_GID=1000 - # Tell r-base not to wait for interactive input. ENV DEBIAN_FRONTEND=noninteractive # Install pre-requisites # - python (we want python!) # - gdal (for geospatial) - # - R (for cffdrs) # - xfonts-75dpi, xfonts-base (for wkhtmltopdf) # - tippecanoe for generating pmtiles - # - Additional libraries for R spatial packages (s2, sf) - # - cmake and libabsl-dev for s2 package compilation - RUN apt-get update --fix-missing && apt-get -y install python3 python3-pip python3-dev python-is-python3 libudunits2-dev r-base-dev xfonts-base xfonts-75dpi curl git build-essential libproj-dev libgeos-dev libsqlite3-dev libpq-dev libtirpc-dev libssl-dev libcurl4-openssl-dev libxml2-dev cmake libabsl-dev + RUN apt-get update --fix-missing && apt-get -y install python3 python3-pip python3-dev python-is-python3 libudunits2-dev xfonts-base xfonts-75dpi curl git build-essential libproj-dev libgeos-dev libsqlite3-dev libpq-dev libtirpc-dev libssl-dev libcurl4-openssl-dev libxml2-dev cmake libabsl-dev RUN curl -sSL https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb > /tmp/wkhtmltox_0.12.6.1-2.jammy_amd64.deb # Enable for mac M1 @@ -93,9 +89,6 @@ objects: # Install tippecanoe for pmtiles RUN git clone https://github.com/felt/tippecanoe.git --branch 2.62.5 && cd tippecanoe && make -j && make install && cd .. - # Install cffdrs - RUN R -e "install.packages('cffdrs')" - # Add the worker user with UID 1010 and assign them to the existing group with GID 1000 RUN useradd --uid $USER_UID --gid $USER_GID -m $USERNAME diff --git a/setup/mac.sh b/setup/mac.sh index ad11c5ef98..b117122f88 100755 --- a/setup/mac.sh +++ b/setup/mac.sh @@ -31,16 +31,6 @@ pyenv global 3.12.3 ### uv brew install uv -### r -brew install --cask r -brew install udunits -brew install proj - -echo "installing r packages, this takes awhile..." -r -e 'install.packages(c("rgdal","sf", "units"),,"https://mac.R-project.org")' -r -e "install.packages('cffdrs', repos = 'http://cran.us.r-project.org')" -echo "finished installing r packages" - ### postgres - Nov 2024 - Commenting out the postgres setup. See MANUAL.md for reasons and manual postgres setup. # echo "installing and configuring postgres" # brew install postgresql