R pipeline that turns the FWRI GFISHER side-scan habitat mapping and the 3LABS video survey
into inputs for the West Florida Shelf Ecospace model: sum-to-1 habitat basemaps, per-group
MaxN heatmaps, and reef-class habitat affinities. One driver script,
process GFISHER data.R, runs four numbered stages.
| Stage | Code | Produces |
|---|---|---|
| 1 Habitat basemaps | R/habitat_basemaps.R |
Nine layers that sum to 1 in every water cell: six GFISHER reef classes (AL AM AH NL NM NH), rock (RCK), unconsolidated bottom (UNC), seagrass (SGR) |
| 2 Video dataset | R/video_dataset.R |
Station x model-group MaxN table from the 3LABS survey files and the species list |
| 3 MaxN heatmaps | R/maxn_maps.R |
One raster per map unit on the Ecospace grid, plus a sampling mask and an effort layer |
| 3b MaxN reference maps (optional) | R/GFISHER maxn sdmTMB.R |
Model-based MaxN predictions for Ecospace calibration, each with a CV layer. Off by default (do.sdmTMB) |
| 4 Reef-class affinities | R/habitat_affinity.R |
Habitat affinity of each model group for the six reef classes, from the class recorded at each video drop, with bootstrap intervals |
R/legacy/habitat_maps_cellarea.R holds the retired cell-area habitat maps, kept so older
Ecospace runs can be reproduced. It is not called by the driver.
Tested with R 4.5.1 on Windows 11. Reading the geodatabase in stage 1 is the slow part; the optional sdmTMB maps add roughly 20 minutes when enabled.
- Clone the repo and open
GFISHER.Rprojin RStudio (orsetwd()to the repo root). The driver refuses to run from anywhere else. - Install the packages (one line, printed by the driver if any is missing):
Optional:
install.packages(c("sf", "sp", "raster", "terra", "FNN", "colorRamps", "reshape2", "readxl"))
mgcv(only fortarget='smooth'in stage 1),gstat/lwgeom/maps(only forR/legacy/),sdmTMB+fmesher(only for the stage 3b reference maps). - Get the inputs that cannot ship with the repo (next section) and, if they are not in the
default folders, copy
config.local.example.Rtoconfig.local.Rand point at them. source("process GFISHER data.R"), or from a terminalRscript "process GFISHER data.R".
Before anything slow runs, the driver prints an input check and stops if a required input is missing:
Input check
----------------------------------------------------------------------------------------------------
input stage need how status / path
bathymetry 1,3,4 required repo OK .../data/bathymetry/depth 5min 66x78.asc
species_list 2 required repo OK .../data/Master Species List.xlsx
geodatabase 1,4 required manual OK .../data/April2026/GFISHER_EAST_Universe_2026.gdb
survey_maxn 2 required manual OK .../data/April2026/maxn3LABS_93to24.csv
survey_env 2,3,4 required manual OK .../data/April2026/env3LABS_93to24.csv
survey_lens 2 required manual OK .../data/April2026/lens3LABS_93to24.csv
dbseabed 1 required auto OK .../data/dbseabed
seagrass 1 optional derived OK .../data/seagrass/seagrass_5min.asc
Everything the pipeline reads, where it comes from, and how a new user obtains it. Sizes are approximate.
| Input | Size | How | Where it goes |
|---|---|---|---|
| Depth grids, 5 and 15 arc-min | 100 KB | ships with the repo | data/bathymetry/ |
Master Species List.xlsx (species to model-group key, size-at-age stanzas) |
220 KB | ships with the repo | data/ |
GFISHER_EAST_Universe_2026.gdb (FWRI side-scan habitat mapping; layers East_Master_Hab_Data_FINAL_2026, East_Master_Microgrid_Mapped_2026) |
275 MB | by request from FWRI (Sean Keenan) or the repo author; cannot be redistributed on GitHub | data/April2026/, or set dir.data / file.gdb |
maxn3LABS_93to24.csv, env3LABS_93to24.csv, lens3LABS_93to24.csv (FWRI 3LABS video survey 1993 to 2024) |
42 MB | by request, same source | data/April2026/, or set dir.data |
dbSEABED raw grids Gmf_{RCK,GVL,SND,MUD}/gmf_*_val.asc |
4.4 MB | ships with the repo (public data from CSDMS dbSEABED; provenance in data/dbseabed/SOURCE.md). fn.pull_dbseabed() re-downloads them if the folder is removed |
data/dbseabed/, or set dir.dbseabed to another copy |
seagrass_5min.asc (seagrass cover on the model grid, derived from FWC Seagrass Habitat in Florida by the EcospaceBasemap repo) |
44 KB | ships with the repo (provenance in data/seagrass/SOURCE.md); optional: without it the SGR layer is zero. No 15-minute version yet |
data/seagrass/, or set file.seagrass |
Expected data/ tree once everything is in place:
data/
bathymetry/ depth 5min 66x78.asc, depth 15min 22x26.asc, excl layer *.asc (tracked)
Master Species List.xlsx (tracked)
April2026/ GFISHER_EAST_Universe_2026.gdb/, maxn/env/lens 3LABS_93to24.csv (gitignored)
dbseabed/ Gmf_RCK/ Gmf_GVL/ Gmf_SND/ Gmf_MUD/, SOURCE.md (tracked)
seagrass/ seagrass_5min.asc, SOURCE.md (tracked; 15min not yet made)
If you keep the FWRI files somewhere else (a shared drive, a OneDrive sync), do not copy them:
point dir.data at that folder in config.local.R. On Windows a directory junction
(New-Item -ItemType Junction -Path data\April2026 -Target <folder>) also works and is ignored
by git.
Defaults live in the SETTINGS block of the driver and are repo-relative. Override any of them
in config.local.R (gitignored; config.local.example.R documents every key). The driver
sources it after the defaults and before reading any input.
| Key | Default | Purpose |
|---|---|---|
res |
5 |
Grid resolution in arc-minutes, 5 or 15 |
group.scheme |
'mice' |
Species grouping: 'mice' or 'ecospace' columns of the species list |
seed |
1 |
Seed for the stage 2 length resampling (see Known caveats). NULL = unseeded |
dir.data |
data/April2026 |
Folder with the geodatabase and the three survey CSVs |
file.gdb |
NULL |
Geodatabase path; NULL finds the single GFISHER_EAST_Universe*.gdb in dir.data |
dir.bathy |
data/bathymetry |
Depth grids |
file.spplist |
data/Master Species List.xlsx |
Species list |
dir.dbseabed |
data/dbseabed |
Raw dbSEABED grids (ship with the repo); set only to use another copy |
file.seagrass |
NULL |
Seagrass raster; NULL = data/seagrass/seagrass_<res>min.asc |
dir.ecospace.maps |
NULL |
The author's external Ecospace maps tree; sets file.seagrass and dir.ewemaps the former way |
dir.maps |
output/maps |
Output root for stage 3 |
dir.ewemaps |
NULL |
Where MaxN heatmaps are written; NULL = dir.maps |
The original author's setup is two lines in his config.local.R: dir.dbseabed and
dir.ecospace.maps pointing at his existing trees, and nothing else.
output/
basemaps/<res>min/ habitat_<CODE>_<res>min_<rows>x<cols>.asc (9 layers) tracked deliverable
habitat_basemap_QC_<res>min.csv, habitat_basemaps_<res>min.png
maps/GFISHER/<res>min/maxn/<scheme>/
GFISHER_maxn_mod<N>_<group>_<res>min_<rows>x<cols>.asc (one per MAP UNIT)
GFISHER_maxn_mask_<res>min_<rows>x<cols>.asc 1 = sampled, NA = background
GFISHER_maxn_effort_<res>min_<rows>x<cols>.asc drops per cell
GFISHER_maxn_heatmaps_<res>min_<rows>x<cols>.pdf
maps/GFISHER/<res>min/maxn_sdmTMB/<scheme>/
GFISHER_sdmTMB_{est,cv,pres}_mod<N>_*.asc, mask, effort,
sdmTMB_fit_summary.csv, GFISHER_sdmTMB_maps_<res>min_*.pdf
affinity_<scheme>/ reefclass_A_wide.csv affinity, best class = 1 <- Ecospace input
reefclass_w_area_wide.csv density vs the area-weighted reef average
reefclass_affinity_long.csv, reefclass_coverage.csv,
reefclass_diagnostics.pdf
invideo_crosscheck.csv / .pdf (diagnostic, not a model input)
GFISHER_species_fg_<scheme>.csv survey taxon -> model group key (gitignored)
Stage 1 writes into output/basemaps/ in place, so a rebuild shows up in git diff. That is
deliberate: the basemaps are the versioned deliverable that feeds Ecospace.
The stage 1 panel figure:
The video cannot resolve six gag or six red grouper stanzas, so several model groups share one
map layer and the unmappable ones get none. Nineteen MICE groups become eleven layers: gag 2, 3
and 4 pool into one gag 2-4 layer, red grouper 3 and 4 into red grouper 3-4, and gag 0, gag 1
and red grouper 0, 1, 2 get no layer at all. The spec and its reasoning are
MAXN.MAPS.MICE / MAXN.DROP.MICE in R/maxn_maps.R. This is map-only: the Ecospace group
structure and the affinity tables still carry every stanza separately.
The cell-mean layers set unsampled water to zero, so 2,707 of 3,838 water cells carry a value
nobody observed. Any objective function scored on these layers must multiply by
GFISHER_maxn_mask_* first, or it is mostly scoring where the boat went. For calibration
proper, prefer the stage 3b sdmTMB maps, which return NA outside the predictable domain
instead of guessing.
The six reef classes are a two-letter code from the FWRI NewHabStrat field: A/N for
artificial or natural, L/M/H for low, medium or high relief. Reef density is the mapped
reef area divided by the scanned area of each cell (GFISHER has scanned about 3% of the
domain), then shrunk toward a region x depth-bin average in cells with little scanning. Rock
comes from the raw dbSEABED rock fraction; unconsolidated bottom is what remains; seagrass is
area-averaged from the seagrass raster. The nine layers are normalised to sum to exactly 1 in
every water cell, and reef is forced to zero below 300 m. The header of
R/habitat_basemaps.R records the alternatives that were tried and why they were rejected.
Stage 4 estimates the affinity of each model group for the six reef classes only:
- use is MaxN at a video drop, assigned to the one class that drop sat on (
HAB_STRATinenv3LABS, cross-checked againstREEF_TYPE); absences are retained. - availability is the mapped area of each class over the side-scan footprint, taken from
stage 1's own microgrid table. It is deliberately not the share of drops: the survey put about
32x more effort into
AHthan its area share, so drop-based availability would measure the survey design rather than the habitat. - depth is held out as a separate stratum effect, because Ecospace applies its own depth response function on top of habitat capacity. An affinity that had absorbed depth preference would be applied twice.
Two scales are reported from the same fit: reefclass_A_wide.csv (affinity, best class = 1 --
the Ecospace input) and reefclass_w_area_wide.csv (density relative to the area-weighted reef
mosaic average, so >1 means more fish per unit area than average reef). They rank identically by
construction; that is what a categorical habitat gives you.
No non-reef affinities are produced. The survey does not identify rock, sand, mud or seagrass
at the drop, and estimating them from cell-averaged availability does not work: the side-scan
footprint covers a median 12% of a 5-min cell, so the reef patches the cameras target are largely
absent from the availability layer and the counts get attributed to mud, which came out top for
most reef groups including every gag stanza. Restricting to well-mapped cells does not rescue it
-- at >=40% footprint only 5 cells and 373 drops survive. The code for that cell-scale analysis
(fn.batch_affinities, LAYER.SPEC) is still in R/habitat_affinity.R but nothing calls it.
- Your paths are yours. Nothing machine-specific belongs in tracked files; it goes in
config.local.R. - Contributing changes. Open an issue, create the branch from its Development sidebar
(
N-short-description), commit with(issue #N)in the subject, and open a pull request againstmainwithFixes #Nin the body. Split code, documentation, regenerated outputs and housekeeping into separate commits. - Review history. The October 2026 reproducibility review is documented in
docs/issue2-review-gfisher-repo-plan.md(findings, evidence, decisions). Note that it predates the stage 4 rework described above, so its 4a/4b/4c sections describe modules that no longer exist.
- Stanza assignment depends on an imputation. Only 31% of counted gag and 54% of red
grouper carry a measured length, so stage 2 imputes the rest by a stratified hot deck:
resampling real measured lengths from the finest depth-band x era stratum that has at least
30 of them. Totals per station and species are exact; the split among stanzas changes with
the seed. With
seed = 1the run is reproducible, but the stanza-level maps remain one realisation. How much this matters for the affinities is tracked in issue #5. - Seagrass. The shipped
data/seagrass/seagrass_5min.asccomes fromEcospaceBasemap. The author's original basemaps used his own rasterisation of the same FWC layer; the two agree in all but 45 of 3,838 water cells. - Stations on grid lines. Eleven survey stations sit exactly on a 5-minute latitude line. Which cell they fall in depends on the template grid's header; the driver checks that all grids share one template so the assignment is consistent within a run.
- Artificial reef is thinly sampled even at the drop.
AL/AM/AHare flagged byreefclass_coverage.csv; read it before using those columns. - Windows plot windows. Interactive runs on Windows open a recording graphics window;
Rscriptand other platforms skip it. Figures are written to files either way. - Legacy geodatabases. Two older geodatabases (
East_Master_Hab_data_Dissolve_byMicro_13Sept24.gdb,FWRI_East_Gulf_Mapping_2023.gdb) were tracked until October 2026 and remain in git history. They are inputs only to the retired legacy module and are not needed. fn.make_GFISHER_habitat_mapsexists in two files --R/GFISHER habitat maps.RandR/legacy/habitat_maps_cellarea.R. Both are superseded by stage 1; one should be removed.
- A fresh clone with only
config.local.Radded runs end to end underRscript. - Stage 1 basemaps regenerate byte-identical given the same inputs.
- Stages 2 to 4 are deterministic given
seed. - MD5 comparison of outputs:
tools::md5sum(list.files("output/basemaps/5min", "\\.asc$", full.names=TRUE)).
