Skip to contents

datamatch fetches from four sources and joins any of them to your observations the same way. This is about which one to reach for, and what changes when you do.

Nothing here is downloaded when the vignette is built — the fetches are shown but not run, because each needs a network and some need several gigabytes.

The four, side by side

accessEnvDat() accessFVCOM() accessHYCOM() accessCCMP()
Source Copernicus Marine NECOFS / any FVCOM HYCOM GOFS 3.1 RSS CCMP v3.1
Kind global reanalysis, forecast, satellite regional coastal model global model wind analysis
Grid 0.083°–4 km, regular unstructured mesh 0.08° regular 0.25° regular
Steps monthly, daily, hourly monthly (GOM3), hourly (GOM7) 3-hourly 6-hourly
Record 1993– 1978–2013, then 2025– 1994–2024 across archives 1993–present
Bottom salinity derived free free
Wind stress yes GOM3 only
Subsetting server-side client-side server-side whole globe

Two of those records are chains rather than one run, and the gaps matter more than the endpoints. FVCOM has nothing between 2014 and 2024 — the GOM3 hindcast stops in 2013 and the GOM7 forecast archive starts in 2025, on a different mesh. HYCOM reaches 2024 only by crossing from the reanalysis into a series of operational experiments, which is a seam in how the values were produced. hycom_covering(date) says which archives hold a given day.

variable_dictionary()   # Copernicus
fvcom_dictionary()      # FVCOM
hycom_dictionary()      # HYCOM
ccmp_dictionary()       # CCMP

They share variable names on purpose

A covariate arrives in a column of the same name whichever source supplied it, so everything downstream — matchData(), resampling, plotting, and whatever model you fit — works unchanged:

bb <- list(xmin = -70, xmax = -66, ymin = 41, ymax = 44)

copernicus <- accessEnvDat(vars = "SST", years = 2010, months = 1:12, bounding_box = bb)
fvcom      <- accessFVCOM(vars = "SST", years = 2010, months = 1:12, bounding_box = bb)
hycom      <- accessHYCOM(vars = "SST", years = 2010, months = 1:12, bounding_box = bb)

All three produce an SST column. They are three different models and three different numbers. Sharing the name makes them mechanically interchangeable, which is the point; it does not make them scientifically interchangeable, which is why it is worth recording which you used.

That property is useful deliberately. Fetching the same variable from two sources and comparing them is a real check on a result:

matched <- matchData(observations, copernicus)          # SST
matched <- matchData(matched, hycom)                    # SST.matched

plot(matched$SST, matched$SST.matched)

A colliding name is suffixed .matched rather than overwriting yours, so both survive the join and the disagreement is visible.

Which to reach for

Start with Copernicus. It has the widest variable list — physics, biogeochemistry, satellite ocean colour, wind and stress — the longest record with a forecast, and server-side subsetting. Everything else here is for something Copernicus does not do well.

Reach for FVCOM when the coast is the point. A Gulf of Maine box holding 1,742 GLORYS cells holds 6,579 GOM3 nodes, concentrated where the bathymetry is complicated. If your question is about a shelf, a bank, or a channel rather than a basin, that resolution is the reason. The hindcast ends in 2013; the GOM7 forecast archive picks up in 2025 on a different mesh, with nothing in between.

Reach for HYCOM for bottom fields, or for a second opinion. It publishes salinity_bottom and water_temp_bottom as real fields where Copernicus has no bottom salinity at all, and being an independent model it makes agreement meaningful. The reanalysis covers 1994–2015; operational archives carry it to September 2024.

Reach for CCMP for winds over a long record. Six-hourly from 1993 to within days of the present, where the Copernicus wind is monthly from mid-1994 or hourly only from 2007. But it carries no stress.

Bottom salinity, four ways

BOTS is the clearest case of the same name costing different amounts:

# Copernicus reanalysis: derived. Fetches ~50 depth levels to keep the deepest
# wet one, so it must be fetched alone, and reports the depth it used.
bots <- accessEnvDat(vars = "BOTS", years = 2010, months = 1:12, bounding_box = bb)
bots$BOTS_depth

# Copernicus forecast: published outright as `sob`, no derivation.
accessEnvDat(vars = c("BOTT", "BOTS"), years = 2026, months = 7,
             bounding_box = bb, mode = "forecast")

# FVCOM: the deepest sigma layer is the sea floor everywhere. Free.
accessFVCOM(vars = c("BOTT", "BOTS"), years = 2010, months = 1:12, bounding_box = bb)

# HYCOM: `salinity_bottom` is its own field. Free.
accessHYCOM(vars = c("BOTT", "BOTS"), years = 2010, months = 1:12, bounding_box = bb)

Only the first is expensive, and only the first returns BOTS_depth — because only the first had to choose a level. The deepest wet model level is not the sea floor, and in deep water sits well above it, so the depth is reported rather than left to assume.

Sub-daily data, and means that are not means

All four publish below daily somewhere, and none of them publishes a daily mean:

Source Native step frequency
Copernicus wind hourly "hourly"
FVCOM GOM7 hourly "hourly", or "daily" for a snapshot
HYCOM 3-hourly "3hourly", or "daily" for a snapshot
CCMP 6-hourly "6hourly", or "daily" for a snapshot

Where a source has no mean, none is invented. frequency = "daily" on HYCOM and CCMP takes one snapshot at a chosen hour — an instant, not an average. A real mean is upscale_time()’s job, which keeps the aggregation visible and the choice of summary yours:

steps <- accessCCMP(vars = c("UWND", "VWND"), frequency = "6hourly",
                    dates = "2010-06-15", bounding_box = bb)

mean_wind <- upscale_time(steps, to = "day")
peak_wind <- upscale_time(steps, to = "day", method = "max")

The coverage check knows each source’s own step, so a complete CCMP day scores 4/4 rather than 4/24.

One caution that is easy to miss: a mean of the components is not a mean speed. Averaging UWND and VWND over a day and taking the magnitude gives the net displacement of air; averaging WSPD gives how hard it blew. On a day the wind reversed, the first is near zero and the second is not.

Longitude, and one trap handled for you

Pass bounding_box with longitudes negative west to every function here. CCMP is stored on a 0–360 grid and is converted on the way in and back on the way out, so its results overlay the others without adjustment. You should never have to think about it — but if you fetch CCMP by hand elsewhere, that is the difference between the Gulf of Maine and the Indian Ocean.

Putting several together

The pattern is the same as chaining two Copernicus products: each call adds columns and leaves the row count alone.

matched <- matchData(observations, accessEnvDat(vars = c("SST", "MLD"),
                                                years = 2010, months = 1:12,
                                                bounding_box = bb))
matched <- matchData(matched, accessHYCOM(vars = "BOTS", years = 2010,
                                          months = 1:12, bounding_box = bb))
matched <- matchData(matched, accessCCMP(vars = "WSPD", years = 2010,
                                         months = 1:12, bounding_box = bb))

matched <- attach_bathymetry(matched, fetch_bathymetry(bounding_box = bb),
                             c("DEPTH", "SLOPE"))
matched <- attach_climate_index(matched, "NAO")

colSums(is.na(sf::st_drop_geometry(matched)))

Each source fails differently at the edges of its record — FVCOM after 2013, HYCOM after 2015, LCR after 2014 — so that last line is worth running before fitting anything.

Citing what you used

Every source here is somebody else’s work, and the obligation travels with the data rather than with this package:

fvcom_archives()$GOM3$reference
hycom_archives()$GLBv53X$reference
ccmp_versions()$`v03.1`$reference
index_dictionary()          # carries the climate index references

The README’s References section lists the Copernicus product DOIs. Cite whichever you actually fetched from.

Satellite or model?

Copernicus serves chlorophyll and primary production from two very different sources, and both are available:

Name Source Resolution Trade-off
CHL, PP Copernicus-GlobColour, satellite 4 km Observed, finer — but surface-only and gappy under persistent cloud
CHL_MODEL, NPP_MODEL Biogeochemistry reanalysis 0.25° Gap-free and depth-resolved — but simulated, and coarser

The plain names default to satellite, since the values are observed rather than simulated. Switch to the model versions where cloud gaps would matter more than resolution.

One caution: satellite PP and model NPP_MODEL are not the same quantity. PP is depth-integrated (mg/m2/day), NPP_MODEL volumetric (mg/m3/day). Substituting one for the other is a units error, not a resolution difference.

Phytoplankton functional types come from the same satellite plankton dataset as CHL, so they can be fetched together:

env <- accessEnvDat(
  vars = c("CHL", "DIATO", "DINO"),   # one request, one dataset
  years = 2003:2017, months = 1:12,
  bounding_box = list(xmin = -76, xmax = -65, ymin = 35, ymax = 45)
)

DIATO and DINO are diatom and dinophyte chlorophyll — the spring-bloom species large copepods prefer, and the later stratified-water group respectively.

Bottom salinity

BOTS pairs with BOTT, but it is not fetched the same way, because GLORYS12V1 does not publish it. The reanalysis has temperature at the sea floor and no salinity counterpart. So it is derived: the full salinity column is fetched and the deepest wet level in each cell kept.

bots <- accessEnvDat(vars = "BOTS", years = 2010:2014, months = 1:12,
                     bounding_box = bb)
#> BOTS is not published by this product. Deriving it from the full 'so' column
#> and keeping the deepest wet level in each cell; the depth used comes back as
#> BOTS_depth.

The depth each value came from is returned as BOTS_depth rather than left to be assumed. That matters because the deepest wet model level is not the sea floor: level spacing coarsens with depth, so in deep water the value can sit a long way above the bottom. In shelf water it is within a few metres.

Two consequences, both deliberate:

  • It must be fetched on its own. The whole depth column is a different request from the single level SST wants, so mixing them is an error rather than a quiet second download. Call twice and chain matchData().
  • It costs far more. Roughly fifty levels are downloaded over the same box to keep one, so a large box is much slower than the same box of SST.

In forecast mode none of this applies. The analysis-and-forecast product publishes sea-floor salinity outright, so BOTS is an ordinary variable there, fetches alongside BOTT, and returns no BOTS_depth:

accessEnvDat(vars = c("BOTT", "BOTS"), years = 2026, months = 8,
             bounding_box = bb, mode = "forecast")

The two are the same quantity by construction but not the same number — one is the deepest level of a 50-level grid, the other Copernicus’s own diagnostic — so a record spanning both modes has a seam in it.

variable_dictionary("biogeochemical")        # filter by product
variable_dictionary("wind")                  # or just the winds
fvcom_dictionary()                           # the FVCOM catalog, for accessFVCOM()
fvcom_archives()                             # which FVCOM archives are built in
hycom_dictionary()                           # the HYCOM catalog, for accessHYCOM()
hycom_archives()                             # which HYCOM archives can be read
hycom_covering("2019-06-15")                 # which of them span a given date
ccmp_dictionary()                            # the CCMP catalog, for accessCCMP()
ccmp_versions()                              # which CCMP versions can be read
erddap_dictionary()                          # MUR and VIIRS, for accessERDDAP()
erddap_datasets()                            # which ERDDAP datasets ship
variable_dataset(c("SST", "CHL"))            # which dataset each comes from
as.data.frame(variable_dictionary())$description  # full descriptions