source: https://fieldnotesjournal.org/new-blog/apearlintheroughoysteraquacultureandhowitworks
Overview
Marine aquaculture has the potential to play an important role in the global food supply as a more sustainable protein option than land-based meat production.1 Gentry et al.
For this small project, we will determine which Exclusive Economic Zones (EEZ) on the West Coast of the US are best suited to developing marine aquaculture for several species of oysters.
Based on previous research, we know that oysters needs the following conditions for optimal growth:
- sea surface temperature: 11-30 deg C
- depth: 0-70 meters below sea level
Utilized skills:
- combining vector raster data
- resampling raster data
- masking raster data
- map algebra
Data
Sea Surface Temperature
We will use average annual sea surface temperature (SST) from the years 2008 to 2012 to characterize the average sea surface temperature within the region. The data we are working with was originally generated from NOAA’s 5km Daily Global Satellite Sea Surface Temperature Anomaly v3.1.
Bathymetry
To characterize the depth of the ocean we will use the General Bathymetric Chart of the Oceans (GEBCO).2
Exclusive Economic Zones
We will be designating maritime boundaries using Exclusive Economic Zones off of the west coast of US from Marineregions.org.
Prepare data
To start, we need to load all necessary data and make sure it has the coordinate reference system.
West coast regions:
# Read in the shapefile for the West Coast EEZ (`wc_regions_clean.shp`)
<- st_read(here(file_path, "data/wc_regions_clean.shp")) wc_regions
Reading layer `wc_regions_clean' from data source
`/Users/jaredpetry/Documents/MEDS/quarto_website/jaredbpetry.github.io/Posts/marine_aquaculture_geospatial/data/wc_regions_clean.shp'
using driver `ESRI Shapefile'
Simple feature collection with 5 features and 5 fields
Geometry type: MULTIPOLYGON
Dimension: XY
Bounding box: xmin: -129.1635 ymin: 30.542 xmax: -117.097 ymax: 49.00031
Geodetic CRS: WGS 84
plot(wc_regions$geometry)
Sea surface tmeperature:
# Read in sea surface temperature rasters and combine them into a raster stack
# use list.files() to read in our data
#--- you start with a bunch of tif files that you want to stack
#--- I created a list of just the ones starting with the letter "a" to get the sst rasters I wanted
<- list.files(path = "data/", pattern = "^[a]", full.names = TRUE)
file_list #--- now read them in using rast... this will create a spatraster with multiple layers
#--- (this isn't an actual raster stack so let's see if this works.. terra calls them the same thing)
<- rast(file_list)
sst_spatrast plot(sst_spatrast$average_annual_sst_2009)
#--- at this point you get a crs that says epsg4326 AND epsg9122 so we'll have to change that
Bathymetry raster:
# Read in bathymetry raster (`depth.tif`)
<- rast(here(file_path, "data/depth.tif"))
depth plot(depth)
Reprojecting data so they match in their coordinate reference system:
#--- depth raster is in lon/lat wgs84 EPSG:4326
#--- wc_regions polygons in wgs84
#--- sst_spatraster says lon/lat wgs84
#depth <- st_transform(depth, crs = crs(sst_spatrast))
set.crs(sst_spatrast, "EPSG:4326")
set.crs(depth, "EPSG:4326")
st_transform(wc_regions, crs = crs(depth)) #--- great now they are same crs
Simple feature collection with 5 features and 5 fields
Geometry type: MULTIPOLYGON
Dimension: XY
Bounding box: xmin: -129.1635 ymin: 30.542 xmax: -117.097 ymax: 49.00031
Geodetic CRS: WGS 84
rgn rgn_key area_m2 rgn_id area_km2
1 Oregon OR 179994061293 1 179994.06
2 Northern California CA-N 164378809215 2 164378.81
3 Central California CA-C 202738329147 3 202738.33
4 Southern California CA-S 206860777840 4 206860.78
5 Washington WA 66898309678 5 66898.31
geometry
1 MULTIPOLYGON (((-123.4318 4...
2 MULTIPOLYGON (((-124.2102 4...
3 MULTIPOLYGON (((-122.9928 3...
4 MULTIPOLYGON (((-120.6505 3...
5 MULTIPOLYGON (((-122.7675 4...
Process data
Next, we need process the SST and depth data so that they can be combined. In this case the SST and depth data have slightly different resolutions, extents, and positions. We don’t want to change the underlying depth data, so we will need to resample to match the SST data using the nearest neighbor approach.
Find the mean SST from 2008-2012
<- terra::app(sst_spatrast, mean) mean_sst_spatrast
Convert SST data from Kelvin to Celsius
<- mean_sst_spatrast - 273.15 mean_sst_C
Crop depth raster to match the extent of the SST raster
<- extend(depth, mean_sst_C) depth_cropped
Resample the NPP data to match the resolution of the SST data using the nearest neighbor approach
<- resample(depth_cropped, mean_sst_C,
depth_cropped_resampled method = "near")
Check that the depth and SST match in resolution, extent, and coordinate reference system… can the rasters be stacked?
#--- try to stack the depth raster and the SST raster:
<- raster::stack(c(depth_cropped_resampled, mean_sst_C))
depth_and_sst #--- yes! they can be stacked
Find suitable locations
In order to find suitable locations for marine aquaculture, we’ll need to find locations that are suitable in terms of both SST and depth.
Reclassify SST and depth data into locations that are suitable for oysters. We will do this by setting suitable values to 1
and unsuitable values to NA
The first plot will show suitable depth for oysters and the second will showsuitable sea surface temperature.
#--- for oysters, we need 11 <= sst(C) <= 30
#------ and 0 <= depth <= 70
#--- set up reclassification matrices
<- matrix(c(-8000, -70, NA,
depth_rcl -70, 0, 1,
0, 5000, NA),
ncol = 3, byrow = TRUE)
<- matrix(c(-Inf, 11, NA,
sst_rcl 11, 30, 1,
30, Inf, NA),
ncol = 3, byrow = TRUE)
#--- use these to classify your rasters
<- classify(mean_sst_C, rcl = sst_rcl)
suitable_sst
<- classify(depth_cropped_resampled, rcl = depth_rcl)
suitable_depth
#--- Plot what you just made for each
::plot(suitable_depth, col = "blue") raster
::plot(suitable_sst, col = "blue") raster
Find locations that satisfy both SST and depth conditions
- create an overlay using the lapp()
function multiplying cell values - we will use terra::lapp()
#--- we want to multiply cell values so that with any NA in either layer, result will also be NA, with two 1s, the result will also be a 1
= function(x,y) {
fun return(x*y)
}
<- lapp(c(suitable_depth, suitable_sst), fun)
oyster_habitat plot(oyster_habitat, col = "blue")
Determine the most suitable EEZ
We want to determine the total suitable area within each EEZ in order to rank zones by priority. To do so, we need to find the total area of suitable locations within each EEZ.
- select suitable cells within West Coast EEZs
- find area of grid cells
- find the total suitable area within each EEZ
- find the percentage of each zone that is suitable
#--- remember, our eez regions are the wc_regions we reprojected earlier
# select suitable cells within West Coast EEZs
#----first rasterize the wc_regions data to create rast_eez
<- terra::rasterize(wc_regions, oyster_habitat, field = "rgn")
rast_regions
# compute the area covered by the raster cells of suitable habitat
#--- terra::cellSize() will compute the area covered by each individual cell
<- cellSize(oyster_habitat, unit = "km", transform = TRUE)
area_habitat
#--- create mask that will display the suitable habitat separated into the different eez regions
<- mask(rast_regions, oyster_habitat)
mask
#--- terra::zonal() will compute summaries of values of a spatraster defined by the "zones" of a different spatraster. We will use this to compute the suitable area
<- terra::zonal(area_habitat, mask, sum)
habitat_area
#--- use left_join() to create a dataframe that contains both the suitable area by region and the percentage of habitat area out of the total area for that region
<- left_join(wc_regions, habitat_area, by = "rgn")
habitat_by_region_df $area_percent = (habitat_by_region_df$area / habitat_by_region_df$area_km2)*100
habitat_by_region_df
#--- show results in a table
<- habitat_by_region_df |>
print_df ::as.data.frame() |>
terra::select(rgn, area, area_percent) |>
dplyr::rename("Oyster Habitat Area" = area,
dplyr"EEZ region" = rgn,
"Habitat Percent of Total Region Area" = area_percent)
print_df
EEZ region Oyster Habitat Area Habitat Percent of Total Region Area
1 Oregon 1074.2720 0.5968374
2 Northern California 178.0268 0.1083028
3 Central California 4069.8766 2.0074530
4 Southern California 3757.2849 1.8163351
5 Washington 2378.3137 3.5551178
Visualize results
Now that we have results, we need to present them!
We will create the following maps:
- total suitable area by region
- percent suitable area by region
#--- map of total area
<- tm_shape(habitat_by_region_df) +
area_map tm_polygons(col = "area",
palette = rev(hcl.colors(3, "BluGrn")),
title = "Habitat Area (square km)",
legend.reverse = TRUE) +
tm_shape(oyster_habitat) +
tm_raster(title = "Habitat") +
tm_layout(legend.outside = TRUE,
main.title.size = 1,
main.title = "Suitable habitat area for oysters by EEZ region")
area_map
#--- map of percentage
<- tm_shape(habitat_by_region_df) +
percent_map tm_polygons(col = "area_percent",
palette = rev(hcl.colors(3, "BluGrn")),
title = "Percentage") +
tm_shape(oyster_habitat) +
tm_raster(title = "Habitat") +
tm_layout(legend.outside = TRUE,
main.title.size = 1,
main.title = "Suitable habitat for oysters: percentage of total EEZ region",
frame = T)
percent_map
Broaden the workflow!
Now that we’ve worked through the suitable habitat areas for one group of species, let’s update our workflow to work for other species. We will do this by creating a function that would allow you to reproduce your results for other species. It will be able to do the following:
- accept temperature and depth ranges and species name as inputs
- create maps of total suitable area and percent suitable area per EEZ with the species name in the title
<- function(spp_name, temp_min, temp_max, depth_min, depth_max) {
find_suitable_habitat <- matrix(c(-Inf, depth_min, NA,
depth_rcl 1,
depth_min, depth_max, Inf, NA),
depth_max, ncol = 3, byrow = TRUE)
<- matrix(c(-Inf, temp_min, NA,
sst_rcl 1,
temp_min, temp_max, Inf, NA),
temp_max, ncol = 3, byrow = TRUE)
<- classify(mean_sst_C, rcl = sst_rcl)
suitable_sst <- classify(depth_cropped_resampled, rcl = depth_rcl)
suitable_depth <- lapp(c(suitable_depth, suitable_sst), fun)
oyster_habitat <- terra::rasterize(wc_regions, oyster_habitat, field = "rgn")
rast_regions <- cellSize(oyster_habitat, unit = "km", transform = TRUE)
area_suitable <- mask(rast_regions, oyster_habitat)
mask <- terra::zonal(area_suitable, mask, sum)
area_per_zone <- left_join(wc_regions, area_per_zone, by = "rgn")
df_habitat_rgn $area_percent <- (df_habitat_rgn$area / df_habitat_rgn$area_km2)*100
df_habitat_rgn#--- map of total area
<- tmap_arrange(tm_shape(df_habitat_rgn) +
area_and_percent_map tm_polygons(col = "area",
palette = rev(hcl.colors(3, "BluGrn")),
title = "Area(km sq.)",
legend.reverse = TRUE) +
tm_shape(oyster_habitat) +
tm_raster(title = "Habitat") +
tm_layout(legend.outside = TRUE,
legend.title.size = 0.8,
legend.text.size = 0.5,
main.title.size = 0.8,
main.title = paste0(spp_name, " habitat by EEZ region")),
<- tm_shape(df_habitat_rgn) +
percent_map tm_polygons(col = "area_percent",
palette = rev(hcl.colors(3, "BluGrn")),
title = "Percentage") +
tm_shape(oyster_habitat) +
tm_raster(title = "Habitat") +
tm_layout(legend.outside = TRUE,
legend.title.size = 0.8,
legend.text.size = 0.5,
main.title.size = 0.8,
main.title = paste0(spp_name, " habitat percentage of EEZ region"),
frame = T))
area_and_percent_map
}#--- test the function (arbitrary values)
find_suitable_habitat(spp_name = "spp of interest", 5, 15, -100, 0)
Anyone who wants to reproduce this workflow can do so easily! Run your function for a species of your choice! You can find information on species depth and temperature requirements on SeaLifeBase. Remember, we are thinking about the potential for marine aquaculture, so these species should have some reasonable potential for commercial consumption.
I chose the American Lobster because it is a highly sought after food that could return lots of profit if grown in the right place. I used to work at a fish market and it was extremely hard to get lobster because it is so hard to get nowadays! The temperature range is 11-19 degC and the depth range is sea level to 480 meters deep.
find_suitable_habitat(spp_name = "American lobster", 11, 19, -480, 0)
SB depth data… just for fun
Can I do a SB analysis with the depth data? I was going to try this to see bathymetry data for surf spot intel… however the data wasn’t high enough resolution. Oh well it still looks cool at least
<- depth
rast_to_crop <- 34.522
lat_up <- 34.085
lat_down <- -120.223
lon_up <- -119.171
lon_down #--- formatting for ext(): -180, 180, -90, 90 (xmin, xmax, ymin, ymax)
<- terra::ext(lon_up, lon_down, lat_down, lat_up)
extent_of_crop <- crop(rast_to_crop, extent_of_crop)
sb_depth
#--- also crop the basemap to the same thing
#sb_basemap <- extend(wc_regions, sb_depth)
tm_shape(sb_depth) + tm_raster()