Management Strategy Evaluation (MSE) is a simulation framework that tests management strategies/procedures before they are applied to a real fishery.
The core idea: Build a virtual population, extract imperfect fishery data from it, apply different management rules and compare which ones perform best, even under such uncertainties.
MSE works as a feedback loop:
Key point: It is not a matter of “Which MP gives the right answer?” MSE tells us“Which MP performs best DESPITE uncertainty and errors?”
By the end of this practical, you will be able to:
# Install if needed:
# install.packages("openMSE")
# install.packages(c("ggplot2", "dplyr", "tidyr"))
library(openMSE)
library(ggplot2)
library(dplyr)
library(tidyr)
# check versions (just in case...)
cat("openMSE version:", as.character(packageVersion("openMSE")), "\n")## openMSE version: 1.0.1
## MSEtool version: 3.7.5
An Operating Model has 4 components:
You can explore the built-in components that are already available in openMSE:
## [1] "Albacore" "Blue_shark" "Bluefin_tuna"
## [4] "Bluefin_tuna_WAtl" "Butterfish" "Herring"
## [7] "Mackerel" "Porgy" "Rockfish"
## [10] "Snapper" "Sole" "Toothfish"
## [1] "DecE_Dom" "DecE_HDom" "DecE_NDom"
## [4] "FlatE_Dom" "FlatE_HDom" "FlatE_NDom"
## [7] "Generic_DecE" "Generic_FlatE" "Generic_Fleet"
## [10] "Generic_IncE" "IncE_HDom" "IncE_NDom"
## [13] "Low_Effort_Non_Target" "Target_All_Fish" "Targeting_Small_Fish"
We use Blue shark, a relatively slow-growing and low-productivity species (even though in the higher productivity spectrum within pelagic sharks).
OM <- new("OM",
Stock = Blue_shark,
Fleet = Generic_Fleet,
Obs = Generic_Obs,
Imp = Perfect_Imp)
# Simulation parameters
OM@nsim <- 48 # Number of stochastic simulations (use >200 for a real project)
OM@proyears <- 30 # Project 30 years into the future (OK for a longer lived shark)
OM@interval <- 3 # MPs update advice every 3 years (relatively common for management preferences)om_summary <- data.frame(
Parameter = c("Species/Stock", "Maximum age", "Natural mortality (M)",
"Asymptotic length (Linf)", "Growth coefficient (K)",
"Steepness (h)", "Current depletion (SSB/SSB0)",
"Historical years"),
Value = c(OM@Name,
paste(OM@maxage, "years"),
paste(OM@M[1], "–", OM@M[2]),
paste(OM@Linf[1], "–", OM@Linf[2], "cm"),
paste(OM@K[1], "–", OM@K[2]),
paste(OM@h[1], "–", OM@h[2]),
paste(OM@D[1], "–", OM@D[2]),
paste(OM@nyears, "years"))
)
knitr::kable(om_summary, col.names = c("Parameter", "Value / Range"),
caption = "Operating Model Summary")| Parameter | Value / Range |
|---|---|
| Species/Stock | Stock:Blue shark Fleet:Generic_Fleet Obs model:Generic_Obs Imp model:Perfect_Imp |
| Maximum age | 32 years |
| Natural mortality (M) | 0.15 – 0.25 |
| Asymptotic length (Linf) | 195 – 205 cm |
| Growth coefficient (K) | 0.18 – 0.24 |
| Steepness (h) | 0.4 – 0.9 |
| Current depletion (SSB/SSB0) | 0.05 – 0.6 |
| Historical years | 50 years |
Question: Why are parameters given as ranges (e.g., M = 0.15–0.25) rather than single values?
Simulate() runs the OM through the historical period,
generating 48 stochastic realisations of the population history.
Look at some values from one of the simulations
##
## === Reference Points (simulation 1) ===
## MSY: 1578 tonnes
## FMSY: 0.091
## BMSY: 23775 tonnes
## SSBMSY: 16973 tonnes
These plots show the “truth” across all 48 simulations. The spread represents our uncertainty about biology.
# SBiomass is [nsim × nyears × areas] — sum across areas
ssb <- apply(Hist@TSdata$SBiomass, c(1, 2), sum)
yrs <- 1:OM@nyears
matplot(yrs, t(ssb), type = "l", col = rgb(0, 0, 0.5, 0.15), lty = 1,
xlab = "Year", ylab = "Spawning Biomass",
main = "Historical Spawning Biomass (all simulations)")
lines(yrs, colMeans(ssb), col = "red", lwd = 2)
legend("topright", "Mean", col = "red", lwd = 2, bty = "n")# F/FMSY trajectories
Fmort <- Hist@TSdata$Find
fmsy_vec <- Hist@Ref$ByYear$FMSY[, OM@nyears]
F_ratio <- Fmort / fmsy_vec
matplot(yrs, t(F_ratio), type = "l", col = rgb(0.5, 0, 0, 0.15), lty = 1,
xlab = "Year", ylab = expression(F/F[MSY]),
main = "Historical F/FMSY (all simulations)")
lines(yrs, colMeans(F_ratio, na.rm = TRUE), col = "red", lwd = 2)
abline(h = 1, lty = 2, col = "grey40")
legend("topright", c("Mean", "F = FMSY"), col = c("red", "grey40"),
lwd = c(2, 1), lty = c(1, 2), bty = "n")# SB/SBMSY trajectories
ssbmsy_vec <- Hist@Ref$ByYear$SSBMSY[, OM@nyears]
SB_ratio <- ssb / ssbmsy_vec
matplot(yrs, t(SB_ratio), type = "l", col = rgb(0, 0.4, 0, 0.15), lty = 1,
xlab = "Year", ylab = expression(SSB/SSB[MSY]),
main = "Historical SSB/SSBMSY (all simulations)")
lines(yrs, colMeans(SB_ratio, na.rm = TRUE), col = "red", lwd = 2)
abline(h = 1, lty = 2, col = "grey40")
legend("topright", c("Mean", "SSB = SSBMSY"), col = c("red", "grey40"),
lwd = c(2, 1), lty = c(1, 2), bty = "n")Question: Looking at the SB/SBMSY plot: are there many simulations where the stock currently overfished (below 1)? Why do different simulations show different trajectories?
Important: the MP does NOT see the truth. It sees
data WITH observation error. The Data object represents
what an MP would receive.
catch_scaled <- Data@Cat[1,] / max(Data@Cat[1,], na.rm = TRUE)
index_scaled <- Data@Ind[1,] / max(Data@Ind[1,], na.rm = TRUE)
plot(Data@Year, catch_scaled, type = "l", lwd = 2,
xlab = "Year", ylab = "Scaled value (0–1)",
main = "Catch vs Index (scaled)", ylim = c(0, 1.1))
lines(Data@Year, index_scaled, lwd = 2, col = "darkgreen")
legend("topright", c("Catch", "Index"), col = c("black", "darkgreen"),
lwd = 2, bty = "n")Question: Does catch and index show similar or opposite trends? What does that mean?
In this plot, the red line is what’s really happening (OM truth). The green dashed line is what the MP sees and works with (i.e., noisy data). There will be some disagreement, meaning the MP will make suboptimal decision. And that gets back into the OM later during the feedback-loop process.
ssb_sim1 <- apply(Hist@TSdata$SBiomass[1, , ], 1, sum)
ssb_scaled <- ssb_sim1 / max(ssb_sim1, na.rm = TRUE)
plot(Data@Year, ssb_scaled, type = "l", lwd = 3, col = "red",
xlab = "Year", ylab = "Scaled value (0–1)",
main = "Truth (OM) vs What the MP Sees (Data)", ylim = c(0, 1.2))
lines(Data@Year, index_scaled, lwd = 2, col = "darkgreen", lty = 2)
legend("topright", c("True SSB (OM)", "Observed Index (with error)"),
col = c("red", "darkgreen"), lwd = c(3, 2), lty = c(1, 2), bty = "n")A Management Procedure is a function that:
| MP | Type | Logic (one sentence) |
|---|---|---|
| FMSYref | Reference benchmark | Fish at exactly FMSY with perfect knowledge |
| AvC | Empirical (no feedback) | TAC = mean historical catch (never adjusts) |
| Iratio | Empirical (level-based) | Adjust TAC by ratio of recent to previous index |
| Islope1 | Empirical (trend-based) | Adjust TAC based on the slope of the index |
| MyMP | Custom (precautionary) | Cut fast (−20%), grow slow (+10%) based on index |
Different MPs use different logic. None of them “knows” the truth.
This is NOT a real MP. It uses the true FMSY from the OM (information that in a real fishery we’d never have). Its purpose is to show the best possible outcome. No real MP can beat it.
# FMSYref can't be tested standalone (it needs OM internals from Project)
# But we can approximate its TAC: FMSY × current vulnerable biomass
fmsy_sim1 <- Hist@Ref$ByYear$FMSY[1, OM@nyears]
vb_sim1 <- sum(Hist@TSdata$VBiomass[1, OM@nyears, ])
TAC_fref <- round(fmsy_sim1 * vb_sim1)
cat("FMSYref Logic: Fish at exactly FMSY (uses true value from OM)\n")## FMSYref Logic: Fish at exactly FMSY (uses true value from OM)
## Approximate TAC year1: 3854 tonnes
The simplest possible MP: TAC = mean of all historical catches. It completely ignores the abundance index. No feedback at all.
This is what happens when we want to maintain status-quo: “just keep catches at their historical average and do nothing else about it”.
Rec_AvC <- AvC(x = 1, Data = Data, reps = 1)
cat("AvC Logic: TAC = mean of all historical catches\n")## AvC Logic: TAC = mean of all historical catches
## Recommended TAC year1: 2264 tonnes
An empirical MP that responds to the index level:
\[\text{TAC}_{\text{new}} = \text{TAC}_{\text{previous}} \times \frac{\text{recent index}}{\text{previous index}}\]
If the recent index is higher than before → increase TAC. If lower → decrease TAC.
It asks: “Where IS the stock?”
Rec_Ir <- Iratio(x = 1, Data = Data, reps = 1)
cat("Iratio Logic: TAC adjusted by ratio of recent to previous index\n")## Iratio Logic: TAC adjusted by ratio of recent to previous index
## Recommended TAC year1: 4453 tonnes
An empirical MP that responds to the index trend. It estimates the slope of the index over recent years and adjusts TAC accordingly.
It asks: “Where is the stock GOING?”
Rec_Is <- Islope1(x = 1, Data = Data, reps = 1)
cat("Islope1 Logic: TAC adjusted based on slope (trend) of recent index\n")## Islope1 Logic: TAC adjusted based on slope (trend) of recent index
## Recommended TAC year1: 4036 tonnes
Question: What is the key difference between Iratio and Islope1? Can you think of a situation where one would give good advice but the other would not?
This is our custom MP. The core idea: “Be precautionary: reduce TAC fast when needed, increase slowly when possible”
The asymmetry is the precautionary principle we are implementing in this casen: stock collapses are hard to reverse biologically (especially for sharks), so we cut more aggressively when things look bad but increase more cautiously when things look better.
MyMP <- function(x, Data, reps = 1, plot = FALSE) {
Rec <- new("Rec")
ny <- length(Data@Year)
# Step 1: Start from last year's catch
base_TAC <- Data@MPrec[x]
# Step 2: Compare recent index (last 3 yrs) to previous (3 yrs before)
idx_recent <- mean(Data@Ind[x, (ny-2):ny], na.rm = TRUE)
idx_previous <- mean(Data@Ind[x, (ny-5):(ny-3)], na.rm = TRUE)
# Step 3: Apply asymmetric HCR in TAC changes
if (!is.na(idx_recent) && !is.na(idx_previous) && idx_previous > 0) {
if (idx_recent >= idx_previous) {
Rec@TAC <- base_TAC * 1.10 # Index rising → cautious +10%
} else {
Rec@TAC <- base_TAC * 0.80 # Index falling → aggressive -20%
}
} else {
Rec@TAC <- base_TAC # No change in data → keep TAC unchanged
}
return(Rec)
}
class(MyMP) <- "MP"
# Test it
Rec_My <- MyMP(x = 1, Data = Data, reps = 1)
cat("MyMP Logic: Cut fast (-20%), grow slow (+10%) based on index direction\n")## MyMP Logic: Cut fast (-20%), grow slow (+10%) based on index direction
## Recommended TAC year1: 3316 tonnes
All MPs look at the same data but give different advice. Why? Because they use different rules on how they are using the data.
TAC_compare <- data.frame(
MP = c("FMSYref*", "AvC", "Iratio", "Islope1", "MyMP"),
TAC = c(TAC_fref, round(Rec_AvC@TAC), round(Rec_Ir@TAC),
round(Rec_Is@TAC), round(Rec_My@TAC))
)
print(TAC_compare)## MP TAC
## 1 FMSYref* 3854
## 2 AvC 2264
## 3 Iratio 4453
## 4 Islope1 4036
## 5 MyMP 3316
barplot(TAC_compare$TAC, names.arg = TAC_compare$MP,
col = c("#E07A5F", "#1C7293", "#81B29A", "#F2CC8F", "#6C4F82"),
main = "Year 1 TAC Recommendations by MP",
ylab = "TAC (tonnes)", las = 2)Question: Which MP recommends the highest TAC in year 1? And the lowest? Is the “perfect knowledge” benchmark (FMSYref) the highest? Why not?
Think about it this way: “Which MP would a fisher prefer in Year 1? But which MP would a fisher prefer in Year 30? Let’s run the MSE and find out.”
Now we run the full MSE: 48 simulations × 30 years × 5 MPs.
mean_catch <- round(apply(MSE@Catch, 2, mean, na.rm = TRUE))
names(mean_catch) <- MPs
# Mean Projected Catch (tonnes/year)
print(mean_catch)## FMSYref AvC Iratio Islope1 MyMP
## 2739 1990 1807 1952 1671
final_BBmsy <- round(apply(MSE@SB_SBMSY[, , OM@proyears], 2, mean, na.rm = TRUE), 2)
names(final_BBmsy) <- MPs
# Mean SB/SBMSY at final projected year (year 30)
print(final_BBmsy)## FMSYref AvC Iratio Islope1 MyMP
## 1.01 0.93 1.47 1.06 1.64
Now we can start to answer some questions: Does what the MP “promised” in Year 1 hold up over 30 years?
comparison <- data.frame(
MP = MPs,
Year1_TAC = c(TAC_fref, round(Rec_AvC@TAC), round(Rec_Ir@TAC),
round(Rec_Is@TAC), round(Rec_My@TAC)),
Mean_Catch_30yr = mean_catch,
Change_pct = round((mean_catch / c(TAC_fref, round(Rec_AvC@TAC),
round(Rec_Ir@TAC), round(Rec_Is@TAC),
round(Rec_My@TAC)) - 1) * 100)
)
# Year 1 TAC vs 30-Year Mean Catch
print(comparison, row.names = FALSE)## MP Year1_TAC Mean_Catch_30yr Change_pct
## FMSYref 3854 2739 -29
## AvC 2264 1990 -12
## Iratio 4453 1807 -59
## Islope1 4036 1952 -52
## MyMP 3316 1671 -50
bar_data <- rbind(comparison$Year1_TAC, comparison$Mean_Catch_30yr)
colnames(bar_data) <- MPs
barplot(bar_data, beside = TRUE, col = c("grey70", "steelblue"),
main = "Year 1 TAC vs 30-Year Mean Catch",
ylab = "Catch (tonnes)", las = 2)
legend("topright", c("Year 1 TAC", "30-yr Mean Catch"),
fill = c("grey70", "steelblue"), bty = "n")Note: Look at the
Change_pctcolumn. A negative value means the MP “promised” more than it could deliver. Which MP has the biggest gap? Which is most sustainable from the start?
We have been looking at the catches (yield) only. Now let’s also looks at the trade-offs with the other management objectices (e.g., status, safety)
PMs quantify how well each MP achieves management objectives. Each PM answers a specific question:
| PM | Question | Management Objective |
|---|---|---|
| P100 | P(SB > SBMSY)? | Stock status (green zone) |
| P50 | P(SB > 0.5 × SBMSY)? | Safety (above limit) |
| LTY | Long-term yield relative to MSY? | Yield |
| AAVY | How variable is catch year-to-year? | Stability |
Load a helper function to extract Performance Metric values:
pm_green <- P100(MSE)
pm_safe <- P50(MSE)
pm_yield <- LTY(MSE)
pm_var <- AAVY(MSE)
PM_table <- data.frame(
MP = MPs,
P_green = round(extract_pm(pm_green), 2),
P_safe = round(extract_pm(pm_safe), 2),
LT_Yield = round(extract_pm(pm_yield), 2),
Catch_Var = round(extract_pm(pm_var), 2)
)
# Performance Metrics Summary
# P_green: P(SB > SBMSY) over last 10 projection years
# P_safe: P(SB > 0.5 × SBMSY): above the limit ref point
# LT_Yield: long-term catch / reference yield (higher = more TAC/captures)
# Catch_Var: avg annual variation in catch (lower = more stable)
print(PM_table, row.names = FALSE)## MP P_green P_safe LT_Yield Catch_Var
## FMSYref 0.50 0.93 0.98 0.23
## AvC 0.46 0.64 0.57 0.79
## Iratio 0.54 0.75 0.46 0.90
## Islope1 0.46 0.72 0.66 0.88
## MyMP 0.60 0.81 0.44 0.96
Question: Which MP has the highest P(SB > SBMSY)? Which has the most stable catches?
## MP PNOF LTY P100 P50 P10 Satisificed
## FMSYref FMSYref 0.70 0.98 0.50 0.93 1.00 FALSE
## AvC AvC 0.45 0.57 0.46 0.64 0.75 FALSE
## Iratio Iratio 0.64 0.46 0.54 0.75 0.91 FALSE
## Islope1 Islope1 0.49 0.66 0.46 0.72 0.83 FALSE
## MyMP MyMP 0.72 0.44 0.60 0.81 0.94 FALSE
Note: The “Satisficed” column uses strict default thresholds. If ALL are not met simultaneously, it shows FALSE. Focus on the individual metric values instead.
So far, all results assume our OM is correct. But what if we’re wrong about the biology? A good MP should perform reasonably well even under wrong assumptions.
We test two “what-if” scenarios for a shark:
OM_lowM <- OM
OM_lowM@M <- OM@M * 0.5
# Robustness Scenario: Lower M (−50%)
cat("Baseline M:", OM@M[1], "–", OM@M[2], "\n")## Baseline M: 0.15 – 0.25
## Lower M: 0.075 – 0.125
OM_lowh <- OM
OM_lowh@h <- c(0.2, 0.4)
# Robustness Scenario: Lower Steepness
cat("Baseline h:", OM@h[1], "–", OM@h[2], "\n")## Baseline h: 0.4 – 0.9
## Scenario h: 0.2 – 0.4
p100_base <- extract_pm(P100(MSE))
p100_lowM <- extract_pm(P100(MSE_lowM))
p100_lowh <- extract_pm(P100(MSE_lowh))
p50_base <- extract_pm(P50(MSE))
p50_lowM <- extract_pm(P50(MSE_lowM))
p50_lowh <- extract_pm(P50(MSE_lowh))
robust_table <- data.frame(
MP = MPs,
P_green_Base = round(p100_base, 2),
P_green_LowM = round(p100_lowM, 2),
P_green_LowH = round(p100_lowh, 2),
P_safe_Base = round(p50_base, 2),
P_safe_LowM = round(p50_lowM, 2),
P_safe_LowH = round(p50_lowh, 2)
)
# Robustness: P(SB > SBMSY) across scenarios
print(robust_table %>% select(MP, starts_with("P_green")), row.names = FALSE)## MP P_green_Base P_green_LowM P_green_LowH
## FMSYref 0.50 0.50 0.37
## AvC 0.46 0.41 0.28
## Iratio 0.54 0.46 0.30
## Islope1 0.46 0.41 0.29
## MyMP 0.60 0.51 0.33
# Robustness: P(SB > 0.5*SBMSY) across scenarios
print(robust_table %>% select(MP, starts_with("P_safe")), row.names = FALSE)## MP P_safe_Base P_safe_LowM P_safe_LowH
## FMSYref 0.93 0.91 0.73
## AvC 0.64 0.61 0.47
## Iratio 0.75 0.74 0.53
## Islope1 0.72 0.69 0.50
## MyMP 0.81 0.78 0.58
robust_long <- robust_table %>%
select(MP, P_green_Base, P_green_LowM, P_green_LowH) %>%
pivot_longer(-MP, names_to = "Scenario", values_to = "P_green") %>%
mutate(Scenario = gsub("P_green_", "", Scenario),
Scenario = factor(Scenario, levels = c("Base", "LowM", "LowH")))
ggplot(robust_long, aes(MP, P_green, fill = Scenario)) +
geom_col(position = "dodge", width = 0.7) +
scale_fill_manual(values = c("Base" = "#1C7293", "LowM" = "#E07A5F",
"LowH" = "#F2CC8F"),
labels = c("Base case", "Lower M (−50%)",
"Lower steepness")) +
labs(title = "Robustness: P(SB > SBMSY) Across Scenarios",
x = "Management Procedure", y = "Probability") +
theme_minimal(base_size = 14) +
ylim(0, 1) +
theme(axis.text.x = element_text(angle = 20, hjust = 1))Question: Which MP is most robust (i.e., more consistent performance across “what-if” scenarios)?
Go back to the MyMP code and change the adjustment factors. Try different combinations and re-run the MSE:
0.80 to
0.90 (equal 10% max up and down)1.05 up,
0.70 downHow do these changes affect the trade-off between yield and stock status?
So far all our real MPs are empirical. Try adding a model-based MP that runs an internal stock assessment:
MPs_new <- c("FMSYref", "AvC", "Iratio", "Islope1", "MyMP", "DDSS_MSY")
MSE_new <- Project(Hist, MPs = MPs_new)
Tplot(MSE_new)How does the model-based MP compare to the empirical ones? Is doing something more complex (i.e., model based) always better?
What happens if the stock is much more depleted than we assumed?
OM_depleted <- OM
OM_depleted@D <- c(0.05, 0.15)
Hist_depleted <- Simulate(OM_depleted)
MSE_depleted <- Project(Hist_depleted, MPs = MPs)
Tplot(MSE_depleted)What MP performs better under a more pessimistic starting place? Does MyMP’s, that is more precautionary and reduces more TAC when needed, helps more when the stock is severely depleted?
You are advising a regional fisheries body. Based on your MSE results, write a 1-paragraph recommendation addressing:
There is no single correct answer. The choice depends on which objective the manager prioritizes. Your job as a scientist is to clearly explain the trade-off space
## Congratulations on completing your first MSE!