Figures generated by the script below and included in this document are available on GitHub, as well.
For analysis, data are filtered to \(\textsf{Rate of spread < 40 m/s}\) and \(\textsf{Maximum temperature > 40}^\circ \textsf{C}\).
We used the multiple imputation method in the mice package to fill in missing values. The method creates 5 datasets, each with different but reasonable values for the missing data based on patterns in existing data, and performs the regression analysis on each. These results are pooled into a single set of results.
Mixed-effect regression models fit with lme4::glmer and constructed as follows:
\[y \approx RH + WS + FM + FL + (1|location/year/plot),~ \text{family = gamma, link = log}\]
where
\(RH\) = Relative humidity,
\(WS\) = Average wind speed,
\(FM\) = Fuel moisture (from clipped samples),
\(FL\) = Fuel load (t/ha from clipped samples)
Weather variables \(RH\) and \(WS\) were taken from NDAWN stations for the hour in which the fire behavior observation occurred.
All variables were scaled to a common range prior to analysis, and units for each predictor variable are not reported.
Response variables included
Thermocouple data recorded with the open-source FeatherFlame system.
Principal Components Analysis (PCA) on fire behavior responses fit with vegan::rda. Post-hoc group (location) and gradient (fire weather) analysis with vegan::envfit.
An additional composite variable, vapor pressure deficit (VPD), was calculated as
\(\text{VPD} = e - e_s\)
where
\(e = 6.11 \cdot 10^{\frac{7.5 \cdot \text{Dewpoint}}{237.3 + \text{Dewpoint}}}\)
and
\(e_s = 6.11 \cdot 10^{\frac{7.5 \cdot \text{Air temperature}}{237.3 + \text{Air temperature}}}\)
| term | t | P |
|---|---|---|
| Relative humidity | -2.67 | 0.02 |
| Wind speed | 5.09 | 0 |
| Fuel moisture | -2.78 | 0.03 |
| Fuel load | -0.28 | 0.78 |
| term | t | P |
|---|---|---|
| Relative humidity | -1.45 | 0.15 |
| Wind speed | 0.28 | 0.78 |
| Fuel moisture | -1.95 | 0.08 |
| Fuel load | 1.26 | 0.27 |
| term | t | P |
|---|---|---|
| Relative humidity | -2.3 | 0.02 |
| Wind speed | 0.13 | 0.9 |
| Fuel moisture | -0.19 | 0.86 |
| Fuel load | 0.46 | 0.66 |
| PC1 | PC2 | PC3 | |
|---|---|---|---|
| Eigenvalue | 1.574 | 1.002 | 0.4243 |
| Proportion Explained | 0.5246 | 0.334 | 0.1414 |
| Cumulative Proportion | 0.5246 | 0.8586 | 1 |
## Centroids:
## PC1 PC2 PC3
## locationCG 0.0497 0.0106 -0.0348
## locationH -0.1148 -0.0244 0.0805
##
## Goodness of fit:
## r2 Pr(>r)
## location 0.0207 0.065 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## Blocks: strata
## Permutation: free
## Number of permutations: 199
No difference in fire behavior patterns between Hettinger and Central Grasslands; this is expected.
##
## ***VECTORS
##
## PC1 PC2 PC3 r2 Pr(>r)
## MaxWindSpeed -0.23784 0.87424 -0.42323 0.0388 0.298
## AirTemp 0.64969 -0.75916 0.03972 0.0433 0.223
## dpC 0.90056 -0.14982 -0.40809 0.0757 0.059 .
## RH 0.60329 0.47220 -0.64270 0.1112 0.003 **
## VPD 0.30924 -0.89685 0.31627 0.0519 0.079 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## Blocks: strata
## Permutation: free
## Number of permutations: 999
dpC, RH, and VPD had statistically-significant associations with variation in fire behavior, but even these relationships were weak.
Principal Components Analysis of fire behavior data (response variables in red) for prescribed burns on rangeland at Hettinger (H) and Central Grasslands (CG) Research Extension Centers. No difference between locations and only the vapor pressure deficit (VPD) and relative humidty (RH) gradients had significant associations with variation in fire behavior.
It is interesting that the gradient in temperature at the soil surface (soil °C) is orthogonal to both canopy temperature and rate of spread (canopy °C and ROS, respectively). This confirms trends observed in the regression analysis that the conditions most conducive to fast, high-intensity fires are not the same that result in surface heating.
Managers might be able to use high windspeeds to mitigate soil heating when fuels are dry, or focus on conditions with low RH to ensure litter burns down to the soil surface.
# Packages
pacman::p_load(tidyverse, readr, mice, broom.mixed, vegan,
lubridate, gridExtra, wesanderson)
set.seed(42)
# Data
fp = url('https://raw.githubusercontent.com/devanmcg/SpatialFireBehavior/main/data/AnalysisDataKY.csv')
AnalysisData <- read_csv(fp)
# Extra scripts
source("https://raw.githubusercontent.com/devanmcg/IntroRangeR/master/11_IntroMultivariate/ordinationsGGplot.R")
source('https://raw.githubusercontent.com/cran/mice/master/R/mipo.R')
###
### N O T R U N
###
AllData <-
read_csv(paste0(FilePath, "/data/fromMZ/CompiledData2.csv")) %>%
filter(location != "OAK") %>%
mutate(date = as.Date(date, format = "%m/%d/%Y"),
L = str_remove(location, "REC"),
B = str_sub(block, 1,3),
Ps = str_replace(pasture, "[.]", ""),
Ps = str_sub(Ps, 1,2),
patch = str_replace(patch, "[.]", ""),
y = format(date, "%y")) %>%
unite("FireCode", c(L,B,Ps,patch,y), sep=".") %>%
mutate(time = str_remove(MaxTempTime, "[.]+[0-9]"))%>%
unite(timestamp, c(date, time), sep = " ") %>%
mutate(timestamp = as.POSIXct(timestamp, format = "%Y-%m-%d %H:%M:%S")) %>%
select(FireCode, timestamp, plot, array, TC, MaxC,
AirTemp, RH, dpC, WindSpeed,
LAI, FMC, KgHa)
# Isolate soil surface temperature (TC 4)
SoilTemp <-
filter(AllData, TC == 4) %>%
select(FireCode, plot, array, MaxC) %>%
rename(SoilC = MaxC)
# Summarize array-level data
DataMeans <-
AllData %>%
filter(TC %in% c('1', '2', '3')) %>%
select(-timestamp) %>%
pivot_longer(cols = c(MaxC:KgHa),
names_to = "var",
values_to = "value") %>%
group_by(FireCode, plot, array, var) %>%
summarize(Mean = mean(value) ) %>%
ungroup() %>%
pivot_wider(names_from = var,
values_from = Mean)
# Calculate Vapor Pressure Deficit
DataMeans <-
DataMeans %>%
mutate(e = 6.11*(10^((7.5*dpC)/(237.3+dpC))),
es = 6.11*(10^((7.5*AirTemp)/(237.3+AirTemp))),
VPD = es - e) %>%
select(-e, -es)
# Calculate ROS
D = 1 # Distance between thermocouples (m)
ROS <-
AllData %>%
filter(TC %in% c('1', '2', '3')) %>%
mutate(timestamp = format(timestamp, "%H:%M:%S"),
ArrivalTime = seconds(hms(timestamp)) ) %>%
select(FireCode, plot, array, ArrivalTime) %>%
group_by(FireCode, plot, array) %>%
arrange(ArrivalTime, .by_group = TRUE) %>%
mutate(position = order(order(ArrivalTime, decreasing=FALSE)),
position = recode(position, "1"="a", "2"="b", "3"="c"),
ArrivalTime = as.numeric(ArrivalTime) /60 ) %>%
spread(position, ArrivalTime) %>%
ungroup %>%
mutate( theta_rad = atan((2*c - b - a) / (sqrt(3)*(b - a))),
ros = case_when(
a == b ~ (sqrt(3) / 2) / (c - a) ,
a != b ~ (D*cos(theta_rad) / (b - a) )
)) %>%
select(-a, -b, -c, -theta_rad)
# Combine, filter into final tibble for analysis
AnalysisData <-
full_join(DataMeans, ROS) %>%
left_join(SoilTemp) %>%
filter( ros <= 40,
MaxC >= 40) %>%
rename(FuelMoisture = FMC,
SoilMaxC = SoilC) %>%
mutate(FuelMoisture = ifelse(FuelMoisture >= 0,
FuelMoisture, NA),
FuelMoisture = FuelMoisture * 100) %>%
separate(FireCode, into = c("location", "block", "pasture",
"patch", "year"),
remove = F)
###
###
###
# Calculate imputed datasets on scaled data
imp_sc <- AnalysisData %>%
select(-LAI, -JD) %>%
mutate_at(vars(AirTemp:tHa), ~as.numeric(scale(., center=F))) %>%
mutate(across(location:array, as.factor)) %>%
mice(m=5, seed = 23109, print=F)
# Reduce mids object to tibble for multivariate analysis
imp_raw <-
complete(imp_sc, 'long') %>%
as_tibble() %>%
unite("TreeID", c(location, block, pasture,
year, plot, array), sep = ".") %>%
select(-patch, -.id, -.imp, -FireCode) %>%
pivot_longer(names_to = "response",
values_to = "values",
-TreeID) %>%
group_by(TreeID, response) %>%
summarize(value = median(values)) %>%
ungroup() %>%
pivot_wider(names_from = response,
values_from = value) %>%
separate(TreeID, c("location", "block", "pasture",
"year", "plot", "array")) %>%
mutate(across(location:array, as.factor))
# Mixed-effect regression models
#
# Rate of spread
#
# Fit model
ros_imp <- with(imp_sc, suppressMessages(
lme4::glmer(ros ~ RH + WindSpeed +
FuelMoisture + tHa +
(1|location/block/year/plot),
family=Gamma(link = "log"),
control=lme4::glmerControl(optimizer="bobyqa",
optCtrl=list(maxfun=100000)) )) )
ros_imp_pooled <- pool(ros_imp)$pooled %>%
as_tibble()
# Get terms
ros_terms <-
full_join(
summary(pool(ros_imp)) %>%
as_tibble() %>%
rownames_to_column("row"),
confint.mipo(pool(ros_imp)) %>%
as_tibble() %>%
rownames_to_column("row") )
#
# Maximum canopy temperature
#
# Fit model
canopy_imp <- with(imp_sc, suppressMessages(
lme4::glmer(MaxC ~ RH + WindSpeed +
FuelMoisture + tHa +
(1|location/block/year/plot),
family=Gamma(link = "log"),
control=lme4::glmerControl(optimizer="bobyqa",
optCtrl=list(maxfun=100000)) )) )
canopy_imp_pooled <- pool(canopy_imp)$pooled %>%
as_tibble()
canopy_terms <-
full_join(
summary(pool(canopy_imp)) %>%
as_tibble() %>%
rownames_to_column("row"),
confint.mipo(pool(canopy_imp)) %>%
as_tibble() %>%
rownames_to_column("row") )
#
# Maximum soil surface temperature
#
# Fit model
soil_imp <- with(imp_sc, suppressMessages(
lme4::glmer(log(SoilMaxC+1) ~ RH + WindSpeed +
FuelMoisture + tHa +
(1|location/block/year/plot),
family=Gamma(link = "log"),
control=lme4::glmerControl(optimizer="bobyqa",
optCtrl=list(maxfun=100000)) )) )
soil_imp_pooled <- pool(soil_imp)$pooled %>%
as_tibble()
soil_terms <-
full_join(
summary(pool(soil_imp)) %>%
as_tibble() %>%
rownames_to_column("row"),
confint.mipo(pool(soil_imp)) %>%
as_tibble() %>%
rownames_to_column("row") )
response_CIs <-
bind_rows(
mutate(soil_terms, response = 'Temp. at surface'),
mutate(canopy_terms, response = 'Temp. above surface'),
mutate(ros_terms, response = "Rate of spread")
) %>%
as_tibble() %>%
rename(lwr = `2.5 %`, upr = `97.5 %`) %>%
filter(term != '(Intercept)') %>%
mutate(term = recode(term, tHa = 'Fuel load',
WindSpeed = 'Wind speed',
RH = 'Relative humidity',
FuelMoisture = 'Fuel moisture'))
response_CIs %>%
filter(response == "Rate of spread") %>%
rename(t = statistic,
P = p.value) %>%
select(term, t, P) %>%
mutate(across(t:P, ~round(., 2))) %>%
pander::pander("Summary statistics for Rate of spread.")
response_CIs %>%
filter(response == "Temp. above surface") %>%
rename(t = statistic,
P = p.value) %>%
select(term, t, P) %>%
mutate(across(t:P, ~round(., 2))) %>%
pander::pander("Summary statistics for flame temperature 15 cm above soil surface.")
response_CIs %>%
filter(response == "Temp. at surface") %>%
rename(t = statistic,
P = p.value) %>%
select(term, t, P) %>%
mutate(across(t:P, ~round(., 2))) %>%
pander::pander("Summary statistics for soil surface temperature.")
# Fire behavior PCA
fb_d <-
imp_raw %>%
select(MaxC, ros, SoilMaxC)
fb_pca <- rda(fb_d ~ 1, 'euc', scale = T)
# Testing for differences between locations
pca_fc <- envfit( fb_pca ~ location, imp_raw,
choices = c(1:3),
strata = imp_raw$year,
199)$factors
pca_fc
# Testing fire weather against PCA
pca_ev <- envfit(fb_pca ~ MaxWindSpeed+AirTemp+dpC+RH+VPD,
data = imp_raw,
choices = c(1:3),
strata = imp_raw$location)
pca_ev