#' Gather information for the NOAA Species Information System (SIS)
#'
#' Processes model results contained in the list created by
#' [SS_output()] in a format that is more convenient for submission
#' to SIS. Currently the results are returned invisibly as a list of two tables
#' and written to a CSV file from which results could be copied into SIS.
#' In the future some more direct link could be explored to avoid the manual
#' copy step.
#' @param model Output from SS_output
#' @param dir Directory in which to write the CSV files.
#' @param writecsv Write results to a CSV file (where the name will have the
#' format "\[stock\]_2019_SIS_info.csv" where `stock`
#' is an additional input
#' @param stock String to prepend id info to filename for CSV file
#' @param assessment_type "Operational" or "Stock Monitoring Updates"
#' (or perhaps additional options as well)
#' @param final_year Year of assessment and reference points
#' (typically will be `model[["endyr"]] + 1`)
#' @param data_year Last year of of timeseries data
#' @param month Month when assessment was conducted (within `final_year`)
#' @param sciencecenter Origin of assessment report
#' @param Mgt_Council Council jurisdiction. Currently only
#' `"PFMC"` (Pacific Fishery Management Council) and `"GM"`
#' (Gulf of Mexico) are the only options.
#' @param SpawnOutputLabel Units for spawning output if not in mt
#' (e.g. "Millions of eggs"). In the future this can be included in the
#' `model` list created by `r4ss::SS_output()`
#' @param contact Name and/or email address for lead author.
#' @param review_result Something like "Full Acceptance"
#' @param catch_input_data Qualitative category for catch input data
#' @param abundance_input_data Qualitative category for abundance input data
#' @param bio_input_data Qualitative category for biological input data
#' @param comp_input_data Qualitative category for size/age composition input
#' data
#' @param ecosystem_linkage Qualitative category for ecosystem linkage
#' @author Ian G. Taylor, Andi Stephens, LaTreese S. Denson
#' @export
#' @seealso [SS_output()]
#' @examples
#' \dontrun{
#' # read the model output
#' model <- SS_output(
#' dir = system.file("extdata/simple_small", package = "r4ss"),
#' printstats = FALSE, verbose = FALSE
#' )
#' # run get_SIS_info:
#' info <- get_SIS_info(model, stock = "SimpleExample", month = 1)
#' }
#'
get_SIS_info <- function(model,
dir = model[["inputs"]][["dir"]],
writecsv = TRUE,
stock = "StockName",
assessment_type = "Operational",
final_year = model[["endyr"]] + 1,
data_year = model[["endyr"]],
month,
sciencecenter = "NWFSC",
Mgt_Council = "PFMC",
# SpawnOutputLabel is only available in an r4ss branch
# https://github.com/r4ss/r4ss/compare/main...spawn_output_label_838
# so will be NULL for most models, which is dealt with later
SpawnOutputLabel = model[["SpawnOutputLabel"]],
contact = "first.last@noaa.gov",
review_result = "XXXX",
catch_input_data = "XXXX",
abundance_input_data = "XXXX",
bio_input_data = "XXXX",
comp_input_data = "XXXX",
ecosystem_linkage = "XXXX") {
# construct filename
filename_values <- paste(gsub(" ", "_", stock), final_year,
"SIS_info_values.csv",
sep = "_"
)
filename_timeseries <- paste(gsub(" ", "_", stock), final_year,
"SIS_info_timeseries.csv",
sep = "_"
)
message(
"writing SIS info to CSV files:\n",
file.path(dir, filename_values), "\n",
file.path(dir, filename_timeseries)
)
# years to report for catch-related quantities
startyr <- model[["startyr"]]
years <- startyr:final_year
# get values from TIME_SERIES table
ts <- model[["timeseries"]]
# if check for unsupported model configurations
if (model[["nseasons"]] > 1) {
stop("multi-season models are not yet supported")
}
# aggregate across areas if needed
if (model[["nareas"]] > 1) {
ts.tmp <- ts # copy of table
ts <- ts.tmp[ts.tmp[["Area"]] == 1, ]
for (iarea in 2:model[["nareas"]]) {
ts[, -(1:4)] <- ts[, -(1:4)] + ts.tmp[ts.tmp[["Area"]] == iarea, -(1:4)]
}
}
# calculate fraction unfished
ts[["FractionUnfished"]] <-
ts[["SpawnBio"]] / ts[["SpawnBio"]][ts[["Era"]] == "VIRG"]
# extract columns of interest
ts_tab <- data.frame(
Year = ts[["Yr"]],
Total_Bio = round(ts[["Bio_all"]], 2),
SpawnBio = round(ts[["SpawnBio"]], 2),
FractionUnfished = round(ts[["FractionUnfished"]], 3),
Summary_Bio = round(ts[["Bio_smry"]], 2),
Recruitment = round(ts[["Recruit_0"]], 2)
)
# subset for years of interest and replace potential bad 0 value with NA
ts_tab <- ts_tab[ts_tab[["Year"]] %in% years, ]
# calculate total dead catch (aggregated across fleets)
dead_bio_columns <- grep("dead(B)", names(model[["timeseries"]]), fixed = TRUE)
dead_N_columns <- grep("dead(N)", names(model[["timeseries"]]), fixed = TRUE)
if (length(dead_bio_columns) > 1) {
catch_tab <- data.frame(
Year = model[["timeseries"]][["Yr"]],
Catch_bio = apply(model[["timeseries"]][, dead_bio_columns],
MARGIN = 1, FUN = sum
),
Catch_n = apply(model[["timeseries"]][, dead_N_columns],
MARGIN = 1, FUN = sum
)
)
}
if (length(dead_bio_columns) == 1) {
catch_tab <- data.frame(
Year = model[["timeseries"]][["Yr"]],
Catch_bio = model[["timeseries"]][, dead_bio_columns],
Catch_n = model[["timeseries"]][, dead_N_columns]
)
}
if (length(dead_bio_columns) == 0) {
stop("No columns matching 'dead(B)' in model[['timeseries'']]")
}
# not sure why this is needed, but it is
catch_tab[["Year"]] <- as.numeric(catch_tab[["Year"]])
if (model[["nareas"]] > 1) {
catch_tab <- aggregate(catch_tab[["Catch"]],
by = list(catch_tab[["Year"]]),
FUN = sum
)
names(catch_tab) <- c("Year", "Catch")
}
catch_tab[, -1] <- round(catch_tab[, -1], 2)
rownames(catch_tab) <- 1:nrow(catch_tab)
# filter years
catch_tab <- catch_tab[catch_tab[["Year"]] %in% years, ]
# calculate proxy-F values (e.g. exploitation rate)
F_tab <- data.frame(
Year = years,
F_values = round(model[["derived_quants"]][paste0("F_", years), "Value"], 4)
)
# filter years
F_tab <- F_tab[F_tab[["Year"]] %in% years, ]
# SPR-related quantities
spr_tab <- model[["sprseries"]][, c("Yr", "SPR_report", "Tot_Exploit", "SPR")]
names(spr_tab)[1] <- "Year"
if (model[["SPRratioLabel"]] != "1-SPR") {
# add 1-SPR if it does not exist as SPR_report already
spr_tab[["1-SPR"]] <- 1 - spr_tab[["SPR"]]
spr_tab <- spr_tab[, c("Year", "SPR_report", "Tot_Exploit", "1-SPR")]
} else {
spr_tab <- spr_tab[, c("Year", "SPR_report", "Tot_Exploit")]
}
# filter years
spr_tab <- spr_tab[spr_tab[["Year"]] %in% years, ]
spr_tab[, -1] <- round(spr_tab[, -1], 3)
# merge columns from time series, catch, F, and SPR together
tab <- merge(merge(merge(ts_tab, catch_tab), F_tab), spr_tab)
if (nrow(tab) != length(years) || any(tab[["Year"]] != years)) {
stop(
"problem with mismatch of years:\n",
"range(years): ", range(years), "\n",
"range(tab[['Year']]): ", range(tab[["Year"]]), "\n"
)
}
# replace NA with 0 in exploitation rate for years with 0 catch
tab[["F_values"]][!is.na(tab[["Catch_bio"]]) & tab[["Catch_bio"]] == 0] <- 0
# again for 8th column (SPR-ratio)
tab[["SPR_report"]][!is.na(tab[["Catch_bio"]]) & tab[["Catch_bio"]] == 0] <- 0
# replace values with NA for years beyond the data years
if (Mgt_Council == "PFMC") {
tab[tab[["Year"]] > data_year, "Catch_bio"] <- NA
tab[tab[["Year"]] > data_year, "Catch_n"] <- NA
tab[tab[["Year"]] > data_year, "F_values"] <- NA
tab[tab[["Year"]] > data_year, "SPR_report"] <- NA
tab[tab[["Year"]] > data_year, "Tot_Exploit"] <- NA
if ("1-SPR" %in% names(tab)) {
tab[tab[["Year"]] > data_year, "1-SPR"] <- NA
}
}
# age for summary biomass
summary_age <- model[["summary_age"]]
if (is.null(summary_age)) {
summary_age <- "X"
}
summary_bio_label <- paste0("Age ", summary_age, "+ biomass")
#######################################
# create new table of metadata to write as multiple header rows
header_info <- data.frame(matrix("", ncol = ncol(tab), nrow = 4),
stringsAsFactors = FALSE
)
colnames(header_info) <- names(tab)
rownames(header_info) <- c("Category", "Primary", "Description", "Unit")
# putting "year" in the right place
header_info["Category", "Year"] <- "Year"
# info on total biomass
header_info["Category", "Total_Bio"] <- "Abundance"
header_info["Primary", "Total_Bio"] <- "N"
header_info["Description", "Total_Bio"] <- "Total Biomass"
header_info["Unit", "Total_Bio"] <- "Metric Tons"
# info on summary biomass
header_info["Category", "Summary_Bio"] <- "Abundance"
header_info["Primary", "Summary_Bio"] <- "N"
header_info["Description", "Summary_Bio"] <- summary_bio_label
header_info["Unit", "Summary_Bio"] <- "Metric Tons"
# info on spawning biomass
header_info["Category", "SpawnBio"] <- "Spawners"
header_info["Primary", "SpawnBio"] <- "Y"
# SpawnOutputUnits indicating numbers vs biomass switch should work for all
# models starting with SS3 version 3.30.20 (September 2020)
if (model[["SpawnOutputUnits"]] == "numbers") {
header_info["Description", "SpawnBio"] <- "Spawning Output(Eggs)"
if (is.null(SpawnOutputLabel)) {
warning("Need to provide a label for the spawning output (e.g. 'millions of eggs')")
SpawnOutputLabel <- "XXXX eggs"
}
header_info["Unit", "SpawnBio"] <- SpawnOutputLabel
B_basis <- "Stock Reproductive Output"
B_unit <- SpawnOutputLabel
} else {
header_info["Description", "SpawnBio"] <- " Female Mature Spawning Biomass"
header_info["Unit", "SpawnBio"] <- "Metric Tons"
B_basis <- "Spawning Stock Biomass"
B_unit <- "mt"
}
# info on fraction unfished
header_info["Category", "FractionUnfished"] <- "Spawners"
header_info["Primary", "FractionUnfished"] <- "N"
header_info["Description", "FractionUnfished"] <- "Female Mature Relative Spawning Biomass SSB/SSB0"
header_info["Unit", "FractionUnfished"] <- "Rate"
# info on recruitment
header_info["Category", "Recruitment"] <- "Recruitment"
header_info["Primary", "Recruitment"] <- "Y"
header_info["Description", "Recruitment"] <- "Abundance at Age 0"
header_info["Unit", "Recruitment"] <- "Number x 1000"
# info on Catch biomass
header_info["Category", "Catch_bio"] <- "Catch"
header_info["Primary", "Catch_bio"] <- "Y"
header_info["Description", "Catch_bio"] <- "Modeled Total Catch"
header_info["Unit", "Catch_bio"] <- "Metric Tons"
# info on Catch numbers
header_info["Category", "Catch_n"] <- "Catch"
header_info["Primary", "Catch_n"] <- "N"
header_info["Description", "Catch_n"] <- "Modeled Total Catch"
header_info["Unit", "Catch_n"] <- "Number x 1000"
# check for redundancy between F_values and Tot_Exploit columns
if (model[["F_report_basis"]] == "_abs_F;_with_F=Exploit(bio)") {
message(
"F_YYYY values are redundant with Tot_Exploit column in SPR series, ",
"excluding from output for SIS."
)
# remove redundant F_values column
header_info <- header_info %>% dplyr::select(-F_values)
tab <- tab %>% dplyr::select(-F_values)
} else {
# info on Exploitation rate (whatever is chosen for the F_YYYY quants)
header_info["Category", "F_values"] <- "Fmort"
header_info["Primary", "F_values"] <-
dplyr::case_when(
Mgt_Council == "PFMC" ~ "N", # not used as primary exploitation rate by PFMC
Mgt_Council == "GM" ~ "XXXX", # fill in value here
TRUE ~ "XXXX" # any other Mgt_Council
)
# clean up F_report_basis (e.g. "_abs_F;_with_F=sum(full_Fs)" to "abs F; with F=sum(full Fs)")
header_info["Description", "F_values"] <- gsub("_", " ", gsub("^_", "", model[["F_report_basis"]]))
header_info["Unit", "F_values"] <- "Rate"
}
# info on total Exploitation rate (catch / summary bio)
header_info["Category", "Tot_Exploit"] <- "Fmort"
header_info["Primary", "Tot_Exploit"] <-
dplyr::case_when(
Mgt_Council == "PFMC" ~ "N", # not used as primary exploitation rate by PFMC
Mgt_Council == "GM" ~ "XXXX", # fill in value here
TRUE ~ "XXXX" # any other Mgt_Council
)
header_info["Description", "Tot_Exploit"] <-
paste0("Relative Exploitation Rate (Catch/", summary_bio_label, ")")
header_info["Unit", "Tot_Exploit"] <- "Rate"
# info on SPR_report (whatever is chosen for SPR ratio)
header_info["Category", "SPR_report"] <- "Fmort"
header_info["Primary", "SPR_report"] <-
dplyr::case_when(
Mgt_Council == "PFMC" ~ "Y", # often used as primary by PFMC
Mgt_Council == "GM" ~ "XXXX", # fill in value here
TRUE ~ "XXXX" # any other Mgt_Council
)
header_info["Description", "SPR_report"] <- model[["SPRratioLabel"]]
header_info["Unit", "SPR_report"] <- "Rate"
# If SPR_report is not "1-SPR" (e.g. if it's a ratio of 1-SPR to something else)
# then add column with 1 - SPR
if (model[["SPRratioLabel"]] != "1-SPR") {
header_info["Category", "1-SPR"] <- "Fmort"
header_info["Primary", "1-SPR"] <-
dplyr::case_when(
Mgt_Council == "PFMC" ~ "Y", # 1 - SPR is used as primary exploitation rate by PFMC
Mgt_Council == "GM" ~ "XXXX", # fill in value here
TRUE ~ "XXXX" # any other Mgt_Council
)
# change the value from the SPR_report set above
header_info["Primary", "SPR_report"] <-
dplyr::case_when(
Mgt_Council == "PFMC" ~ "N", # SPR_ratio is not primary if not equal to 1-SPR
Mgt_Council == "GM" ~ "XXXX", # fill in value here
TRUE ~ "XXXX" # any other Mgt_Council
)
header_info["Description", "1-SPR"] <- "1 - SPR"
header_info["Unit", "1-SPR"] <- "Rate"
}
# add extra column to help with format of CSV file
tab <- cbind("", tab)
# end of time series calculations
######################################################################
# start gathering quantities of interest
if (model[["sprtarg"]] == -999) {
SPRtarg_text <- "SPR_XX%"
model[["sprtarg"]] <- NA
warning("No value for model[['sprtarg']]")
} else {
SPRtarg_text <- paste0("SPR", 100 * model[["sprtarg"]], "%") # e.g. SPR50%
}
# PFMC Settings (F limit and MSY proxy are in units of 1-SPR)
if (Mgt_Council == "PFMC") {
F_limit <- 1 - model[["sprtarg"]]
F_limit_basis <- paste0("1-SPR_", SPRtarg_text)
F_msy <- F_limit
F_msy_basis <- F_limit_basis
}
# GM Settings: Using F_SPR target
if (Mgt_Council == "GM") {
# Annual F_SPR names changed between versions
if ("annF_SPR" %in% rownames(model[["derived_quants"]])) {
F_limit <- round(model[["derived_quants"]]["annF_SPR", "Value"], digits = 3)
} else {
F_limit <- round(model[["derived_quants"]]["Fstd_SPRtgt", "Value"], digits = 3)
}
F_msy <- F_limit
F_limit_basis <- paste0("F", 100 * model[["btarg"]], "%")
F_msy_basis <- paste0("F", 100 * model[["btarg"]], "% as Proxy")
}
if (model[["btarg"]] == -999) {
Btarg_text <- "BXX%"
model[["btarg"]] <- NA
warning("No value for model[['btarg']]")
} else {
# PFMC Settings
if (Mgt_Council == "PFMC") {
Btarg_text <- paste0("B", 100 * model[["btarg"]], "%") # e.g. B40%
}
# GM Settings:
if (Mgt_Council == "GM") {
Btarg_text <- paste0("SPR", 100 * model[["btarg"]], "%")
}
}
if (model[["minbthresh"]] == -999) {
MinBthresh_text <- "SSBXX%"
model[["minbthresh"]] <- NA
warning("No value for model[['minbthresh']]")
} else {
MinBthresh_text <- paste0("SSB", 100 * model[["minbthresh"]], "%") # e.g. B25%
B_msy_basis <- paste0("SS", Btarg_text)
B_msy <- model[["SBzero"]] * model[["btarg"]]
B_limit <- model[["SBzero"]] * model[["minbthresh"]]
}
# GM Settings:
if (Mgt_Council == "GM") {
MinBthresh_text <- paste0("(1-M)*SPR", 100 * model[["btarg"]], "%") # e.g. B25%
B_msy_basis <- Btarg_text
eq_year <- data_year + model[["nforecastyears"]]
B_msy <- model[["derived_quants"]][["Value"]][which(model[["derived_quants"]][["Label"]] == paste0("SSB_", eq_year))]
B_limit <- (0.5 * B_msy)
}
B_year <- final_year
ts_tab_short <- data.frame(
Year = ts[["Yr"]],
SpawnBio = round(ts[["SpawnBio"]], 2)
)
Bcurrent <- ts_tab_short[["SpawnBio"]][ts_tab_short[["Year"]] == final_year]
# GM Settings:
if (Mgt_Council == "GM") {
B_year <- data_year
Bcurrent <- tab[["SpawnBio"]][tab[["Year"]] == data_year]
}
# MSY-proxy labels were changed at some point
if ("Dead_Catch_SPR" %in% rownames(model[["derived_quants"]])) {
Yield_at_SPR_target <- round(model[["derived_quants"]]["Dead_Catch_SPR", "Value"], 1)
Yield_at_B_target <- round(model[["derived_quants"]]["Dead_Catch_Btgt", "Value"], 1)
} else {
Yield_at_SPR_target <- round(model[["derived_quants"]]["TotYield_SPRtgt", "Value"], 1)
Yield_at_B_target <- round(model[["derived_quants"]]["TotYield_Btgt", "Value"], 1)
}
if (Mgt_Council == "PFMC") {
best_F_column <- "1-SPR"
if (!best_F_column %in% names(tab)) {
best_F_column <- "SPR_report"
}
Best_F_Est <- tab[tab[["Year"]] == data_year, best_F_column]
F_basis <- "Equilibrium SPR"
F_unit <- "1-SPR"
}
# GM Settings: Best F estimate = geometric mean of last three data years
if (Mgt_Council == "GM") {
Best_F_Est <- round(mean(tab[["F_values"]][tab[["Year"]] %in% c((data_year - 2):data_year)]), 2)
F_basis <- paste0("Total_Catch/Total_Biomass_Geometric_Mean_", (data_year - 2), "-", data_year)
F_unit <- "Exploitation Rate"
}
# SS3 version
SS_version <- strsplit(model[["SS_version"]], split = ";")[[1]][1]
gsub("-safe", "", SS_version)
gsub("-opt", "", SS_version)
stock_level_to_MSY <- Bcurrent / B_msy
if (stock_level_to_MSY > 1) {
stock_level_to_MSY <- "ABOVE"
} else if (stock_level_to_MSY < 0.90) {
stock_level_to_MSY <- "BELOW"
} else if (stock_level_to_MSY > 0.90 & stock_level_to_MSY < 1) {
stock_level_to_MSY <- "NEAR"
} else {
stock_level_to_MSY <- "UNKNOWN"
}
# make big 2-column table of info about each model
info_tab <- c(
paste0("SAIP:,", "https://spo.nmfs.noaa.gov/sites/default/files/TMSPO183.pdf"),
"",
paste0("Stock / Entity Name,", stock),
paste0("Assessment Type,", assessment_type),
paste0("Ensemble/Multimodel Approach,", "NA"),
# sprintf command in line below converts month 9 to "09" (for example)
paste0("Asmt Year and Month,", final_year, ".", sprintf("%02d", month)),
paste0("Last Data Year,", data_year),
paste0("Asmt Model Category,", "6 - Statistical Catch-at-Age (SS, ASAP, AMAK, BAM, MultifancL< CASAL)"),
paste0("Asmt Model Category,", "SS"),
paste0("Model Version,", SS_version),
paste0("Lead Lab,", sciencecenter),
paste0("Point of Contact (assessment lead),", contact),
paste0("Review Result,", review_result),
paste0("Catch Input Data,", catch_input_data),
paste0("Abundance Input Data,", abundance_input_data),
paste0("Biological Input Data,", bio_input_data),
paste0("Size/Age Composition Input Data,", comp_input_data),
paste0("Ecosystem Linkage,", ecosystem_linkage),
"",
"Fishing Mortality Estimates",
paste0("F Year, ", data_year),
paste0("Min F Estimate, ", ""),
paste0("Max F Estimate, ", ""),
paste0("Best F Estimate, ", Best_F_Est),
paste0("F Basis, ", F_basis),
paste0("F Unit, ", F_unit),
"",
"Domestic Fishing Mortality Derive Management Quantities",
paste0("Flimit, ", F_limit),
paste0("Flimit Basis, ", F_limit_basis),
paste0("FMSY, ", F_msy),
paste0("FMSY Basis, ", F_msy_basis),
paste0("F target, ", ""),
paste0("F target Basis, ", ""),
paste0("F/F_limit, ", round(Best_F_Est / F_limit, 3)),
paste0("F/F_MSY, ", round(Best_F_Est / F_msy, 3)),
paste0("F/F_target, ", ""),
"",
"Biomass Estimates",
paste0("B Year, ", B_year),
paste0("Min B. Estimate, ", ""),
paste0("Max. B Estimate, ", ""),
paste0("Best B Estimate, ", round(Bcurrent, 1)),
paste0("B Basis, ", B_basis),
paste0("B Unit, ", B_unit),
paste0("MSY, ", Yield_at_B_target),
paste0("MSY Unit, ", "mt"),
"",
"Domestic Biomass Derived Management Quantities",
paste0("Blimit, ", round(B_limit, 1)),
paste0("Blimit Basis, ", MinBthresh_text),
paste0("BMSY, ", round(B_msy, 1)),
paste0("BMSY Basis, ", B_msy_basis), # e.g. SSB40%
paste0("B/Blimit, ", round((Bcurrent / B_limit), 3)),
paste0("B/Bmsy, ", round((Bcurrent / B_msy), 3)),
paste0("Stock Level (relative) to BMSY, ", stock_level_to_MSY),
"",
""
)
if (writecsv) {
# write 2-column table with quantities of interest
write.table(info_tab,
file = file.path(dir, filename_values),
quote = FALSE, sep = ",", row.names = FALSE, col.names = FALSE
)
# write header rows for time series table
write.table(header_info,
file = file.path(dir, filename_timeseries),
quote = FALSE, sep = ",", row.names = TRUE, col.names = FALSE,
append = FALSE
)
# add time series values (suppressing warning about 'appending column names to file')
suppressWarnings(
write.table(tab,
file = file.path(dir, filename_timeseries),
quote = FALSE, sep = ",", row.names = FALSE, col.names = FALSE,
append = TRUE
)
)
}
# return the list
invisible(list(info = info_tab, timeseries = tab, header_info = header_info))
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.