292 lines
9.1 KiB
Python
292 lines
9.1 KiB
Python
"""CSC110 Fall 2021 Assignment 3: Forest Fire Weather Index System
|
|
|
|
Copyright and Usage Information
|
|
===============================
|
|
|
|
This file is provided solely for the personal and private use of students
|
|
taking CSC110 at the University of Toronto St. George campus. All forms of
|
|
distribution of this code, whether as given or with any changes, are
|
|
expressly prohibited. For more information on copyright for CSC110 materials,
|
|
please consult our Course Syllabus.
|
|
|
|
This file is Copyright (c) 2021 Mario Badr and Tom Fairgrieve.
|
|
"""
|
|
import math
|
|
from dataclasses import dataclass
|
|
|
|
# Initial values that can be used for FFMC, DMC, and DC
|
|
INITIAL_FFMC = 85.0
|
|
INITIAL_DMC = 6.0
|
|
INITIAL_DC = 15.0
|
|
|
|
# Per-month lookup tables
|
|
DMC_DAY_LENGTH_EFFECTIVE = {
|
|
1: 6.5, 2: 7.5, 3: 9.0, 4: 12.8, 5: 13.9, 6: 13.9,
|
|
7: 12.4, 8: 10.9, 9: 9.4, 10: 8.0, 11: 7.0, 12: 6.0
|
|
}
|
|
DC_DAY_LENGTH_FACTORS = {
|
|
1: -1.6, 2: -1.6, 3: -1.6, 4: 0.9, 5: 3.8, 6: 5.8,
|
|
7: 6.4, 8: 5.0, 9: 2.4, 10: 0.4, 11: -1.6, 12: -1.6
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class WeatherMetrics:
|
|
"""A bundle of metrics that are measured by weather stations.
|
|
|
|
Instance Attributes:
|
|
- month: The month of the year (e.g., January is 1, December is 12)
|
|
- day: the day of the month
|
|
- temperature: The noon temperature in degrees Celsius
|
|
- humidity: The noon relative humidity, in %
|
|
- wind_speed: The noon wind speed, in km/h
|
|
- precipitation: The rainfall at noon, in mm
|
|
|
|
Representation Invariants:
|
|
- self.month in {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
|
|
- self.humidity >= 0.0
|
|
- self.wind_speed >= 0.0
|
|
- self.precipitation >= 0.0
|
|
"""
|
|
month: int
|
|
day: int
|
|
temperature: float
|
|
humidity: float
|
|
wind_speed: float
|
|
precipitation: float
|
|
|
|
|
|
@dataclass
|
|
class FfwiOutput:
|
|
"""A bundle of the output metrics in the Canadian Forest Fire Weather Index System.
|
|
|
|
Instance Attributes:
|
|
- ffmc: the Fine Fuel Moisture Code
|
|
- dmc: the Duff Moisture Code
|
|
- dc: the Drought Code
|
|
- isi: the Initial Spread Index
|
|
- bui: the Buildup Index
|
|
- fwi: the Fire Weather Index
|
|
|
|
Representation Invariants:
|
|
- 0.0 <= self.ffmc <= 101.0
|
|
- self.dmc >= 1.0
|
|
- self.bui >= 0.0
|
|
"""
|
|
ffmc: float
|
|
dmc: float
|
|
dc: float
|
|
isi: float
|
|
bui: float
|
|
fwi: float
|
|
|
|
|
|
def calculate_mr(precipitation: float, mo: float) -> float:
|
|
"""Return the fine fuel moisture content after rain (mr) based on the effective
|
|
rainfall in precipitation and the fine fuel moisture content from the previous day.
|
|
|
|
Preconditions:
|
|
- precipitation > 0.5
|
|
"""
|
|
# Equation 2
|
|
rf = precipitation - 0.5
|
|
|
|
if mo > 150.0:
|
|
# Equation 3b
|
|
mr = (mo + 42.5 * rf * math.exp(-100.0 / (251.0 - mo)) * (1.0 - math.exp(-6.93 / rf))) + (
|
|
0.0015 * (mo - 150.0) ** 2) * math.sqrt(rf)
|
|
else:
|
|
# Equation 3a
|
|
mr = mo + 42.5 * rf * math.exp(-100.0 / (251.0 - mo)) * (1.0 - math.exp(-6.93 / rf))
|
|
|
|
return mr
|
|
|
|
|
|
def calculate_m(wm: WeatherMetrics, ed: float, mo: float) -> float:
|
|
"""Return the fine fuel moisture content after drying (m) based on the measurements in wm, the
|
|
EMC for drying in ed, and the fine fuel moisture content from the previous day in mo.
|
|
|
|
Preconditions:
|
|
- mo <= 250.0
|
|
"""
|
|
if mo == ed:
|
|
return mo
|
|
|
|
if mo < ed:
|
|
# Equation 5
|
|
ew = 0.618 * (wm.humidity ** .753) + (10.0 * math.exp((wm.humidity - 100.0) / 10.0)) \
|
|
+ 0.18 * (21.1 - wm.temperature) * (1.0 - 1.0 / math.exp(0.115 * wm.humidity))
|
|
|
|
if mo <= ew:
|
|
# Use log wetting rate
|
|
# Equation 7a
|
|
k1 = 0.424 * (1.0 - ((100.0 - wm.humidity) / 100.0) ** 1.7) + \
|
|
(.0694 * math.sqrt(wm.wind_speed)) * (1.0 - ((100.0 - wm.humidity) / 100.0) ** 8)
|
|
kw = k1 * (0.581 * math.exp(0.0365 * wm.temperature)) # Equation 7b
|
|
return ew - (ew - mo) / 10.0 ** kw # Equation 9
|
|
else:
|
|
return mo
|
|
|
|
# Use log drying rate
|
|
# Equation 6a
|
|
k0 = 0.424 * (1.0 - (wm.humidity / 100.0) ** 1.7) + (
|
|
(0.0694 * math.sqrt(wm.wind_speed)) * (1.0 - (wm.humidity / 100.0) ** 8))
|
|
kd = k0 * (0.581 * math.exp(0.0365 * wm.temperature)) # Equation 6b
|
|
m = ed + (mo - ed) / 10.0 ** kd # Equation 8
|
|
|
|
return m
|
|
|
|
|
|
def calculate_ffmc(wm: WeatherMetrics, f0: float) -> float:
|
|
"""Return the Fine Fuel Moisture Code (FFMC) based on the measurements in wm and the previous
|
|
day's FFMC in f0.
|
|
"""
|
|
# Calculate the fine fuel moisture content from the previous day
|
|
mo = (147.2 * (101.0 - f0)) / (59.5 + f0) # Equation 1
|
|
if wm.precipitation > 0.5:
|
|
mo = calculate_mr(wm.precipitation, mo)
|
|
|
|
if mo > 250.0:
|
|
mo = 250.0
|
|
|
|
# Equation 4 - Fine Fuel equilibrium moisture content (EMC) for drying
|
|
ed = 0.942 * (wm.humidity ** .679) + (11.0 * math.exp((wm.humidity - 100.0) / 10.0)) + (
|
|
0.18 * (21.1 - wm.temperature) * (1.0 - 1.0 / math.exp(0.1150 * wm.humidity)))
|
|
|
|
m = calculate_m(wm, ed, mo)
|
|
|
|
# Equation 10
|
|
f = (59.5 * (250.0 - m)) / (147.2 + m)
|
|
|
|
if f > 101.0:
|
|
f = 101.0
|
|
elif f <= 0.0:
|
|
f = 0.0
|
|
|
|
return f
|
|
|
|
|
|
def calculate_dmr(precipitation: float, dm0: float) -> float:
|
|
"""Calculate the Duff moisture content after rain based on the current precipitation and the
|
|
previous day's DMC in dm0.
|
|
|
|
Preconditions:
|
|
- precipitation > 1.5
|
|
"""
|
|
rw = 0.92 * precipitation - 1.27 # Equation 11
|
|
wmi = 20.0 + 280.0 / math.exp(0.023 * dm0) # Equation 12
|
|
|
|
if dm0 <= 33.0:
|
|
b = 100.0 / (0.5 + 0.3 * dm0) # Equation 13a
|
|
else:
|
|
if dm0 <= 65.0:
|
|
b = 14.0 - 1.3 * math.log(dm0) # Equation 13b
|
|
else:
|
|
b = 6.2 * math.log(dm0) - 17.2 # Equation 13c
|
|
|
|
return wmi + (1000 * rw) / (48.77 + b * rw) # Equation 14
|
|
|
|
|
|
def calculate_dmc_k(temperature: float, humidity: float, month: int) -> float:
|
|
"""Return the log drying rate in DMC based on the temperature, humidity, and month."""
|
|
if temperature < -1.1:
|
|
# Cannot use temperatures less than -1.1 in Equation 16
|
|
temperature = -1.1
|
|
|
|
# Equations 16 and 17
|
|
return 1.894 * (temperature + 1.1) * (100.0 - humidity) * (
|
|
DMC_DAY_LENGTH_EFFECTIVE[month] * 0.0001)
|
|
|
|
|
|
def calculate_dmc(wm: WeatherMetrics, dm0: float) -> float:
|
|
"""Return the Duff Moisture Code (DMC) based on the measurements in wm and the previous day's
|
|
DMC in dm0.
|
|
"""
|
|
if wm.precipitation <= 1.5:
|
|
pr = dm0
|
|
else:
|
|
dmr = calculate_dmr(wm.precipitation, dm0)
|
|
pr = 43.43 * (5.6348 - math.log(dmr - 20.0)) # Equation 15
|
|
|
|
# The DMC after rain cannot, theoretically, be less than 0; ensure it is at least 0
|
|
pr = max(0.0, pr)
|
|
|
|
rk = calculate_dmc_k(wm.temperature, wm.humidity, wm.month)
|
|
d = pr + rk
|
|
|
|
if d <= 1.0:
|
|
d = 1.0
|
|
return d
|
|
|
|
|
|
def calculate_qr(precipitation: float, dc0: float) -> float:
|
|
"""Return the moisture equivalent after rain based on the current precipitation and the
|
|
previous day's DC in dc0.
|
|
|
|
Preconditions:
|
|
- precipitation > 2.8
|
|
"""
|
|
rd = 0.83 * precipitation - 1.27 # Equation 18
|
|
qo = 800.0 * math.exp(-dc0 / 400.0) # Equation 19
|
|
qr = qo + 3.937 * rd # Equation 20
|
|
|
|
return qr
|
|
|
|
|
|
def calculate_dc(wm: WeatherMetrics, dc0: float) -> float:
|
|
"""Return the Drought Code (DC) based on the measurements in wm and the previous day's DC in
|
|
dc0.
|
|
"""
|
|
temperature = wm.temperature
|
|
if wm.temperature < -2.8:
|
|
temperature = -2.8
|
|
|
|
v = 0.36 * (temperature + 2.8) + DC_DAY_LENGTH_FACTORS[wm.month] # Equation 22
|
|
# The potential evapotranspiration cannot, theoretically, be less than 0; ensure it's at least 0
|
|
v = max(0.0, v)
|
|
|
|
if wm.precipitation > 2.8:
|
|
qr = calculate_qr(wm.precipitation, dc0)
|
|
dr = 400 * math.log(800 / qr) # Equation 21
|
|
return dr + 0.5 * v # Equation 23
|
|
else:
|
|
return dc0 + 0.5 * v # Equation 23
|
|
|
|
|
|
def calculate_isi(wm: WeatherMetrics, ffmc: float) -> float:
|
|
"""Return the Initial Spread Index (ISI) based on the measurements in wm and the current ffmc.
|
|
"""
|
|
mo = 147.2 * (101.0 - ffmc) / (59.5 + ffmc)
|
|
ff = 19.115 * math.exp(mo * -0.1386) * (1.0 + (mo ** 5.31) / 49300000.0)
|
|
|
|
return ff * math.exp(0.05039 * wm.wind_speed)
|
|
|
|
|
|
def calculate_bui(dmc: float, dc: float) -> float:
|
|
"""Return the Buildup Index (BUI) based on the current dmc and the current dc.
|
|
"""
|
|
if dmc <= 0.4 * dc:
|
|
b = (0.8 * dc * dmc) / (dmc + 0.4 * dc)
|
|
else:
|
|
b = dmc - (1.0 - 0.8 * dc / (dmc + 0.4 * dc)) * (0.92 + (0.0114 * dmc) ** 1.7)
|
|
|
|
if b < 0.0:
|
|
b = 0.0
|
|
|
|
return b
|
|
|
|
|
|
def calculate_fwi(isi: float, bui: float) -> float:
|
|
"""Return the Fire Weather Index (FWI) based on the current isi and the current bui.
|
|
"""
|
|
if bui <= 80.0:
|
|
f_d = 0.626 * bui ** 0.809 + 2.0 # Equation 28a
|
|
else:
|
|
f_d = 1000.0 / (25. + 108.64 * math.exp(-0.023 * bui)) # Equation 28b
|
|
|
|
bb = 0.1 * isi * f_d # Equation 29
|
|
if bb <= 1.0:
|
|
return bb # Equation 30b
|
|
else:
|
|
return math.exp(2.72 * (0.434 * math.log(bb)) ** 0.647) # Equation 30a
|